Skip to content

Commit 3f2aba9

Browse files
feat(cli): add ignoredKeys (#778)
1 parent e27638e commit 3f2aba9

7 files changed

Lines changed: 352 additions & 146 deletions

File tree

.changeset/odd-zebras-smile.md

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+
add ignoredKeys

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

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -184,6 +184,7 @@ export default new Command()
184184
},
185185
bucket.lockedKeys,
186186
bucket.lockedPatterns,
187+
bucket.ignoredKeys,
187188
);
188189
bucketLoader.setDefaultLocale(sourceLocale);
189190
await bucketLoader.init();
@@ -464,6 +465,7 @@ export default new Command()
464465
},
465466
bucket.lockedKeys,
466467
bucket.lockedPatterns,
468+
bucket.ignoredKeys,
467469
);
468470
bucketLoader.setDefaultLocale(sourceLocale);
469471
await bucketLoader.init();
Lines changed: 86 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,86 @@
1+
import { describe, it, expect } from "vitest";
2+
import createIgnoredKeysLoader from "./ignored-keys";
3+
4+
// Helper values
5+
const defaultLocale = "en";
6+
const targetLocale = "es";
7+
8+
// Common ignored keys list used across tests
9+
const IGNORED_KEYS = ["meta", "todo"];
10+
11+
/**
12+
* Creates a fresh loader instance with the default locale already set.
13+
*/
14+
function createLoader() {
15+
const loader = createIgnoredKeysLoader(IGNORED_KEYS);
16+
loader.setDefaultLocale(defaultLocale);
17+
return loader;
18+
}
19+
20+
describe("ignored-keys loader", () => {
21+
it("should omit the ignored keys when pulling the default locale", async () => {
22+
const loader = createLoader();
23+
const input = {
24+
greeting: "hello",
25+
meta: "some meta information",
26+
todo: "translation pending",
27+
};
28+
29+
const result = await loader.pull(defaultLocale, input);
30+
31+
expect(result).toEqual({ greeting: "hello" });
32+
});
33+
34+
it("should omit the ignored keys when pulling a target locale", async () => {
35+
const loader = createLoader();
36+
37+
// First pull for the default locale (required by createLoader)
38+
await loader.pull(defaultLocale, {
39+
greeting: "hello",
40+
meta: "meta en",
41+
});
42+
43+
// Now pull the target locale
44+
const targetInput = {
45+
greeting: "hola",
46+
meta: "meta es",
47+
todo: "todo es",
48+
};
49+
const result = await loader.pull(targetLocale, targetInput);
50+
51+
expect(result).toEqual({ greeting: "hola" });
52+
});
53+
54+
it("should merge the ignored keys back when pushing a target locale", async () => {
55+
const loader = createLoader();
56+
57+
// Initial pull for the default locale
58+
await loader.pull(defaultLocale, {
59+
greeting: "hello",
60+
meta: "meta en",
61+
todo: "todo en",
62+
});
63+
64+
// Pull for the target locale (simulating a translator editing the file)
65+
const targetInput = {
66+
greeting: "hola",
67+
meta: "meta es",
68+
todo: "todo es",
69+
};
70+
await loader.pull(targetLocale, targetInput);
71+
72+
// Data that will be pushed (ignored keys are intentionally missing)
73+
const dataToPush = {
74+
greeting: "hola",
75+
};
76+
77+
const pushResult = await loader.push(targetLocale, dataToPush);
78+
79+
// The loader should have re-inserted the ignored keys from the pull input.
80+
expect(pushResult).toEqual({
81+
greeting: "hola",
82+
meta: "meta es",
83+
todo: "todo es",
84+
});
85+
});
86+
});
Lines changed: 18 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,18 @@
1+
import { ILoader } from "./_types";
2+
import { createLoader } from "./_utils";
3+
import _ from "lodash";
4+
5+
export default function createIgnoredKeysLoader(
6+
ignoredKeys: string[],
7+
): ILoader<Record<string, any>, Record<string, any>> {
8+
return createLoader({
9+
pull: async (locale, data) => {
10+
const result = _.chain(data).omit(ignoredKeys).value();
11+
return result;
12+
},
13+
push: async (locale, data, originalInput, originalLocale, pullInput) => {
14+
const result = _.merge({}, data, _.pick(pullInput, ignoredKeys));
15+
return result;
16+
},
17+
});
18+
}

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

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -38,6 +38,7 @@ import createMdxCodePlaceholderLoader from "./mdx2/code-placeholder";
3838
import createLocalizableMdxDocumentLoader from "./mdx2/localizable-document";
3939
import createMdxSectionsSplit2Loader from "./mdx2/sections-split-2";
4040
import createMdxLockedPatternsLoader from "./mdx2/locked-patterns";
41+
import createIgnoredKeysLoader from "./ignored-keys";
4142

