Skip to content

Commit 5fe747b

Browse files
committed
chore: simplify scope handler and clenaup old snapshots
1 parent fcc2d2a commit 5fe747b

3 files changed

Lines changed: 92 additions & 79 deletions

File tree

cmp/compiler/src/plugin/transform/__snapshots__/transform.test.ts.snap

Lines changed: 0 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -962,16 +962,6 @@ exports[`transformComponent > translation overrides (data-lingo-override) > shou
962962
}"
963963
`;
964964
965-
exports[`transformComponent > translation overrides (data-lingo-override) > should parse override attribute with JSON string 1`] = `
966-
"import { useTranslation } from "@lingo.dev/compiler/react";
967-
export function Button() {
968-
const {
969-
t
970-
} = useTranslation(["43e1aff00fc2"]);
971-
return <button>{t("43e1aff00fc2", "Click")}</button>;
972-
}"
973-
`;
974-
975965
exports[`transformComponent > translation overrides (data-lingo-override) > should parse override attribute with object expression 1`] = `
976966
"import { useTranslation } from "@lingo.dev/compiler/react";
977967
export function Button() {

cmp/compiler/src/plugin/transform/process-file.ts

Lines changed: 92 additions & 67 deletions
Original file line numberDiff line numberDiff line change
@@ -161,11 +161,7 @@ function translateAttributes(
161161
node.loc?.start.line,
162162
node.loc?.start.column,
163163
);
164-
state.newEntries.push(entry);
165-
166-
const hashes = state.componentHashes.get(component.name) || [];
167-
hashes.push(entry.hash);
168-
state.componentHashes.set(component.name, hashes);
164+
registerEntry(entry, state, component.name);
169165

170166
attr.value = constructTranslationCall(entry.hash, text);
171167
}
@@ -263,7 +259,6 @@ interface SerializedJSX {
263259
components: Map<string, t.JSXElement>;
264260
}
265261

266-
// TODO (AleksandrSl 28/11/2025): Check whitespace logic, the rest seems reasonable
267262
/**
268263
* Serialize JSX children to a translation string with placeholders
269264
*/
@@ -397,87 +392,117 @@ function transformVoidElement(
397392
translateAttributes(node, state);
398393
}
399394

395+
type TranslationScope =
396+
| { kind: "mixed"; text: string; args: any }
397+
| { kind: "text"; text: string; textNodeIndex: number };
398+
399+
function getTranslationScope(
400+
node: t.JSXElement | t.JSXFragment,
401+
state: VisitorsInternalState,
402+
): TranslationScope | null {
403+
if (hasMixedContent(node)) {
404+
const serialized = serializeJSXChildren(node.children, state);
405+
const text = serialized.text.trim();
406+
if (text.length === 0) return null;
407+
408+
return {
409+
kind: "mixed",
410+
text,
411+
args: {
412+
variables: serialized.variables,
413+
expressions: serialized.expressions,
414+
components: serialized.components,
415+
},
416+
};
417+
}
418+
419+
// Non-mixed: allow exactly one meaningful JSXText + optional void elements + whitespace
420+
const textNodeIndex = node.children.findIndex(
421+
(child) => child.type === "JSXText" && child.value.trim().length > 0,
422+
);
423+
if (textNodeIndex === -1) return null;
424+
425+
const allowed = node.children.every(
426+
(child) =>
427+
child.type === "JSXText" ||
428+
(child.type === "JSXElement" && isVoidElement(child)),
429+
);
430+
if (!allowed) return null;
431+
432+
const textNode = node.children[textNodeIndex] as t.JSXText;
433+
const text = normalizeWhitespace(textNode.value);
434+
if (text.length === 0) return null;
435+
436+
return { kind: "text", text, textNodeIndex };
437+
}
438+
439+
function registerEntry(
440+
entry: ReturnType<typeof createTranslationEntry>,
441+
state: VisitorsInternalState,
442+
componentName: string,
443+
) {
444+
state.newEntries.push(entry);
445+
446+
const hashes = state.componentHashes.get(componentName) ?? [];
447+
hashes.push(entry.hash);
448+
state.componentHashes.set(componentName, hashes);
449+
}
450+
451+
function rewriteChildren(
452+
path: NodePath<t.JSXElement | t.JSXFragment>,
453+
state: VisitorsInternalState,
454+
translationScope: TranslationScope,
455+
entryHash: string,
456+
) {
457+
if (translationScope.kind === "mixed") {
458+
path.node.children = [
459+
constructTranslationCall(
460+
entryHash,
461+
translationScope.text,
462+
translationScope.args,
463+
),
464+
];
465+
return;
466+
}
467+
468+
const { textNodeIndex, text } = translationScope;
469+
470+
path.node.children = path.node.children.map((child, index) => {
471+
if (index === textNodeIndex) {
472+
return constructTranslationCall(entryHash, text);
473+
}
474+
if (child.type === "JSXElement") {
475+
transformVoidElement(child, state);
476+
}
477+
return child;
478+
});
479+
}
480+
400481
function processJSXElement(
401482
path: NodePath<t.JSXElement | t.JSXFragment>,
402483
state: VisitorsInternalState,
403484
): void {
404485
const component = state.componentsStack.at(-1);
405486
if (!component) return;
406487

407-
let type = undefined;
408-
let textNode;
409-
let textNodeIndex: number | undefined;
410-
if (hasMixedContent(path.node)) {
411-
type = "mixed";
412-
} else {
413-
// If there were several text elements we will be in the mixed content above
414-
textNodeIndex = path.node.children.findIndex(
415-
(child) => child.type === "JSXText" && child.value.trim().length > 0,
416-
);
417-
if (
418-
textNodeIndex !== -1 &&
419-
path.node.children.every(
420-
(child) =>
421-
child.type === "JSXText" ||
422-
(child.type === "JSXElement" && isVoidElement(child)),
423-
)
424-
) {
425-
type = "text";
426-
textNode = path.node.children[textNodeIndex] as t.JSXText;
427-
}
428-
}
429-
430-
if (!type) {
431-
return;
432-
}
488+
const scope = getTranslationScope(path.node, state);
489+
if (!scope) return;
433490

434491
const overrides = processOverrideAttributes(path);
435492

436-
let text;
437-
let args;
438-
if (type === "mixed") {
439-
const serialized = serializeJSXChildren(path.node.children, state);
440-
text = serialized.text.trim();
441-
args = {
442-
variables: serialized.variables,
443-
expressions: serialized.expressions,
444-
components: serialized.components,
445-
};
446-
} else {
447-
text = normalizeWhitespace(textNode!.value);
448-
}
449-
450-
if (text.length == 0) return;
451-
452493
const entry = createTranslationEntry(
453494
"content",
454-
text,
495+
scope.text,
455496
{ componentName: component.name },
456497
state.filePath,
457498
path.node.loc?.start.line,
458499
path.node.loc?.start.column,
459500
overrides,
460501
);
461-
state.newEntries.push(entry);
462502

463-
const hashes = state.componentHashes.get(component.name) || [];
464-
hashes.push(entry.hash);
465-
state.componentHashes.set(component.name, hashes);
503+
registerEntry(entry, state, component.name);
504+
rewriteChildren(path, state, scope, entry.hash);
466505

467-
if (type === "mixed") {
468-
path.node.children = [constructTranslationCall(entry.hash, text, args)];
469-
} else {
470-
path.node.children = path.node.children.map((it, index) => {
471-
if (index === textNodeIndex) {
472-
return constructTranslationCall(entry.hash, text);
473-
} else if (it.type === "JSXElement") {
474-
transformVoidElement(it, state);
475-
return it;
476-
} else {
477-
return it;
478-
}
479-
});
480-
}
481506
path.skip();
482507
}
483508

cmp/compiler/src/react/shared/__snapshots__/render-rich-text.test.tsx.snap

Lines changed: 0 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -22,8 +22,6 @@ exports[`renderRichText > should handle nested components inside component tags
2222
]
2323
`;
2424

25-
exports[`renderRichText > should handle text with literal angle brackets 1`] = `"To translate it you have to wrap it into the <'>some text</>"`;
26-
2725
exports[`renderRichText > should handle the page.tsx example case 1`] = `
2826
[
2927
<React.Fragment

0 commit comments

Comments
 (0)