Skip to content

Commit 020424f

Browse files
authored
fix(cli): jsonc hints arrays (#1710)
* chore: fix pnpm i * fix: support hints in arrays * chore: add changeset
1 parent b2d335b commit 020424f

File tree

9 files changed

+367
-107
lines changed

9 files changed

+367
-107
lines changed

.changeset/little-deserts-lay.md

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,6 @@
1+
---
2+
"@compiler/demo-next": patch
3+
"lingo.dev": patch
4+
---
5+
6+
Add support for JSONC comments in arrays

demo/new-compiler-next16/package.json

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -9,7 +9,7 @@
99
"lint": "eslint"
1010
},
1111
"dependencies": {
12-
"@lingo.dev/compiler": "workspace:^1.0.0-beta",
12+
"@lingo.dev/compiler": "workspace:*",
1313
"next": "^16.0.4",
1414
"react": "19.2.0",
1515
"react-dom": "19.2.0"

demo/new-compiler-vite-react-spa/public/translations/de.json

Lines changed: 11 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,15 @@
22
"version": 0.1,
33
"locale": "de",
44
"entries": {
5+
"daa4d8839395": "{counter} mal geklickt",
6+
"52ed9ee761d8": "Hallo Welt",
7+
"f11fc78c3ac0": "<b0>Gemischter</b0> Inhalt <i0>Fragment</i0>",
8+
"556f5956dca7": "Willkommen zur Lingo.dev Compiler Demo",
9+
"02704ec4e52a": "Es extrahiert automatisch Text aus Ihrem JSX und übersetzt ihn in andere Sprachen.",
10+
"de6bfb30be49": "Text, der als <code0></code0> eingefügt wird, wird nicht übersetzt: {text}",
11+
"5c15bd35e916": "Um es zu übersetzen, müssen Sie es in '<'>{translatableText} '<'/> einschließen",
12+
"93b50fe805b7": "Text außerhalb der Komponente wird nicht übersetzt: {externalText}",
13+
"d756b03ffbf5": "Inhalte, die Text und andere Tags enthalten, werden als eine Einheit übersetzt: {translatableMixedContextFragment}",
514
"8492c53cfbaf": "Über Lingo.dev",
615
"8aa4fe3f0590": "Dies ist eine Demo-Anwendung, die den Lingo.dev-Compiler für automatische Übersetzungen in React-Anwendungen präsentiert.",
716
"af76f667703b": "Hauptfunktionen",
@@ -12,15 +21,6 @@
1221
"aca12d550fe2": "Unterstützung für Server- und Client-Komponenten",
1322
"44a3311c3a4a": "Wie es funktioniert",
1423
"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.",
15-
"07d84d34dd3a": "Fügen Sie einfach die Direktive \"use i18n\" am Anfang Ihrer Komponentendateien hinzu, und der Compiler erledigt den Rest!",
16-
"daa4d8839395": "{counter} mal geklickt",
17-
"52ed9ee761d8": "Hallo Welt",
18-
"f11fc78c3ac0": "<b0>Gemischter</b0> Inhalt <i0>Fragment</i0>",
19-
"556f5956dca7": "Willkommen zur Lingo.dev Compiler Demo",
20-
"02704ec4e52a": "Es extrahiert automatisch Text aus Ihrem JSX und übersetzt ihn in andere Sprachen.",
21-
"de6bfb30be49": "Text, der als <code0></code0> eingefügt wird, wird nicht übersetzt: {text}",
22-
"5c15bd35e916": "Um es zu übersetzen, müssen Sie es in '<'>{translatableText} '<'/> einschließen",
23-
"93b50fe805b7": "Text außerhalb der Komponente wird nicht übersetzt: {externalText}",
24-
"d756b03ffbf5": "Inhalte, die Text und andere Tags enthalten, werden als eine Einheit übersetzt: {translatableMixedContextFragment}"
24+
"07d84d34dd3a": "Fügen Sie einfach die Direktive \"use i18n\" am Anfang Ihrer Komponentendateien hinzu, und der Compiler erledigt den Rest!"
2525
}
26-
}
26+
}

demo/new-compiler-vite-react-spa/public/translations/en.json

