Skip to content

Commit 348dba8

Browse files
committed
fix(cli): --frozen flag
1 parent a404e2b commit 348dba8

File tree

5 files changed

+141
-46
lines changed

5 files changed

+141
-46
lines changed

.changeset/nice-cars-cover.md

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,5 @@
1+
---
2+
"lingo.dev": patch
3+
---
4+
5+
fix --frozen flag

packages/cli/src/cli/cmd/i18n.ts

Lines changed: 44 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -151,31 +151,68 @@ export default new Command()
151151

152152
if (flags.frozen) {
153153
ora.start("Checking for lockfile updates...");
154-
let requiresUpdate = false;
155-
for (const bucket of buckets) {
154+
let requiresUpdate: string | null = null;
155+
bucketLoop: for (const bucket of buckets) {
156156
for (const bucketConfig of bucket.config) {
157157
const sourceLocale = resolveOverridenLocale(i18nConfig!.locale.source, bucketConfig.delimiter);
158158

159159
const bucketLoader = createBucketLoader(bucket.type, bucketConfig.pathPattern, {
160160
isCacheRestore: false,
161161
defaultLocale: sourceLocale,
162+
returnUnlocalizedKeys: true,
162163
});
163164
bucketLoader.setDefaultLocale(sourceLocale);
164165
await bucketLoader.init();
165166

166-
const sourceData = await bucketLoader.pull(i18nConfig!.locale.source);
167+
const { unlocalizable: sourceUnlocalizable, ...sourceData } = await bucketLoader.pull(
168+
i18nConfig!.locale.source,
169+
);
167170
const updatedSourceData = lockfileHelper.extractUpdatedData(bucketConfig.pathPattern, sourceData);
168171

172+
// translation was updated in the source file
169173
if (Object.keys(updatedSourceData).length > 0) {
170-
requiresUpdate = true;
171-
break;
174+
requiresUpdate = "updated";
175+
break bucketLoop;
176+
}
177+
178+
for (const _targetLocale of targetLocales) {
179+
const targetLocale = resolveOverridenLocale(_targetLocale, bucketConfig.delimiter);
180+
const { unlocalizable: targetUnlocalizable, ...targetData } = await bucketLoader.pull(targetLocale);
181+
182+
const missingKeys = _.difference(Object.keys(sourceData), Object.keys(targetData));
183+
const extraKeys = _.difference(Object.keys(targetData), Object.keys(sourceData));
184+
const unlocalizableDataDiff = !_.isEqual(sourceUnlocalizable, targetUnlocalizable);
185+
186+
// translation is missing in the target file
187+
if (missingKeys.length > 0) {
188+
requiresUpdate = "missing";
189+
break bucketLoop;
190+
}
191+
192+
// target file has extra translations
193+
if (extraKeys.length > 0) {
194+
requiresUpdate = "extra";
195+
break bucketLoop;
196+
}
197+
198+
// unlocalizable keys do not match
199+
if (unlocalizableDataDiff) {
200+
requiresUpdate = "unlocalizable";
201+
break bucketLoop;
202+
}
172203
}
173204
}
174-
if (requiresUpdate) break;
175205
}
176206

177207
if (requiresUpdate) {
178-
ora.fail("Localization data has changed; please update i18n.lock or run without --frozen.");
208+
const message = {
209+
updated: "Source file has been updated.",
210+
missing: "Target file is missing translations.",
211+
extra: "Target file has extra translations not present in the source file.",
212+
unlocalizable: "Unlocalizable data (such as booleans, dates, URLs, etc.) do not match.",
213+
}[requiresUpdate];
214+
ora.fail(`Localization data has changed; please update i18n.lock or run without --frozen.`);
215+
ora.fail(` Details: ${message}`);
179216
process.exit(1);
180217
} else {
181218
ora.succeed("No lockfile updates required.");

packages/cli/src/cli/loaders/index.ts

Lines changed: 21 additions & 20 deletions
Original file line numberDiff line numberDiff line change
@@ -32,6 +32,7 @@ import createVueJsonLoader from "./vue-json";
3232

3333
type BucketLoaderOptions = {
3434
isCacheRestore: boolean;
35+
returnUnlocalizedKeys?: boolean;
3536
defaultLocale: string;
3637
};
3738

@@ -49,23 +50,23 @@ export default function createBucketLoader(
4950
createAndroidLoader(),
5051
createFlatLoader(),
5152
createSyncLoader(),
52-
createUnlocalizableLoader(options.isCacheRestore),
53+
createUnlocalizableLoader(options.isCacheRestore, options.returnUnlocalizedKeys),
5354
);
5455
case "csv":
5556
return composeLoaders(
5657
createTextFileLoader(bucketPathPattern),
5758
createCsvLoader(),
5859
createFlatLoader(),
5960
createSyncLoader(),
60-
createUnlocalizableLoader(options.isCacheRestore),
61+
createUnlocalizableLoader(options.isCacheRestore, options.returnUnlocalizedKeys),
6162
);
6263
case "html":
6364
return composeLoaders(
6465
createTextFileLoader(bucketPathPattern),
6566
createPrettierLoader({ parser: "html", alwaysFormat: true }),
6667
createHtmlLoader(),
6768
createSyncLoader(),
68-
createUnlocalizableLoader(options.isCacheRestore),
69+
createUnlocalizableLoader(options.isCacheRestore, options.returnUnlocalizedKeys),
6970
);
7071
case "json":
7172
return composeLoaders(
@@ -74,46 +75,46 @@ export default function createBucketLoader(
7475
createJsonLoader(),
7576
createFlatLoader(),
7677
createSyncLoader(),
77-
createUnlocalizableLoader(options.isCacheRestore),
78+
createUnlocalizableLoader(options.isCacheRestore, options.returnUnlocalizedKeys),
7879
);
7980
case "markdown":
8081
return composeLoaders(
8182
createTextFileLoader(bucketPathPattern),
8283
createPrettierLoader({ parser: "markdown" }),
8384
createMarkdownLoader(),
8485
createSyncLoader(),
85-
createUnlocalizableLoader(options.isCacheRestore),
86+
createUnlocalizableLoader(options.isCacheRestore, options.returnUnlocalizedKeys),
8687
);
8788
case "po":
8889
return composeLoaders(
8990
createTextFileLoader(bucketPathPattern),
9091
createPoLoader(),
9192
createFlatLoader(),
9293
createSyncLoader(),
93-
createUnlocalizableLoader(options.isCacheRestore),
94+
createUnlocalizableLoader(options.isCacheRestore, options.returnUnlocalizedKeys),
9495
createVariableLoader({ type: "python" }),
9596
);
9697
case "properties":
9798
return composeLoaders(
9899
createTextFileLoader(bucketPathPattern),
99100
createPropertiesLoader(),
100101
createSyncLoader(),
101-
createUnlocalizableLoader(options.isCacheRestore),
102+
createUnlocalizableLoader(options.isCacheRestore, options.returnUnlocalizedKeys),
102103
);
103104
case "xcode-strings":
104105
return composeLoaders(
105106
createTextFileLoader(bucketPathPattern),
106107
createXcodeStringsLoader(),
107108
createSyncLoader(),
108-
createUnlocalizableLoader(options.isCacheRestore),
109+
createUnlocalizableLoader(options.isCacheRestore, options.returnUnlocalizedKeys),
109110
);
110111
case "xcode-stringsdict":
111112
return composeLoaders(
112113
createTextFileLoader(bucketPathPattern),
113114
createXcodeStringsdictLoader(),
114115
createFlatLoader(),
115116
createSyncLoader(),
116-
createUnlocalizableLoader(options.isCacheRestore),
117+
createUnlocalizableLoader(options.isCacheRestore, options.returnUnlocalizedKeys),
117118
);
118119
case "xcode-xcstrings":
119120
return composeLoaders(
@@ -123,7 +124,7 @@ export default function createBucketLoader(
123124
createXcodeXcstringsLoader(options.defaultLocale),
124125
createFlatLoader(),
125126
createSyncLoader(),
126-
createUnlocalizableLoader(options.isCacheRestore),
127+
createUnlocalizableLoader(options.isCacheRestore, options.returnUnlocalizedKeys),
127128
createVariableLoader({ type: "ieee" }),
128129
);
129130
case "yaml":
@@ -133,7 +134,7 @@ export default function createBucketLoader(
133134
createYamlLoader(),
134135
createFlatLoader(),
135136
createSyncLoader(),
136-
createUnlocalizableLoader(options.isCacheRestore),
137+
createUnlocalizableLoader(options.isCacheRestore, options.returnUnlocalizedKeys),
137138
);
138139
case "yaml-root-key":
139140
return composeLoaders(
@@ -143,7 +144,7 @@ export default function createBucketLoader(
143144
createRootKeyLoader(true),
144145
createFlatLoader(),
145146
createSyncLoader(),
146-
createUnlocalizableLoader(options.isCacheRestore),
147+
createUnlocalizableLoader(options.isCacheRestore, options.returnUnlocalizedKeys),
147148
);
148149
case "flutter":
149150
return composeLoaders(
@@ -153,60 +154,60 @@ export default function createBucketLoader(
153154
createFlutterLoader(),
154155
createFlatLoader(),
155156
createSyncLoader(),
156-
createUnlocalizableLoader(options.isCacheRestore),
157+
createUnlocalizableLoader(options.isCacheRestore, options.returnUnlocalizedKeys),
157158
);
158159
case "xliff":
159160
return composeLoaders(
160161
createTextFileLoader(bucketPathPattern),
161162
createXliffLoader(),
162163
createFlatLoader(),
163164
createSyncLoader(),
164-
createUnlocalizableLoader(options.isCacheRestore),
165+
createUnlocalizableLoader(options.isCacheRestore, options.returnUnlocalizedKeys),
165166
);
166167
case "xml":
167168
return composeLoaders(
168169
createTextFileLoader(bucketPathPattern),
169170
createXmlLoader(),
170171
createFlatLoader(),
171172
createSyncLoader(),
172-
createUnlocalizableLoader(options.isCacheRestore),
173+
createUnlocalizableLoader(options.isCacheRestore, options.returnUnlocalizedKeys),
173174
);
174175
case "srt":
175176
return composeLoaders(
176177
createTextFileLoader(bucketPathPattern),
177178
createSrtLoader(),
178179
createSyncLoader(),
179-
createUnlocalizableLoader(options.isCacheRestore),
180+
createUnlocalizableLoader(options.isCacheRestore, options.returnUnlocalizedKeys),
180181
);
181182
case "dato":
182183
return composeLoaders(
183184
createDatoLoader(bucketPathPattern),
184185
createSyncLoader(),
185186
createFlatLoader(),
186-
createUnlocalizableLoader(options.isCacheRestore),
187+
createUnlocalizableLoader(options.isCacheRestore, options.returnUnlocalizedKeys),
187188
);
188189
case "vtt":
189190
return composeLoaders(
190191
createTextFileLoader(bucketPathPattern),
191192
createVttLoader(),
192193
createSyncLoader(),
193-
createUnlocalizableLoader(options.isCacheRestore),
194+
createUnlocalizableLoader(options.isCacheRestore, options.returnUnlocalizedKeys),
194195
);
195196
case "php":
196197
return composeLoaders(
197198
createTextFileLoader(bucketPathPattern),
198199
createPhpLoader(),
199200
createSyncLoader(),
200201
createFlatLoader(),
201-
createUnlocalizableLoader(options.isCacheRestore),
202+
createUnlocalizableLoader(options.isCacheRestore, options.returnUnlocalizedKeys),
202203
);
203204
case "vue-json":
204205
return composeLoaders(
205206
createTextFileLoader(bucketPathPattern),
206207
createVueJsonLoader(),
207208
createSyncLoader(),
208209
createFlatLoader(),
209-
createUnlocalizableLoader(),
210+
createUnlocalizableLoader(options.isCacheRestore, options.returnUnlocalizedKeys),
210211
);
211212
}
212213
}

packages/cli/src/cli/loaders/unlocalizable.spec.ts

Lines changed: 65 additions & 19 deletions
Original file line numberDiff line numberDiff line change
@@ -16,30 +16,76 @@ describe("unlocalizable loader", () => {
1616
systemId: "Ab1cdefghijklmnopqrst2",
1717
};
1818

19-
describe.each([true, false])("cache restoration '%s'", (cacheRestoration) => {
20-
it("should remove unlocalizable keys on pull", async () => {
21-
const loader = createUnlocalizableLoader(cacheRestoration);
22-
loader.setDefaultLocale("en");
23-
const result = await loader.pull("en", data);
24-
25-
expect(result).toEqual({
26-
foo: "bar",
27-
numStr: "1.0",
28-
boolStr: "false",
29-
bar: "foo",
19+
describe("cache restoration", () => {
20+
describe.each([true, false])("%s", (cacheRestoration) => {
21+
it("should remove unlocalizable keys on pull", async () => {
22+
const loader = createUnlocalizableLoader(cacheRestoration);
23+
loader.setDefaultLocale("en");
24+
const result = await loader.pull("en", data);
25+
26+
expect(result).toEqual({
27+
foo: "bar",
28+
numStr: "1.0",
29+
boolStr: "false",
30+
bar: "foo",
31+
});
32+
});
33+
34+
it("should handle unlocalizable keys on push", async () => {
35+
const pushData = { foo: "bar-es", bar: "foo-es" };
36+
37+
const loader = createUnlocalizableLoader(cacheRestoration);
38+
loader.setDefaultLocale("en");
39+
await loader.pull("en", data);
40+
const result = await loader.push("es", pushData);
41+
42+
const expectedData = cacheRestoration ? { ...pushData } : { ...data, ...pushData };
43+
expect(result).toEqual(expectedData);
3044
});
3145
});
46+
});
3247

33-
it("should handle unlocalizable keys on push", async () => {
34-
const pushData = { foo: "bar-es", bar: "foo-es" };
48+
describe("return unlocalizable keys", () => {
49+
describe.each([true, false])("%s", (returnUnlocalizedKeys) => {
50+
it("should return unlocalizable keys on pull", async () => {
51+
const loader = createUnlocalizableLoader(false, returnUnlocalizedKeys);
52+
loader.setDefaultLocale("en");
53+
const result = await loader.pull("en", data);
3554

36-
const loader = createUnlocalizableLoader(cacheRestoration);
37-
loader.setDefaultLocale("en");
38-
await loader.pull("en", data);
39-
const result = await loader.push("es", pushData);
55+
const extraUnlocalizableData = returnUnlocalizedKeys
56+
? {
57+
unlocalizable: {
58+
num: 1,
59+
empty: "",
60+
bool: true,
61+
isoDate: "2025-02-21",
62+
isoDateTime: "2025-02-21T00:00:00.000Z",
63+
url: "https://example.com",
64+
systemId: "Ab1cdefghijklmnopqrst2",
65+
},
66+
}
67+
: {};
4068

41-
const expectedData = cacheRestoration ? { ...pushData } : { ...data, ...pushData };
42-
expect(result).toEqual(expectedData);
69+
expect(result).toEqual({
70+
foo: "bar",
71+
numStr: "1.0",
72+
boolStr: "false",
73+
bar: "foo",
74+
...extraUnlocalizableData,
75+
});
76+
});
77+
78+
it("should not affect push", async () => {
79+
const pushData = { foo: "bar-es", bar: "foo-es" };
80+
81+
const loader = createUnlocalizableLoader(false, returnUnlocalizedKeys);
82+
loader.setDefaultLocale("en");
83+
await loader.pull("en", data);
84+
const result = await loader.push("es", pushData);
85+
86+
const expectedData = { ...data, ...pushData };
87+
expect(result).toEqual(expectedData);
88+
});
4389
});
4490
});
4591
});

packages/cli/src/cli/loaders/unlocalizable.ts

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -7,6 +7,7 @@ import { createLoader } from "./_utils";
77

88
export default function createUnlocalizableLoader(
99
isCacheRestore: boolean = false,
10+
returnUnlocalizedKeys: boolean = false,
1011
): ILoader<Record<string, any>, Record<string, any>> {
1112
const rules = {
1213
isEmpty: (v: any) => _.isEmpty(v),
@@ -31,6 +32,11 @@ export default function createUnlocalizableLoader(
3132
.map(([key, _]) => key);
3233

3334
const result = _.omitBy(input, (_, key) => passthroughKeys.includes(key));
35+
36+
if (returnUnlocalizedKeys) {
37+
result.unlocalizable = _.omitBy(input, (_, key) => !passthroughKeys.includes(key));
38+
}
39+
3440
return result;
3541
},
3642
async push(locale, data, originalInput) {

0 commit comments

Comments
 (0)