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
6 changes: 6 additions & 0 deletions .changeset/strange-insects-greet.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,6 @@
---
"@lingo.dev/_spec": patch
"lingo.dev": patch
---

inject locale
9 changes: 5 additions & 4 deletions packages/cli/src/cli/cmd/cleanup.ts
Original file line number Diff line number Diff line change
Expand Up @@ -14,9 +14,10 @@ export default new Command()
.option("--locale <locale>", "Specific locale to cleanup")
.option("--bucket <bucket>", "Specific bucket to cleanup")
.option("--dry-run", "Show what would be removed without making changes")
.option("--verbose", "Show detailed output including:\n" +
" - List of keys that would be removed.\n" +
" - Processing steps.")
.option(
"--verbose",
"Show detailed output including:\n" + " - List of keys that would be removed.\n" + " - Processing steps.",
)
.action(async function (options) {
const ora = Ora();
const results: any = [];
Expand All @@ -39,7 +40,7 @@ export default new Command()
console.log();
ora.info(`Processing bucket: ${bucket.type}`);

for (const bucketConfig of bucket.config) {
for (const bucketConfig of bucket.paths) {
const sourceLocale = resolveOverriddenLocale(i18nConfig!.locale.source, bucketConfig.delimiter);
const bucketOra = Ora({ indent: 2 }).info(`Processing path: ${bucketConfig.pathPattern}`);
const bucketLoader = createBucketLoader(bucket.type, bucketConfig.pathPattern, {
Expand Down
62 changes: 32 additions & 30 deletions packages/cli/src/cli/cmd/i18n.ts
Original file line number Diff line number Diff line change
Expand Up @@ -83,21 +83,19 @@ export default new Command()
if (flags.file?.length) {
buckets = buckets
.map((bucket: any) => {
const config = bucket.config.filter((config: any) =>
flags.file!.find((file) => config.pathPattern?.match(file)),
);
return { ...bucket, config };
const paths = bucket.paths.filter((path: any) => flags.file!.find((file) => path.pathPattern?.match(file)));
return { ...bucket, paths };
})
.filter((bucket: any) => bucket.config.length > 0);
.filter((bucket: any) => bucket.paths.length > 0);
if (buckets.length === 0) {
ora.fail("No buckets found. All buckets were filtered out by --file option.");
process.exit(1);
} else {
ora.info(`\x1b[36mProcessing only filtered buckets:\x1b[0m`);
buckets.map((bucket: any) => {
ora.info(` ${bucket.type}:`);
bucket.config.forEach((config: any) => {
ora.info(` - ${config.pathPattern}`);
bucket.paths.forEach((path: any) => {
ora.info(` - ${path.pathPattern}`);
});
});
}
Expand All @@ -111,18 +109,19 @@ export default new Command()
if (!lockfileHelper.isLockfileExists()) {
ora.start("Creating i18n.lock...");
for (const bucket of buckets) {
for (const bucketConfig of bucket.config) {
const sourceLocale = resolveOverriddenLocale(i18nConfig!.locale.source, bucketConfig.delimiter);
for (const bucketPath of bucket.paths) {
const sourceLocale = resolveOverriddenLocale(i18nConfig!.locale.source, bucketPath.delimiter);

const bucketLoader = createBucketLoader(bucket.type, bucketConfig.pathPattern, {
const bucketLoader = createBucketLoader(bucket.type, bucketPath.pathPattern, {
isCacheRestore: false,
defaultLocale: sourceLocale,
injectLocale: bucket.injectLocale,
});
bucketLoader.setDefaultLocale(sourceLocale);
await bucketLoader.init();

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

for (const bucket of buckets) {
cacheOra.info(`Processing bucket: ${bucket.type}`);
for (const bucketConfig of bucket.config) {
for (const bucketPath of bucket.paths) {
const bucketOra = Ora({ indent: 4 });
bucketOra.info(`Processing path: ${bucketConfig.pathPattern}`);
bucketOra.info(`Processing path: ${bucketPath.pathPattern}`);

const sourceLocale = resolveOverriddenLocale(i18nConfig!.locale.source, bucketConfig.delimiter);
const bucketLoader = createBucketLoader(bucket.type, bucketConfig.pathPattern, {
const sourceLocale = resolveOverriddenLocale(i18nConfig!.locale.source, bucketPath.delimiter);
const bucketLoader = createBucketLoader(bucket.type, bucketPath.pathPattern, {
isCacheRestore: true,
defaultLocale: sourceLocale,
injectLocale: bucket.injectLocale,
});
bucketLoader.setDefaultLocale(sourceLocale);
await bucketLoader.init();
Expand All @@ -166,7 +166,7 @@ export default new Command()
}

await bucketLoader.push(targetLocale, targetData);
lockfileHelper.registerPartialSourceData(bucketConfig.pathPattern, cachedSourceData);
lockfileHelper.registerPartialSourceData(bucketPath.pathPattern, cachedSourceData);

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

const bucketLoader = createBucketLoader(bucket.type, bucketConfig.pathPattern, {
const bucketLoader = createBucketLoader(bucket.type, bucketPath.pathPattern, {
isCacheRestore: false,
defaultLocale: sourceLocale,
returnUnlocalizedKeys: true,
injectLocale: bucket.injectLocale,
});
bucketLoader.setDefaultLocale(sourceLocale);
await bucketLoader.init();

const { unlocalizable: sourceUnlocalizable, ...sourceData } = await bucketLoader.pull(
i18nConfig!.locale.source,
);
const updatedSourceData = lockfileHelper.extractUpdatedData(bucketConfig.pathPattern, sourceData);
const updatedSourceData = lockfileHelper.extractUpdatedData(bucketPath.pathPattern, sourceData);

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

for (const _targetLocale of targetLocales) {
const targetLocale = resolveOverriddenLocale(_targetLocale, bucketConfig.delimiter);
const targetLocale = resolveOverriddenLocale(_targetLocale, bucketPath.delimiter);
const { unlocalizable: targetUnlocalizable, ...targetData } = await bucketLoader.pull(targetLocale);

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

const sourceLocale = resolveOverriddenLocale(i18nConfig!.locale.source, bucketConfig.delimiter);
const sourceLocale = resolveOverriddenLocale(i18nConfig!.locale.source, bucketPath.delimiter);

const bucketLoader = createBucketLoader(bucket.type, bucketConfig.pathPattern, {
const bucketLoader = createBucketLoader(bucket.type, bucketPath.pathPattern, {
isCacheRestore: false,
defaultLocale: sourceLocale,
injectLocale: bucket.injectLocale,
});
bucketLoader.setDefaultLocale(sourceLocale);
await bucketLoader.init();
let sourceData = await bucketLoader.pull(sourceLocale);

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

sourceData = await bucketLoader.pull(sourceLocale);

const updatedSourceData = flags.force
? sourceData
: lockfileHelper.extractUpdatedData(bucketConfig.pathPattern, sourceData);
: lockfileHelper.extractUpdatedData(bucketPath.pathPattern, sourceData);

const targetData = await bucketLoader.pull(targetLocale);
let processableData = calculateDataDelta({
Expand Down Expand Up @@ -333,7 +335,7 @@ export default new Command()
if (flags.interactive) {
bucketOra.stop();
const reviewedData = await reviewChanges({
pathPattern: bucketConfig.pathPattern,
pathPattern: bucketPath.pathPattern,
targetLocale,
currentData: targetData,
proposedData: finalTargetData,
Expand All @@ -342,7 +344,7 @@ export default new Command()
});

finalTargetData = reviewedData;
bucketOra.start(`Applying changes to ${bucketConfig} (${targetLocale})`);
bucketOra.start(`Applying changes to ${bucketPath} (${targetLocale})`);
}

const finalDiffSize = _.chain(finalTargetData)
Expand All @@ -369,7 +371,7 @@ export default new Command()
}
}

lockfileHelper.registerSourceData(bucketConfig.pathPattern, sourceData);
lockfileHelper.registerSourceData(bucketPath.pathPattern, sourceData);
}
} catch (_error: any) {
const error = new Error(`Failed to process bucket ${bucket.type}: ${_error.message}`);
Expand Down
2 changes: 1 addition & 1 deletion packages/cli/src/cli/cmd/lockfile.ts
Original file line number Diff line number Diff line change
Expand Up @@ -24,7 +24,7 @@ export default new Command()
const buckets = getBuckets(i18nConfig!);

for (const bucket of buckets) {
for (const bucketConfig of bucket.config) {
for (const bucketConfig of bucket.paths) {
const sourceLocale = resolveOverriddenLocale(i18nConfig!.locale.source, bucketConfig.delimiter);
const bucketLoader = createBucketLoader(bucket.type, bucketConfig.pathPattern, {
isCacheRestore: false,
Expand Down
2 changes: 1 addition & 1 deletion packages/cli/src/cli/cmd/show/files.ts
Original file line number Diff line number Diff line change
Expand Up @@ -27,7 +27,7 @@ export default new Command()

const buckets = getBuckets(i18nConfig);
for (const bucket of buckets) {
for (const bucketConfig of bucket.config) {
for (const bucketConfig of bucket.paths) {
const sourceLocale = resolveOverriddenLocale(i18nConfig.locale.source, bucketConfig.delimiter);
const sourcePath = bucketConfig.pathPattern.replace(/\[locale\]/g, sourceLocale);
const targetPaths = i18nConfig.locale.targets.map((_targetLocale) => {
Expand Down
39 changes: 39 additions & 0 deletions packages/cli/src/cli/loaders/index.spec.ts
Original file line number Diff line number Diff line change
Expand Up @@ -466,6 +466,45 @@ describe("bucket loaders", () => {
expect(fs.access).toHaveBeenCalledWith("i18n/en/en.json");
expect(fs.writeFile).toHaveBeenCalledWith("i18n/es/es.json", expectedOutput, { encoding: "utf-8", flag: "w" });
});

it("should remove injected locales from json data", async () => {
setupFileMocks();

const input = { "button.title": "Submit", settings: { locale: "en" }, "not-a-locale": "bar" };
mockFileOperations(JSON.stringify(input));

const jsonLoader = createBucketLoader("json", "i18n/[locale].json", {
isCacheRestore: false,
defaultLocale: "en",
injectLocale: ["settings.locale", "not-a-locale"],
});
jsonLoader.setDefaultLocale("en");
const data = await jsonLoader.pull("en");

expect(data).toEqual({ "button.title": "Submit", "not-a-locale": "bar" });
});

it("should inject locales into json data", async () => {
setupFileMocks();

const input = { "button.title": "Submit", "not-a-locale": "bar", settings: { locale: "en" } };
const payload = { "button.title": "Enviar", "not-a-locale": "bar" };
const expectedOutput = JSON.stringify({ ...payload, settings: { locale: "es" } }, null, 2);

mockFileOperations(JSON.stringify(input));

const jsonLoader = createBucketLoader("json", "i18n/[locale].json", {
isCacheRestore: false,
defaultLocale: "en",
injectLocale: ["settings.locale", "not-a-locale"],
});
jsonLoader.setDefaultLocale("en");
await jsonLoader.pull("en");

await jsonLoader.push("es", payload);

expect(fs.writeFile).toHaveBeenCalledWith("i18n/es.json", expectedOutput, { encoding: "utf-8", flag: "w" });
});
});

describe("markdown bucket loader", () => {
Expand Down
3 changes: 3 additions & 0 deletions packages/cli/src/cli/loaders/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -29,11 +29,13 @@ import createSyncLoader from "./sync";
import createPlutilJsonTextLoader from "./plutil-json-loader";
import createPhpLoader from "./php";
import createVueJsonLoader from "./vue-json";
import createInjectLocaleLoader from "./inject-locale";

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

export default function createBucketLoader(
Expand Down Expand Up @@ -73,6 +75,7 @@ export default function createBucketLoader(
createTextFileLoader(bucketPathPattern),
createPrettierLoader({ parser: "json", bucketPathPattern }),
createJsonLoader(),
createInjectLocaleLoader(options.injectLocale),
createFlatLoader(),
createSyncLoader(),
createUnlocalizableLoader(options.isCacheRestore, options.returnUnlocalizedKeys),
Expand Down
31 changes: 31 additions & 0 deletions packages/cli/src/cli/loaders/inject-locale.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,31 @@
import _ from "lodash";
import { ILoader } from "./_types";
import { createLoader } from "./_utils";

export default function createInjectLocaleLoader(
injectLocaleKeys?: string[],
): ILoader<Record<string, any>, Record<string, any>> {
return createLoader({
async pull(locale, data) {
if (!injectLocaleKeys) {
return data;
}
const omitKeys = injectLocaleKeys.filter((key) => {
return _.get(data, key) === locale;
});
const result = _.omit(data, omitKeys);
return result;
},
async push(locale, data, originalInput, originalLocale) {
if (!injectLocaleKeys) {
return data;
}
injectLocaleKeys.forEach((key) => {
if (_.get(originalInput, key) === originalLocale) {
_.set(data, key, locale);
}
});
return data;
},
});
}
8 changes: 4 additions & 4 deletions packages/cli/src/cli/utils/buckets.spec.ts
Original file line number Diff line number Diff line change
Expand Up @@ -31,7 +31,7 @@ describe("getBuckets", () => {
expect(buckets).toEqual([
{
type: "json",
config: [
paths: [
{ pathPattern: "src/i18n/[locale].json", delimiter: null },
{ pathPattern: "src/translations/[locale]/messages.json", delimiter: null },
],
Expand Down Expand Up @@ -61,7 +61,7 @@ describe("getBuckets", () => {
expect(buckets).toEqual([
{
type: "json",
config: [
paths: [
{ pathPattern: "src/translations/landing.[locale].json", delimiter: null },
{ pathPattern: "src/translations/app.[locale].json", delimiter: null },
{ pathPattern: "src/translations/email.[locale].json", delimiter: null },
Expand All @@ -83,7 +83,7 @@ describe("getBuckets", () => {
mockGlobSync(["src/i18n/en.json"]);
const i18nConfig = makeI18nConfig([{ path: "src/i18n/[locale].json", delimiter: "-" }]);
const buckets = getBuckets(i18nConfig);
expect(buckets).toEqual([{ type: "json", config: [{ pathPattern: "src/i18n/[locale].json", delimiter: "-" }] }]);
expect(buckets).toEqual([{ type: "json", paths: [{ pathPattern: "src/i18n/[locale].json", delimiter: "-" }] }]);
});

it("should return bucket with multiple locale placeholders", () => {
Expand All @@ -96,7 +96,7 @@ describe("getBuckets", () => {
expect(buckets).toEqual([
{
type: "json",
config: [
paths: [
{ pathPattern: "src/i18n/[locale]/[locale].json", delimiter: null },
{ pathPattern: "src/[locale]/translations/[locale]/messages.json", delimiter: null },
],
Expand Down
16 changes: 13 additions & 3 deletions packages/cli/src/cli/utils/buckets.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2,18 +2,28 @@ import _ from "lodash";
import path from "path";
import { glob } from "glob";
import { CLIError } from "./errors";
import { I18nConfig, resolveOverriddenLocale, BucketItem } from "@lingo.dev/_spec";
import { I18nConfig, resolveOverriddenLocale, BucketItem, LocaleDelimiter } from "@lingo.dev/_spec";
import { bucketTypeSchema } from "@lingo.dev/_spec";
import Z from "zod";

type BucketConfig = {
type: Z.infer<typeof bucketTypeSchema>;
paths: Array<{ pathPattern: string; delimiter?: LocaleDelimiter }>;
injectLocale?: string[];
};

export function getBuckets(i18nConfig: I18nConfig) {
const result = Object.entries(i18nConfig.buckets).map(([bucketType, bucketEntry]) => {
const includeItems = bucketEntry.include.map((item) => resolveBucketItem(item));
const excludeItems = bucketEntry.exclude?.map((item) => resolveBucketItem(item));
return {
const config: BucketConfig = {
type: bucketType as Z.infer<typeof bucketTypeSchema>,
config: extractPathPatterns(i18nConfig.locale.source, includeItems, excludeItems),
paths: extractPathPatterns(i18nConfig.locale.source, includeItems, excludeItems),
};
if (bucketEntry.injectLocale) {
config.injectLocale = bucketEntry.injectLocale;
}
return config;
});

return result;
Expand Down
Loading