Skip to content
Open
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
20 changes: 10 additions & 10 deletions demo/new-compiler-vite-react-spa/public/translations/de.json
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,15 @@
"version": 0.1,
"locale": "de",
"entries": {
"daa4d8839395": "{counter} mal geklickt",
"52ed9ee761d8": "Hallo Welt",
"f11fc78c3ac0": "<b0>Gemischter</b0> Inhalt <i0>Fragment</i0>",
"556f5956dca7": "Willkommen zur Lingo.dev Compiler Demo",
"02704ec4e52a": "Es extrahiert automatisch Text aus Ihrem JSX und übersetzt ihn in andere Sprachen.",
"de6bfb30be49": "Text, der als <code0></code0> eingefügt wird, wird nicht übersetzt: {text}",
"5c15bd35e916": "Um es zu übersetzen, müssen Sie es in '<'>{translatableText} '<'/> einschließen",
"93b50fe805b7": "Text außerhalb der Komponente wird nicht übersetzt: {externalText}",
"d756b03ffbf5": "Inhalte, die Text und andere Tags enthalten, werden als eine Einheit übersetzt: {translatableMixedContextFragment}",
"8492c53cfbaf": "Über Lingo.dev",
"8aa4fe3f0590": "Dies ist eine Demo-Anwendung, die den Lingo.dev-Compiler für automatische Übersetzungen in React-Anwendungen präsentiert.",
"af76f667703b": "Hauptfunktionen",
Expand All @@ -12,15 +21,6 @@
"aca12d550fe2": "Unterstützung für Server- und Client-Komponenten",
"44a3311c3a4a": "Wie es funktioniert",
"0add30e37450": "Der Compiler analysiert Ihre React-Komponenten zur Build-Zeit und extrahiert automatisch alle übersetzbaren Strings. Anschließend generiert er Übersetzungen mit Ihrem konfigurierten Übersetzungsanbieter.",
"07d84d34dd3a": "Fügen Sie einfach die Direktive \"use i18n\" am Anfang Ihrer Komponentendateien hinzu, und der Compiler erledigt den Rest!",
"daa4d8839395": "{counter} mal geklickt",
"52ed9ee761d8": "Hallo Welt",
"f11fc78c3ac0": "<b0>Gemischter</b0> Inhalt <i0>Fragment</i0>",
"556f5956dca7": "Willkommen zur Lingo.dev Compiler Demo",
"02704ec4e52a": "Es extrahiert automatisch Text aus Ihrem JSX und übersetzt ihn in andere Sprachen.",
"de6bfb30be49": "Text, der als <code0></code0> eingefügt wird, wird nicht übersetzt: {text}",
"5c15bd35e916": "Um es zu übersetzen, müssen Sie es in '<'>{translatableText} '<'/> einschließen",
"93b50fe805b7": "Text außerhalb der Komponente wird nicht übersetzt: {externalText}",
"d756b03ffbf5": "Inhalte, die Text und andere Tags enthalten, werden als eine Einheit übersetzt: {translatableMixedContextFragment}"
"07d84d34dd3a": "Fügen Sie einfach die Direktive \"use i18n\" am Anfang Ihrer Komponentendateien hinzu, und der Compiler erledigt den Rest!"
}
}
20 changes: 10 additions & 10 deletions demo/new-compiler-vite-react-spa/public/translations/es.json
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,15 @@
"version": 0.1,
"locale": "es",
"entries": {
"daa4d8839395": "Clicado {counter} veces",
"52ed9ee761d8": "Hola Mundo",
"f11fc78c3ac0": "Contenido <b0>mixto</b0> <i0>fragmento</i0>",
"556f5956dca7": "Bienvenido a la demo de Lingo.dev compiler",
"02704ec4e52a": "Extrae automáticamente texto de tu JSX y lo traduce a otros idiomas.",
"de6bfb30be49": "El texto insertado como <code0></code0> no se traduce: {text}",
"5c15bd35e916": "Para traducirlo tienes que envolverlo en el '<'>{translatableText} '<'/>",
"93b50fe805b7": "El texto externo al componente no se traduce: {externalText}",
"d756b03ffbf5": "El contenido que tiene texto y otras etiquetas dentro se traducirá como una sola entidad: {translatableMixedContextFragment}",
"8492c53cfbaf": "Acerca de Lingo.dev",
"8aa4fe3f0590": "Esta es una aplicación de demostración que muestra el compilador Lingo.dev para traducciones automáticas en aplicaciones React.",
"af76f667703b": "Características principales",
Expand All @@ -12,15 +21,6 @@
"aca12d550fe2": "Soporte para componentes de servidor y cliente",
"44a3311c3a4a": "Cómo funciona",
"0add30e37450": "El compilador analiza tus componentes de React en tiempo de compilación y extrae automáticamente todas las cadenas traducibles. Luego genera traducciones utilizando tu proveedor de traducción configurado.",
"07d84d34dd3a": "¡Simplemente agrega la directiva \"use i18n\" en la parte superior de tus archivos de componentes, y el compilador se encarga del resto!",
"daa4d8839395": "Clicado {counter} veces",
"52ed9ee761d8": "Hola Mundo",
"f11fc78c3ac0": "Contenido <b0>mixto</b0> <i0>fragmento</i0>",
"556f5956dca7": "Bienvenido a la demo de Lingo.dev compiler",
"02704ec4e52a": "Extrae automáticamente texto de tu JSX y lo traduce a otros idiomas.",
"de6bfb30be49": "El texto insertado como <code0></code0> no se traduce: {text}",
"5c15bd35e916": "Para traducirlo tienes que envolverlo en el '<'>{translatableText} '<'/>",
"93b50fe805b7": "El texto externo al componente no se traduce: {externalText}",
"d756b03ffbf5": "El contenido que tiene texto y otras etiquetas dentro se traducirá como una sola entidad: {translatableMixedContextFragment}"
"07d84d34dd3a": "¡Simplemente agrega la directiva \"use i18n\" en la parte superior de tus archivos de componentes, y el compilador se encarga del resto!"
}
}
20 changes: 10 additions & 10 deletions demo/new-compiler-vite-react-spa/public/translations/fr.json
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,15 @@
"version": 0.1,
"locale": "fr",
"entries": {
"daa4d8839395": "Cliqué {counter} fois",
"52ed9ee761d8": "Bonjour le monde",
"f11fc78c3ac0": "<b0>Contenu</b0> mixte <i0>fragment</i0>",
"556f5956dca7": "Bienvenue dans la démo du compilateur Lingo.dev",
"02704ec4e52a": "Il extrait automatiquement le texte de votre JSX et le traduit dans d'autres langues.",
"de6bfb30be49": "Le texte inséré comme un <code0></code0> n'est pas traduit : {text}",
"5c15bd35e916": "Pour le traduire, vous devez l'envelopper dans le '<'>{translatableText} '<'/>",
"93b50fe805b7": "Le texte externe au composant n'est pas traduit : {externalText}",
"d756b03ffbf5": "Le contenu qui contient du texte et d'autres balises sera traduit comme une seule entité : {translatableMixedContextFragment}",
"8492c53cfbaf": "À propos de Lingo.dev",
"8aa4fe3f0590": "Ceci est une application de démonstration présentant le compilateur Lingo.dev pour les traductions automatiques dans les applications React.",
"af76f667703b": "Fonctionnalités clés",
Expand All @@ -12,15 +21,6 @@
"aca12d550fe2": "Prise en charge des composants serveur et client",
"44a3311c3a4a": "Comment ça fonctionne",
"0add30e37450": "Le compilateur analyse vos composants React au moment de la compilation et extrait automatiquement toutes les chaînes traduisibles. Il génère ensuite des traductions en utilisant votre fournisseur de traduction configuré.",
"07d84d34dd3a": "Ajoutez simplement la directive \"use i18n\" en haut de vos fichiers de composants, et le compilateur s'occupe du reste !",
"daa4d8839395": "Cliqué {counter} fois",
"52ed9ee761d8": "Bonjour le monde",
"f11fc78c3ac0": "<b0>Contenu</b0> mixte <i0>fragment</i0>",
"556f5956dca7": "Bienvenue dans la démo du compilateur Lingo.dev",
"02704ec4e52a": "Il extrait automatiquement le texte de votre JSX et le traduit dans d'autres langues.",
"de6bfb30be49": "Le texte inséré comme un <code0></code0> n'est pas traduit : {text}",
"5c15bd35e916": "Pour le traduire, vous devez l'envelopper dans le '<'>{translatableText} '<'/>",
"93b50fe805b7": "Le texte externe au composant n'est pas traduit : {externalText}",
"d756b03ffbf5": "Le contenu qui contient du texte et d'autres balises sera traduit comme une seule entité : {translatableMixedContextFragment}"
"07d84d34dd3a": "Ajoutez simplement la directive \"use i18n\" en haut de vos fichiers de composants, et le compilateur s'occupe du reste !"
}
}
2 changes: 1 addition & 1 deletion packages/compiler/src/_const.ts
Original file line number Diff line number Diff line change
Expand Up @@ -4,4 +4,4 @@ export const ModuleId = {
ReactRouter: ["lingo.dev/react/react-router", "lingo.dev/react-router"],
};