Lines changed: 11 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,15 @@
22
"version": 0.1,
33
"locale": "en",
44
"entries": {
5+
"daa4d8839395": "Clicked {counter} times",
6+
"52ed9ee761d8": "Hello World",
7+
"f11fc78c3ac0": "<b0>Mixed</b0> content <i0>fragment</i0>",
8+
"556f5956dca7": "Welcome to Lingo.dev compiler demo",
9+
"02704ec4e52a": "It automatically extract text from your JSX and translate it to other languages.",
10+
"de6bfb30be49": "Text inserted as an <code0></code0> is not translated: {text}",
11+
"5c15bd35e916": "To translate it you have to wrap it into the '<'>{translatableText} '<'/>",
12+
"93b50fe805b7": "Text external to the component is not translated: {externalText}",
13+
"d756b03ffbf5": "Content that has text and other tags inside will br translated as a single entity: {translatableMixedContextFragment}",
514
"8492c53cfbaf": "About Lingo.dev",
615
"8aa4fe3f0590": "This is a demo application showcasing the Lingo.dev compiler for automatic translations in React applications.",
716
"af76f667703b": "Key Features",
@@ -12,15 +21,6 @@
1221
"aca12d550fe2": "Server and client component support",
1322
"44a3311c3a4a": "How It Works",
1423
"0add30e37450": "The compiler analyzes your React components at build time and automatically extracts all translatable strings. It then generates translations using your configured translation provider.",
15-
"07d84d34dd3a": "Simply add the \"use i18n\" directive at the top of your component files, and the compiler handles the rest!",
16-
"daa4d8839395": "Clicked {counter} times",
17-
"52ed9ee761d8": "Hello World",
18-
"f11fc78c3ac0": "<b0>Mixed</b0> content <i0>fragment</i0>",
19-
"556f5956dca7": "Welcome to Lingo.dev compiler demo",
20-
"02704ec4e52a": "It automatically extract text from your JSX and translate it to other languages.",
21-
"de6bfb30be49": "Text inserted as an <code0></code0> is not translated: {text}",
22-
"5c15bd35e916": "To translate it you have to wrap it into the '<'>{translatableText} '<'/>",
23-
"93b50fe805b7": "Text external to the component is not translated: {externalText}",
24-
"d756b03ffbf5": "Content that has text and other tags inside will br translated as a single entity: {translatableMixedContextFragment}"
24+
"07d84d34dd3a": "Simply add the \"use i18n\" directive at the top of your component files, and the compiler handles the rest!"
2525
}
26-
}
26+
}

demo/new-compiler-vite-react-spa/public/translations/es.json

Lines changed: 11 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,15 @@
22
"version": 0.1,
33
"locale": "es",
44
"entries": {
5+
"daa4d8839395": "Clicado {counter} veces",
6+
"52ed9ee761d8": "Hola Mundo",
7+
"f11fc78c3ac0": "Contenido <b0>mixto</b0> <i0>fragmento</i0>",
8+
"556f5956dca7": "Bienvenido a la demo de Lingo.dev compiler",
9+
"02704ec4e52a": "Extrae automáticamente texto de tu JSX y lo traduce a otros idiomas.",
10+
"de6bfb30be49": "El texto insertado como <code0></code0> no se traduce: {text}",
11+
"5c15bd35e916": "Para traducirlo tienes que envolverlo en el '<'>{translatableText} '<'/>",
12+
"93b50fe805b7": "El texto externo al componente no se traduce: {externalText}",
13+
"d756b03ffbf5": "El contenido que tiene texto y otras etiquetas dentro se traducirá como una sola entidad: {translatableMixedContextFragment}",
514
"8492c53cfbaf": "Acerca de Lingo.dev",
615
"8aa4fe3f0590": "Esta es una aplicación de demostración que muestra el compilador Lingo.dev para traducciones automáticas en aplicaciones React.",
716
"af76f667703b": "Características principales",
@@ -12,15 +21,6 @@
1221
"aca12d550fe2": "Soporte para componentes de servidor y cliente",
1322
"44a3311c3a4a": "Cómo funciona",
1423
"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.",
15-
"07d84d34dd3a": "¡Simplemente agrega la directiva \"use i18n\" en la parte superior de tus archivos de componentes, y el compilador se encarga del resto!",
16-
"daa4d8839395": "Clicado {counter} veces",
17-
"52ed9ee761d8": "Hola Mundo",
18-
"f11fc78c3ac0": "Contenido <b0>mixto</b0> <i0>fragmento</i0>",
19-
"556f5956dca7": "Bienvenido a la demo de Lingo.dev compiler",
20-
"02704ec4e52a": "Extrae automáticamente texto de tu JSX y lo traduce a otros idiomas.",
21-
"de6bfb30be49": "El texto insertado como <code0></code0> no se traduce: {text}",
22-
"5c15bd35e916": "Para traducirlo tienes que envolverlo en el '<'>{translatableText} '<'/>",
23-
"93b50fe805b7": "El texto externo al componente no se traduce: {externalText}",
24-
"d756b03ffbf5": "El contenido que tiene texto y otras etiquetas dentro se traducirá como una sola entidad: {translatableMixedContextFragment}"
24+
"07d84d34dd3a": "¡Simplemente agrega la directiva \"use i18n\" en la parte superior de tus archivos de componentes, y el compilador se encarga del resto!"
2525
}
26-
}
26+
}

