2019-05-03 19:30:46 -04:00
|
|
|
'use strict';
|
|
|
|
|
|
|
|
// ============================================================================
|
|
|
|
// Imports
|
|
|
|
// ============================================================================
|
|
|
|
|
|
|
|
import dayjs from 'dayjs';
|
2019-10-06 20:01:22 -04:00
|
|
|
import RelativeTime from 'dayjs/plugin/relativeTime';
|
|
|
|
|
|
|
|
dayjs.extend(RelativeTime);
|
|
|
|
|
|
|
|
|
2019-05-03 19:30:46 -04:00
|
|
|
import Parser from '@ffz/icu-msgparser';
|
|
|
|
|
2021-06-17 14:27:04 -04:00
|
|
|
const DEFAULT_PARSER_OPTIONS = {
|
|
|
|
allowTags: false,
|
|
|
|
requireOther: false
|
|
|
|
};
|
|
|
|
|
2019-05-03 19:30:46 -04:00
|
|
|
import {get} from 'utilities/object';
|
2019-05-03 22:36:26 -04:00
|
|
|
import {duration_to_string} from 'utilities/time';
|
2019-05-03 19:30:46 -04:00
|
|
|
|
|
|
|
|
|
|
|
// ============================================================================
|
|
|
|
// Types
|
|
|
|
// ============================================================================
|
|
|
|
|
|
|
|
export const DEFAULT_TYPES = {
|
2019-06-01 02:11:22 -04:00
|
|
|
tostring(val) {
|
|
|
|
return `${val}`
|
|
|
|
},
|
|
|
|
|
2019-05-03 19:30:46 -04:00
|
|
|
select(val, node, locale, out, ast, data) {
|
|
|
|
const sub_ast = node.o && (node.o[val] || node.o.other);
|
|
|
|
if ( ! sub_ast )
|
|
|
|
return undefined;
|
|
|
|
|
|
|
|
return this._processAST(sub_ast, data, locale);
|
|
|
|
},
|
|
|
|
|
|
|
|
plural(val, node, locale, out, ast, data) {
|
|
|
|
const sub_ast = node.o && (node.o[`=${val}`] || node.o[getCardinalName(locale, val)] || node.o.other);
|
|
|
|
if ( ! sub_ast )
|
|
|
|
return undefined;
|
|
|
|
|
|
|
|
return this._processAST(sub_ast, data, locale);
|
|
|
|
},
|
|
|
|
|
|
|
|
selectordinal(val, node, locale, out, ast, data) {
|
|
|
|
const sub_ast = node.o && (node.o[`=${val}`] || node.o[getOrdinalName(locale, val)] || node.o.other);
|
|
|
|
if ( ! sub_ast )
|
|
|
|
return undefined;
|
|
|
|
|
|
|
|
return this._processAST(sub_ast, data, locale);
|
|
|
|
},
|
|
|
|
|
|
|
|
number(val, node) {
|
2020-08-04 18:26:11 -04:00
|
|
|
if ( typeof val !== 'number' ) {
|
2020-09-15 19:17:29 -04:00
|
|
|
let new_val = parseFloat(val);
|
2020-08-04 18:26:11 -04:00
|
|
|
if ( isNaN(new_val) || ! isFinite(new_val) )
|
2020-09-15 19:17:29 -04:00
|
|
|
new_val = parseInt(val, 10);
|
2020-08-04 18:26:11 -04:00
|
|
|
if ( isNaN(new_val) || ! isFinite(new_val) )
|
|
|
|
return val;
|
|
|
|
|
|
|
|
val = new_val;
|
|
|
|
}
|
2019-05-03 19:30:46 -04:00
|
|
|
|
|
|
|
return this.formatNumber(val, node.f);
|
|
|
|
},
|
|
|
|
|
2023-03-06 17:08:47 -05:00
|
|
|
currency(val, node) {
|
|
|
|
if ( typeof val !== 'number' ) {
|
|
|
|
let new_val = parseFloat(val);
|
|
|
|
if ( isNaN(new_val) || ! isFinite(new_val) )
|
|
|
|
new_val = parseInt(val, 10);
|
|
|
|
if ( isNaN(new_val) || ! isFinite(new_val) )
|
|
|
|
return val;
|
|
|
|
|
|
|
|
val = new_val;
|
|
|
|
}
|
|
|
|
|
|
|
|
return this.formatCurrency(val, node.f);
|
|
|
|
},
|
|
|
|
|
2019-05-03 19:30:46 -04:00
|
|
|
date(val, node) {
|
|
|
|
return this.formatDate(val, node.f);
|
|
|
|
},
|
|
|
|
|
|
|
|
time(val, node) {
|
|
|
|
return this.formatTime(val, node.f);
|
|
|
|
},
|
|
|
|
|
|
|
|
datetime(val, node) {
|
|
|
|
return this.formatDateTime(val, node.f);
|
|
|
|
},
|
|
|
|
|
2019-05-03 22:36:26 -04:00
|
|
|
duration(val) {
|
2020-07-26 17:50:14 -04:00
|
|
|
return this.formatDuration(val);
|
2019-05-03 19:30:46 -04:00
|
|
|
},
|
|
|
|
|
2019-05-03 22:36:26 -04:00
|
|
|
localestring(val) {
|
2019-05-03 19:30:46 -04:00
|
|
|
return this.toLocaleString(val);
|
|
|
|
},
|
|
|
|
|
2019-10-06 20:01:22 -04:00
|
|
|
relativetime(val, node) {
|
|
|
|
return this.formatRelativeTime(val, node.f);
|
|
|
|
},
|
|
|
|
|
2019-05-03 19:30:46 -04:00
|
|
|
humantime(val, node) {
|
2019-10-06 20:01:22 -04:00
|
|
|
return this.formatRelativeTime(val, node.f);
|
2019-05-03 19:30:46 -04:00
|
|
|
},
|
|
|
|
|
|
|
|
en_plural: v => v !== 1 ? 's' : ''
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
export const DEFAULT_FORMATS = {
|
|
|
|
number: {
|
|
|
|
currency: {
|
|
|
|
style: 'currency'
|
|
|
|
},
|
|
|
|
percent: {
|
|
|
|
style: 'percent'
|
|
|
|
}
|
|
|
|
},
|
|
|
|
|
|
|
|
date: {
|
|
|
|
short: {
|
|
|
|
month: 'numeric',
|
|
|
|
day: 'numeric',
|
|
|
|
year: '2-digit'
|
|
|
|
},
|
|
|
|
|
2020-08-04 18:26:11 -04:00
|
|
|
default: {},
|
|
|
|
|
|
|
|
medium: {
|
|
|
|
month: 'short',
|
|
|
|
day: 'numeric',
|
|
|
|
year: 'numeric'
|
|
|
|
},
|
|
|
|
|
2019-05-03 19:30:46 -04:00
|
|
|
long: {
|
|
|
|
month: 'long',
|
|
|
|
day: 'numeric',
|
|
|
|
year: 'numeric'
|
|
|
|
},
|
|
|
|
|
|
|
|
full: {
|
|
|
|
weekday: 'long',
|
|
|
|
month: 'long',
|
|
|
|
day: 'numeric',
|
|
|
|
year: 'numeric'
|
|
|
|
}
|
|
|
|
},
|
|
|
|
|
|
|
|
time: {
|
|
|
|
short: {
|
|
|
|
hour: 'numeric',
|
|
|
|
minute: 'numeric'
|
|
|
|
},
|
|
|
|
|
|
|
|
medium: {
|
|
|
|
hour: 'numeric',
|
|
|
|
minute: 'numeric',
|
|
|
|
second: 'numeric'
|
|
|
|
},
|
|
|
|
|
|
|
|
full: {
|
|
|
|
hour: 'numeric',
|
|
|
|
minute: 'numeric',
|
|
|
|
second: 'numeric',
|
|
|
|
timeZoneName: 'short'
|
|
|
|
}
|
|
|
|
},
|
|
|
|
|
|
|
|
datetime: {
|
|
|
|
short: {
|
|
|
|
month: 'numeric',
|
|
|
|
day: 'numeric',
|
|
|
|
year: '2-digit',
|
|
|
|
hour: 'numeric',
|
|
|
|
minute: 'numeric'
|
|
|
|
},
|
|
|
|
|
2020-08-04 18:26:11 -04:00
|
|
|
medium: {},
|
2019-05-03 19:30:46 -04:00
|
|
|
|
|
|
|
long: {
|
|
|
|
month: 'long',
|
|
|
|
day: 'numeric',
|
|
|
|
year: 'numeric',
|
|
|
|
hour: 'numeric',
|
|
|
|
minute: 'numeric',
|
|
|
|
second: 'numeric',
|
|
|
|
timeZoneName: 'short'
|
|
|
|
},
|
|
|
|
|
|
|
|
full: {
|
|
|
|
weekday: 'long',
|
|
|
|
month: 'long',
|
|
|
|
day: 'numeric',
|
|
|
|
year: 'numeric',
|
|
|
|
hour: 'numeric',
|
|
|
|
minute: 'numeric',
|
|
|
|
second: 'numeric',
|
|
|
|
timeZoneName: 'short'
|
|
|
|
}
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
// ============================================================================
|
|
|
|
// TranslationCore
|
|
|
|
// ============================================================================
|
|
|
|
|
|
|
|
export default class TranslationCore {
|
|
|
|
constructor(options) {
|
|
|
|
options = options || {};
|
|
|
|
|
|
|
|
this.warn = options.warn;
|
|
|
|
this._locale = options.locale || 'en';
|
2022-04-25 13:47:10 -04:00
|
|
|
this._dayjs_locale = options.dayjsLocale || 'en';
|
2019-05-03 19:30:46 -04:00
|
|
|
this.defaultLocale = options.defaultLocale || this._locale;
|
|
|
|
this.transformation = null;
|
|
|
|
|
2020-08-04 18:26:11 -04:00
|
|
|
this.defaultDateFormat = options.defaultDateFormat;
|
|
|
|
this.defaultTimeFormat = options.defaultTimeFormat;
|
|
|
|
this.defaultDateTimeFormat = options.defaultDateTimeFormat;
|
|
|
|
|
2019-05-03 19:30:46 -04:00
|
|
|
this.phrases = new Map;
|
|
|
|
this.cache = new Map;
|
|
|
|
|
|
|
|
this.numberFormats = new Map;
|
2023-03-06 17:08:47 -05:00
|
|
|
this.currencyFormats = new Map;
|
2019-05-03 19:30:46 -04:00
|
|
|
|
|
|
|
this.formats = Object.assign({}, DEFAULT_FORMATS);
|
|
|
|
if ( options.formats )
|
|
|
|
for(const key of Object.keys(options.formats))
|
|
|
|
this.formats[key] = Object.assign({}, this.formats[key], options.formats[key]);
|
|
|
|
|
|
|
|
this.types = Object.assign({}, DEFAULT_TYPES, options.types || {});
|
2021-06-17 14:27:04 -04:00
|
|
|
this.parser = new Parser(Object.assign({}, DEFAULT_PARSER_OPTIONS, options.parserOptions));
|
2019-05-03 19:30:46 -04:00
|
|
|
|
|
|
|
if ( options.phrases )
|
|
|
|
this.extend(options.phrases);
|
|
|
|
}
|
|
|
|
|
|
|
|
get locale() {
|
|
|
|
return this._locale;
|
|
|
|
}
|
|
|
|
|
|
|
|
set locale(val) {
|
|
|
|
if ( val !== this._locale ) {
|
|
|
|
this._locale = val;
|
|
|
|
this.numberFormats.clear();
|
2023-03-06 17:08:47 -05:00
|
|
|
this.currencyFormats.clear();
|
2019-05-03 19:30:46 -04:00
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
toLocaleString(thing) {
|
|
|
|
if ( thing && thing.toLocaleString )
|
|
|
|
return thing.toLocaleString(this._locale);
|
|
|
|
return thing;
|
|
|
|
}
|
|
|
|
|
2020-08-04 18:26:11 -04:00
|
|
|
formatRelativeTime(value, f) { // eslint-disable-line class-methods-use-this
|
|
|
|
const d = dayjs(value),
|
|
|
|
without_suffix = f === 'plain';
|
2019-05-03 19:30:46 -04:00
|
|
|
|
2019-10-06 20:01:22 -04:00
|
|
|
try {
|
2022-04-25 13:47:10 -04:00
|
|
|
return d.locale(this._dayjs_locale).fromNow(without_suffix);
|
2019-10-06 20:01:22 -04:00
|
|
|
} catch(err) {
|
2020-08-04 18:26:11 -04:00
|
|
|
return d.fromNow(without_suffix);
|
2019-10-06 20:01:22 -04:00
|
|
|
}
|
2019-05-03 19:30:46 -04:00
|
|
|
}
|
|
|
|
|
2023-03-06 17:08:47 -05:00
|
|
|
formatCurrency(value, currency) {
|
|
|
|
let formatter = this.currencyFormats.get(currency);
|
|
|
|
if ( ! formatter ) {
|
|
|
|
formatter = new Intl.NumberFormat(navigator.languages, {
|
|
|
|
style: 'currency',
|
|
|
|
currency
|
|
|
|
});
|
|
|
|
|
|
|
|
this.currencyFormats.set(currency, formatter);
|
|
|
|
}
|
|
|
|
|
|
|
|
return formatter.format(value);
|
|
|
|
}
|
|
|
|
|
2019-05-03 19:30:46 -04:00
|
|
|
formatNumber(value, format) {
|
|
|
|
let formatter = this.numberFormats.get(format);
|
|
|
|
if ( ! formatter ) {
|
2020-09-15 19:17:29 -04:00
|
|
|
if ( this.formats.number[format] )
|
|
|
|
formatter = new Intl.NumberFormat(this.locale, this.formats.number[format]);
|
|
|
|
else if ( typeof format === 'number' )
|
|
|
|
formatter = new Intl.NumberFormat(this.locale, {
|
|
|
|
minimumFractionDigits: format,
|
|
|
|
maximumFractionDigits: format
|
|
|
|
});
|
|
|
|
else
|
|
|
|
formatter = new Intl.NumberFormat(this.locale);
|
|
|
|
|
2019-05-03 19:30:46 -04:00
|
|
|
this.numberFormats.set(format, formatter);
|
|
|
|
}
|
|
|
|
|
|
|
|
return formatter.format(value);
|
|
|
|
}
|
|
|
|
|
2020-07-26 17:50:14 -04:00
|
|
|
formatDuration(value) { // eslint-disable-line class-methods-use-this
|
|
|
|
return duration_to_string(value);
|
|
|
|
}
|
|
|
|
|
2019-05-03 19:30:46 -04:00
|
|
|
formatDate(value, format) {
|
2020-08-04 18:26:11 -04:00
|
|
|
if ( ! format )
|
|
|
|
format = this.defaultDateFormat;
|
|
|
|
|
|
|
|
if ( format && ! this.formats.date[format] ) {
|
|
|
|
const d = dayjs(value);
|
2019-05-03 19:30:46 -04:00
|
|
|
try {
|
2022-04-25 13:47:10 -04:00
|
|
|
return d.locale(this._dayjs_locale).format(format);
|
2019-05-03 19:30:46 -04:00
|
|
|
} catch(err) {
|
2020-08-04 18:26:11 -04:00
|
|
|
return d.format(format);
|
2019-05-03 19:30:46 -04:00
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
if ( !(value instanceof Date) )
|
|
|
|
value = new Date(value);
|
|
|
|
|
|
|
|
return value.toLocaleDateString(this._locale, this.formats.date[format] || {});
|
|
|
|
}
|
|
|
|
|
|
|
|
formatTime(value, format) {
|
2020-08-04 18:26:11 -04:00
|
|
|
if ( ! format )
|
|
|
|
format = this.defaultTimeFormat;
|
|
|
|
|
|
|
|
if ( format && ! this.formats.time[format] ) {
|
|
|
|
const d = dayjs(value);
|
2019-05-03 19:30:46 -04:00
|
|
|
try {
|
2022-04-25 13:47:10 -04:00
|
|
|
return d.locale(this._dayjs_locale).format(format);
|
2019-05-03 19:30:46 -04:00
|
|
|
} catch(err) {
|
2020-08-04 18:26:11 -04:00
|
|
|
return d.format(format);
|
2019-05-03 19:30:46 -04:00
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
if ( !(value instanceof Date) )
|
|
|
|
value = new Date(value);
|
|
|
|
|
|
|
|
return value.toLocaleTimeString(this._locale, this.formats.time[format] || {});
|
|
|
|
}
|
|
|
|
|
|
|
|
formatDateTime(value, format) {
|
2020-08-04 18:26:11 -04:00
|
|
|
if ( ! format )
|
|
|
|
format = this.defaultDateTimeFormat;
|
|
|
|
|
|
|
|
if ( format && ! this.formats.datetime[format] ) {
|
|
|
|
const d = dayjs(value);
|
2019-05-03 19:30:46 -04:00
|
|
|
try {
|
2022-04-25 13:47:10 -04:00
|
|
|
return d.locale(this._dayjs_locale).format(format);
|
2019-05-03 19:30:46 -04:00
|
|
|
} catch(err) {
|
2020-08-04 18:26:11 -04:00
|
|
|
return d.format(format);
|
2019-05-03 19:30:46 -04:00
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
if ( !(value instanceof Date) )
|
|
|
|
value = new Date(value);
|
|
|
|
|
|
|
|
return value.toLocaleString(this._locale, this.formats.datetime[format] || {});
|
|
|
|
}
|
|
|
|
|
|
|
|
extend(phrases, prefix) {
|
|
|
|
const added = [];
|
|
|
|
if ( ! phrases || typeof phrases !== 'object' )
|
|
|
|
return added;
|
|
|
|
|
|
|
|
for(const key of Object.keys(phrases)) {
|
|
|
|
const full_key = prefix ? key === '_' ? prefix : `${prefix}.${key}` : key,
|
|
|
|
phrase = phrases[key];
|
|
|
|
|
|
|
|
if ( typeof phrase === 'object' )
|
|
|
|
added.push(...this.extend(phrase, full_key));
|
|
|
|
else {
|
|
|
|
let parsed;
|
|
|
|
try {
|
|
|
|
parsed = this.parser.parse(phrase);
|
|
|
|
} catch(err) {
|
|
|
|
if ( this.warn )
|
|
|
|
this.warn(`Error parsing i18n phrase for key "${full_key}": ${phrase}`, err);
|
|
|
|
|
|
|
|
continue;
|
|
|
|
}
|
|
|
|
|
|
|
|
this.phrases.set(full_key, phrase);
|
|
|
|
this.cache.set(full_key, parsed);
|
|
|
|
added.push(full_key);
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
return added;
|
|
|
|
}
|
|
|
|
|
|
|
|
unset(phrases, prefix) {
|
|
|
|
if ( typeof phrases === 'string' )
|
|
|
|
phrases = [phrases];
|
|
|
|
|
|
|
|
const keys = Array.isArray(phrases) ? phrases : Object.keys(phrases);
|
|
|
|
for(const key of keys) {
|
|
|
|
const full_key = prefix ? key === '_' ? prefix : `${prefix}.${key}` : key,
|
|
|
|
phrase = phrases[key];
|
|
|
|
|
|
|
|
if ( typeof phrase === 'object' )
|
|
|
|
this.unset(phrases, full_key);
|
|
|
|
else {
|
|
|
|
this.phrases.delete(full_key);
|
|
|
|
this.cache.delete(full_key);
|
|
|
|
}
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
has(key) {
|
|
|
|
return this.phrases.has(key);
|
|
|
|
}
|
|
|
|
|
|
|
|
set(key, phrase) {
|
|
|
|
const parsed = this.parser.parse(phrase);
|
|
|
|
this.phrases.set(key, phrase);
|
|
|
|
this.cache.set(key, parsed);
|
|
|
|
}
|
|
|
|
|
|
|
|
clear() {
|
|
|
|
this.phrases.clear();
|
|
|
|
this.cache.clear();
|
|
|
|
}
|
|
|
|
|
|
|
|
replace(phrases) {
|
|
|
|
this.clear();
|
|
|
|
this.extend(phrases);
|
|
|
|
}
|
|
|
|
|
2019-10-06 20:01:22 -04:00
|
|
|
_preTransform(key, phrase, options, settings = {}) {
|
2019-05-03 19:30:46 -04:00
|
|
|
let ast, locale, data = options == null ? {} : options;
|
|
|
|
if ( typeof data === 'number' )
|
|
|
|
data = {count: data};
|
|
|
|
|
2019-10-06 20:01:22 -04:00
|
|
|
if ( ! settings.noCache && this.phrases.has(key) ) {
|
2019-05-03 19:30:46 -04:00
|
|
|
ast = this.cache.get(key);
|
|
|
|
locale = this.locale;
|
|
|
|
|
2019-10-06 20:01:22 -04:00
|
|
|
} else if ( ! settings.noCache && this.cache.has(key) ) {
|
2019-05-03 19:30:46 -04:00
|
|
|
ast = this.cache.get(key);
|
|
|
|
locale = this.defaultLocale;
|
|
|
|
|
|
|
|
} else {
|
|
|
|
let parsed = null;
|
|
|
|
try {
|
|
|
|
parsed = this.parser.parse(phrase);
|
|
|
|
} catch(err) {
|
2019-10-06 20:01:22 -04:00
|
|
|
if ( settings.throwParse )
|
|
|
|
throw err;
|
|
|
|
|
|
|
|
if ( ! settings.noWarn && this.warn )
|
2019-05-03 19:30:46 -04:00
|
|
|
this.warn(`Error parsing i18n phrase for key "${key}": ${phrase}`, err);
|
|
|
|
|
|
|
|
ast = ['parsing error'];
|
|
|
|
locale = this.defaultLocale;
|
|
|
|
}
|
|
|
|
|
|
|
|
if ( parsed ) {
|
|
|
|
ast = parsed;
|
|
|
|
locale = this.locale;
|
|
|
|
|
2019-10-06 20:01:22 -04:00
|
|
|
if ( ! settings.noCache ) {
|
|
|
|
if ( this.locale === this.defaultLocale )
|
|
|
|
this.phrases.set(key, phrase);
|
2019-05-03 19:30:46 -04:00
|
|
|
|
2019-10-06 20:01:22 -04:00
|
|
|
this.cache.set(key, parsed);
|
|
|
|
}
|
2019-05-03 19:30:46 -04:00
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
if ( this.transformation )
|
|
|
|
ast = this.transformation(key, ast);
|
|
|
|
|
|
|
|
return [ast, data, locale];
|
|
|
|
}
|
|
|
|
|
2019-10-06 20:01:22 -04:00
|
|
|
t(key, phrase, options, settings) {
|
|
|
|
return listToString(this.tList(key, phrase, options, settings));
|
2019-05-03 19:30:46 -04:00
|
|
|
}
|
|
|
|
|
2019-10-06 20:01:22 -04:00
|
|
|
tList(key, phrase, options, settings) {
|
|
|
|
return this._processAST(...this._preTransform(key, phrase, options, settings));
|
2019-05-03 19:30:46 -04:00
|
|
|
}
|
|
|
|
|
2019-06-09 19:48:26 -04:00
|
|
|
formatNode(node, data, locale = null, out = null, ast = null) {
|
|
|
|
if ( ! node || typeof node !== 'object' )
|
|
|
|
return node;
|
|
|
|
|
|
|
|
if ( locale == null )
|
|
|
|
locale = this.locale;
|
|
|
|
|
2019-06-19 20:57:14 -04:00
|
|
|
const val = get(node.v, data);
|
2019-06-09 19:48:26 -04:00
|
|
|
if ( val == null )
|
|
|
|
return null;
|
|
|
|
|
|
|
|
if ( node.t ) {
|
|
|
|
if ( this.types[node.t] )
|
|
|
|
return this.types[node.t].call(this, val, node, locale, out, ast, data);
|
|
|
|
else if ( this.warn )
|
|
|
|
this.warn(`Encountered unknown type "${node.t}" when formatting node.`);
|
|
|
|
}
|
|
|
|
|
|
|
|
return val;
|
|
|
|
}
|
|
|
|
|
2019-05-03 19:30:46 -04:00
|
|
|
_processAST(ast, data, locale) {
|
|
|
|
const out = [];
|
|
|
|
|
|
|
|
for(const node of ast) {
|
2019-06-09 19:48:26 -04:00
|
|
|
const val = this.formatNode(node, data, locale, out, ast);
|
|
|
|
if( val != null )
|
2019-05-03 19:30:46 -04:00
|
|
|
out.push(val);
|
|
|
|
}
|
|
|
|
|
|
|
|
return out;
|
|
|
|
}
|
|
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
function listToString(list) {
|
|
|
|
if ( ! Array.isArray(list) )
|
|
|
|
return String(list);
|
|
|
|
|
|
|
|
return list.map(listToString).join('');
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
// ============================================================================
|
|
|
|
// Plural Handling
|
|
|
|
// ============================================================================
|
|
|
|
|
|
|
|
const CARDINAL_TO_LANG = {
|
|
|
|
arabic: ['ar'],
|
2019-10-05 20:55:32 -04:00
|
|
|
czech: ['cs'],
|
2019-05-03 19:30:46 -04:00
|
|
|
danish: ['da'],
|
|
|
|
german: ['de', 'el', 'en', 'es', 'fi', 'hu', 'it', 'nl', 'no', 'nb', 'tr', 'sv'],
|
|
|
|
hebrew: ['he'],
|
|
|
|
persian: ['fa'],
|
2019-10-09 16:02:25 -04:00
|
|
|
polish: ['pl'],
|
|
|
|
serbian: ['sr'],
|
2019-05-03 19:30:46 -04:00
|
|
|
french: ['fr', 'pt'],
|
2019-10-25 12:07:16 -04:00
|
|
|
russian: ['ru','uk'],
|
|
|
|
slov: ['sl']
|
2019-05-03 19:30:46 -04:00
|
|
|
}
|
|
|
|
|
|
|
|
const CARDINAL_TYPES = {
|
|
|
|
other: () => 5,
|
|
|
|
|
|
|
|
arabic(n) {
|
|
|
|
if ( n === 0 ) return 0;
|
|
|
|
if ( n === 1 ) return 1;
|
|
|
|
if ( n === 2 ) return 2;
|
|
|
|
const n1 = n % 1000;
|
|
|
|
if ( n1 >= 3 && n1 <= 10 ) return 3;
|
|
|
|
return n1 >= 11 ? 4 : 5;
|
|
|
|
},
|
|
|
|
|
2019-10-05 20:55:32 -04:00
|
|
|
czech: (n,i,v) => {
|
|
|
|
if ( v !== 0 ) return 4;
|
|
|
|
if ( i === 1 ) return 1;
|
|
|
|
if ( i >= 2 && i <= 4 ) return 3;
|
|
|
|
return 5;
|
|
|
|
},
|
|
|
|
|
2019-05-03 19:30:46 -04:00
|
|
|
danish: (n,i,v,t) => (n === 1 || (t !== 0 && (i === 0 || i === 1))) ? 1 : 5,
|
|
|
|
french: (n, i) => (i === 0 || i === 1) ? 1 : 5,
|
|
|
|
german: n => n === 1 ? 1 : 5,
|
|
|
|
|
|
|
|
hebrew(n) {
|
|
|
|
if ( n === 1 ) return 1;
|
|
|
|
if ( n === 2 ) return 2;
|
|
|
|
return (n > 10 && n % 10 === 0) ? 4 : 5;
|
|
|
|
},
|
|
|
|
|
|
|
|
persian: (n, i) => (i === 0 || n === 1) ? 1 : 5,
|
|
|
|
|
2019-10-25 12:07:16 -04:00
|
|
|
slov(n, i, v) {
|
|
|
|
if ( v !== 0 ) return 3;
|
|
|
|
const n1 = n % 100;
|
|
|
|
if ( n1 === 1 ) return 1;
|
|
|
|
if ( n1 === 2 ) return 2;
|
|
|
|
if ( n1 === 3 || n1 === 4 ) return 3;
|
|
|
|
return 5;
|
|
|
|
},
|
|
|
|
|
2019-10-09 16:02:25 -04:00
|
|
|
serbian(n, i, v, t) {
|
|
|
|
if ( v !== 0 ) return 5;
|
|
|
|
const i1 = i % 10, i2 = i % 100;
|
|
|
|
const t1 = t % 10, t2 = t % 100;
|
|
|
|
if ( i1 === 1 && i2 !== 11 ) return 1;
|
|
|
|
if ( t1 === 1 && t2 !== 11 ) return 1;
|
|
|
|
if ( i1 >= 2 && i1 <= 4 && !(i2 >= 12 && i2 <= 14) ) return 3;
|
|
|
|
if ( t1 >= 2 && t1 <= 4 && !(t2 >= 12 && t2 <= 14) ) return 3;
|
|
|
|
return 5;
|
|
|
|
},
|
|
|
|
|
|
|
|
polish(n, i, v) {
|
|
|
|
if ( v !== 0 ) return 5;
|
|
|
|
if ( n === 1 ) return 1;
|
|
|
|
const n1 = n % 10, n2 = n % 100;
|
|
|
|
if ( n1 >= 2 && n1 <= 4 && !(n2 >= 12 && n2 <= 14) ) return 3;
|
|
|
|
if ( i !== 1 && (n1 === 0 || n1 === 1) ) return 4;
|
|
|
|
if ( n1 >= 5 && n1 <= 9 ) return 4;
|
|
|
|
if ( n2 >= 12 && n2 <= 14 ) return 4;
|
|
|
|
return 5;
|
|
|
|
},
|
|
|
|
|
2019-05-03 19:30:46 -04:00
|
|
|
russian(n,i,v) {
|
|
|
|
const n1 = n % 10, n2 = n % 100;
|
|
|
|
if ( n1 === 1 && n2 !== 11 ) return 1;
|
|
|
|
if ( v === 0 && (n1 >= 2 && n1 <= 4) && (n2 < 12 || n2 > 14) ) return 3;
|
|
|
|
return ( v === 0 && (n1 === 0 || (n1 >= 5 && n1 <= 9) || (n2 >= 11 || n2 <= 14)) ) ? 4 : 5
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
const ORDINAL_TO_LANG = {
|
|
|
|
english: ['en'],
|
|
|
|
hungarian: ['hu'],
|
|
|
|
italian: ['it'],
|
|
|
|
one: ['fr', 'lo', 'ms'],
|
2019-10-07 03:35:53 -04:00
|
|
|
swedish: ['sv'],
|
|
|
|
ukranian: ['uk']
|
2019-05-03 19:30:46 -04:00
|
|
|
};
|
|
|
|
|
|
|
|
const ORDINAL_TYPES = {
|
|
|
|
other: () => 5,
|
|
|
|
one: n => n === 1 ? 1 : 5,
|
|
|
|
|
|
|
|
english(n) {
|
|
|
|
const n1 = n % 10, n2 = n % 100;
|
|
|
|
if ( n1 === 1 && n2 !== 11 ) return 1;
|
|
|
|
if ( n1 === 2 && n2 !== 12 ) return 2;
|
|
|
|
if ( n1 === 3 && n2 !== 13 ) return 3;
|
|
|
|
return 5;
|
|
|
|
},
|
|
|
|
|
2019-10-07 03:35:53 -04:00
|
|
|
ukranian(n) {
|
|
|
|
const n1 = n % 10, n2 = n % 100;
|
|
|
|
if ( n1 === 3 && n2 !== 13 ) return 3;
|
|
|
|
return 5;
|
|
|
|
},
|
|
|
|
|
2019-05-03 19:30:46 -04:00
|
|
|
hungarian: n => (n === 1 || n === 5) ? 1 : 5,
|
|
|
|
italian: n => (n === 11 || n === 8 || n === 80 || n === 800) ? 4 : 5,
|
|
|
|
|
|
|
|
swedish(n) {
|
|
|
|
const n1 = n % 10, n2 = n % 100;
|
|
|
|
return ((n1 === 1 || n1 === 2) && (n2 !== 11 && n2 !== 12)) ? 1 : 5;
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
const PLURAL_TO_NAME = [
|
|
|
|
'zero', // 0
|
|
|
|
'one', // 1
|
|
|
|
'two', // 2
|
|
|
|
'few', // 3
|
|
|
|
'many', // 4
|
|
|
|
'other' // 5
|
|
|
|
];
|
|
|
|
|
|
|
|
const CARDINAL_LANG_TO_TYPE = {},
|
|
|
|
ORDINAL_LANG_TO_TYPE = {};
|
|
|
|
|
|
|
|
for(const type of Object.keys(CARDINAL_TO_LANG))
|
|
|
|
for(const lang of CARDINAL_TO_LANG[type])
|
|
|
|
CARDINAL_LANG_TO_TYPE[lang] = type;
|
|
|
|
|
|
|
|
for(const type of Object.keys(ORDINAL_TO_LANG))
|
|
|
|
for(const lang of ORDINAL_TO_LANG[type])
|
|
|
|
ORDINAL_LANG_TO_TYPE[lang] = type;
|
|
|
|
|
|
|
|
function executePlural(fn, input) {
|
|
|
|
input = Math.abs(Number(input));
|
|
|
|
const i = Math.floor(input);
|
|
|
|
let v, t;
|
|
|
|
|
|
|
|
if ( i === input ) {
|
|
|
|
v = 0;
|
|
|
|
t = 0;
|
|
|
|
} else {
|
|
|
|
t = `${input}`.split('.')[1]
|
|
|
|
v = t ? t.length : 0;
|
|
|
|
t = t ? Number(t) : 0;
|
|
|
|
}
|
|
|
|
|
|
|
|
return PLURAL_TO_NAME[fn(
|
|
|
|
input,
|
|
|
|
i,
|
|
|
|
v,
|
|
|
|
t
|
|
|
|
)]
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
export function getCardinalName(locale, input) {
|
|
|
|
let type = CARDINAL_LANG_TO_TYPE[locale];
|
|
|
|
if ( ! type ) {
|
|
|
|
const idx = locale.indexOf('-');
|
|
|
|
type = (idx !== -1 && CARDINAL_LANG_TO_TYPE[locale.slice(0, idx)]) || 'other';
|
|
|
|
CARDINAL_LANG_TO_TYPE[locale] = type;
|
|
|
|
}
|
|
|
|
|
|
|
|
return executePlural(CARDINAL_TYPES[type], input);
|
|
|
|
}
|
|
|
|
|
|
|
|
export function getOrdinalName(locale, input) {
|
|
|
|
let type = ORDINAL_LANG_TO_TYPE[locale];
|
|
|
|
if ( ! type ) {
|
|
|
|
const idx = locale.indexOf('-');
|
|
|
|
type = (idx !== -1 && ORDINAL_LANG_TO_TYPE[locale.slice(0, idx)]) || 'other';
|
|
|
|
ORDINAL_LANG_TO_TYPE[locale] = type;
|
|
|
|
}
|
|
|
|
|
|
|
|
return executePlural(ORDINAL_TYPES[type], input);
|
|
|
|
}
|