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/real-deers-shave.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
---
"lingo.dev": patch
---

fix bucket path with \* filenames
58 changes: 57 additions & 1 deletion packages/cli/src/cli/utils/buckets.spec.ts
Original file line number Diff line number Diff line change
@@ -1,12 +1,16 @@
import { describe, it, expect, vi } from "vitest";
import { getBuckets } from "./buckets";
import { glob, Path } from "glob";

vi.mock("glob", () => ({
sync: vi.fn().mockImplementation((path) => [{ isFile: () => true, fullpath: () => path }]),
glob: {
sync: vi.fn(),
},
}));

describe("getBuckets", () => {
const makeI18nConfig = (include: any[]) => ({
$schema: "https://lingo.dev/schema/i18n.json",
version: 0,
locale: {
source: "en",
Expand All @@ -20,6 +24,8 @@ describe("getBuckets", () => {
});

it("should return correct buckets", () => {
mockGlobSync(["src/i18n/en.json"], ["src/translations/en/messages.json"]);

const i18nConfig = makeI18nConfig(["src/i18n/[locale].json", "src/translations/[locale]/messages.json"]);
const buckets = getBuckets(i18nConfig);
expect(buckets).toEqual([
Expand All @@ -33,13 +39,55 @@ describe("getBuckets", () => {
]);
});

it("should return correct buckets for paths with asterisk", () => {
mockGlobSync(
["src/translations/landing.en.json", "src/translations/app.en.json", "src/translations/email.en.json"],
["src/locale/landing/messages.en.json", "src/locale/app/data.en.json", "src/locale/email/custom.en.json"],
["src/i18n/landing/en.messages.json", "src/i18n/app/en.data.json", "src/i18n/email/en.custom.json"],
[
"src/i18n/data-landing-en-strings/en.messages.json",
"src/i18n/data-app-en-strings/en.data.json",
"src/i18n/data-email-en-strings/en.custom.json",
],
);

const i18nConfig = makeI18nConfig([
"src/translations/*.[locale].json",
"src/locale/*/*.[locale].json",
"src/i18n/*/[locale].*.json",
"src/i18n/data-*-[locale]-*/[locale].*.json",
]);
const buckets = getBuckets(i18nConfig);
expect(buckets).toEqual([
{
type: "json",
config: [
{ pathPattern: "src/translations/landing.[locale].json", delimiter: null },
{ pathPattern: "src/translations/app.[locale].json", delimiter: null },
{ pathPattern: "src/translations/email.[locale].json", delimiter: null },
{ pathPattern: "src/locale/landing/messages.[locale].json", delimiter: null },
{ pathPattern: "src/locale/app/data.[locale].json", delimiter: null },
{ pathPattern: "src/locale/email/custom.[locale].json", delimiter: null },
{ pathPattern: "src/i18n/landing/[locale].messages.json", delimiter: null },
{ pathPattern: "src/i18n/app/[locale].data.json", delimiter: null },
{ pathPattern: "src/i18n/email/[locale].custom.json", delimiter: null },
{ pathPattern: "src/i18n/data-landing-[locale]-strings/[locale].messages.json", delimiter: null },
{ pathPattern: "src/i18n/data-app-[locale]-strings/[locale].data.json", delimiter: null },
{ pathPattern: "src/i18n/data-email-[locale]-strings/[locale].custom.json", delimiter: null },
],
},
]);
});

it("should return correct bucket with delimiter", () => {
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: "-" }] }]);
});

it("should return bucket with multiple locale placeholders", () => {
mockGlobSync(["src/i18n/en/en.json"], ["src/en/translations/en/messages.json"]);
const i18nConfig = makeI18nConfig([
"src/i18n/[locale]/[locale].json",
"src/[locale]/translations/[locale]/messages.json",
Expand All @@ -56,3 +104,11 @@ describe("getBuckets", () => {
]);
});
});

function mockGlobSync(...args: string[][]) {
args.forEach((files) => {
vi.mocked(glob.sync).mockReturnValueOnce(
files.map((file) => ({ isFile: () => true, fullpath: () => file }) as Path),
);
});
}
20 changes: 12 additions & 8 deletions packages/cli/src/cli/utils/buckets.ts
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
import _ from "lodash";
import path from "path";
import * as glob from "glob";
import { glob } from "glob";
import { CLIError } from "./errors";
import { I18nConfig, resolveOverridenLocale, BucketItem } from "@lingo.dev/_spec";
import { bucketTypeSchema } from "@lingo.dev/_spec";
Expand Down Expand Up @@ -82,13 +82,17 @@ function expandPlaceholderedGlob(_pathPattern: string, sourceLocale: string): st
const sourcePathChunks = sourcePath.split(path.sep);
localeSegmentIndexes.forEach((localeSegmentIndex) => {
// Find the position of the "[locale]" placeholder within the segment
const localePlaceholderIndex = pathPatternChunks[localeSegmentIndex]?.indexOf("[locale]") ?? -1;
if (localeSegmentIndex >= 0 && localePlaceholderIndex >= 0) {
const placeholderedPathChunk = sourcePathChunks[localeSegmentIndex];
const placeholderedSegment =
placeholderedPathChunk.substring(0, localePlaceholderIndex) +
"[locale]" +
placeholderedPathChunk.substring(localePlaceholderIndex + sourceLocale.length);
const pathPatternChunk = pathPatternChunks[localeSegmentIndex];
const sourcePathChunk = sourcePathChunks[localeSegmentIndex];
const regexp = new RegExp(
"(" +
pathPatternChunk.replaceAll(".", "\\.").replaceAll("*", ".*").replace("[locale]", `)${sourceLocale}(`) +
")",
);
const match = sourcePathChunk.match(regexp);
if (match) {
const [, prefix, suffix] = match;
const placeholderedSegment = prefix + "[locale]" + suffix;
sourcePathChunks[localeSegmentIndex] = placeholderedSegment;
}
});
Expand Down