diff --git a/.changeset/nice-cars-cover.md b/.changeset/nice-cars-cover.md new file mode 100644 index 000000000..8cf1ec724 --- /dev/null +++ b/.changeset/nice-cars-cover.md @@ -0,0 +1,5 @@ +--- +"lingo.dev": patch +--- + +fix --frozen flag diff --git a/packages/cli/src/cli/cmd/i18n.ts b/packages/cli/src/cli/cmd/i18n.ts index 11c133455..3be84a710 100644 --- a/packages/cli/src/cli/cmd/i18n.ts +++ b/packages/cli/src/cli/cmd/i18n.ts @@ -151,31 +151,68 @@ export default new Command() if (flags.frozen) { ora.start("Checking for lockfile updates..."); - let requiresUpdate = false; - for (const bucket of buckets) { + let requiresUpdate: string | null = null; + bucketLoop: for (const bucket of buckets) { for (const bucketConfig of bucket.config) { const sourceLocale = resolveOverridenLocale(i18nConfig!.locale.source, bucketConfig.delimiter); const bucketLoader = createBucketLoader(bucket.type, bucketConfig.pathPattern, { isCacheRestore: false, defaultLocale: sourceLocale, + returnUnlocalizedKeys: true, }); bucketLoader.setDefaultLocale(sourceLocale); await bucketLoader.init(); - const sourceData = await bucketLoader.pull(i18nConfig!.locale.source); + const { unlocalizable: sourceUnlocalizable, ...sourceData } = await bucketLoader.pull( + i18nConfig!.locale.source, + ); const updatedSourceData = lockfileHelper.extractUpdatedData(bucketConfig.pathPattern, sourceData); + // translation was updated in the source file if (Object.keys(updatedSourceData).length > 0) { - requiresUpdate = true; - break; + requiresUpdate = "updated"; + break bucketLoop; + } + + for (const _targetLocale of targetLocales) { + const targetLocale = resolveOverridenLocale(_targetLocale, bucketConfig.delimiter); + const { unlocalizable: targetUnlocalizable, ...targetData } = await bucketLoader.pull(targetLocale); + + const missingKeys = _.difference(Object.keys(sourceData), Object.keys(targetData)); + const extraKeys = _.difference(Object.keys(targetData), Object.keys(sourceData)); + const unlocalizableDataDiff = !_.isEqual(sourceUnlocalizable, targetUnlocalizable); + + // translation is missing in the target file + if (missingKeys.length > 0) { + requiresUpdate = "missing"; + break bucketLoop; + } + + // target file has extra translations + if (extraKeys.length > 0) { + requiresUpdate = "extra"; + break bucketLoop; + } + + // unlocalizable keys do not match + if (unlocalizableDataDiff) { + requiresUpdate = "unlocalizable"; + break bucketLoop; + } } } - if (requiresUpdate) break; } if (requiresUpdate) { - ora.fail("Localization data has changed; please update i18n.lock or run without --frozen."); + const message = { + updated: "Source file has been updated.", + missing: "Target file is missing translations.", + extra: "Target file has extra translations not present in the source file.", + unlocalizable: "Unlocalizable data (such as booleans, dates, URLs, etc.) do not match.", + }[requiresUpdate]; + ora.fail(`Localization data has changed; please update i18n.lock or run without --frozen.`); + ora.fail(` Details: ${message}`); process.exit(1); } else { ora.succeed("No lockfile updates required."); diff --git a/packages/cli/src/cli/loaders/index.ts b/packages/cli/src/cli/loaders/index.ts index f04b7dfd2..0ac1ad9eb 100644 --- a/packages/cli/src/cli/loaders/index.ts +++ b/packages/cli/src/cli/loaders/index.ts @@ -32,6 +32,7 @@ import createVueJsonLoader from "./vue-json"; type BucketLoaderOptions = { isCacheRestore: boolean; + returnUnlocalizedKeys?: boolean; defaultLocale: string; }; @@ -49,7 +50,7 @@ export default function createBucketLoader( createAndroidLoader(), createFlatLoader(), createSyncLoader(), - createUnlocalizableLoader(options.isCacheRestore), + createUnlocalizableLoader(options.isCacheRestore, options.returnUnlocalizedKeys), ); case "csv": return composeLoaders( @@ -57,7 +58,7 @@ export default function createBucketLoader( createCsvLoader(), createFlatLoader(), createSyncLoader(), - createUnlocalizableLoader(options.isCacheRestore), + createUnlocalizableLoader(options.isCacheRestore, options.returnUnlocalizedKeys), ); case "html": return composeLoaders( @@ -65,7 +66,7 @@ export default function createBucketLoader( createPrettierLoader({ parser: "html", alwaysFormat: true }), createHtmlLoader(), createSyncLoader(), - createUnlocalizableLoader(options.isCacheRestore), + createUnlocalizableLoader(options.isCacheRestore, options.returnUnlocalizedKeys), ); case "json": return composeLoaders( @@ -74,7 +75,7 @@ export default function createBucketLoader( createJsonLoader(), createFlatLoader(), createSyncLoader(), - createUnlocalizableLoader(options.isCacheRestore), + createUnlocalizableLoader(options.isCacheRestore, options.returnUnlocalizedKeys), ); case "markdown": return composeLoaders( @@ -82,7 +83,7 @@ export default function createBucketLoader( createPrettierLoader({ parser: "markdown" }), createMarkdownLoader(), createSyncLoader(), - createUnlocalizableLoader(options.isCacheRestore), + createUnlocalizableLoader(options.isCacheRestore, options.returnUnlocalizedKeys), ); case "po": return composeLoaders( @@ -90,7 +91,7 @@ export default function createBucketLoader( createPoLoader(), createFlatLoader(), createSyncLoader(), - createUnlocalizableLoader(options.isCacheRestore), + createUnlocalizableLoader(options.isCacheRestore, options.returnUnlocalizedKeys), createVariableLoader({ type: "python" }), ); case "properties": @@ -98,14 +99,14 @@ export default function createBucketLoader( createTextFileLoader(bucketPathPattern), createPropertiesLoader(), createSyncLoader(), - createUnlocalizableLoader(options.isCacheRestore), + createUnlocalizableLoader(options.isCacheRestore, options.returnUnlocalizedKeys), ); case "xcode-strings": return composeLoaders( createTextFileLoader(bucketPathPattern), createXcodeStringsLoader(), createSyncLoader(), - createUnlocalizableLoader(options.isCacheRestore), + createUnlocalizableLoader(options.isCacheRestore, options.returnUnlocalizedKeys), ); case "xcode-stringsdict": return composeLoaders( @@ -113,7 +114,7 @@ export default function createBucketLoader( createXcodeStringsdictLoader(), createFlatLoader(), createSyncLoader(), - createUnlocalizableLoader(options.isCacheRestore), + createUnlocalizableLoader(options.isCacheRestore, options.returnUnlocalizedKeys), ); case "xcode-xcstrings": return composeLoaders( @@ -123,7 +124,7 @@ export default function createBucketLoader( createXcodeXcstringsLoader(options.defaultLocale), createFlatLoader(), createSyncLoader(), - createUnlocalizableLoader(options.isCacheRestore), + createUnlocalizableLoader(options.isCacheRestore, options.returnUnlocalizedKeys), createVariableLoader({ type: "ieee" }), ); case "yaml": @@ -133,7 +134,7 @@ export default function createBucketLoader( createYamlLoader(), createFlatLoader(), createSyncLoader(), - createUnlocalizableLoader(options.isCacheRestore), + createUnlocalizableLoader(options.isCacheRestore, options.returnUnlocalizedKeys), ); case "yaml-root-key": return composeLoaders( @@ -143,7 +144,7 @@ export default function createBucketLoader( createRootKeyLoader(true), createFlatLoader(), createSyncLoader(), - createUnlocalizableLoader(options.isCacheRestore), + createUnlocalizableLoader(options.isCacheRestore, options.returnUnlocalizedKeys), ); case "flutter": return composeLoaders( @@ -153,7 +154,7 @@ export default function createBucketLoader( createFlutterLoader(), createFlatLoader(), createSyncLoader(), - createUnlocalizableLoader(options.isCacheRestore), + createUnlocalizableLoader(options.isCacheRestore, options.returnUnlocalizedKeys), ); case "xliff": return composeLoaders( @@ -161,7 +162,7 @@ export default function createBucketLoader( createXliffLoader(), createFlatLoader(), createSyncLoader(), - createUnlocalizableLoader(options.isCacheRestore), + createUnlocalizableLoader(options.isCacheRestore, options.returnUnlocalizedKeys), ); case "xml": return composeLoaders( @@ -169,28 +170,28 @@ export default function createBucketLoader( createXmlLoader(), createFlatLoader(), createSyncLoader(), - createUnlocalizableLoader(options.isCacheRestore), + createUnlocalizableLoader(options.isCacheRestore, options.returnUnlocalizedKeys), ); case "srt": return composeLoaders( createTextFileLoader(bucketPathPattern), createSrtLoader(), createSyncLoader(), - createUnlocalizableLoader(options.isCacheRestore), + createUnlocalizableLoader(options.isCacheRestore, options.returnUnlocalizedKeys), ); case "dato": return composeLoaders( createDatoLoader(bucketPathPattern), createSyncLoader(), createFlatLoader(), - createUnlocalizableLoader(options.isCacheRestore), + createUnlocalizableLoader(options.isCacheRestore, options.returnUnlocalizedKeys), ); case "vtt": return composeLoaders( createTextFileLoader(bucketPathPattern), createVttLoader(), createSyncLoader(), - createUnlocalizableLoader(options.isCacheRestore), + createUnlocalizableLoader(options.isCacheRestore, options.returnUnlocalizedKeys), ); case "php": return composeLoaders( @@ -198,7 +199,7 @@ export default function createBucketLoader( createPhpLoader(), createSyncLoader(), createFlatLoader(), - createUnlocalizableLoader(options.isCacheRestore), + createUnlocalizableLoader(options.isCacheRestore, options.returnUnlocalizedKeys), ); case "vue-json": return composeLoaders( @@ -206,7 +207,7 @@ export default function createBucketLoader( createVueJsonLoader(), createSyncLoader(), createFlatLoader(), - createUnlocalizableLoader(), + createUnlocalizableLoader(options.isCacheRestore, options.returnUnlocalizedKeys), ); } } diff --git a/packages/cli/src/cli/loaders/unlocalizable.spec.ts b/packages/cli/src/cli/loaders/unlocalizable.spec.ts index cdb6c14f1..29ed846d1 100644 --- a/packages/cli/src/cli/loaders/unlocalizable.spec.ts +++ b/packages/cli/src/cli/loaders/unlocalizable.spec.ts @@ -16,30 +16,76 @@ describe("unlocalizable loader", () => { systemId: "Ab1cdefghijklmnopqrst2", }; - describe.each([true, false])("cache restoration '%s'", (cacheRestoration) => { - it("should remove unlocalizable keys on pull", async () => { - const loader = createUnlocalizableLoader(cacheRestoration); - loader.setDefaultLocale("en"); - const result = await loader.pull("en", data); - - expect(result).toEqual({ - foo: "bar", - numStr: "1.0", - boolStr: "false", - bar: "foo", + describe("cache restoration", () => { + describe.each([true, false])("%s", (cacheRestoration) => { + it("should remove unlocalizable keys on pull", async () => { + const loader = createUnlocalizableLoader(cacheRestoration); + loader.setDefaultLocale("en"); + const result = await loader.pull("en", data); + + expect(result).toEqual({ + foo: "bar", + numStr: "1.0", + boolStr: "false", + bar: "foo", + }); + }); + + it("should handle unlocalizable keys on push", async () => { + const pushData = { foo: "bar-es", bar: "foo-es" }; + + const loader = createUnlocalizableLoader(cacheRestoration); + loader.setDefaultLocale("en"); + await loader.pull("en", data); + const result = await loader.push("es", pushData); + + const expectedData = cacheRestoration ? { ...pushData } : { ...data, ...pushData }; + expect(result).toEqual(expectedData); }); }); + }); - it("should handle unlocalizable keys on push", async () => { - const pushData = { foo: "bar-es", bar: "foo-es" }; + describe("return unlocalizable keys", () => { + describe.each([true, false])("%s", (returnUnlocalizedKeys) => { + it("should return unlocalizable keys on pull", async () => { + const loader = createUnlocalizableLoader(false, returnUnlocalizedKeys); + loader.setDefaultLocale("en"); + const result = await loader.pull("en", data); - const loader = createUnlocalizableLoader(cacheRestoration); - loader.setDefaultLocale("en"); - await loader.pull("en", data); - const result = await loader.push("es", pushData); + const extraUnlocalizableData = returnUnlocalizedKeys + ? { + unlocalizable: { + num: 1, + empty: "", + bool: true, + isoDate: "2025-02-21", + isoDateTime: "2025-02-21T00:00:00.000Z", + url: "https://example.com", + systemId: "Ab1cdefghijklmnopqrst2", + }, + } + : {}; - const expectedData = cacheRestoration ? { ...pushData } : { ...data, ...pushData }; - expect(result).toEqual(expectedData); + expect(result).toEqual({ + foo: "bar", + numStr: "1.0", + boolStr: "false", + bar: "foo", + ...extraUnlocalizableData, + }); + }); + + it("should not affect push", async () => { + const pushData = { foo: "bar-es", bar: "foo-es" }; + + const loader = createUnlocalizableLoader(false, returnUnlocalizedKeys); + loader.setDefaultLocale("en"); + await loader.pull("en", data); + const result = await loader.push("es", pushData); + + const expectedData = { ...data, ...pushData }; + expect(result).toEqual(expectedData); + }); }); }); }); diff --git a/packages/cli/src/cli/loaders/unlocalizable.ts b/packages/cli/src/cli/loaders/unlocalizable.ts index fea298521..06a962df5 100644 --- a/packages/cli/src/cli/loaders/unlocalizable.ts +++ b/packages/cli/src/cli/loaders/unlocalizable.ts @@ -7,6 +7,7 @@ import { createLoader } from "./_utils"; export default function createUnlocalizableLoader( isCacheRestore: boolean = false, + returnUnlocalizedKeys: boolean = false, ): ILoader, Record> { const rules = { isEmpty: (v: any) => _.isEmpty(v), @@ -31,6 +32,11 @@ export default function createUnlocalizableLoader( .map(([key, _]) => key); const result = _.omitBy(input, (_, key) => passthroughKeys.includes(key)); + + if (returnUnlocalizedKeys) { + result.unlocalizable = _.omitBy(input, (_, key) => !passthroughKeys.includes(key)); + } + return result; }, async push(locale, data, originalInput) {