From b7dddbd95ae7c338f19bcf5ba90931a8db4e073a Mon Sep 17 00:00:00 2001 From: jasoncheng-jora <134347003+jasoncheng-jora@users.noreply.github.com> Date: Fri, 20 Oct 2023 10:44:27 +0800 Subject: [PATCH] global key support --- .changeset/lemon-mails-battle.md | 27 +++++++ .changeset/many-apricots-try.md | 14 ++++ .changeset/slimy-dingos-begin.md | 20 +++++ README.md | 32 ++++++++ .../mytranslations.vocab/fr.translations.json | 3 + .../mytranslations.vocab/translations.json | 7 ++ packages/cli/src/index.ts | 11 ++- packages/core/src/load-translations.ts | 11 +++ packages/core/src/types.ts | 1 + packages/phrase/src/pull-translations.test.ts | 78 ++++++++++++++++++- packages/phrase/src/pull-translations.ts | 15 +++- packages/phrase/src/push-translations.test.ts | 50 ++++++++++++ packages/phrase/src/push-translations.ts | 7 +- 13 files changed, 269 insertions(+), 7 deletions(-) create mode 100644 .changeset/lemon-mails-battle.md create mode 100644 .changeset/many-apricots-try.md create mode 100644 .changeset/slimy-dingos-begin.md diff --git a/.changeset/lemon-mails-battle.md b/.changeset/lemon-mails-battle.md new file mode 100644 index 00000000..813d6507 --- /dev/null +++ b/.changeset/lemon-mails-battle.md @@ -0,0 +1,27 @@ +--- +'@vocab/phrase': minor +'@vocab/core': minor +--- + +`vocab push` and `vocab pull` can support global keys mapping. When you want certain translations to use a specific/custom key in Phrase, add the `globalKey` to the structure. + +**EXAMPLE USAGE**: + +```jsonc +// translations.json +{ + "Hello": { + "message": "Hello", + "globalKey": "hello" + }, + "Goodbye": { + "message": "Goodbye", + "globalKey": "app.goodbye.label" + } +} +``` + +In the above example, + +- `vocab push` will push the `hello` and `app.goodbye.label` keys to Phrase. +- `vocab pull` will pull translations from Phrase and map them to the `hello` and `app.goodbye.label` keys. diff --git a/.changeset/many-apricots-try.md b/.changeset/many-apricots-try.md new file mode 100644 index 00000000..da673d1a --- /dev/null +++ b/.changeset/many-apricots-try.md @@ -0,0 +1,14 @@ +--- +'@vocab/cli': minor +--- + +Error on no translation for global key + +By default, `vocab pull` will not error if a translation is missing in Phrase for a translation with a global key. +If you want to throw an error in this situation, pass the `--error-on-no-global-key-translation` flag: + +**EXAMPLE USAGE**: + +```sh +vocab pull --error-on-no-global-key-translation +``` diff --git a/.changeset/slimy-dingos-begin.md b/.changeset/slimy-dingos-begin.md new file mode 100644 index 00000000..f4bf79e9 --- /dev/null +++ b/.changeset/slimy-dingos-begin.md @@ -0,0 +1,20 @@ +--- +'@vocab/phrase': minor +--- + +Add an optional `errorOnNoGlobalKeyTranslation` flag to `pull` function. If set to `true`, it will error if a translation is missing in Phrase for a translation with a global key. + +**EXAMPLE USAGE**: + +```js +import { pull } from '@vocab/phrase'; + +const vocabConfig = { + devLanguage: 'en', + language: ['en', 'fr'], +}; + +await pull({ branch: 'myBranch', errorOnNoGlobalKeyTranslation: true }, vocabConfig); +``` + + diff --git a/README.md b/README.md index df92f9e9..9905e0aa 100644 --- a/README.md +++ b/README.md @@ -506,6 +506,38 @@ Tags on keys in other languages will be ignored. [tags]: https://support.phrase.com/hc/en-us/articles/5822598372252-Tags-Strings- [configuration]: #Configuration +#### Global key + +`vocab push` and `vocab pull` can support global keys mapping. When you want certain translations to use a specific/custom key in Phrase, add the `globalKey` to the structure. + +```jsonc +// translations.json +{ + "Hello": { + "message": "Hello", + "globalKey": "hello" + }, + "Goodbye": { + "message": "Goodbye", + "globalKey": "app.goodbye.label" + } +} +``` + +In the above example, + +- `vocab push` will push the `hello` and `app.goodbye.label` keys to Phrase. +- `vocab pull` will pull translations from Phrase and map them to the `hello` and `app.goodbye.label` keys. + +##### Error on no translation for global key + +By default, `vocab pull` will not error if a translation is missing in Phrase for a translation with a global key. +If you want to throw an error in this situation, pass the `--error-on-no-global-key-translation` flag: + +```sh +vocab pull --error-on-no-global-key-translation +``` + ## Troubleshooting ### Problem: Passed locale is being ignored or using en-US instead diff --git a/fixtures/phrase/src/mytranslations.vocab/fr.translations.json b/fixtures/phrase/src/mytranslations.vocab/fr.translations.json index 152d976b..4095c307 100644 --- a/fixtures/phrase/src/mytranslations.vocab/fr.translations.json +++ b/fixtures/phrase/src/mytranslations.vocab/fr.translations.json @@ -6,5 +6,8 @@ }, "world": { "message": "monde" + }, + "profile": { + "message": "profil" } } diff --git a/fixtures/phrase/src/mytranslations.vocab/translations.json b/fixtures/phrase/src/mytranslations.vocab/translations.json index 62691a1f..cb8f0084 100644 --- a/fixtures/phrase/src/mytranslations.vocab/translations.json +++ b/fixtures/phrase/src/mytranslations.vocab/translations.json @@ -8,5 +8,12 @@ }, "world": { "message": "world" + }, + "thanks": { + "message": "Thanks", + "globalKey": "app.thanks.label" + }, + "profile": { + "message": "profil" } } diff --git a/packages/cli/src/index.ts b/packages/cli/src/index.ts index 8aa6ffea..c1b8d84f 100755 --- a/packages/cli/src/index.ts +++ b/packages/cli/src/index.ts @@ -44,7 +44,16 @@ yargs(process.argv.slice(2)) }) .command({ command: 'pull', - builder: () => yargs.options({ branch: branchDefinition }), + builder: () => + yargs.options({ + branch: branchDefinition, + 'error-on-no-global-key-translation': { + type: 'boolean', + describe: + 'Throw an error when there is no translation for a global key', + default: false, + }, + }), handler: async (options) => { await pull(options, config!); }, diff --git a/packages/core/src/load-translations.ts b/packages/core/src/load-translations.ts index c1836694..eb80303f 100644 --- a/packages/core/src/load-translations.ts +++ b/packages/core/src/load-translations.ts @@ -410,6 +410,17 @@ export async function loadAllTranslations( ); } keys.add(uniqueKey); + + const globalKey = + loadedTranslation.languages[config.devLanguage][key].globalKey; + if (globalKey) { + if (keys.has(globalKey)) { + throw new Error( + `Duplicate keys found. Key with global key ${globalKey} and key ${key} was found multiple times`, + ); + } + keys.add(globalKey); + } } } return result; diff --git a/packages/core/src/types.ts b/packages/core/src/types.ts index 07a0f5d3..44e78425 100644 --- a/packages/core/src/types.ts +++ b/packages/core/src/types.ts @@ -137,6 +137,7 @@ export interface TranslationData { message: TranslationMessage; description?: string; tags?: Tags; + globalKey?: string; } export type TranslationsByKey = Record< diff --git a/packages/phrase/src/pull-translations.test.ts b/packages/phrase/src/pull-translations.test.ts index 46975063..bf1a168f 100644 --- a/packages/phrase/src/pull-translations.test.ts +++ b/packages/phrase/src/pull-translations.test.ts @@ -19,9 +19,13 @@ const devLanguage = 'en'; function runPhrase(options: { languages: LanguageTarget[]; generatedLanguages: GeneratedLanguageTarget[]; + errorOnNoGlobalKeyTranslation?: boolean; }) { return pull( - { branch: 'tester' }, + { + branch: 'tester', + errorOnNoGlobalKeyTranslation: options.errorOnNoGlobalKeyTranslation, + }, { ...options, devLanguage, @@ -41,11 +45,17 @@ describe('pull translations', () => { 'hello.mytranslations': { message: 'Hi there', }, + 'app.thanks.label': { + message: 'Thank you.', + }, }, fr: { 'hello.mytranslations': { message: 'merci', }, + 'app.thanks.label': { + message: 'Merci.', + }, }, }), ); @@ -98,6 +108,13 @@ describe('pull translations', () => { "greeting", ], }, + "profile": { + "message": "profil", + }, + "thanks": { + "globalKey": "app.thanks.label", + "message": "Thank you.", + }, "world": { "message": "world", }, @@ -106,6 +123,12 @@ describe('pull translations', () => { "hello": { "message": "merci", }, + "profile": { + "message": "profil", + }, + "thanks": { + "message": "Merci.", + }, "world": { "message": "monde", }, @@ -182,6 +205,13 @@ describe('pull translations', () => { "greeting", ], }, + "profile": { + "message": "profil", + }, + "thanks": { + "globalKey": "app.thanks.label", + "message": "Thanks", + }, "world": { "message": "world", }, @@ -190,6 +220,9 @@ describe('pull translations', () => { "hello": { "message": "merci", }, + "profile": { + "message": "profil", + }, "world": { "message": "monde", }, @@ -237,4 +270,47 @@ describe('pull translations', () => { expect(jest.mocked(writeFile)).toHaveBeenCalledTimes(0); }); }); + + describe('when pulling translations and some global keys do not have any translations', () => { + beforeEach(() => { + jest.mocked(pullAllTranslations).mockClear(); + jest.mocked(writeFile).mockClear(); + jest.mocked(pullAllTranslations).mockImplementation(() => + Promise.resolve({ + en: { + 'hello.mytranslations': { + message: 'Hi there', + }, + }, + fr: { + 'hello.mytranslations': { + message: 'merci', + }, + }, + }), + ); + }); + + const options = { + languages: [{ name: 'en' }, { name: 'fr' }], + generatedLanguages: [ + { + name: 'generatedLanguage', + extends: 'en', + generator: { + transformMessage: (message: string) => `[${message}]`, + }, + }, + ], + errorOnNoGlobalKeyTranslation: true, + }; + + it('should throw an error', async () => { + await expect(runPhrase(options)).rejects.toThrow( + new Error(`Missing translation for global key thanks in language fr`), + ); + + expect(jest.mocked(writeFile)).toHaveBeenCalledTimes(1); + }); + }); }); diff --git a/packages/phrase/src/pull-translations.ts b/packages/phrase/src/pull-translations.ts index 5fac3921..d7d607d1 100644 --- a/packages/phrase/src/pull-translations.ts +++ b/packages/phrase/src/pull-translations.ts @@ -15,10 +15,11 @@ import { trace } from './logger'; interface PullOptions { branch?: string; deleteUnusedKeys?: boolean; + errorOnNoGlobalKeyTranslation?: boolean; } export async function pull( - { branch = 'local-development' }: PullOptions, + { branch = 'local-development', errorOnNoGlobalKeyTranslation }: PullOptions, config: UserConfig, ) { trace(`Pulling translations from branch ${branch}`); @@ -61,7 +62,8 @@ export async function pull( defaultValues[key] = { ...defaultValues[key], ...allPhraseTranslations[config.devLanguage][ - getUniqueKey(key, loadedTranslation.namespace) + defaultValues[key].globalKey ?? + getUniqueKey(key, loadedTranslation.namespace) ], }; } @@ -85,7 +87,9 @@ export async function pull( allPhraseTranslations[alternativeLanguage]; for (const key of localKeys) { - const phraseKey = getUniqueKey(key, loadedTranslation.namespace); + const phraseKey = + defaultValues[key].globalKey ?? + getUniqueKey(key, loadedTranslation.namespace); const phraseTranslationMessage = phraseAltTranslations[phraseKey]?.message; @@ -93,6 +97,11 @@ export async function pull( trace( `Missing translation. No translation for key ${key} in phrase as ${phraseKey} in language ${alternativeLanguage}.`, ); + if (errorOnNoGlobalKeyTranslation && defaultValues[key].globalKey) { + throw new Error( + `Missing translation for global key ${key} in language ${alternativeLanguage}`, + ); + } continue; } diff --git a/packages/phrase/src/push-translations.test.ts b/packages/phrase/src/push-translations.test.ts index 11b781ab..a1d7472c 100644 --- a/packages/phrase/src/push-translations.test.ts +++ b/packages/phrase/src/push-translations.test.ts @@ -63,6 +63,17 @@ describe('push', () => { .toMatchInlineSnapshot(` { "en": { + "app.thanks.label": { + "globalKey": "app.thanks.label", + "message": "Thanks", + "tags": [ + "every", + "key", + "gets", + "these", + "tags", + ], + }, "hello.mytranslations": { "message": "Hello", "tags": [ @@ -75,6 +86,16 @@ describe('push', () => { "tags", ], }, + "profile.mytranslations": { + "message": "profil", + "tags": [ + "every", + "key", + "gets", + "these", + "tags", + ], + }, "world.mytranslations": { "message": "world", "tags": [ @@ -91,6 +112,10 @@ describe('push', () => { "description": undefined, "message": "Bonjour", }, + "profile.mytranslations": { + "description": undefined, + "message": "profil", + }, "world.mytranslations": { "description": undefined, "message": "monde", @@ -134,6 +159,17 @@ describe('push', () => { .toMatchInlineSnapshot(` { "en": { + "app.thanks.label": { + "globalKey": "app.thanks.label", + "message": "Thanks", + "tags": [ + "every", + "key", + "gets", + "these", + "tags", + ], + }, "hello.mytranslations": { "message": "Hello", "tags": [ @@ -146,6 +182,16 @@ describe('push', () => { "tags", ], }, + "profile.mytranslations": { + "message": "profil", + "tags": [ + "every", + "key", + "gets", + "these", + "tags", + ], + }, "world.mytranslations": { "message": "world", "tags": [ @@ -162,6 +208,10 @@ describe('push', () => { "description": undefined, "message": "Bonjour", }, + "profile.mytranslations": { + "description": undefined, + "message": "profil", + }, "world.mytranslations": { "description": undefined, "message": "monde", diff --git a/packages/phrase/src/push-translations.ts b/packages/phrase/src/push-translations.ts index 777c3714..4363fa84 100644 --- a/packages/phrase/src/push-translations.ts +++ b/packages/phrase/src/push-translations.ts @@ -54,12 +54,15 @@ export async function push( } = loadedTranslation; for (const localKey of Object.keys(localTranslations)) { - const phraseKey = getUniqueKey(localKey, loadedTranslation.namespace); const { tags = [], ...localTranslation } = localTranslations[localKey]; - if (language === config.devLanguage) { (localTranslation as TranslationData).tags = [...tags, ...sharedTags]; } + const globalKey = + loadedTranslation.languages[config.devLanguage][localKey].globalKey; + + const phraseKey = + globalKey ?? getUniqueKey(localKey, loadedTranslation.namespace); phraseTranslations[language][phraseKey] = localTranslation; }