@v4fire/core
Version:
V4Fire core library
219 lines (185 loc) • 6.85 kB
text/typescript
/*!
* V4Fire Core
* https://github.com/V4Fire/Core
*
* Released under the MIT license
* https://github.com/V4Fire/Core/blob/master/LICENSE
*/
import log from 'core/log';
import extend from 'core/prelude/extend';
import langPacs, { Translation, PluralTranslation } from 'lang';
import { locale } from 'core/prelude/i18n/const';
import type { I18nOpts, PluralizationCount, I18nMeta } from 'core/prelude/i18n/interface';
/** @see [[i18n]] */
extend(globalThis, 'i18n', i18nFactory);
const
logger = log.namespace('i18n');
/**
* Creates a function to internationalize strings in an application based on the given locale and keyset.
* Keyset allows you to share the same keys in different contexts.
* For example, the key "Next" may have a different value in different components of the application, therefore,
* we can use the name of the component as a keyset value.
*
* @param keysetNameOrNames - the name of keyset or array with names of keysets to use.
* If passed as an array, the priority of the cases will be arranged in the order of the elements,
* the first one will have the highest priority.
*
* @param [customLocale] - the locale used to search for translations (the default is taken from
* the application settings)
*/
export function i18nFactory(
keysetNameOrNames: string | string[], customLocale?: Language
): (key: string, params?: I18nParams) => string {
const
resolvedLocale = customLocale ?? locale.value,
keysetNames = Object.isArray(keysetNameOrNames) ? keysetNameOrNames : [keysetNameOrNames];
if (resolvedLocale == null) {
throw new ReferenceError('The locale for internationalization is not defined');
}
const pluralRules: CanUndef<Intl.PluralRules> = getPluralRules(resolvedLocale);
return function i18n(value: string | TemplateStringsArray, params?: I18nParams) {
if (Object.isArray(value) && value.length !== 1) {
throw new SyntaxError('Using i18n with template literals is allowed only without variables');
}
const
key = Object.isString(value) ? value : value[0],
correctKeyset = keysetNames.find((keysetName) => langPacs[resolvedLocale]?.[keysetName]?.[key]),
translateValue = langPacs[resolvedLocale]?.[correctKeyset ?? '']?.[key],
meta: I18nMeta = {language: resolvedLocale, keyset: correctKeyset, key};
if (translateValue != null && translateValue !== '') {
return resolveTemplate(translateValue, params, {pluralRules, meta});
}
logger.error(
'Translation for the given key is not found',
`Key: ${key}, KeysetNames: ${keysetNames.join(', ')}, LocaleName: ${resolvedLocale}, available locales: ${Object.keys(langPacs).join(', ')}`
);
return resolveTemplate(key, params, {pluralRules, meta});
};
}
/**
* Returns the form for plural sentences and resolves variables from the passed template
*
* @param value - a string for the default case, or an array of strings for the plural case
* @param params - a dictionary with parameters for internationalization
* @params [opts] - additional options for current translation
*
* @example
* ```typescript
* const example = resolveTemplate('My name is {name}, I live in {city}', {name: 'John', city: 'Denver'});
*
* console.log(example); // 'My name is John, I live in Denver'
*
* const examplePluralize = resolveTemplate({
* one: {count} product,
* few: {count} products,
* many: {count} products,
* zero: {count} products,
* }, {count: 5});
*
* console.log(examplePluralize); // '5 products'
* ```
*/
export function resolveTemplate(value: Translation, params?: I18nParams, opts: I18nOpts = {}): string {
const
template = Object.isPlainObject(value) ? pluralizeText(value, params?.count, opts) : value;
return template.replace(/{([^}]+)}/g, (_, key) => {
if (params?.[key] == null) {
logger.error('Undeclared variable', `Name: "${key}", Template: "${template}"`);
return key;
}
return params[key];
});
}
/**
* Returns the correct plural form to translate based on the given count
*
* @param pluralTranslation - list of translation variants
* @param count - the value on the basis of which the form of pluralization will be selected
* @params [opts] - additional options for current translation
*
* @example
* ```typescript
* const result = pluralizeText({
* one: {count} product,
* few: {count} products,
* many: {count} products,
* zero: {count} products,
* other: {count} products,
* }, 5, {pluralRules: new Intl.PluralRulse('en')});
*
* console.log(result); // '{count} products'
* ```
*/
export function pluralizeText(
pluralTranslation: PluralTranslation,
count: CanUndef<PluralizationCount>,
opts: I18nOpts = {}
): string {
const {pluralRules, meta} = opts;
let normalizedCount;
if (Object.isNumber(count)) {
normalizedCount = count;
} else if (Object.isString(count)) {
const translation = pluralTranslation[count];
if (translation != null) {
return translation;
}
}
if (normalizedCount == null) {
logger.error(
'Invalid value of the `count` parameter for string pluralization',
`Count: ${count}, Key: ${meta?.key}, Language: ${meta?.language}, Keyset: ${meta?.keyset}`
);
normalizedCount = 1;
}
const
pluralFormName = getPluralFormName(normalizedCount, pluralRules),
translation = pluralTranslation[pluralFormName];
if (translation == null) {
logger.error(
`Plural form ${pluralFormName} doesn't exist.`,
`Key: ${meta?.key}, Language: ${meta?.language}, Keyset: ${meta?.keyset}`
);
return pluralTranslation.one;
}
return translation;
}
/**
* Returns the plural form name for a given number `n` based on the specified pluralization rules.
* Otherwise will be used default set of rules.
*
* If a `rules` object implementing `Intl.PluralRules` is provided, it will use that to determine the plural form.
* Otherwise, it will fall back to a custom rule set:
* - Returns 'zero' for `n === 0`.
* - Returns 'one' for `n === 1`.
* - Returns 'few' for `n > 1 && n < 5`.
* - Returns 'many' for all other values of `n`.
*
* @param n - The number to evaluate for pluralization.
* @param rules - Plural rules object. If undefined, a default rule set is used.
*/
export function getPluralFormName(n: number, rules?: CanUndef<Intl.PluralRules>): keyof Required<PluralTranslation> {
if (rules != null) {
return <keyof PluralTranslation>rules.select(n);
}
switch (n) {
case 0:
return 'zero';
case 1:
return 'one';
default:
if (n > 1 && n < 5) {
return 'few';
}
return 'many';
}
}
/**
* Returns an instance of `Intl.PluralRules` for a given locale, if supported.
* @param locale - The locale for which to generate plural rules.
*/
export function getPluralRules(locale: Language): CanUndef<Intl.PluralRules> {
if ('PluralRules' in globalThis['Intl']) {
return new globalThis['Intl'].PluralRules(locale);
}
}