4243
type BucketLoaderOptions = {
4344
isCacheRestore: boolean;
@@ -52,6 +53,7 @@ export default function createBucketLoader(
5253
options: BucketLoaderOptions,
5354
lockedKeys?: string[],
5455
lockedPatterns?: string[],
56+
ignoredKeys?: string[],
5557
): ILoader<void, Record<string, string>> {
5658
switch (bucketType) {
5759
default:
@@ -311,6 +313,8 @@ export default function createBucketLoader(
311313
createTypescriptLoader(),
312314
createFlatLoader(),
313315
createSyncLoader(),
316+
createLockedKeysLoader(lockedKeys || [], options.isCacheRestore),
317+
createIgnoredKeysLoader(ignoredKeys || []),
314318
createUnlocalizableLoader(
315319
options.isCacheRestore,
316320
options.returnUnlocalizedKeys,

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

Lines changed: 79 additions & 41 deletions
Original file line numberDiff line numberDiff line change
@@ -2,7 +2,12 @@ import _ from "lodash";
22
import path from "path";
33
import { glob } from "glob";
44
import { CLIError } from "./errors";
5-
import { I18nConfig, resolveOverriddenLocale, BucketItem, LocaleDelimiter } from "@lingo.dev/_spec";
5+
import {
6+
I18nConfig,
7+
resolveOverriddenLocale,
8+
BucketItem,
9+
LocaleDelimiter,
10+
} from "@lingo.dev/_spec";
611
import { bucketTypeSchema } from "@lingo.dev/_spec";
712
import Z from "zod";
813

@@ -12,54 +17,81 @@ type BucketConfig = {
1217
injectLocale?: string[];
1318
lockedKeys?: string[];
1419
lockedPatterns?: string[];
20+
ignoredKeys?: string[];
1521
};
1622

1723
export function getBuckets(i18nConfig: I18nConfig) {
18-
const result = Object.entries(i18nConfig.buckets).map(([bucketType, bucketEntry]) => {
19-
const includeItems = bucketEntry.include.map((item) => resolveBucketItem(item));
20-
const excludeItems = bucketEntry.exclude?.map((item) => resolveBucketItem(item));
21-
const config: BucketConfig = {
22-
type: bucketType as Z.infer<typeof bucketTypeSchema>,
23-
paths: extractPathPatterns(i18nConfig.locale.source, includeItems, excludeItems),
24-
};
25-
if (bucketEntry.injectLocale) {
26-
config.injectLocale = bucketEntry.injectLocale;
27-
}
28-
if (bucketEntry.lockedKeys) {
29-
config.lockedKeys = bucketEntry.lockedKeys;
30-
}
31-
if (bucketEntry.lockedPatterns) {
32-
config.lockedPatterns = bucketEntry.lockedPatterns;
33-
}
34-
return config;
35-
});
24+
const result = Object.entries(i18nConfig.buckets).map(
25+
([bucketType, bucketEntry]) => {
26+
const includeItems = bucketEntry.include.map((item) =>
27+
resolveBucketItem(item),
28+
);
29+
const excludeItems = bucketEntry.exclude?.map((item) =>
30+
resolveBucketItem(item),
31+
);
32+
const config: BucketConfig = {
33+
type: bucketType as Z.infer<typeof bucketTypeSchema>,
34+
paths: extractPathPatterns(
35+
i18nConfig.locale.source,
36+
includeItems,
37+
excludeItems,
38+
),
39+
};
40+
if (bucketEntry.injectLocale) {
41+
config.injectLocale = bucketEntry.injectLocale;
42+
}
43+
if (bucketEntry.lockedKeys) {
44+
config.lockedKeys = bucketEntry.lockedKeys;
45+
}
46+
if (bucketEntry.lockedPatterns) {
47+
config.lockedPatterns = bucketEntry.lockedPatterns;
48+
}
49+
if (bucketEntry.ignoredKeys) {
50+
config.ignoredKeys = bucketEntry.ignoredKeys;
51+
}
52+
return config;
53+
},
54+
);
3655

3756
return result;
3857
}
3958

40-
function extractPathPatterns(sourceLocale: string, include: BucketItem[], exclude?: BucketItem[]) {
59+
function extractPathPatterns(
60+
sourceLocale: string,
61+
include: BucketItem[],
62+
exclude?: BucketItem[],
63+
) {
4164
const includedPatterns = include.flatMap((pattern) =>
42-
expandPlaceholderedGlob(pattern.path, resolveOverriddenLocale(sourceLocale, pattern.delimiter)).map(
43-
(pathPattern) => ({
44-
pathPattern,
45-
delimiter: pattern.delimiter,
46-
}),
47-
),
65+
expandPlaceholderedGlob(
66+
pattern.path,
67+
resolveOverriddenLocale(sourceLocale, pattern.delimiter),
68+
).map((pathPattern) => ({
69+
pathPattern,
70+
delimiter: pattern.delimiter,
71+
})),
4872
);
4973
const excludedPatterns = exclude?.flatMap((pattern) =>
50-
expandPlaceholderedGlob(pattern.path, resolveOverriddenLocale(sourceLocale, pattern.delimiter)).map(
51-
(pathPattern) => ({
52-
pathPattern,
53-
delimiter: pattern.delimiter,
54-
}),
55-
),
74+
expandPlaceholderedGlob(
75+
pattern.path,
76+
resolveOverriddenLocale(sourceLocale, pattern.delimiter),
77+
).map((pathPattern) => ({
78+
pathPattern,
79+
delimiter: pattern.delimiter,
80+
})),
81+
);
82+
const result = _.differenceBy(
83+
includedPatterns,
84+
excludedPatterns ?? [],
85+
(item) => item.pathPattern,
5686
);
57-
const result = _.differenceBy(includedPatterns, excludedPatterns ?? [], (item) => item.pathPattern);
5887
return result;
5988
}
6089

6190
// Path expansion
62-
function expandPlaceholderedGlob(_pathPattern: string, sourceLocale: string): string[] {
91+
function expandPlaceholderedGlob(
92+
_pathPattern: string,
93+
sourceLocale: string,
94+
): string[] {
6395
// Throw if pathPattern is an absolute path
6496
const absolutePathPattern = path.resolve(_pathPattern);
6597
const pathPattern = path.relative(process.cwd(), absolutePathPattern);
@@ -81,12 +113,15 @@ function expandPlaceholderedGlob(_pathPattern: string, sourceLocale: string): st
81113
// Break down path pattern into parts
82114
const pathPatternChunks = pathPattern.split(path.sep);
83115
// Find the index of the segment containing "[locale]"
84-
const localeSegmentIndexes = pathPatternChunks.reduce((indexes, segment, index) => {
85-
if (segment.includes("[locale]")) {
86-
indexes.push(index);
87-
}
88-
return indexes;
89-
}, [] as number[]);
116+
const localeSegmentIndexes = pathPatternChunks.reduce(
117+
(indexes, segment, index) => {
118+
if (segment.includes("[locale]")) {
119+
indexes.push(index);
120+
}
121+
return indexes;
122+
},
123+
[] as number[],
124+
);
90125
// substitute [locale] in pathPattern with sourceLocale
91126
const sourcePathPattern = pathPattern.replaceAll(/\[locale\]/g, sourceLocale);
92127
// get all files that match the sourcePathPattern
@@ -104,7 +139,10 @@ function expandPlaceholderedGlob(_pathPattern: string, sourceLocale: string): st
104139
const sourcePathChunk = sourcePathChunks[localeSegmentIndex];
105140
const regexp = new RegExp(
106141
"(" +
107-
pathPatternChunk.replaceAll(".", "\\.").replaceAll("*", ".*").replace("[locale]", `)${sourceLocale}(`) +
142+
pathPatternChunk
143+
.replaceAll(".", "\\.")
144+
.replaceAll("*", ".*")
145+
.replace("[locale]", `)${sourceLocale}(`) +
108146
")",
109147
);
110148
const match = sourcePathChunk.match(regexp);

0 commit comments

Comments
 (0)