Skip to content

Commit

Permalink
feat: change translations format (#443)
Browse files Browse the repository at this point in the history
* feat: change translations format

* chore: changelog

* add tests for plural forms function

* add cyrillyc tests without Intl rules

---------

Co-authored-by: Maksim Sinelnikov <[email protected]>
  • Loading branch information
sinelnikov-web and Maksim Sinelnikov authored Sep 25, 2024
1 parent 5e72e03 commit fa964eb
Show file tree
Hide file tree
Showing 10 changed files with 242 additions and 113 deletions.
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

0 comments on commit fa964eb

Please sign in to comment.