export const LCP_DICTIONARY_FILE_NAME = "dictionary.js";
export const LCP_DICTIONARY_FILE_NAME = "dictionary.json";
6 changes: 3 additions & 3 deletions packages/compiler/src/_loader-utils.spec.ts
Original file line number Diff line number Diff line change
Expand Up @@ -66,7 +66,7 @@ describe("loadDictionary", () => {
);

const result = await loadDictionary({
resourcePath: "/project/src/lingo/dictionary.js",
resourcePath: "/project/src/lingo/dictionary.json",
resourceQuery: "",
params: {},
sourceRoot: "src",
Expand Down Expand Up @@ -98,7 +98,7 @@ describe("loadDictionary", () => {
});

const result = await loadDictionary({
resourcePath: "/project/src/lingo/dictionary.js",
resourcePath: "/project/src/lingo/dictionary.json",
resourceQuery: "?locale=es",
params: { sourceLocale: "en", targetLocales: ["es"], foo: "bar" },
sourceRoot: "src",
Expand Down Expand Up @@ -131,7 +131,7 @@ describe("loadDictionary", () => {
(serverMod.LCPServer.loadDictionaries as any).mockResolvedValueOnce({});
await expect(
loadDictionary({
resourcePath: "/project/src/lingo/dictionary.js",
resourcePath: "/project/src/lingo/dictionary.json",
resourceQuery: "?locale=fr",
params: { sourceLocale: "en", targetLocales: ["fr"] },
sourceRoot: "src",
Expand Down
8 changes: 4 additions & 4 deletions packages/compiler/src/_utils.spec.ts
Original file line number Diff line number Diff line change
Expand Up @@ -13,13 +13,13 @@ describe("getDictionaryPath", () => {
sourceRoot: "src",
lingoDir: "lingo",
relativeFilePath: "./components/Button.tsx",
expected: "./../lingo/dictionary.js",
expected: "./../lingo/dictionary.json",
},
{
sourceRoot: "src/app/content",
lingoDir: "i18n",
relativeFilePath: "../../components/Button.tsx",
expected: "./../app/content/i18n/dictionary.js",
expected: "./../app/content/i18n/dictionary.json",
},
])(
"returns correct path for file $relativeFilePath in $sourceRoot",
Expand All @@ -45,7 +45,7 @@ describe("getDictionaryPath", () => {
relativeFilePath: "/project/src/components/Button.tsx",
});

expect(result).toBe("./../lingo/dictionary.js");
expect(result).toBe("./../lingo/dictionary.json");
// Ensure no back-slashes slip through
expect(result).not.toMatch(/\\/);
});
Expand All @@ -65,7 +65,7 @@ describe("getDictionaryPath", () => {
relativeFilePath: "C:\\project\\src\\components\\Button.tsx",
});

expect(result).toBe("./../lingo/dictionary.js");
expect(result).toBe("./../lingo/dictionary.json");
expect(result).not.toMatch(/\\/);
});
});
192 changes: 190 additions & 2 deletions packages/compiler/src/lib/lcp/cache.spec.ts
Original file line number Diff line number Diff line change
Expand Up @@ -4,11 +4,12 @@ import { LCPCache, LCPCacheParams } from "./cache";
import { LCPSchema } from "./schema";
import { LCP_DICTIONARY_FILE_NAME } from "../../_const";

const { mockExistsSync, mockReadFileSync, mockWriteFileSync, mockPrettierFormat, mockPrettierResolveConfig } = vi.hoisted(() => {
const { mockExistsSync, mockReadFileSync, mockWriteFileSync, mockMkdirSync, mockPrettierFormat, mockPrettierResolveConfig } = vi.hoisted(() => {
return {
mockExistsSync: vi.fn(),
mockReadFileSync: vi.fn(),
mockWriteFileSync: vi.fn(),
mockMkdirSync: vi.fn(),
mockPrettierFormat: vi.fn(),
mockPrettierResolveConfig: vi.fn(),
};
Expand All @@ -18,15 +19,21 @@ vi.mock("fs", () => ({
existsSync: mockExistsSync,
readFileSync: mockReadFileSync,
writeFileSync: mockWriteFileSync,
mkdirSync: mockMkdirSync,
}));

vi.mock("prettier", () => ({
format: mockPrettierFormat,
resolveConfig: mockPrettierResolveConfig,
}));

// cached JSON is stored in JS file, we need to add export default to make it valid JS file
// cached JSON is stored as plain JSON (new format)
function toCachedString(cache: any) {
return JSON.stringify(cache, null, 2);
}

// helper to create legacy format cache (for migration tests)
function toLegacyCachedString(cache: any) {
return `export default ${JSON.stringify(cache, null, 2)};`;
}

Expand Down Expand Up @@ -453,4 +460,185 @@ describe("LCPCache", () => {
expect(mockWriteFileSync).toHaveBeenCalledWith(cachePath, "formatted");
});
});

describe("ensureDictionaryFile", () => {
it("creates empty JSON cache when file does not exist", () => {
mockExistsSync.mockReturnValue(false);
mockWriteFileSync.mockImplementation(() => {});

LCPCache.ensureDictionaryFile({
sourceRoot: params.sourceRoot,
lingoDir: params.lingoDir,
});

expect(mockWriteFileSync).toHaveBeenCalledWith(cachePath, "{}");
});

it("does not overwrite existing cache file", () => {
mockExistsSync.mockReturnValue(true);
mockWriteFileSync.mockImplementation(() => {});

LCPCache.ensureDictionaryFile({
sourceRoot: params.sourceRoot,
lingoDir: params.lingoDir,
});

expect(mockWriteFileSync).not.toHaveBeenCalled();
});
});

describe("legacy format migration", () => {
it("reads and migrates legacy export default format to JSON", () => {
mockExistsSync.mockReturnValue(true);
mockReadFileSync.mockReturnValue(
toLegacyCachedString({
version: 0.1,
files: {
"test.ts": {
entries: {
key1: {
content: {
en: "Hello",
},
hash: "123",
},
},
},
},
}),
);
mockWriteFileSync.mockImplementation(() => {});

const dictionary = LCPCache.readLocaleDictionary("en", params);

// Verify data was read correctly
expect(dictionary).toEqual({
version: 0.1,
locale: "en",
files: {
"test.ts": {
entries: {
key1: "Hello",
},
},
},
});

// Verify file was migrated to new JSON format
expect(mockWriteFileSync).toHaveBeenCalledWith(
cachePath,
toCachedString({
version: 0.1,
files: {
"test.ts": {
entries: {
key1: {
content: {
en: "Hello",
},
hash: "123",
},
},
},
},
}),
);
});

it("preserves all data during migration from legacy format", () => {
mockExistsSync.mockReturnValue(true);
const complexCache = {
version: 0.1,
files: {
"test.ts": {
entries: {
key1: {
content: {
en: "Hello",
fr: "Bonjour",
es: "Hola",
},
hash: "123",
},
newKey: {
content: {
en: "New",
fr: "Nouveau",
},
hash: "111",
},
},
},
"old.ts": {
entries: {
oldKey: {
content: {
en: "Old",
fr: "Vieux",
},
hash: "456",
},
},
},

},
};
mockReadFileSync.mockReturnValue(toLegacyCachedString(complexCache));
mockWriteFileSync.mockImplementation(() => {});

const dictionary = LCPCache.readLocaleDictionary("fr", params);

// Verify all French translations were preserved
expect(dictionary.files["test.ts"].entries.key1).toBe("Bonjour");
expect(dictionary.files["test.ts"].entries.newKey).toBe("Nouveau");
expect(dictionary.files["old.ts"].entries.oldKey).toBe("Vieux");

// Verify migrated cache contains all original data
expect(mockWriteFileSync).toHaveBeenCalledWith(
cachePath,
toCachedString(complexCache),
);
});

it("reads new JSON format without migration", () => {
mockExistsSync.mockReturnValue(true);
// Mock new JSON format (no export default)
mockReadFileSync.mockReturnValue(
toCachedString({
version: 0.1,
files: {
"test.ts": {
entries: {
key1: {
content: {
en: "Hello",
},
hash: "123",
},
},
},
},
}),
);
mockWriteFileSync.mockImplementation(() => {});

const dictionary = LCPCache.readLocaleDictionary("en", params);

// Verify data was read correctly
expect(dictionary).toEqual({
version: 0.1,
locale: "en",
files: {
"test.ts": {
entries: {
key1: "Hello",
},
},
},
});

// Verify NO migration write occurred
expect(mockWriteFileSync).not.toHaveBeenCalled();
});
});
});
Loading