diff --git a/CHANGELOG.md b/CHANGELOG.md index 47374b9f1..cdf2372e2 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -11,6 +11,14 @@ Changelog _Note: Gaps between patch versions are faulty, broken or test releases._ +## v4.0.0-alpha.49 (2024-10-31) + +#### :bug: Bug Fix + +* `core/prelude/i18n/helpers` + * Fix logging bug in `pluralizeText`. + * Add logging info in i18n helpers. + ## v4.0.0-alpha.48 (2024-10-30) #### :bug: Bug Fix diff --git a/package.json b/package.json index 1d7a0dee0..7b7c62666 100644 --- a/package.json +++ b/package.json @@ -5,7 +5,7 @@ "main": "lib/core/index.js", "typings": "index.d.ts", "license": "MIT", - "version": "4.0.0-alpha.48", + "version": "4.0.0-alpha.49", "author": "kobezzza (https://github.com/kobezzza)", "repository": { "type": "git", diff --git a/src/core/prelude/CHANGELOG.md b/src/core/prelude/CHANGELOG.md index 096c16542..e5cceb1ed 100644 --- a/src/core/prelude/CHANGELOG.md +++ b/src/core/prelude/CHANGELOG.md @@ -9,6 +9,13 @@ Changelog > - :house: [Internal] > - :nail_care: [Polish] +## v4.0.0-alpha.49 (2024-10-31) + +#### :bug: Bug Fix + +* Fix logging bug in `pluralizeText`. +* Add logging info in i18n helpers. + ## v4.0.0-alpha.47.speedup (2024-10-01) #### :rocket: New Feature diff --git a/src/core/prelude/i18n/README.md b/src/core/prelude/i18n/README.md index 25443469c..ff58239f8 100644 --- a/src/core/prelude/i18n/README.md +++ b/src/core/prelude/i18n/README.md @@ -89,30 +89,32 @@ i18n('my-component')('My name is {name}', {name: 'John'}); ## Pluralization of translations Some keys may have multiple translations depending on some numeric value. For example, "1 apple" or "5 apples". -To specify such translations, a special macro `{count}` is used, and translations are specified as a tuple `[one, some, many, none]`. +To specify such translations, a special macro `{count}` is used, and translations are specified as a dictionary `{zero, one, two, few, many, other}`. ```js export default { ru: { "my-component": { "time": "время", - "{count} product": [ - "{count} продукт", - "{count} продукта", - "{count} продуктов", - "{count} продуктов" - ] + "{count} product": { + "one": "{count} product", + "few": "{count} products", + "many": "{count} products", + "zero": "{count} products", + "other": "{count} products", + } } }, en: { "my-component": { - "{count} product": [ - "{count} product", - "{count} products", - "{count} products", - "{count} products" - ] + "{count} product": { + "one": "{count} product", + "few": "{count} products", + "many": "{count} products", + "zero": "{count} products", + "other": "{count} products", + } } } }; diff --git a/src/core/prelude/i18n/helpers.ts b/src/core/prelude/i18n/helpers.ts index 2d125642d..f7edbfc52 100644 --- a/src/core/prelude/i18n/helpers.ts +++ b/src/core/prelude/i18n/helpers.ts @@ -11,7 +11,7 @@ import log from 'core/log'; import langPacs, { Translation, PluralTranslation } from 'lang'; import { locale } from 'core/prelude/i18n/const'; -import type { I18nOpts, PluralizationCount } from 'core/prelude/i18n/interface'; +import type { I18nOpts, PluralizationCount, I18nMeta } from 'core/prelude/i18n/interface'; const logger = log.namespace('i18n'); @@ -50,10 +50,11 @@ export function i18nFactory( const key = Object.isString(value) ? value : value[0], correctKeyset = keysetNames.find((keysetName) => langPacs[resolvedLocale]?.[keysetName]?.[key]), - translateValue = langPacs[resolvedLocale]?.[correctKeyset ?? '']?.[key]; + translateValue = langPacs[resolvedLocale]?.[correctKeyset ?? '']?.[key], + meta: I18nMeta = {language: resolvedLocale, keyset: correctKeyset, key}; if (translateValue != null && translateValue !== '') { - return resolveTemplate(translateValue, params, {pluralRules}); + return resolveTemplate(translateValue, params, {pluralRules, meta}); } logger.error( @@ -61,7 +62,7 @@ export function i18nFactory( `Key: ${key}, KeysetNames: ${keysetNames.join(', ')}, LocaleName: ${resolvedLocale}, available locales: ${Object.keys(langPacs).join(', ')}` ); - return resolveTemplate(key, params, {pluralRules}); + return resolveTemplate(key, params, {pluralRules, meta}); }; } @@ -70,6 +71,7 @@ export function i18nFactory( * * @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 @@ -77,19 +79,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, opts: I18nOpts = {}): string { const - template = Object.isPlainObject(value) ? pluralizeText(value, params?.count, opts.pluralRules) : value; + template = Object.isPlainObject(value) ? pluralizeText(value, params?.count, opts) : value; return template.replace(/{([^}]+)}/g, (_, key) => { if (params?.[key] == null) { @@ -106,15 +108,17 @@ export function resolveTemplate(value: Translation, params?: I18nParams, opts: I * * @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([ - * {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, {pluralRules: new Intl.PluralRulse('en')}); * * console.log(result); // '{count} products' * ``` @@ -122,8 +126,10 @@ export function resolveTemplate(value: Translation, params?: I18nParams, opts: I export function pluralizeText( pluralTranslation: PluralTranslation, count: CanUndef, - rules: CanUndef + opts: I18nOpts = {} ): string { + const {pluralRules, meta} = opts; + let normalizedCount; if (Object.isNumber(count)) { @@ -138,16 +144,24 @@ export function pluralizeText( } if (normalizedCount == null) { - logger.error('Invalid value of the `count` parameter for string pluralization', `String: ${pluralTranslation[0]}`); + 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, rules), + pluralFormName = getPluralFormName(normalizedCount, pluralRules), translation = pluralTranslation[pluralFormName]; if (translation == null) { - logger.error(`Plural form ${pluralFormName} doesn't exist.`, `String: ${pluralTranslation[0]}`); + logger.error( + `Plural form ${pluralFormName} doesn't exist.`, + `Key: ${meta?.key}, Language: ${meta?.language}, Keyset: ${meta?.keyset}` + ); + return pluralTranslation.one; } diff --git a/src/core/prelude/i18n/interface.ts b/src/core/prelude/i18n/interface.ts index 9981d87ae..fe97cbd39 100644 --- a/src/core/prelude/i18n/interface.ts +++ b/src/core/prelude/i18n/interface.ts @@ -43,6 +43,14 @@ export interface LocaleKVStorage { export type PluralizationCount = StringPluralizationForms | number; +export interface I18nMeta { + language: string; + key: string; + keyset?: string; +} + export interface I18nOpts { pluralRules?: Intl.PluralRules; + meta?: I18nMeta; } + diff --git a/src/core/prelude/i18n/spec.ts b/src/core/prelude/i18n/spec.ts index 69cd3b5cb..e28c1bb8e 100644 --- a/src/core/prelude/i18n/spec.ts +++ b/src/core/prelude/i18n/spec.ts @@ -42,7 +42,7 @@ describe('core/prelude/i18n', () => { describe('text pluralization', () => { it('using pluralization constants to choose the right form', () => { formNames.forEach((form) => { - expect(pluralizeText(forms, form, rules)).toBe(forms[form]); + expect(pluralizeText(forms, form, {pluralRules: rules})).toBe(forms[form]); }); }); @@ -53,7 +53,28 @@ describe('core/prelude/i18n', () => { }; [forms.one, forms.other, forms.other, forms.other].forEach((form, index) => { - expect(pluralizeText(input.forms, input.count[index], rules)).toBe(form); + expect(pluralizeText(input.forms, input.count[index], {pluralRules: rules})).toBe(form); + }); + }); + + it('returns "one" form when required plural form is missing', () => { + const input = { + forms, + count: [1, 2, 100, 0] + }; + + [forms.one, forms.one, forms.one, forms.one].forEach((form, index) => { + expect(pluralizeText({one: input.forms.one}, input.count[index], {pluralRules: rules})).toBe(form); + }); + }); + + it('returns "one" form when count is invalid', () => { + const input = { + forms + }; + + [forms.one, forms.one, forms.one, forms.one].forEach((form) => { + expect(pluralizeText({one: input.forms.one}, undefined, {pluralRules: rules})).toBe(form); }); }); });