demo/new-compiler-vite-react-spa/public/translations/fr.json

Lines changed: 11 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,15 @@
22
"version": 0.1,
33
"locale": "fr",
44
"entries": {
5+
"daa4d8839395": "Cliqué {counter} fois",
6+
"52ed9ee761d8": "Bonjour le monde",
7+
"f11fc78c3ac0": "<b0>Contenu</b0> mixte <i0>fragment</i0>",
8+
"556f5956dca7": "Bienvenue dans la démo du compilateur Lingo.dev",
9+
"02704ec4e52a": "Il extrait automatiquement le texte de votre JSX et le traduit dans d'autres langues.",
10+
"de6bfb30be49": "Le texte inséré comme un <code0></code0> n'est pas traduit : {text}",
11+
"5c15bd35e916": "Pour le traduire, vous devez l'envelopper dans le '<'>{translatableText} '<'/>",
12+
"93b50fe805b7": "Le texte externe au composant n'est pas traduit : {externalText}",
13+
"d756b03ffbf5": "Le contenu qui contient du texte et d'autres balises sera traduit comme une seule entité : {translatableMixedContextFragment}",
514
"8492c53cfbaf": "À propos de Lingo.dev",
615
"8aa4fe3f0590": "Ceci est une application de démonstration présentant le compilateur Lingo.dev pour les traductions automatiques dans les applications React.",
716
"af76f667703b": "Fonctionnalités clés",
@@ -12,15 +21,6 @@
1221
"aca12d550fe2": "Prise en charge des composants serveur et client",
1322
"44a3311c3a4a": "Comment ça fonctionne",
1423
"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é.",
15-
"07d84d34dd3a": "Ajoutez simplement la directive \"use i18n\" en haut de vos fichiers de composants, et le compilateur s'occupe du reste !",
16-
"daa4d8839395": "Cliqué {counter} fois",
17-
"52ed9ee761d8": "Bonjour le monde",
18-
"f11fc78c3ac0": "<b0>Contenu</b0> mixte <i0>fragment</i0>",
19-
"556f5956dca7": "Bienvenue dans la démo du compilateur Lingo.dev",
20-
"02704ec4e52a": "Il extrait automatiquement le texte de votre JSX et le traduit dans d'autres langues.",
21-
"de6bfb30be49": "Le texte inséré comme un <code0></code0> n'est pas traduit : {text}",
22-
"5c15bd35e916": "Pour le traduire, vous devez l'envelopper dans le '<'>{translatableText} '<'/>",
23-
"93b50fe805b7": "Le texte externe au composant n'est pas traduit : {externalText}",
24-
"d756b03ffbf5": "Le contenu qui contient du texte et d'autres balises sera traduit comme une seule entité : {translatableMixedContextFragment}"
24+
"07d84d34dd3a": "Ajoutez simplement la directive \"use i18n\" en haut de vos fichiers de composants, et le compilateur s'occupe du reste !"
2525
}
26-
}
26+
}

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

