Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
5 changes: 5 additions & 0 deletions .changeset/nice-cars-cover.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
---
"lingo.dev": patch
---

fix --frozen flag
51 changes: 44 additions & 7 deletions packages/cli/src/cli/cmd/i18n.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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.");
Expand Down
41 changes: 21 additions & 20 deletions packages/cli/src/cli/loaders/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -32,6 +32,7 @@ import createVueJsonLoader from "./vue-json";

type BucketLoaderOptions = {
isCacheRestore: boolean;
returnUnlocalizedKeys?: boolean;
defaultLocale: string;
};

Expand All @@ -49,23 +50,23 @@ export default function createBucketLoader(
createAndroidLoader(),
createFlatLoader(),
createSyncLoader(),
createUnlocalizableLoader(options.isCacheRestore),
createUnlocalizableLoader(options.isCacheRestore, options.returnUnlocalizedKeys),
);
case "csv":
return composeLoaders(
createTextFileLoader(bucketPathPattern),
createCsvLoader(),
createFlatLoader(),
createSyncLoader(),
createUnlocalizableLoader(options.isCacheRestore),
createUnlocalizableLoader(options.isCacheRestore, options.returnUnlocalizedKeys),
);
case "html":
return composeLoaders(
createTextFileLoader(bucketPathPattern),
createPrettierLoader({ parser: "html", alwaysFormat: true }),
createHtmlLoader(),
createSyncLoader(),
createUnlocalizableLoader(options.isCacheRestore),
createUnlocalizableLoader(options.isCacheRestore, options.returnUnlocalizedKeys),
);
case "json":
return composeLoaders(
Expand All @@ -74,46 +75,46 @@ export default function createBucketLoader(
createJsonLoader(),
createFlatLoader(),
createSyncLoader(),
createUnlocalizableLoader(options.isCacheRestore),
createUnlocalizableLoader(options.isCacheRestore, options.returnUnlocalizedKeys),
);
case "markdown":
return composeLoaders(
createTextFileLoader(bucketPathPattern),
createPrettierLoader({ parser: "markdown" }),
createMarkdownLoader(),
createSyncLoader(),
createUnlocalizableLoader(options.isCacheRestore),
createUnlocalizableLoader(options.isCacheRestore, options.returnUnlocalizedKeys),
);
case "po":
return composeLoaders(
createTextFileLoader(bucketPathPattern),
createPoLoader(),
createFlatLoader(),
createSyncLoader(),
createUnlocalizableLoader(options.isCacheRestore),
createUnlocalizableLoader(options.isCacheRestore, options.returnUnlocalizedKeys),
createVariableLoader({ type: "python" }),
);
case "properties":
return composeLoaders(
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(
createTextFileLoader(bucketPathPattern),
createXcodeStringsdictLoader(),
createFlatLoader(),
createSyncLoader(),
createUnlocalizableLoader(options.isCacheRestore),
createUnlocalizableLoader(options.isCacheRestore, options.returnUnlocalizedKeys),
);
case "xcode-xcstrings":
return composeLoaders(
Expand All @@ -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":
Expand All @@ -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(
Expand All @@ -143,7 +144,7 @@ export default function createBucketLoader(
createRootKeyLoader(true),
createFlatLoader(),
createSyncLoader(),
createUnlocalizableLoader(options.isCacheRestore),
createUnlocalizableLoader(options.isCacheRestore, options.returnUnlocalizedKeys),
);
case "flutter":
return composeLoaders(
Expand All @@ -153,60 +154,60 @@ export default function createBucketLoader(
createFlutterLoader(),
createFlatLoader(),
createSyncLoader(),
createUnlocalizableLoader(options.isCacheRestore),
createUnlocalizableLoader(options.isCacheRestore, options.returnUnlocalizedKeys),
);
case "xliff":
return composeLoaders(
createTextFileLoader(bucketPathPattern),
createXliffLoader(),
createFlatLoader(),
createSyncLoader(),
createUnlocalizableLoader(options.isCacheRestore),
createUnlocalizableLoader(options.isCacheRestore, options.returnUnlocalizedKeys),
);
case "xml":
return composeLoaders(
createTextFileLoader(bucketPathPattern),
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(
createTextFileLoader(bucketPathPattern),
createPhpLoader(),
createSyncLoader(),
createFlatLoader(),
createUnlocalizableLoader(options.isCacheRestore),
createUnlocalizableLoader(options.isCacheRestore, options.returnUnlocalizedKeys),
);
case "vue-json":
return composeLoaders(
createTextFileLoader(bucketPathPattern),
createVueJsonLoader(),
createSyncLoader(),
createFlatLoader(),
createUnlocalizableLoader(),
createUnlocalizableLoader(options.isCacheRestore, options.returnUnlocalizedKeys),
);
}
}
84 changes: 65 additions & 19 deletions packages/cli/src/cli/loaders/unlocalizable.spec.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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);
});
});
});
});
6 changes: 6 additions & 0 deletions packages/cli/src/cli/loaders/unlocalizable.ts
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,7 @@ import { createLoader } from "./_utils";

export default function createUnlocalizableLoader(
isCacheRestore: boolean = false,
returnUnlocalizedKeys: boolean = false,
): ILoader<Record<string, any>, Record<string, any>> {
const rules = {
isEmpty: (v: any) => _.isEmpty(v),
Expand All @@ -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) {
Expand Down