Skip to content

Commit 1dbbfd2

Browse files
authored
feat(cli): inject locale code into locale files (#605)
Add array of keys as "injectLocale" to JSON bucket config.
1 parent 27964ba commit 1dbbfd2

File tree

11 files changed

+140
-47
lines changed

11 files changed

+140
-47
lines changed
Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,6 @@
1+
---
2+
"@lingo.dev/_spec": patch
3+
"lingo.dev": patch
4+
---
5+
6+
inject locale

packages/cli/src/cli/cmd/cleanup.ts

Lines changed: 5 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -14,9 +14,10 @@ export default new Command()
1414
.option("--locale <locale>", "Specific locale to cleanup")
1515
.option("--bucket <bucket>", "Specific bucket to cleanup")
1616
.option("--dry-run", "Show what would be removed without making changes")
17-
.option("--verbose", "Show detailed output including:\n" +
18-
" - List of keys that would be removed.\n" +
19-
" - Processing steps.")
17+
.option(
18+
"--verbose",
19+
"Show detailed output including:\n" + " - List of keys that would be removed.\n" + " - Processing steps.",
20+
)
2021
.action(async function (options) {
2122
const ora = Ora();
2223
const results: any = [];
@@ -39,7 +40,7 @@ export default new Command()
3940
console.log();
4041
ora.info(`Processing bucket: ${bucket.type}`);
4142

42-
for (const bucketConfig of bucket.config) {
43+
for (const bucketConfig of bucket.paths) {
4344
const sourceLocale = resolveOverriddenLocale(i18nConfig!.locale.source, bucketConfig.delimiter);
4445
const bucketOra = Ora({ indent: 2 }).info(`Processing path: ${bucketConfig.pathPattern}`);
4546
const bucketLoader = createBucketLoader(bucket.type, bucketConfig.pathPattern, {

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

Lines changed: 32 additions & 30 deletions
Original file line numberDiff line numberDiff line change
@@ -83,21 +83,19 @@ export default new Command()
8383
if (flags.file?.length) {
8484
buckets = buckets
8585
.map((bucket: any) => {
86-
const config = bucket.config.filter((config: any) =>
87-
flags.file!.find((file) => config.pathPattern?.match(file)),
88-
);
89-
return { ...bucket, config };
86+
const paths = bucket.paths.filter((path: any) => flags.file!.find((file) => path.pathPattern?.match(file)));
87+
return { ...bucket, paths };
9088
})
91-
.filter((bucket: any) => bucket.config.length > 0);
89+
.filter((bucket: any) => bucket.paths.length > 0);
9290
if (buckets.length === 0) {
9391
ora.fail("No buckets found. All buckets were filtered out by --file option.");
9492
process.exit(1);
9593
} else {
9694
ora.info(`\x1b[36mProcessing only filtered buckets:\x1b[0m`);
9795
buckets.map((bucket: any) => {
9896
ora.info(` ${bucket.type}:`);
99-
bucket.config.forEach((config: any) => {
100-
ora.info(` - ${config.pathPattern}`);
97+
bucket.paths.forEach((path: any) => {
98+
ora.info(` - ${path.pathPattern}`);
10199
});
102100
});
103101
}
@@ -111,18 +109,19 @@ export default new Command()
111109
if (!lockfileHelper.isLockfileExists()) {
112110
ora.start("Creating i18n.lock...");
113111
for (const bucket of buckets) {
114-
for (const bucketConfig of bucket.config) {
115-
const sourceLocale = resolveOverriddenLocale(i18nConfig!.locale.source, bucketConfig.delimiter);
112+
for (const bucketPath of bucket.paths) {
113+
const sourceLocale = resolveOverriddenLocale(i18nConfig!.locale.source, bucketPath.delimiter);
116114

117-
const bucketLoader = createBucketLoader(bucket.type, bucketConfig.pathPattern, {
115+
const bucketLoader = createBucketLoader(bucket.type, bucketPath.pathPattern, {
118116
isCacheRestore: false,
119117
defaultLocale: sourceLocale,
118+
injectLocale: bucket.injectLocale,
120119
});
121120
bucketLoader.setDefaultLocale(sourceLocale);
122121
await bucketLoader.init();
123122

124123
const sourceData = await bucketLoader.pull(i18nConfig!.locale.source);
125-
lockfileHelper.registerSourceData(bucketConfig.pathPattern, sourceData);
124+
lockfileHelper.registerSourceData(bucketPath.pathPattern, sourceData);
126125
}
127126
}
128127
ora.succeed("i18n.lock created");
@@ -139,14 +138,15 @@ export default new Command()
139138

140139
for (const bucket of buckets) {
141140
cacheOra.info(`Processing bucket: ${bucket.type}`);
142-
for (const bucketConfig of bucket.config) {
141+
for (const bucketPath of bucket.paths) {
143142
const bucketOra = Ora({ indent: 4 });
144-
bucketOra.info(`Processing path: ${bucketConfig.pathPattern}`);
143+
bucketOra.info(`Processing path: ${bucketPath.pathPattern}`);
145144

146-
const sourceLocale = resolveOverriddenLocale(i18nConfig!.locale.source, bucketConfig.delimiter);
147-
const bucketLoader = createBucketLoader(bucket.type, bucketConfig.pathPattern, {
145+
const sourceLocale = resolveOverriddenLocale(i18nConfig!.locale.source, bucketPath.delimiter);
146+
const bucketLoader = createBucketLoader(bucket.type, bucketPath.pathPattern, {
148147
isCacheRestore: true,
149148
defaultLocale: sourceLocale,
149+
injectLocale: bucket.injectLocale,
150150
});
151151
bucketLoader.setDefaultLocale(sourceLocale);
152152
await bucketLoader.init();
@@ -166,7 +166,7 @@ export default new Command()
166166
}
167167

168168
await bucketLoader.push(targetLocale, targetData);
169-
lockfileHelper.registerPartialSourceData(bucketConfig.pathPattern, cachedSourceData);
169+
lockfileHelper.registerPartialSourceData(bucketPath.pathPattern, cachedSourceData);
170170

171171
bucketOra.succeed(
172172
`[${sourceLocale} -> ${targetLocale}] Recovered ${Object.keys(cachedSourceData).length} entries from cache`,
@@ -186,21 +186,22 @@ export default new Command()
186186
ora.start("Checking for lockfile updates...");
187187
let requiresUpdate: string | null = null;
188188
bucketLoop: for (const bucket of buckets) {
189-
for (const bucketConfig of bucket.config) {
190-
const sourceLocale = resolveOverriddenLocale(i18nConfig!.locale.source, bucketConfig.delimiter);
189+
for (const bucketPath of bucket.paths) {
190+
const sourceLocale = resolveOverriddenLocale(i18nConfig!.locale.source, bucketPath.delimiter);
191191

192-
const bucketLoader = createBucketLoader(bucket.type, bucketConfig.pathPattern, {
192+
const bucketLoader = createBucketLoader(bucket.type, bucketPath.pathPattern, {
193193
isCacheRestore: false,
194194
defaultLocale: sourceLocale,
195195
returnUnlocalizedKeys: true,
196+
injectLocale: bucket.injectLocale,
196197
});
197198
bucketLoader.setDefaultLocale(sourceLocale);
198199
await bucketLoader.init();
199200

200201
const { unlocalizable: sourceUnlocalizable, ...sourceData } = await bucketLoader.pull(
201202
i18nConfig!.locale.source,
202203
);
203-
const updatedSourceData = lockfileHelper.extractUpdatedData(bucketConfig.pathPattern, sourceData);
204+
const updatedSourceData = lockfileHelper.extractUpdatedData(bucketPath.pathPattern, sourceData);
204205

205206
// translation was updated in the source file
206207
if (Object.keys(updatedSourceData).length > 0) {
@@ -209,7 +210,7 @@ export default new Command()
209210
}
210211

211212
for (const _targetLocale of targetLocales) {
212-
const targetLocale = resolveOverriddenLocale(_targetLocale, bucketConfig.delimiter);
213+
const targetLocale = resolveOverriddenLocale(_targetLocale, bucketPath.delimiter);
213214
const { unlocalizable: targetUnlocalizable, ...targetData } = await bucketLoader.pull(targetLocale);
214215

215216
const missingKeys = _.difference(Object.keys(sourceData), Object.keys(targetData));
@@ -257,29 +258,30 @@ export default new Command()
257258
try {
258259
console.log();
259260
ora.info(`Processing bucket: ${bucket.type}`);
260-
for (const bucketConfig of bucket.config) {
261-
const bucketOra = Ora({ indent: 2 }).info(`Processing path: ${bucketConfig.pathPattern}`);
261+
for (const bucketPath of bucket.paths) {
262+
const bucketOra = Ora({ indent: 2 }).info(`Processing path: ${bucketPath.pathPattern}`);
262263

263-
const sourceLocale = resolveOverriddenLocale(i18nConfig!.locale.source, bucketConfig.delimiter);
264+
const sourceLocale = resolveOverriddenLocale(i18nConfig!.locale.source, bucketPath.delimiter);
264265

265-
const bucketLoader = createBucketLoader(bucket.type, bucketConfig.pathPattern, {
266+
const bucketLoader = createBucketLoader(bucket.type, bucketPath.pathPattern, {
266267
isCacheRestore: false,
267268
defaultLocale: sourceLocale,
269+
injectLocale: bucket.injectLocale,
268270
});
269271
bucketLoader.setDefaultLocale(sourceLocale);
270272
await bucketLoader.init();
271273
let sourceData = await bucketLoader.pull(sourceLocale);
272274

273275
for (const _targetLocale of targetLocales) {
274-
const targetLocale = resolveOverriddenLocale(_targetLocale, bucketConfig.delimiter);
276+
const targetLocale = resolveOverriddenLocale(_targetLocale, bucketPath.delimiter);
275277
try {
276278
bucketOra.start(`[${sourceLocale} -> ${targetLocale}] (0%) Localization in progress...`);
277279

278280
sourceData = await bucketLoader.pull(sourceLocale);
279281

280282
const updatedSourceData = flags.force
281283
? sourceData
282-
: lockfileHelper.extractUpdatedData(bucketConfig.pathPattern, sourceData);
284+
: lockfileHelper.extractUpdatedData(bucketPath.pathPattern, sourceData);
283285

284286
const targetData = await bucketLoader.pull(targetLocale);
285287
let processableData = calculateDataDelta({
@@ -333,7 +335,7 @@ export default new Command()
333335
if (flags.interactive) {
334336
bucketOra.stop();
335337
const reviewedData = await reviewChanges({
336-
pathPattern: bucketConfig.pathPattern,
338+
pathPattern: bucketPath.pathPattern,
337339
targetLocale,
338340
currentData: targetData,
339341
proposedData: finalTargetData,
@@ -342,7 +344,7 @@ export default new Command()
342344
});
343345

344346
finalTargetData = reviewedData;
345-
bucketOra.start(`Applying changes to ${bucketConfig} (${targetLocale})`);
347+
bucketOra.start(`Applying changes to ${bucketPath} (${targetLocale})`);
346348
}
347349

348350
const finalDiffSize = _.chain(finalTargetData)
@@ -369,7 +371,7 @@ export default new Command()
369371
}
370372
}
371373

372-
lockfileHelper.registerSourceData(bucketConfig.pathPattern, sourceData);
374+
lockfileHelper.registerSourceData(bucketPath.pathPattern, sourceData);
373375
}
374376
} catch (_error: any) {
375377
const error = new Error(`Failed to process bucket ${bucket.type}: ${_error.message}`);

packages/cli/src/cli/cmd/lockfile.ts

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -24,7 +24,7 @@ export default new Command()
2424
const buckets = getBuckets(i18nConfig!);
2525

2626
for (const bucket of buckets) {
27-
for (const bucketConfig of bucket.config) {
27+
for (const bucketConfig of bucket.paths) {
2828
const sourceLocale = resolveOverriddenLocale(i18nConfig!.locale.source, bucketConfig.delimiter);
2929
const bucketLoader = createBucketLoader(bucket.type, bucketConfig.pathPattern, {
3030
isCacheRestore: false,

packages/cli/src/cli/cmd/show/files.ts

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -27,7 +27,7 @@ export default new Command()
2727

2828
const buckets = getBuckets(i18nConfig);
2929
for (const bucket of buckets) {
30-
for (const bucketConfig of bucket.config) {
30+
for (const bucketConfig of bucket.paths) {
3131
const sourceLocale = resolveOverriddenLocale(i18nConfig.locale.source, bucketConfig.delimiter);
3232
const sourcePath = bucketConfig.pathPattern.replace(/\[locale\]/g, sourceLocale);
3333
const targetPaths = i18nConfig.locale.targets.map((_targetLocale) => {

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

Lines changed: 39 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -466,6 +466,45 @@ describe("bucket loaders", () => {
466466
expect(fs.access).toHaveBeenCalledWith("i18n/en/en.json");
467467
expect(fs.writeFile).toHaveBeenCalledWith("i18n/es/es.json", expectedOutput, { encoding: "utf-8", flag: "w" });
468468
});
469+
470+
it("should remove injected locales from json data", async () => {
471+
setupFileMocks();
472+
473+
const input = { "button.title": "Submit", settings: { locale: "en" }, "not-a-locale": "bar" };
474+
mockFileOperations(JSON.stringify(input));
475+
476+
const jsonLoader = createBucketLoader("json", "i18n/[locale].json", {
477+
isCacheRestore: false,
478+
defaultLocale: "en",
479+
injectLocale: ["settings.locale", "not-a-locale"],
480+
});
481+
jsonLoader.setDefaultLocale("en");
482+
const data = await jsonLoader.pull("en");
483+
484+
expect(data).toEqual({ "button.title": "Submit", "not-a-locale": "bar" });
485+
});
486+
487+
it("should inject locales into json data", async () => {
488+
setupFileMocks();
489+
490+
const input = { "button.title": "Submit", "not-a-locale": "bar", settings: { locale: "en" } };
491+
const payload = { "button.title": "Enviar", "not-a-locale": "bar" };
492+
const expectedOutput = JSON.stringify({ ...payload, settings: { locale: "es" } }, null, 2);
493+
494+
mockFileOperations(JSON.stringify(input));
495+
496+
const jsonLoader = createBucketLoader("json", "i18n/[locale].json", {
497+
isCacheRestore: false,
498+
defaultLocale: "en",
499+
injectLocale: ["settings.locale", "not-a-locale"],
500+
});
501+
jsonLoader.setDefaultLocale("en");
502+
await jsonLoader.pull("en");
503+
504+
await jsonLoader.push("es", payload);
505+
506+
expect(fs.writeFile).toHaveBeenCalledWith("i18n/es.json", expectedOutput, { encoding: "utf-8", flag: "w" });
507+
});
469508
});
470509

471510
describe("markdown bucket loader", () => {

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

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -29,11 +29,13 @@ import createSyncLoader from "./sync";
2929
import createPlutilJsonTextLoader from "./plutil-json-loader";
3030
import createPhpLoader from "./php";
3131
import createVueJsonLoader from "./vue-json";
32+
import createInjectLocaleLoader from "./inject-locale";
3233

3334
type BucketLoaderOptions = {
3435
isCacheRestore: boolean;
3536
returnUnlocalizedKeys?: boolean;
3637
defaultLocale: string;
38+
injectLocale?: string[];
3739
};
3840

3941
export default function createBucketLoader(
@@ -73,6 +75,7 @@ export default function createBucketLoader(
7375
createTextFileLoader(bucketPathPattern),
7476
createPrettierLoader({ parser: "json", bucketPathPattern }),
7577
createJsonLoader(),
78+
createInjectLocaleLoader(options.injectLocale),
7679
createFlatLoader(),
7780
createSyncLoader(),
7881
createUnlocalizableLoader(options.isCacheRestore, options.returnUnlocalizedKeys),
Lines changed: 31 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,31 @@
1+
import _ from "lodash";
2+
import { ILoader } from "./_types";
3+
import { createLoader } from "./_utils";
4+
5+
export default function createInjectLocaleLoader(
6+
injectLocaleKeys?: string[],
7+
): ILoader<Record<string, any>, Record<string, any>> {
8+
return createLoader({
9+
async pull(locale, data) {
10+
if (!injectLocaleKeys) {
11+
return data;
12+
}
13+
const omitKeys = injectLocaleKeys.filter((key) => {
14+
return _.get(data, key) === locale;
15+
});
16+
const result = _.omit(data, omitKeys);
17+
return result;
18+
},
19+
async push(locale, data, originalInput, originalLocale) {
20+
if (!injectLocaleKeys) {
21+
return data;
22+
}
23+
injectLocaleKeys.forEach((key) => {
24+
if (_.get(originalInput, key) === originalLocale) {
25+
_.set(data, key, locale);
26+
}
27+
});
28+
return data;
29+
},
30+
});
31+
}

packages/cli/src/cli/utils/buckets.spec.ts

Lines changed: 4 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -31,7 +31,7 @@ describe("getBuckets", () => {
3131
expect(buckets).toEqual([
3232
{
3333
type: "json",
34-
config: [
34+
paths: [
3535
{ pathPattern: "src/i18n/[locale].json", delimiter: null },
3636
{ pathPattern: "src/translations/[locale]/messages.json", delimiter: null },
3737
],
@@ -61,7 +61,7 @@ describe("getBuckets", () => {
6161
expect(buckets).toEqual([
6262
{
6363
type: "json",
64-
config: [
64+
paths: [
6565
{ pathPattern: "src/translations/landing.[locale].json", delimiter: null },
6666
{ pathPattern: "src/translations/app.[locale].json", delimiter: null },
6767
{ pathPattern: "src/translations/email.[locale].json", delimiter: null },
@@ -83,7 +83,7 @@ describe("getBuckets", () => {
8383
mockGlobSync(["src/i18n/en.json"]);
8484
const i18nConfig = makeI18nConfig([{ path: "src/i18n/[locale].json", delimiter: "-" }]);
8585
const buckets = getBuckets(i18nConfig);
86-
expect(buckets).toEqual([{ type: "json", config: [{ pathPattern: "src/i18n/[locale].json", delimiter: "-" }] }]);
86+
expect(buckets).toEqual([{ type: "json", paths: [{ pathPattern: "src/i18n/[locale].json", delimiter: "-" }] }]);
8787
});
8888

8989
it("should return bucket with multiple locale placeholders", () => {
@@ -96,7 +96,7 @@ describe("getBuckets", () => {
9696
expect(buckets).toEqual([
9797
{
9898
type: "json",
99-
config: [
99+
paths: [
100100
{ pathPattern: "src/i18n/[locale]/[locale].json", delimiter: null },
101101
{ pathPattern: "src/[locale]/translations/[locale]/messages.json", delimiter: null },
102102
],

packages/cli/src/cli/utils/buckets.ts

Lines changed: 13 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -2,18 +2,28 @@ import _ from "lodash";
22
import path from "path";
33
import { glob } from "glob";
44
import { CLIError } from "./errors";
5-
import { I18nConfig, resolveOverriddenLocale, BucketItem } from "@lingo.dev/_spec";
5+
import { I18nConfig, resolveOverriddenLocale, BucketItem, LocaleDelimiter } from "@lingo.dev/_spec";
66
import { bucketTypeSchema } from "@lingo.dev/_spec";
77
import Z from "zod";
88

9+
type BucketConfig = {
10+
type: Z.infer<typeof bucketTypeSchema>;
11+
paths: Array<{ pathPattern: string; delimiter?: LocaleDelimiter }>;
12+
injectLocale?: string[];
13+
};
14+
915
export function getBuckets(i18nConfig: I18nConfig) {
1016
const result = Object.entries(i18nConfig.buckets).map(([bucketType, bucketEntry]) => {
1117
const includeItems = bucketEntry.include.map((item) => resolveBucketItem(item));
1218
const excludeItems = bucketEntry.exclude?.map((item) => resolveBucketItem(item));
13-
return {
19+
const config: BucketConfig = {
1420
type: bucketType as Z.infer<typeof bucketTypeSchema>,
15-
config: extractPathPatterns(i18nConfig.locale.source, includeItems, excludeItems),
21+
paths: extractPathPatterns(i18nConfig.locale.source, includeItems, excludeItems),
1622
};
23+
if (bucketEntry.injectLocale) {
24+
config.injectLocale = bucketEntry.injectLocale;
25+
}
26+
return config;
1727
});
1828

1929
return result;

0 commit comments

Comments
 (0)