Lines changed: 37 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -219,4 +219,41 @@ describe("jsonc loader", () => {
219219
},
220220
});
221221
});
222+
223+
it("pullHints should extract comments from arrays", async () => {
224+
const loader = createJsoncLoader();
225+
loader.setDefaultLocale("en");
226+
const jsoncInput = `{
227+
"items": [
228+
{
229+
"value": "First item",
230+
"type": "heading"
231+
},
232+
{
233+
// This is a hint for the second item
234+
"value": "Second item",
235+
"type": "text"
236+
},
237+
{
238+
// This is a hint for the third item
239+
"value": "Third item",
240+
"type": "text"
241+
}
242+
]
243+
}`;
244+
245+
await loader.pull("en", jsoncInput);
246+
const comments = await loader.pullHints(jsoncInput);
247+
248+
expect(comments).toEqual({
249+
items: {
250+
"1": {
251+
value: { hint: "This is a hint for the second item" },
252+
},
253+
"2": {
254+
value: { hint: "This is a hint for the third item" },
255+
},
256+
},
257+
});
258+
});
222259
});

packages/cli/src/cli/loaders/jsonc.ts

Lines changed: 84 additions & 15 deletions
Original file line numberDiff line numberDiff line change
@@ -23,8 +23,9 @@ function extractCommentsFromJsonc(jsoncString: string): Record<string, any> {
2323
return {};
2424
}
2525

26-
// Track nesting context
27-
const contextStack: Array<{ key: string; isArray: boolean }> = [];
26+
// Track nesting context with array indices
27+
const contextStack: Array<{ key: string; isArray: boolean; arrayIndex?: number }> = [];
28+
let arrayObjectCount: Record<number, number> = {}; // Track object count per array depth
2829

