Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

feat: change translations format #443

Merged
merged 17 commits into from
Sep 25, 2024
8 changes: 8 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,14 @@ Changelog

_Note: Gaps between patch versions are faulty, broken or test releases._

## v3.101.0 (2024-09-25)

#### :boom: breaking change
* `core/prelude/i18n/helpers`
* changed `i18n` translations format.
* added `intl` support for pluralization.
* now `i18n` prefer to use `intl` api for pluralization if it's possible, otherwise fallback to old plural form logic.

## v3.100.1 (2024-09-10)

#### :bug: Bug Fix
Expand Down
6 changes: 4 additions & 2 deletions index.d.ts
Original file line number Diff line number Diff line change
Expand Up @@ -173,14 +173,16 @@ declare function i18n(
/**
* Parameters for the internationalization function
*/
type I18nParams = {count?: number | StringPluralizationForms} & {
type I18nParams = {
count?: number | StringPluralizationForms;
} & {
[key: string]: string | number;
};

/**
* String pluralization constants that can be used instead of numbers
*/
type StringPluralizationForms = 'one' | 'some' | 'many' | 'none';
type StringPluralizationForms = 'one' | 'two' | 'few' | 'many' | 'other' | 'zero';

declare function setImmediate(fn: AnyFunction): number;
declare function clearImmediate(id: number): void;
Expand Down
2 changes: 1 addition & 1 deletion package.json
Original file line number Diff line number Diff line change
Expand Up @@ -5,7 +5,7 @@
"main": "lib/core/index.js",
"typings": "index.d.ts",
"license": "MIT",
"version": "3.100.1",
"version": "3.101.0",
"author": "kobezzza <[email protected]> (https://github.com/kobezzza)",
"repository": {
"type": "git",
Expand Down
8 changes: 8 additions & 0 deletions src/core/prelude/CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,14 @@ Changelog
> - :house: [Internal]
> - :nail_care: [Polish]

## v3.101.0 (2024-09-25)

#### :boom: breaking change

* changed `i18n` translations format.
* added `intl` support for pluralization.
* now `i18n` prefer to use `intl` api for pluralization if it's possible, otherwise fallback to old plural form logic.

## v3.99.0 (2024-04-25)

#### :rocket: New Feature
Expand Down
10 changes: 0 additions & 10 deletions src/core/prelude/i18n/const.ts
Original file line number Diff line number Diff line change
Expand Up @@ -37,13 +37,3 @@ export const region: RegionStore = {
value: undefined,
isDefault: false
};

/**
* A dictionary to map literal pluralization forms to numbers
*/
export const pluralizeMap = Object.createDict({
none: 0,
one: 1,
some: 2,
many: 5
});
105 changes: 78 additions & 27 deletions src/core/prelude/i18n/helpers.ts
Original file line number Diff line number Diff line change
Expand Up @@ -11,8 +11,8 @@ import extend from 'core/prelude/extend';

import langPacs, { Translation, PluralTranslation } from 'lang';

import { locale, pluralizeMap } from 'core/prelude/i18n/const';
import type { PluralizationCount } from 'core/prelude/i18n/interface';
import { locale } from 'core/prelude/i18n/const';
import type { I18nOpts, PluralizationCount } from 'core/prelude/i18n/interface';

/** @see [[i18n]] */
extend(globalThis, 'i18n', i18nFactory);
Expand Down Expand Up @@ -44,6 +44,8 @@ export function i18nFactory(
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');
Expand All @@ -55,15 +57,15 @@ export function i18nFactory(
translateValue = langPacs[resolvedLocale]?.[correctKeyset ?? '']?.[key];

if (translateValue != null && translateValue !== '') {
return resolveTemplate(translateValue, params);
return resolveTemplate(translateValue, params, {pluralRules});
}

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);
return resolveTemplate(key, params, {pluralRules});
};
}

Expand All @@ -79,19 +81,19 @@ export function i18nFactory(
*
* console.log(example); // 'My name is John, I live in Denver'
*
* const examplePluralize = resolveTemplate([
* {count} product, // One
* {count} products, // Some
* {count} products, // Many
* {count} products, // None
* ], {count: 5});
* 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): string {
export function resolveTemplate(value: Translation, params?: I18nParams, opts: I18nOpts = {}): string {
const
template = Object.isArray(value) ? pluralizeText(value, params?.count) : value;
template = Object.isPlainObject(value) ? pluralizeText(value, params?.count, opts.pluralRules) : value;

return template.replace(/{([^}]+)}/g, (_, key) => {
if (params?.[key] == null) {
Expand All @@ -108,28 +110,36 @@ export function resolveTemplate(value: Translation, params?: I18nParams): string
*
* @param pluralTranslation - list of translation variants
* @param count - the value on the basis of which the form of pluralization will be selected
* @param rules - Intl plural rules for selected locale
*
* @example
* ```typescript
* const result = pluralizeText([
* {count} product, // One
* {count} products, // Some
* {count} products, // Many
* {count} products, // None
* ], 5);
* const result = pluralizeText({
* one: {count} product,
* few: {count} products,
* many: {count} products,
* zero: {count} products,
* other: {count} products,
* }, 5, new Intl.PluralRulse('en'));
*
* console.log(result); // '{count} products'
* ```
*/
export function pluralizeText(pluralTranslation: PluralTranslation, count: CanUndef<PluralizationCount>): string {
export function pluralizeText(
pluralTranslation: PluralTranslation,
count: CanUndef<PluralizationCount>,
rules: CanUndef<Intl.PluralRules>
): string {
let normalizedCount;

if (Object.isNumber(count)) {
normalizedCount = count;

} else if (Object.isString(count)) {
if (count in pluralizeMap) {
normalizedCount = pluralizeMap[count];
const translation = pluralTranslation[count];

if (translation != null) {
return translation;
}
}

Expand All @@ -138,18 +148,59 @@ export function pluralizeText(pluralTranslation: PluralTranslation, count: CanUn
normalizedCount = 1;
}

switch (normalizedCount) {
const
pluralFormName = getPluralFormName(normalizedCount, rules),
translation = pluralTranslation[pluralFormName];

if (translation == null) {
logger.error(`Plural form ${pluralFormName} doesn't exist.`, `String: ${pluralTranslation[0]}`);
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 pluralTranslation[3];
return 'zero';

case 1:
return pluralTranslation[0];
return 'one';

default:
if (normalizedCount > 1 && normalizedCount < 5) {
return pluralTranslation[1];
if (n > 1 && n < 5) {
return 'few';
}

return pluralTranslation[2];
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);
}
}
6 changes: 5 additions & 1 deletion src/core/prelude/i18n/interface.ts
Original file line number Diff line number Diff line change
Expand Up @@ -41,4 +41,8 @@ export interface LocaleKVStorage {
set?: SyncStorage['set'];
}

export type PluralizationCount = StringPluralizationForms | string | number;
export type PluralizationCount = StringPluralizationForms | number;

export interface I18nOpts {
pluralRules?: Intl.PluralRules;
}
71 changes: 0 additions & 71 deletions src/core/prelude/i18n/spec.js

This file was deleted.

Loading
Loading