From d492ae1c3efe00178df45f7038a025c1a4adbbfd Mon Sep 17 00:00:00 2001 From: Matej Lednicky Date: Wed, 5 Mar 2025 20:16:36 +0100 Subject: [PATCH] feat(cli): support JSON messages in block of .vue files --- .changeset/brave-beans-mix.md | 6 + packages/cli/src/cli/cmd/init.ts | 75 ++- packages/cli/src/cli/loaders/index.spec.ts | 588 ++++++++++++------ packages/cli/src/cli/loaders/index.ts | 9 + packages/cli/src/cli/loaders/vue-json.ts | 30 + .../src/cli/utils/find-locale-paths.spec.ts | 4 +- .../cli/src/cli/utils/find-locale-paths.ts | 2 +- packages/spec/src/formats.ts | 1 + 8 files changed, 489 insertions(+), 226 deletions(-) create mode 100644 .changeset/brave-beans-mix.md create mode 100644 packages/cli/src/cli/loaders/vue-json.ts diff --git a/.changeset/brave-beans-mix.md b/.changeset/brave-beans-mix.md new file mode 100644 index 000000000..87c72c8c1 --- /dev/null +++ b/.changeset/brave-beans-mix.md @@ -0,0 +1,6 @@ +--- +"@lingo.dev/_spec": patch +"lingo.dev": patch +--- + +support JSON messages in block of .vue files diff --git a/packages/cli/src/cli/cmd/init.ts b/packages/cli/src/cli/cmd/init.ts index 887295714..ba3a32f65 100644 --- a/packages/cli/src/cli/cmd/init.ts +++ b/packages/cli/src/cli/cmd/init.ts @@ -119,43 +119,56 @@ export default new InteractiveCommand() }; } else { let selectedPatterns: string[] = []; - const { patterns, defaultPatterns } = findLocaleFiles(options.bucket); + const localeFiles = findLocaleFiles(options.bucket); + + if (!localeFiles) { + spinner.warn( + `Bucket type "${options.bucket}" does not supported automatic initialization. Add paths to "i18n.json" manually.`, + ); + newConfig.buckets = { + [options.bucket]: { + include: options.paths || [], + }, + }; + } else { + const { patterns, defaultPatterns } = localeFiles; - if (patterns.length > 0) { - spinner.succeed("Found existing locale files:"); + if (patterns.length > 0) { + spinner.succeed("Found existing locale files:"); - selectedPatterns = await checkbox({ - message: "Select the paths to use", - choices: patterns.map((value) => ({ - value, - })), - }); - } else { - spinner.succeed("No existing locale files found."); - } + selectedPatterns = await checkbox({ + message: "Select the paths to use", + choices: patterns.map((value) => ({ + value, + })), + }); + } else { + spinner.succeed("No existing locale files found."); + } - if (selectedPatterns.length === 0) { - const useDefault = await confirm({ - message: `Use (and create) default path ${defaultPatterns.join(", ")}?`, - }); - if (useDefault) { - ensurePatterns(defaultPatterns, options.source); - selectedPatterns = defaultPatterns; + if (selectedPatterns.length === 0) { + const useDefault = await confirm({ + message: `Use (and create) default path ${defaultPatterns.join(", ")}?`, + }); + if (useDefault) { + ensurePatterns(defaultPatterns, options.source); + selectedPatterns = defaultPatterns; + } } - } - if (selectedPatterns.length === 0) { - const customPaths = await input({ - message: "Enter paths to use", - }); - selectedPatterns = customPaths.includes(",") ? customPaths.split(",") : customPaths.split(" "); - } + if (selectedPatterns.length === 0) { + const customPaths = await input({ + message: "Enter paths to use", + }); + selectedPatterns = customPaths.includes(",") ? customPaths.split(",") : customPaths.split(" "); + } - newConfig.buckets = { - [options.bucket]: { - include: selectedPatterns || [], - }, - }; + newConfig.buckets = { + [options.bucket]: { + include: selectedPatterns || [], + }, + }; + } } await saveConfig(newConfig); diff --git a/packages/cli/src/cli/loaders/index.spec.ts b/packages/cli/src/cli/loaders/index.spec.ts index c2d0853cb..4dcdac4b6 100644 --- a/packages/cli/src/cli/loaders/index.spec.ts +++ b/packages/cli/src/cli/loaders/index.spec.ts @@ -23,7 +23,10 @@ describe("bucket loaders", () => { mockFileOperations(input); - const androidLoader = createBucketLoader("android", "values-[locale]/strings.xml", { isCacheRestore: false, defaultLocale: "en" }); + const androidLoader = createBucketLoader("android", "values-[locale]/strings.xml", { + isCacheRestore: false, + defaultLocale: "en", + }); androidLoader.setDefaultLocale("en"); const data = await androidLoader.pull("en"); @@ -44,7 +47,10 @@ describe("bucket loaders", () => { mockFileOperations(input); - const androidLoader = createBucketLoader("android", "values-[locale]/strings.xml", { isCacheRestore: false, defaultLocale: "en" }); + const androidLoader = createBucketLoader("android", "values-[locale]/strings.xml", { + isCacheRestore: false, + defaultLocale: "en", + }); androidLoader.setDefaultLocale("en"); const data = await androidLoader.pull("en"); @@ -64,7 +70,10 @@ describe("bucket loaders", () => { mockFileOperations(input); - const androidLoader = createBucketLoader("android", "values-[locale]/strings.xml", { isCacheRestore: false, defaultLocale: "en" }); + const androidLoader = createBucketLoader("android", "values-[locale]/strings.xml", { + isCacheRestore: false, + defaultLocale: "en", + }); androidLoader.setDefaultLocale("en"); await androidLoader.pull("en"); @@ -136,7 +145,10 @@ describe("bucket loaders", () => { mockFileOperations(input); - const flutterLoader = createBucketLoader("flutter", "lib/l10n/app_[locale].arb", { isCacheRestore: false, defaultLocale: "en" }); + const flutterLoader = createBucketLoader("flutter", "lib/l10n/app_[locale].arb", { + isCacheRestore: false, + defaultLocale: "en", + }); flutterLoader.setDefaultLocale("en"); const data = await flutterLoader.pull("en"); @@ -180,7 +192,10 @@ describe("bucket loaders", () => { mockFileOperations(input); - const flutterLoader = createBucketLoader("flutter", "lib/l10n/app_[locale].arb", { isCacheRestore: false, defaultLocale: "en" }); + const flutterLoader = createBucketLoader("flutter", "lib/l10n/app_[locale].arb", { + isCacheRestore: false, + defaultLocale: "en", + }); flutterLoader.setDefaultLocale("en"); await flutterLoader.pull("en"); @@ -233,7 +248,10 @@ describe("bucket loaders", () => { mockFileOperations(input); - const htmlLoader = createBucketLoader("html", "i18n/[locale].html", { isCacheRestore: false, defaultLocale: "en" }); + const htmlLoader = createBucketLoader("html", "i18n/[locale].html", { + isCacheRestore: false, + defaultLocale: "en", + }); htmlLoader.setDefaultLocale("en"); const data = await htmlLoader.pull("en"); @@ -291,7 +309,10 @@ describe("bucket loaders", () => { mockFileOperations(input); - const htmlLoader = createBucketLoader("html", "i18n/[locale].html", { isCacheRestore: false, defaultLocale: "en" }); + const htmlLoader = createBucketLoader("html", "i18n/[locale].html", { + isCacheRestore: false, + defaultLocale: "en", + }); htmlLoader.setDefaultLocale("en"); await htmlLoader.pull("en"); @@ -308,7 +329,10 @@ describe("bucket loaders", () => { const input = { "button.title": "Submit" }; mockFileOperations(JSON.stringify(input)); - const jsonLoader = createBucketLoader("json", "i18n/[locale].json", { isCacheRestore: false, defaultLocale: "en" }); + const jsonLoader = createBucketLoader("json", "i18n/[locale].json", { + isCacheRestore: false, + defaultLocale: "en", + }); jsonLoader.setDefaultLocale("en"); const data = await jsonLoader.pull("en"); @@ -324,7 +348,10 @@ describe("bucket loaders", () => { mockFileOperations(JSON.stringify(input)); - const jsonLoader = createBucketLoader("json", "i18n/[locale].json", { isCacheRestore: false, defaultLocale: "en" }); + const jsonLoader = createBucketLoader("json", "i18n/[locale].json", { + isCacheRestore: false, + defaultLocale: "en", + }); jsonLoader.setDefaultLocale("en"); await jsonLoader.pull("en"); @@ -342,7 +369,10 @@ describe("bucket loaders", () => { mockFileOperations(JSON.stringify(input)); - const jsonLoader = createBucketLoader("json", "i18n/[locale].json", { isCacheRestore: false, defaultLocale: "en" }); + const jsonLoader = createBucketLoader("json", "i18n/[locale].json", { + isCacheRestore: false, + defaultLocale: "en", + }); jsonLoader.setDefaultLocale("en"); await jsonLoader.pull("en"); @@ -360,7 +390,10 @@ describe("bucket loaders", () => { mockFileOperations(JSON.stringify(input)); - const jsonLoader = createBucketLoader("json", "i18n/[locale].json", { isCacheRestore: false, defaultLocale: "en" }); + const jsonLoader = createBucketLoader("json", "i18n/[locale].json", { + isCacheRestore: false, + defaultLocale: "en", + }); jsonLoader.setDefaultLocale("en"); await jsonLoader.pull("en"); @@ -378,7 +411,10 @@ describe("bucket loaders", () => { mockFileOperations(JSON.stringify(input)); - const jsonLoader = createBucketLoader("json", "i18n/[locale].json", { isCacheRestore: false, defaultLocale: "en" }); + const jsonLoader = createBucketLoader("json", "i18n/[locale].json", { + isCacheRestore: false, + defaultLocale: "en", + }); jsonLoader.setDefaultLocale("en"); await jsonLoader.pull("en"); @@ -396,7 +432,10 @@ describe("bucket loaders", () => { mockFileOperations(JSON.stringify(input)); - const jsonLoader = createBucketLoader("json", "i18n/[locale].json", { isCacheRestore: true, defaultLocale: "en" }); + const jsonLoader = createBucketLoader("json", "i18n/[locale].json", { + isCacheRestore: true, + defaultLocale: "en", + }); jsonLoader.setDefaultLocale("en"); await jsonLoader.pull("en"); @@ -414,7 +453,10 @@ describe("bucket loaders", () => { mockFileOperations(JSON.stringify(input)); - const jsonLoader = createBucketLoader("json", "i18n/[locale]/[locale].json", { isCacheRestore: false, defaultLocale: "en" }); + const jsonLoader = createBucketLoader("json", "i18n/[locale]/[locale].json", { + isCacheRestore: false, + defaultLocale: "en", + }); jsonLoader.setDefaultLocale("en"); const data = await jsonLoader.pull("en"); @@ -452,7 +494,10 @@ Another paragraph with **bold** and *italic* text.`; mockFileOperations(input); - const markdownLoader = createBucketLoader("markdown", "i18n/[locale].md", { isCacheRestore: false, defaultLocale: "en" }); + const markdownLoader = createBucketLoader("markdown", "i18n/[locale].md", { + isCacheRestore: false, + defaultLocale: "en", + }); markdownLoader.setDefaultLocale("en"); const data = await markdownLoader.pull("en"); @@ -498,7 +543,10 @@ Otro párrafo con texto en **negrita** y en _cursiva_. mockFileOperations(input); - const markdownLoader = createBucketLoader("markdown", "i18n/[locale].md", { isCacheRestore: false, defaultLocale: "en" }); + const markdownLoader = createBucketLoader("markdown", "i18n/[locale].md", { + isCacheRestore: false, + defaultLocale: "en", + }); markdownLoader.setDefaultLocale("en"); await markdownLoader.pull("en"); @@ -535,7 +583,10 @@ user.password=Password mockFileOperations(input); - const propertiesLoader = createBucketLoader("properties", "i18n/[locale].properties", { isCacheRestore: false, defaultLocale: "en" }); + const propertiesLoader = createBucketLoader("properties", "i18n/[locale].properties", { + isCacheRestore: false, + defaultLocale: "en", + }); propertiesLoader.setDefaultLocale("en"); const data = await propertiesLoader.pull("en"); @@ -572,7 +623,10 @@ user.password=Contraseña mockFileOperations(input); - const propertiesLoader = createBucketLoader("properties", "i18n/[locale].properties", { isCacheRestore: false, defaultLocale: "en" }); + const propertiesLoader = createBucketLoader("properties", "i18n/[locale].properties", { + isCacheRestore: false, + defaultLocale: "en", + }); propertiesLoader.setDefaultLocale("en"); await propertiesLoader.pull("en"); @@ -599,7 +653,10 @@ user.password=Contraseña mockFileOperations(input); - const xcodeStringsLoader = createBucketLoader("xcode-strings", "i18n/[locale].strings", { isCacheRestore: false, defaultLocale: "en" }); + const xcodeStringsLoader = createBucketLoader("xcode-strings", "i18n/[locale].strings", { + isCacheRestore: false, + defaultLocale: "en", + }); xcodeStringsLoader.setDefaultLocale("en"); const data = await xcodeStringsLoader.pull("en"); @@ -617,7 +674,10 @@ user.password=Contraseña mockFileOperations(input); - const xcodeStringsLoader = createBucketLoader("xcode-strings", "i18n/[locale].strings", { isCacheRestore: false, defaultLocale: "en" }); + const xcodeStringsLoader = createBucketLoader("xcode-strings", "i18n/[locale].strings", { + isCacheRestore: false, + defaultLocale: "en", + }); xcodeStringsLoader.setDefaultLocale("en"); await xcodeStringsLoader.pull("en"); @@ -668,7 +728,10 @@ user.password=Contraseña mockFileOperations(input); - const xcodeStringsdictLoader = createBucketLoader("xcode-stringsdict", "i18n/[locale].stringsdict", { isCacheRestore: false, defaultLocale: "en" }); + const xcodeStringsdictLoader = createBucketLoader("xcode-stringsdict", "i18n/[locale].stringsdict", { + isCacheRestore: false, + defaultLocale: "en", + }); xcodeStringsdictLoader.setDefaultLocale("en"); const data = await xcodeStringsdictLoader.pull("en"); @@ -701,7 +764,10 @@ user.password=Contraseña mockFileOperations(input); - const xcodeStringsdictLoader = createBucketLoader("xcode-stringsdict", "[locale].lproj/Localizable.stringsdict", { isCacheRestore: false, defaultLocale: "en" }); + const xcodeStringsdictLoader = createBucketLoader("xcode-stringsdict", "[locale].lproj/Localizable.stringsdict", { + isCacheRestore: false, + defaultLocale: "en", + }); xcodeStringsdictLoader.setDefaultLocale("en"); await xcodeStringsdictLoader.pull("en"); @@ -719,73 +785,76 @@ user.password=Contraseña setupFileMocks(); const input = JSON.stringify({ - "sourceLanguage": "en", - "strings": { - "greeting": { - "extractionState": "manual", - "localizations": { - "en": { - "stringUnit": { - "state": "translated", - "value": "Hello!" - } - } - } + sourceLanguage: "en", + strings: { + greeting: { + extractionState: "manual", + localizations: { + en: { + stringUnit: { + state: "translated", + value: "Hello!", + }, + }, + }, }, - "message": { - "extractionState": "manual", - "localizations": { - "en": { - "stringUnit": { - "state": "translated", - "value": "Welcome to our app" - } - } - } + message: { + extractionState: "manual", + localizations: { + en: { + stringUnit: { + state: "translated", + value: "Welcome to our app", + }, + }, + }, }, - "items_count": { - "extractionState": "manual", - "localizations": { - "en": { - "variations": { - "plural": { - "zero": { - "stringUnit": { - "state": "translated", - "value": "No items" - } + items_count: { + extractionState: "manual", + localizations: { + en: { + variations: { + plural: { + zero: { + stringUnit: { + state: "translated", + value: "No items", + }, }, - "one": { - "stringUnit": { - "state": "translated", - "value": "%d item" - } + one: { + stringUnit: { + state: "translated", + value: "%d item", + }, }, - "other": { - "stringUnit": { - "state": "translated", - "value": "%d items" - } - } - } - } - } - } - } - } + other: { + stringUnit: { + state: "translated", + value: "%d items", + }, + }, + }, + }, + }, + }, + }, + }, }); const expectedOutput = { - "greeting": "Hello!", - "message": "Welcome to our app", + greeting: "Hello!", + message: "Welcome to our app", "items_count/zero": "No items", "items_count/one": "{variable:0} item", - "items_count/other": "{variable:0} items" + "items_count/other": "{variable:0} items", }; mockFileOperations(input); - const xcodeXcstringsLoader = createBucketLoader("xcode-xcstrings", "i18n/[locale].xcstrings", { isCacheRestore: false, defaultLocale: "en" }); + const xcodeXcstringsLoader = createBucketLoader("xcode-xcstrings", "i18n/[locale].xcstrings", { + isCacheRestore: false, + defaultLocale: "en", + }); xcodeXcstringsLoader.setDefaultLocale("en"); const data = await xcodeXcstringsLoader.pull("en"); @@ -796,53 +865,56 @@ user.password=Contraseña setupFileMocks(); const input = JSON.stringify({ - "sourceLanguage": "en", - "strings": { - "greeting": { - "extractionState": "manual", - "localizations": { - "en": { - "stringUnit": { - "state": "translated", - "value": "Hello!" - } - } - } + sourceLanguage: "en", + strings: { + greeting: { + extractionState: "manual", + localizations: { + en: { + stringUnit: { + state: "translated", + value: "Hello!", + }, + }, + }, }, " and ": { - "extractionState": "manual", - "localizations": { - "en": { - "stringUnit": { - "state": "translated", - "value": " and " - } - } - } + extractionState: "manual", + localizations: { + en: { + stringUnit: { + state: "translated", + value: " and ", + }, + }, + }, }, - "key_with_no_default": { - "extractionState": "manual", - "localizations": { - "fr": { - "stringUnit": { - "state": "translated", - "value": "Valeur traduite" - } - } - } - } - } + key_with_no_default: { + extractionState: "manual", + localizations: { + fr: { + stringUnit: { + state: "translated", + value: "Valeur traduite", + }, + }, + }, + }, + }, }); const expectedOutput = { - "greeting": "Hello!", + greeting: "Hello!", "%20and%20": " and ", - "key_with_no_default": "key_with_no_default" + key_with_no_default: "key_with_no_default", }; mockFileOperations(input); - const xcodeXcstringsLoader = createBucketLoader("xcode-xcstrings", "i18n/[locale].xcstrings", { isCacheRestore: false, defaultLocale: "en" }); + const xcodeXcstringsLoader = createBucketLoader("xcode-xcstrings", "i18n/[locale].xcstrings", { + isCacheRestore: false, + defaultLocale: "en", + }); xcodeXcstringsLoader.setDefaultLocale("en"); const data = await xcodeXcstringsLoader.pull("en"); @@ -853,33 +925,36 @@ user.password=Contraseña setupFileMocks(); const originalInput = { - "sourceLanguage": "en", - "strings": { - "greeting": { - "extractionState": "manual", - "localizations": { - "en": { - "stringUnit": { - "state": "translated", - "value": "Hello!" - } - } - } - } - } + sourceLanguage: "en", + strings: { + greeting: { + extractionState: "manual", + localizations: { + en: { + stringUnit: { + state: "translated", + value: "Hello!", + }, + }, + }, + }, + }, }; mockFileOperations(JSON.stringify(originalInput)); const payload = { - "greeting": "Bonjour!", - "message": "Bienvenue dans notre application", + greeting: "Bonjour!", + message: "Bienvenue dans notre application", "items_count/zero": "Aucun élément", "items_count/one": "%d élément", - "items_count/other": "%d éléments" + "items_count/other": "%d éléments", }; - const xcodeXcstringsLoader = createBucketLoader("xcode-xcstrings", "i18n/[locale].xcstrings", { isCacheRestore: false, defaultLocale: "en" }); + const xcodeXcstringsLoader = createBucketLoader("xcode-xcstrings", "i18n/[locale].xcstrings", { + isCacheRestore: false, + defaultLocale: "en", + }); xcodeXcstringsLoader.setDefaultLocale("en"); await xcodeXcstringsLoader.pull("en"); await xcodeXcstringsLoader.push("fr", payload); @@ -890,15 +965,23 @@ user.password=Contraseña expect(writtenContent.strings.greeting.localizations.fr).toBeDefined(); expect(writtenContent.strings.greeting.localizations.fr.stringUnit.value).toBe("Bonjour!"); - + if (writtenContent.strings.message) { - expect(writtenContent.strings.message.localizations.fr.stringUnit.value).toBe("Bienvenue dans notre application"); + expect(writtenContent.strings.message.localizations.fr.stringUnit.value).toBe( + "Bienvenue dans notre application", + ); } if (writtenContent.strings.items_count) { - expect(writtenContent.strings.items_count.localizations.fr.variations.plural.zero.stringUnit.value).toBe("Aucun élément"); - expect(writtenContent.strings.items_count.localizations.fr.variations.plural.one.stringUnit.value).toBe("%d élément"); - expect(writtenContent.strings.items_count.localizations.fr.variations.plural.other.stringUnit.value).toBe("%d éléments"); + expect(writtenContent.strings.items_count.localizations.fr.variations.plural.zero.stringUnit.value).toBe( + "Aucun élément", + ); + expect(writtenContent.strings.items_count.localizations.fr.variations.plural.one.stringUnit.value).toBe( + "%d élément", + ); + expect(writtenContent.strings.items_count.localizations.fr.variations.plural.other.stringUnit.value).toBe( + "%d éléments", + ); } }); @@ -956,11 +1039,14 @@ user.password=Contraseña mockFileOperations(input); - const xcodeXcstringsLoader = createBucketLoader("xcode-xcstrings", "i18n/[locale].xcstrings", { isCacheRestore: false, defaultLocale: "en" }); + const xcodeXcstringsLoader = createBucketLoader("xcode-xcstrings", "i18n/[locale].xcstrings", { + isCacheRestore: false, + defaultLocale: "en", + }); xcodeXcstringsLoader.setDefaultLocale("en"); const data = await xcodeXcstringsLoader.pull("en"); - Object.keys(data).forEach(key => { + Object.keys(data).forEach((key) => { if (key === "") { expect(data[key]).toBe("Empty key"); } else if (key.includes("%20") || key === " ") { @@ -974,8 +1060,8 @@ user.password=Contraseña }); const payload: Record = {}; - - Object.keys(data).forEach(key => { + + Object.keys(data).forEach((key) => { if (key === "") { payload[key] = "Vide"; } else if (key.includes("%20") || key === " ") { @@ -999,11 +1085,11 @@ user.password=Contraseña } const hasSpaceKey = Object.keys(writtenContent.strings).some( - key => key === " " || key === "%20" || key.includes("%20") + (key) => key === " " || key === "%20" || key.includes("%20"), ); if (hasSpaceKey) { const spaceKey = Object.keys(writtenContent.strings).find( - key => key === " " || key === "%20" || key.includes("%20") + (key) => key === " " || key === "%20" || key.includes("%20"), ); console.log(`Found space key in written content: "${spaceKey}"`); if (spaceKey) { @@ -1025,14 +1111,14 @@ user.password=Contraseña expect(stringKeys.includes("")).toBe(true); expect(stringKeys.includes(" ") || stringKeys.includes("%20")).toBe(true); expect(stringKeys.includes("apple")).toBe(true); - + expect(stringKeys.indexOf("25")).toBeLessThan(stringKeys.indexOf("")); - + const spaceIdx = stringKeys.indexOf(" ") === -1 ? stringKeys.indexOf("%20") : stringKeys.indexOf(" "); if (spaceIdx !== -1) { expect(stringKeys.indexOf("")).toBeLessThan(spaceIdx); } - + if (spaceIdx !== -1) { expect(spaceIdx).toBeLessThan(stringKeys.indexOf("apple")); } @@ -1071,35 +1157,38 @@ user.password=Contraseña mockFileOperations(input); - const xcodeXcstringsLoader = createBucketLoader("xcode-xcstrings", "i18n/[locale].xcstrings", { isCacheRestore: false, defaultLocale: "en" }); + const xcodeXcstringsLoader = createBucketLoader("xcode-xcstrings", "i18n/[locale].xcstrings", { + isCacheRestore: false, + defaultLocale: "en", + }); xcodeXcstringsLoader.setDefaultLocale("en"); - + const data = await xcodeXcstringsLoader.pull("en"); - + expect(data).toHaveProperty("normal_key", "This should be translated"); expect(data).not.toHaveProperty("do_not_translate"); - + const payload = { - "normal_key": "Ceci devrait être traduit" + normal_key: "Ceci devrait être traduit", }; - + await xcodeXcstringsLoader.push("fr", payload); - + expect(fs.writeFile).toHaveBeenCalled(); const writeFileCall = (fs.writeFile as any).mock.calls[0]; const writtenContent = JSON.parse(writeFileCall[1]); - + expect(writtenContent.strings.normal_key.localizations.fr.stringUnit.value).toBe("Ceci devrait être traduit"); - + expect(writtenContent.strings.do_not_translate).toHaveProperty("shouldTranslate", false); - + expect(writtenContent.strings.do_not_translate.localizations).not.toHaveProperty("fr"); - + await xcodeXcstringsLoader.push("fr", {}); - + const secondWriteFileCall = (fs.writeFile as any).mock.calls[1]; const secondWrittenContent = JSON.parse(secondWriteFileCall[1]); - + expect(secondWrittenContent.strings.do_not_translate).toHaveProperty("shouldTranslate", false); }); }); @@ -1115,7 +1204,10 @@ user.password=Contraseña mockFileOperations(input); - const yamlLoader = createBucketLoader("yaml", "i18n/[locale].yaml", { isCacheRestore: false, defaultLocale: "en" }); + const yamlLoader = createBucketLoader("yaml", "i18n/[locale].yaml", { + isCacheRestore: false, + defaultLocale: "en", + }); yamlLoader.setDefaultLocale("en"); const data = await yamlLoader.pull("en"); @@ -1133,7 +1225,10 @@ user.password=Contraseña mockFileOperations(input); - const yamlLoader = createBucketLoader("yaml", "i18n/[locale].yaml", { isCacheRestore: false, defaultLocale: "en" }); + const yamlLoader = createBucketLoader("yaml", "i18n/[locale].yaml", { + isCacheRestore: false, + defaultLocale: "en", + }); yamlLoader.setDefaultLocale("en"); await yamlLoader.pull("en"); @@ -1155,7 +1250,10 @@ user.password=Contraseña mockFileOperations(input); - const yamlRootKeyLoader = createBucketLoader("yaml-root-key", "i18n/[locale].yaml", { isCacheRestore: false, defaultLocale: "en" }); + const yamlRootKeyLoader = createBucketLoader("yaml-root-key", "i18n/[locale].yaml", { + isCacheRestore: false, + defaultLocale: "en", + }); yamlRootKeyLoader.setDefaultLocale("en"); const data = await yamlRootKeyLoader.pull("en"); @@ -1174,7 +1272,10 @@ user.password=Contraseña mockFileOperations(input); - const yamlRootKeyLoader = createBucketLoader("yaml-root-key", "i18n/[locale].yaml", { isCacheRestore: false, defaultLocale: "en" }); + const yamlRootKeyLoader = createBucketLoader("yaml-root-key", "i18n/[locale].yaml", { + isCacheRestore: false, + defaultLocale: "en", + }); yamlRootKeyLoader.setDefaultLocale("en"); await yamlRootKeyLoader.pull("en"); @@ -1466,7 +1567,10 @@ Mundo!`; mockFileOperations(input); - const xliffLoader = createBucketLoader("xliff", "i18n/[locale].xliff", { isCacheRestore: false, defaultLocale: "en" }); + const xliffLoader = createBucketLoader("xliff", "i18n/[locale].xliff", { + isCacheRestore: false, + defaultLocale: "en", + }); xliffLoader.setDefaultLocale("en"); const data = await xliffLoader.pull("en"); @@ -1542,7 +1646,10 @@ Mundo!`; mockFileOperations(input); - const xliffLoader = createBucketLoader("xliff", "i18n/[locale].xlf", { isCacheRestore: false, defaultLocale: "en" }); + const xliffLoader = createBucketLoader("xliff", "i18n/[locale].xlf", { + isCacheRestore: false, + defaultLocale: "en", + }); xliffLoader.setDefaultLocale("en"); await xliffLoader.pull("en"); @@ -1652,9 +1759,9 @@ Mundo!`; mockFileOperations(input); - const jsonLoader = createBucketLoader("php", "i18n/[locale].php", { isCacheRestore: false, defaultLocale: "en" }); - jsonLoader.setDefaultLocale("en"); - const data = await jsonLoader.pull("en"); + const phpLoader = createBucketLoader("php", "i18n/[locale].php", { isCacheRestore: false, defaultLocale: "en" }); + phpLoader.setDefaultLocale("en"); + const data = await phpLoader.pull("en"); expect(data).toEqual(expectedOutput); }); @@ -1686,11 +1793,11 @@ return array( mockFileOperations(input); - const jsonLoader = createBucketLoader("php", "i18n/[locale].php", { isCacheRestore: false, defaultLocale: "en" }); - jsonLoader.setDefaultLocale("en"); - await jsonLoader.pull("en"); + const phpLoader = createBucketLoader("php", "i18n/[locale].php", { isCacheRestore: false, defaultLocale: "en" }); + phpLoader.setDefaultLocale("en"); + await phpLoader.pull("en"); - await jsonLoader.push("es", { + await phpLoader.push("es", { "button.title": "Enviar", "button.description/0": "Hola", "button.description/1": "Adiós", @@ -1698,41 +1805,138 @@ return array( expect(fs.writeFile).toHaveBeenCalledWith("i18n/es.php", expectedOutput, { encoding: "utf-8", flag: "w" }); }); + }); - describe("po bucket loader", () => { - it("should load po file", async () => { - setupFileMocks(); + describe("po bucket loader", () => { + it("should load po file", async () => { + setupFileMocks(); + + const input = `msgid "Hello"\nmsgstr "Hello"`; + const expectedOutput = { "Hello/singular": "Hello" }; + + mockFileOperations(input); + + const poLoader = createBucketLoader("po", "i18n/[locale].po", { isCacheRestore: false, defaultLocale: "en" }); + poLoader.setDefaultLocale("en"); + const data = await poLoader.pull("en"); + + expect(data).toEqual(expectedOutput); + }); - const input = `msgid "Hello"\nmsgstr "Hello"`; - const expectedOutput = { "Hello/singular": "Hello" }; + it("should save po file", async () => { + setupFileMocks(); + + const input = `msgid "Hello"\nmsgstr "Hello"`; + const expectedOutput = `msgid "Hello"\nmsgstr "Hola"`; - mockFileOperations(input); + mockFileOperations(input); - const jsonLoader = createBucketLoader("po", "i18n/[locale].po", { isCacheRestore: false, defaultLocale: "en" }); - jsonLoader.setDefaultLocale("en"); - const data = await jsonLoader.pull("en"); + const poLoader = createBucketLoader("po", "i18n/[locale].po", { isCacheRestore: false, defaultLocale: "en" }); + poLoader.setDefaultLocale("en"); + await poLoader.pull("en"); - expect(data).toEqual(expectedOutput); + await poLoader.push("es", { + "Hello/singular": "Hola", }); - it("should save po file", async () => { - setupFileMocks(); + expect(fs.writeFile).toHaveBeenCalledWith("i18n/es.po", expectedOutput, { encoding: "utf-8", flag: "w" }); + }); + }); - const input = `msgid "Hello"\nmsgstr "Hello"`; - const expectedOutput = `msgid "Hello"\nmsgstr "Hola"`; + describe("vue-json bucket loader", () => { + const template = ``; + const script = ``; - mockFileOperations(input); + it("should load vue-json file", async () => { + setupFileMocks(); - const jsonLoader = createBucketLoader("po", "i18n/[locale].po", { isCacheRestore: false, defaultLocale: "en" }); - jsonLoader.setDefaultLocale("en"); - await jsonLoader.pull("en"); + const input = `${template} - await jsonLoader.push("es", { - "Hello/singular": "Hola", - }); + +{ + "en": { + "hello": "hello world!" + } +} + + +${script}`; + const expectedOutput = { hello: "hello world!" }; + + mockFileOperations(input); + + const vueLoader = createBucketLoader("vue-json", "i18n/[locale].vue", { + isCacheRestore: false, + defaultLocale: "en", + }); + vueLoader.setDefaultLocale("en"); + const data = await vueLoader.pull("en"); - expect(fs.writeFile).toHaveBeenCalledWith("i18n/es.po", expectedOutput, { encoding: "utf-8", flag: "w" }); + expect(data).toEqual(expectedOutput); + }); + + it("should save vue-json file", async () => { + setupFileMocks(); + + const input = `${template} + + +{ + "en": { + "hello": "hello world!" + } +} + + +${script}`; + const expectedOutput = `${template} + + +{ + "en": { + "hello": "hello world!" + }, + "es": { + "hello": "hola mundo!" + } +} + + +${script}`; + + mockFileOperations(input); + + const vueLoader = createBucketLoader("vue-json", "i18n/App.vue", { isCacheRestore: false, defaultLocale: "en" }); + vueLoader.setDefaultLocale("en"); + await vueLoader.pull("en"); + + await vueLoader.push("es", { + hello: "hola mundo!", }); + + expect(fs.writeFile).toHaveBeenCalledWith("i18n/App.vue", expectedOutput, { encoding: "utf-8", flag: "w" }); }); }); }); diff --git a/packages/cli/src/cli/loaders/index.ts b/packages/cli/src/cli/loaders/index.ts index bae80aff7..f04b7dfd2 100644 --- a/packages/cli/src/cli/loaders/index.ts +++ b/packages/cli/src/cli/loaders/index.ts @@ -28,6 +28,7 @@ import createVariableLoader from "./variable"; import createSyncLoader from "./sync"; import createPlutilJsonTextLoader from "./plutil-json-loader"; import createPhpLoader from "./php"; +import createVueJsonLoader from "./vue-json"; type BucketLoaderOptions = { isCacheRestore: boolean; @@ -199,5 +200,13 @@ export default function createBucketLoader( createFlatLoader(), createUnlocalizableLoader(options.isCacheRestore), ); + case "vue-json": + return composeLoaders( + createTextFileLoader(bucketPathPattern), + createVueJsonLoader(), + createSyncLoader(), + createFlatLoader(), + createUnlocalizableLoader(), + ); } } diff --git a/packages/cli/src/cli/loaders/vue-json.ts b/packages/cli/src/cli/loaders/vue-json.ts new file mode 100644 index 000000000..3ea101816 --- /dev/null +++ b/packages/cli/src/cli/loaders/vue-json.ts @@ -0,0 +1,30 @@ +import { jsonrepair } from "jsonrepair"; +import { ILoader } from "./_types"; +import { createLoader } from "./_utils"; + +export default function createVueJsonLoader(): ILoader> { + return createLoader({ + pull: async (locale, input, ctx) => { + const { i18n } = parseVueFile(input); + return i18n[locale] ?? {}; + }, + push: async (locale, data, originalInput) => { + const { before, i18n, after } = parseVueFile(originalInput ?? ""); + i18n[locale] = data; + return `${before}\n${JSON.stringify(i18n, null, 2)}\n${after}`; + }, + }); +} + +function parseVueFile(input: string) { + const [, before, jsonString = "{}", after] = input.match(/^([\s\S]*)([\s\S]*)<\/i18n>([\s\S]*)$/) || []; + + let i18n: Record; + try { + i18n = JSON.parse(jsonString); + } catch (error) { + i18n = JSON.parse(jsonrepair(jsonString)); + } + + return { before, after, i18n }; +} diff --git a/packages/cli/src/cli/utils/find-locale-paths.spec.ts b/packages/cli/src/cli/utils/find-locale-paths.spec.ts index a34c6ede4..1bc004162 100644 --- a/packages/cli/src/cli/utils/find-locale-paths.spec.ts +++ b/packages/cli/src/cli/utils/find-locale-paths.spec.ts @@ -139,7 +139,7 @@ describe("findLocaleFiles", () => { }); }); - it("should throw error for unsupported bucket type", () => { - expect(() => findLocaleFiles("invalid")).toThrow("Unsupported bucket type: invalid"); + it("should return null unsupported bucket type", () => { + expect(findLocaleFiles("invalid")).toBeNull(); }); }); diff --git a/packages/cli/src/cli/utils/find-locale-paths.ts b/packages/cli/src/cli/utils/find-locale-paths.ts index 8a3090036..bd3da33da 100644 --- a/packages/cli/src/cli/utils/find-locale-paths.ts +++ b/packages/cli/src/cli/utils/find-locale-paths.ts @@ -27,7 +27,7 @@ export default function findLocaleFiles(bucket: string) { case "xcode-stringsdict": return findLocaleFilesForFilename("Localizable.stringsdict"); default: - throw new Error(`Unsupported bucket type: ${bucket}`); + return null; } } diff --git a/packages/spec/src/formats.ts b/packages/spec/src/formats.ts index 9cf2f55de..b78ebdba8 100644 --- a/packages/spec/src/formats.ts +++ b/packages/spec/src/formats.ts @@ -22,6 +22,7 @@ export const bucketTypes = [ "vtt", "php", "po", + "vue-json", ] as const; export const bucketTypeSchema = Z.enum(bucketTypes);