2930
for (let i = 0; i < lines.length; i++) {
3031
const line = lines[i];
@@ -42,12 +43,12 @@ function extractCommentsFromJsonc(jsoncString: string): Record<string, any> {
4243
const keyMatch = line.match(/^\s*["']?([^"':,\s]+)["']?\s*:/);
4344
if (keyMatch) {
4445
const key = keyMatch[1];
45-
const path = contextStack.map((ctx) => ctx.key).filter(Boolean);
46+
const path = contextStack.map((ctx) => ctx.arrayIndex !== undefined ? String(ctx.arrayIndex) : ctx.key).filter(Boolean);
4647
keyInfo = { key, path };
4748
}
4849
} else {
4950
// For standalone comments, find the next key
50-
keyInfo = findAssociatedKey(lines, commentData.lineIndex, contextStack);
51+
keyInfo = findAssociatedKey(lines, commentData.lineIndex, contextStack, arrayObjectCount);
5152
}
5253

5354
if (keyInfo && keyInfo.key) {
@@ -60,7 +61,7 @@ function extractCommentsFromJsonc(jsoncString: string): Record<string, any> {
6061
}
6162

6263
// Update context for object/array nesting
63-
updateContext(contextStack, line, result);
64+
updateContext(contextStack, line, result, arrayObjectCount);
6465
}
6566

6667
return comments;
@@ -166,7 +167,8 @@ function extractBlockComment(
166167
function findAssociatedKey(
167168
lines: string[],
168169
commentLineIndex: number,
169-
contextStack: Array<{ key: string; isArray: boolean }>,
170+
contextStack: Array<{ key: string; isArray: boolean; arrayIndex?: number }>,
171+
arrayObjectCount: Record<number, number>,
170172
): { key: string | null; path: string[] } {
171173
// Look for the next key after the comment
172174
for (let i = commentLineIndex + 1; i < lines.length; i++) {
@@ -175,18 +177,50 @@ function findAssociatedKey(
175177
if (
176178
!line ||
177179
line.startsWith("//") ||
178-
line.startsWith("/*") ||
179-
line === "{" ||
180-
line === "}"
180+
line.startsWith("/*")
181181
) {
182182
continue;
183183
}
184184

185+
// Check if we're about to enter an array object
186+
if (line === "{" && contextStack.length > 0) {
187+
const parent = contextStack[contextStack.length - 1];
188+
if (parent.isArray) {
189+
// Get the current array index from arrayObjectCount
190+
const depth = contextStack.length - 1;
191+
const arrayIndex = arrayObjectCount[depth] || 0;
192+
193+
// Continue looking for the key inside this object
194+
for (let j = i + 1; j < lines.length; j++) {
195+
const innerLine = lines[j].trim();
196+
if (!innerLine || innerLine.startsWith("//") || innerLine.startsWith("/*")) continue;
197+
198+
const keyMatch = innerLine.match(/^\s*["']?([^"':,\s]+)["']?\s*:/);
199+
if (keyMatch) {
200+
const key = keyMatch[1];
201+
const path = contextStack
202+
.map((ctx) => ctx.arrayIndex !== undefined ? String(ctx.arrayIndex) : ctx.key)
203+
.filter(Boolean);
204+
path.push(String(arrayIndex));
205+
return { key, path };
206+
}
207+
208+
if (innerLine === "}") break;
209+
}
210+
}
211+
}
212+
213+
if (line === "{" || line === "}") {
214+
continue;
215+
}
216+
185217
// Extract key from line
186218
const keyMatch = line.match(/^\s*["']?([^"':,\s]+)["']?\s*:/);
187219
if (keyMatch) {
188220
const key = keyMatch[1];
189-
const path = contextStack.map((ctx) => ctx.key).filter(Boolean);
221+
const path = contextStack
222+
.map((ctx) => ctx.arrayIndex !== undefined ? String(ctx.arrayIndex) : ctx.key)
223+
.filter(Boolean);
190224
return { key, path };
191225
}
192226
}
@@ -195,12 +229,23 @@ function findAssociatedKey(
195229
}
196230

197231
function updateContext(
198-
contextStack: Array<{ key: string; isArray: boolean }>,
232+
contextStack: Array<{ key: string; isArray: boolean; arrayIndex?: number }>,
199233
line: string,
200234
parsedJson: any,
235+
arrayObjectCount: Record<number, number>,
201236
): void {
202-
// This is a simplified context tracking - in a full implementation,
203-
// you'd want more sophisticated AST-based tracking
237+
const trimmed = line.trim();
238+
239+
// Track opening of arrays
240+
const arrayMatch = line.match(/^\s*["']?([^"':,\s]+)["']?\s*:\s*\[/);
241+
if (arrayMatch) {
242+
const depth = contextStack.length;
243+
arrayObjectCount[depth] = 0; // Initialize counter for this array
244+
contextStack.push({ key: arrayMatch[1], isArray: true });
245+
return;
246+
}
247+
248+
// Track opening of objects
204249
const openBraces = (line.match(/\{/g) || []).length;
205250
const closeBraces = (line.match(/\}/g) || []).length;
206251

@@ -209,13 +254,37 @@ function updateContext(
209254
const keyMatch = line.match(/^\s*["']?([^"':,\s]+)["']?\s*:\s*\{/);
210255
if (keyMatch) {
211256
contextStack.push({ key: keyMatch[1], isArray: false });
257+
} else if (trimmed === '{' && contextStack.length > 0) {
258+
// This is an object within an array
259+
const parent = contextStack[contextStack.length - 1];
260+
if (parent.isArray) {
261+
const depth = contextStack.length - 1;
262+
const arrayIndex = arrayObjectCount[depth] || 0;
263+
contextStack.push({ key: '', isArray: false, arrayIndex });
264+
arrayObjectCount[depth]++;
265+
}
212266
}
213-
} else if (closeBraces > openBraces) {
214-
// Pop context when closing braces
267+
}
268+
269+
// Track closing of objects and arrays
270+
const openBrackets = (line.match(/\[/g) || []).length;
271+
const closeBrackets = (line.match(/\]/g) || []).length;
272+
273+
if (closeBraces > openBraces) {
215274
for (let i = 0; i < closeBraces - openBraces; i++) {
216275
contextStack.pop();
217276
}
218277
}
278+
279+
if (closeBrackets > openBrackets) {
280+
for (let i = 0; i < closeBrackets - openBrackets; i++) {
281+
const popped = contextStack.pop();
282+
if (popped?.isArray) {
283+
const depth = contextStack.length;
284+
delete arrayObjectCount[depth]; // Clean up counter
285+
}
286+
}
287+
}
219288
}
220289

221290
function setCommentAtPath(

0 commit comments

Comments
 (0)