From 444f51ae8a194e6aad0dd85e818af663ec625d04 Mon Sep 17 00:00:00 2001 From: Kevin Kasper Date: Tue, 28 Nov 2023 11:55:40 +0100 Subject: [PATCH] Transform nested translations into flat translations during assignment --- src/plugins/piral-translate/package.json | 3 +- .../piral-translate/src/create.test.ts | 16 +++++++ src/plugins/piral-translate/src/create.ts | 7 ++-- .../src/flatten-translations.test.ts | 42 +++++++++++++++++++ .../src/flatten-translations.ts | 15 +++++++ .../piral-translate/src/localize.test.ts | 2 +- src/plugins/piral-translate/src/localize.ts | 14 ++++--- src/plugins/piral-translate/src/types.ts | 16 ++++++- yarn.lock | 17 +++----- 9 files changed, 108 insertions(+), 24 deletions(-) create mode 100644 src/plugins/piral-translate/src/flatten-translations.test.ts create mode 100644 src/plugins/piral-translate/src/flatten-translations.ts diff --git a/src/plugins/piral-translate/package.json b/src/plugins/piral-translate/package.json index c2b7b7238..ffce84c29 100644 --- a/src/plugins/piral-translate/package.json +++ b/src/plugins/piral-translate/package.json @@ -64,11 +64,10 @@ }, "dependencies": { "deepmerge": "^4.2.2", - "get-value": "^3.0.1" + "flat": "^6.0.1" }, "devDependencies": { "@types/deepmerge": "^2.2.0", - "@types/get-value": "^3.0.5", "@types/react": "^18.0.0", "piral-core": "^1.3.3", "react": "^18.0.0" diff --git a/src/plugins/piral-translate/src/create.test.ts b/src/plugins/piral-translate/src/create.test.ts index 7acd9a8c5..2ff755f71 100644 --- a/src/plugins/piral-translate/src/create.test.ts +++ b/src/plugins/piral-translate/src/create.test.ts @@ -73,6 +73,22 @@ describe('Create Localize API', () => { expect(result).toEqual('bár'); }); + it('createApi can translate from local-global translations using the current language and passed nested translations', () => { + const config = { + language: 'en' + }; + const api = (createLocaleApi(setupLocalizer(config))(context) as any)(); + api.setTranslations({ + en: { + header: { + title: 'Hello world' + } + } + }); + const result = api.translate('header.title'); + expect(result).toBe('Hello world'); + }); + it('createApi falls back to standard string if language is not found', () => { const config = { language: 'en', diff --git a/src/plugins/piral-translate/src/create.ts b/src/plugins/piral-translate/src/create.ts index 2283cb45f..f96b543d1 100644 --- a/src/plugins/piral-translate/src/create.ts +++ b/src/plugins/piral-translate/src/create.ts @@ -4,7 +4,8 @@ import type { PiralPlugin } from 'piral-core'; import { createActions } from './actions'; import { Localizer } from './localize'; import { DefaultPicker } from './default'; -import { PiletLocaleApi, LocalizationMessages, Localizable, PiralSelectLanguageEvent } from './types'; +import { PiletLocaleApi, LocalizationMessages, Localizable, PiralSelectLanguageEvent, NestedLocalizationMessages } from './types'; +import { flattenTranslations } from './flatten-translations'; export interface TranslationFallback { (key: string, language: string): string; @@ -72,7 +73,7 @@ export function createLocaleApi(localizer: Localizable = setupLocalizer()): Pira let localTranslations: LocalizationMessages = {}; return { - addTranslations(messages: LocalizationMessages[], isOverriding: boolean = true) { + addTranslations(messages: (LocalizationMessages | NestedLocalizationMessages)[], isOverriding: boolean = true) { const messagesToMerge: LocalizationMessages[] = messages; if (isOverriding) { @@ -100,7 +101,7 @@ export function createLocaleApi(localizer: Localizable = setupLocalizer()): Pira return selected; }, setTranslations(messages) { - localTranslations = messages; + localTranslations = flattenTranslations(messages); }, getTranslations() { return localTranslations; diff --git a/src/plugins/piral-translate/src/flatten-translations.test.ts b/src/plugins/piral-translate/src/flatten-translations.test.ts new file mode 100644 index 000000000..610648051 --- /dev/null +++ b/src/plugins/piral-translate/src/flatten-translations.test.ts @@ -0,0 +1,42 @@ +/** + * @vitest-environment jsdom + */ +import { describe, it, expect } from 'vitest'; +import { flattenTranslations } from './flatten-translations'; + +describe('Flatten translations', () => { + it('flattenTranslations can handle flat keys', () => { + const messages = { + en: { + key: 'value' + }, + fr: { + key: 'value (fr)' + } + }; + const flatMessages = flattenTranslations(messages); + expect(flatMessages.fr.key).toEqual('value (fr)'); + }); + + it('flattenTranslations can handle flat dot keys', () => { + const messages = { + en: { + 'header.title': 'Hello world' + } + }; + const flatMessages = flattenTranslations(messages); + expect(flatMessages.en['header.title']).toBe('Hello world'); + }); + + it('flattenTranslations can handle nested keys', () => { + const messages = { + en: { + header: { + title: 'Hello world' + } + } + }; + const flatMessages = flattenTranslations(messages); + expect(flatMessages.en['header.title']).toBe('Hello world'); + }); +}); diff --git a/src/plugins/piral-translate/src/flatten-translations.ts b/src/plugins/piral-translate/src/flatten-translations.ts new file mode 100644 index 000000000..351aee2f8 --- /dev/null +++ b/src/plugins/piral-translate/src/flatten-translations.ts @@ -0,0 +1,15 @@ +import { flatten } from 'flat'; +import { LocalizationMessages, NestedLocalizationMessages } from './types'; + +export function flattenTranslations(messages: LocalizationMessages | NestedLocalizationMessages): LocalizationMessages { + return Object.fromEntries( + Object + .entries(messages) + .map(([ language, translations ]) => { + return [ + language, + flatten(translations) + ] + }) + ); +} diff --git a/src/plugins/piral-translate/src/localize.test.ts b/src/plugins/piral-translate/src/localize.test.ts index 91ea96a1c..510f8025d 100644 --- a/src/plugins/piral-translate/src/localize.test.ts +++ b/src/plugins/piral-translate/src/localize.test.ts @@ -80,7 +80,7 @@ describe('Localize Module', () => { expect(result).toBe('Hi , welcome back'); }); - it('localizeGlobal uses nested translations if the key contains dots', () => { + it('localizeGlobal translates from global translations using passed nested translations', () => { const localizer = new Localizer(messages, 'en'); const result = localizer.localizeGlobal('header.title'); diff --git a/src/plugins/piral-translate/src/localize.ts b/src/plugins/piral-translate/src/localize.ts index c8bbdc169..fbed98e3a 100644 --- a/src/plugins/piral-translate/src/localize.ts +++ b/src/plugins/piral-translate/src/localize.ts @@ -1,5 +1,5 @@ -import getValue from 'get-value'; -import { LocalizationMessages, Localizable } from './types'; +import { LocalizationMessages, Localizable, NestedLocalizationMessages } from './types'; +import { flattenTranslations } from './flatten-translations'; function defaultFallback(key: string, language: string): string { if (process.env.NODE_ENV === 'production') { @@ -21,15 +21,19 @@ function formatMessage(message: string, variables: T): string } export class Localizer implements Localizable { + public messages: LocalizationMessages; + /** * Creates a new instance of a localizer. */ constructor( - public messages: LocalizationMessages, + messages: LocalizationMessages | NestedLocalizationMessages, public language: string, public languages: Array, private fallback = defaultFallback, - ) {} + ) { + this.messages = flattenTranslations(messages); + } /** * Localizes the given key via the global translations. @@ -70,7 +74,7 @@ export class Localizer implements Localizable { private translateMessage(messages: LocalizationMessages, key: string, variables?: T) { const language = this.language; const translations = language && messages[language]; - const translation = translations && getValue(translations, key); + const translation = translations && translations[key]; return translation && (variables ? formatMessage(translation, variables) : translation); } } diff --git a/src/plugins/piral-translate/src/types.ts b/src/plugins/piral-translate/src/types.ts index 6c2d5942e..755075fea 100644 --- a/src/plugins/piral-translate/src/types.ts +++ b/src/plugins/piral-translate/src/types.ts @@ -100,12 +100,19 @@ export interface Translations { [tag: string]: string; } +export interface NestedTranslations { + /** + * The available wordings (tag to translation or nested translations). + */ + [tag: string]: string | NestedTranslations; +} + export interface LanguageLoader { (language: string, current: LanguageData): Promise; } export interface Localizable { - messages: LocalizationMessages; + messages: LocalizationMessages | NestedLocalizationMessages; language: string; languages: Array; localizeGlobal(key: string, variables?: T): string; @@ -119,6 +126,13 @@ export interface LocalizationMessages { [lang: string]: Translations; } +export interface NestedLocalizationMessages { + /** + * The available languages (lang to wordings or nested wordings). + */ + [lang: string]: NestedTranslations; +} + export interface PiletLocaleApi { /** * Adds a list of translations to the existing translations. diff --git a/yarn.lock b/yarn.lock index 95f785688..10c60d450 100644 --- a/yarn.lock +++ b/yarn.lock @@ -2443,11 +2443,6 @@ dependencies: "@types/node" "*" -"@types/get-value@^3.0.5": - version "3.0.5" - resolved "https://registry.yarnpkg.com/@types/get-value/-/get-value-3.0.5.tgz#4ea0e0b0a31c256636b3e7e0026c2ad38baea6f6" - integrity sha512-+o8nw0TId5cDwtdVrhlc8rvzaxbCU+JksFeu8ZunY9vUaODxngXiNceTFj2gkSwGWNRpe3PtaSWt1y0VB71PvA== - "@types/glob@*": version "8.1.0" resolved "https://registry.yarnpkg.com/@types/glob/-/glob-8.1.0.tgz#b63e70155391b0584dce44e7ea25190bbc38f2fc" @@ -6081,6 +6076,11 @@ flat@^5.0.2: resolved "https://registry.yarnpkg.com/flat/-/flat-5.0.2.tgz#8ca6fe332069ffa9d324c327198c598259ceb241" integrity sha512-b6suED+5/3rTpUBdG1gupIl8MPFCAMA0QXwmljLhvCUKcUvdE4gWky9zpuGCcXHOsz4J9wPGNWq6OKpmIzz3hQ== +flat@^6.0.1: + version "6.0.1" + resolved "https://registry.yarnpkg.com/flat/-/flat-6.0.1.tgz#09070cf918293b401577f20843edeadf4d3e8755" + integrity sha512-/3FfIa8mbrg3xE7+wAhWeV+bd7L2Mof+xtZb5dRDKZ+wDvYJK4WDYeIOuOhre5Yv5aQObZrlbRmk3RTSiuQBtw== + flexsearch@^0.6.32: version "0.6.32" resolved "https://registry.yarnpkg.com/flexsearch/-/flexsearch-0.6.32.tgz#1e20684d317af65baa445cdd9864a5f5b320f510" @@ -6355,13 +6355,6 @@ get-them-args@1.3.2: resolved "https://registry.yarnpkg.com/get-them-args/-/get-them-args-1.3.2.tgz#74a20ba8a4abece5ae199ad03f2bcc68fdfc9ba5" integrity sha512-LRn8Jlk+DwZE4GTlDbT3Hikd1wSHgLMme/+7ddlqKd7ldwR6LjJgTVWzBnR01wnYGe4KgrXjg287RaI22UHmAw== -get-value@^3.0.1: - version "3.0.1" - resolved "https://registry.yarnpkg.com/get-value/-/get-value-3.0.1.tgz#5efd2a157f1d6a516d7524e124ac52d0a39ef5a8" - integrity sha512-mKZj9JLQrwMBtj5wxi6MH8Z5eSKaERpAwjg43dPtlGI1ZVEgH/qC7T8/6R2OBSUA+zzHBZgICsVJaEIV2tKTDA== - dependencies: - isobject "^3.0.1" - git-raw-commits@^2.0.8: version "2.0.11" resolved "https://registry.yarnpkg.com/git-raw-commits/-/git-raw-commits-2.0.11.tgz#bc3576638071d18655e1cc60d7f524920008d723"