From 4917db0ccfd375add21bab752bba7944df6c28df Mon Sep 17 00:00:00 2001 From: Nils Date: Tue, 29 Oct 2024 15:32:55 +0100 Subject: [PATCH] add pluralization to useI18n hook --- .../src/composables/useI18n.spec.ts | 60 +++++++++++++++++++ .../src/composables/useI18n.ts | 35 +++++++++-- 2 files changed, 90 insertions(+), 5 deletions(-) diff --git a/packages/component-library/src/composables/useI18n.spec.ts b/packages/component-library/src/composables/useI18n.spec.ts index e305493f8..cf97ea431 100644 --- a/packages/component-library/src/composables/useI18n.spec.ts +++ b/packages/component-library/src/composables/useI18n.spec.ts @@ -61,4 +61,64 @@ describe("useI18n", () => { expect(screen.getByText("Hello, {name}!")).toBeInTheDocument(); }); + + it("translates the singular version", () => { + render({ + setup() { + const { t } = useI18n({ + messages: { en: { apple: "apple | apples" } }, + }); + + return { t }; + }, + template: "{{ t('apple', { n: 1 }) }}", + }); + + expect(screen.getByText("apple")).toBeInTheDocument(); + }); + + it("translates the pluralized version", () => { + render({ + setup() { + const { t } = useI18n({ + messages: { en: { apple: "apple | apples" } }, + }); + + return { t }; + }, + template: "{{ t('apple', { n: 2 }) }}", + }); + + expect(screen.getByText("apples")).toBeInTheDocument; + }); + + it("translates the 'none' version", () => { + render({ + setup() { + const { t } = useI18n({ + messages: { en: { apple: "no apple | apple | apples" } }, + }); + + return { t }; + }, + template: "{{ t('apple', { n: 0 }) }}", + }); + + expect(screen.getByText("no apple")).toBeInTheDocument(); + }); + + it("translates the pluralized version with custom values", () => { + render({ + setup() { + const { t } = useI18n({ + messages: { en: { apple: "no apple | apple | {n} apples" } }, + }); + + return { t }; + }, + template: "{{ t('apple', { n: 3 }) }}", + }); + + expect(screen.getByText("3 apples")).toBeInTheDocument(); + }); }); diff --git a/packages/component-library/src/composables/useI18n.ts b/packages/component-library/src/composables/useI18n.ts index afc4426af..d94b57cfd 100644 --- a/packages/component-library/src/composables/useI18n.ts +++ b/packages/component-library/src/composables/useI18n.ts @@ -30,6 +30,10 @@ const defaultI18nState = { const i18nInjectionKey = Symbol("mt-i18n"); +function limit(value: number, max: number) { + return Math.min(value, max); +} + export function provideI18n(locale: string = "en") { const state = { ...defaultI18nState, @@ -45,16 +49,37 @@ export function useI18n({ messages }: Options) { const i18n = inject(i18nInjectionKey, defaultI18nState); function translate(path: string, customValues: Record = {}): string { + function resolveCustomKeys(translation: string) { + return translation.replace(CUSTOM_VALUE_REGEX, (match: string, key: string) => { + return Object.prototype.hasOwnProperty.call(customValues, key) + ? customValues[key].toString() + : match; + }); + } + const translation = get(messages, `${i18n.locale}.${path}`) || get(messages, `${i18n.defaultLocale}.${path}`); if (!translation) return path; - return translation.replace(CUSTOM_VALUE_REGEX, (match: string, key: string) => { - return Object.prototype.hasOwnProperty.call(customValues, key) - ? customValues[key].toString() - : match; - }); + const canBePluralized = /\|/.test(translation); + if (canBePluralized) { + if (typeof customValues.n !== "number") + throw new Error('The "n" key is required for pluralization'); + + const versions = translation.split("|"); + + // "no apple | apple | apples"; The first version is the zero form + const includesZeroForm = versions.length === 3; + + const index = limit(includesZeroForm ? customValues.n : customValues.n - 1, 2); + const resolvedTranslation = versions.at(index); + if (!resolvedTranslation) return path; + + return resolveCustomKeys(resolvedTranslation); + } + + return resolveCustomKeys(translation); } return { t: translate };