Skip to content

Commit 981f702

Browse files
committed
feat: correct fragments translation
1 parent 7d68372 commit 981f702

3 files changed

Lines changed: 103 additions & 42 deletions

File tree

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

Lines changed: 30 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -210,26 +210,49 @@ export const Button = function () {
210210
};"
211211
`;
212212

213-
exports[`transformComponent > corner cases > should handle separate fragments used in expressions correctly 1`] = `
213+
exports[`transformComponent > fragments > should handle separate fragments used in expressions correctly 1`] = `
214214
"import { useTranslation } from "@lingo.dev/_compiler/react";
215215
export default function Home() {
216-
const t = useTranslation(["4ea986f9331c", "f6c9a24ac814", "2c582e458cdf"]);
217-
const translatableText = <>Hello World</>;
216+
const t = useTranslation(["4ea986f9331c", "2c582e458cdf"]);
218217
const translatableMixedContextFragment = <>{t("4ea986f9331c", "<b0>Mixed</b0> content <i0>fragment</i0>", {
219218
b0: chunks => <b>{chunks}</b>,
220219
i0: chunks => <i>{chunks}</i>
221220
})}</>;
222221
return <main>
223-
<div>{t("f6c9a24ac814", "To translate it you have to wrap it into the {translatableText}", {
224-
translatableText
225-
})}</div>
226-
<div>{t("2c582e458cdf", "Content that has text and other tags inside will br translated as a single entity: {translatableMixedContextFragment}", {
222+
<div>{t("2c582e458cdf", "Content that has text and other tags inside will br translated as a single entity: {translatableMixedContextFragment}", {
227223
translatableMixedContextFragment
228224
})}</div>
229225
</main>;
230226
}"
231227
`;
232228

229+
exports[`transformComponent > fragments > should translate both <> and <Fragment> 1`] = `
230+
"import { useTranslation } from "@lingo.dev/_compiler/react";
231+
export default function Home() {
232+
const t = useTranslation(["26797a9fdc8b", "ce6bd657be8d"]);
233+
const translatableText = <>
234+
<>{t("26797a9fdc8b", "These two pieces of text.")}</>
235+
<Fragment>{t("ce6bd657be8d", "Considered separate, because no one in the world should write this code.")}</Fragment>
236+
</>;
237+
return <div>
238+
{translatableText}
239+
</div>;
240+
}"
241+
`;
242+
243+
exports[`transformComponent > fragments > should translate fragments 1`] = `
244+
"import { useTranslation } from "@lingo.dev/_compiler/react";
245+
export default function Home() {
246+
const t = useTranslation(["27578dfabc31", "f6c9a24ac814"]);
247+
const translatableText = <>{t("27578dfabc31", "Hello World")}</>;
248+
return <main>
249+
<div>{t("f6c9a24ac814", "To translate it you have to wrap it into the {translatableText}", {
250+
translatableText
251+
})}</div>
252+
</main>;
253+
}"
254+
`;
255+
233256
exports[`transformComponent > isomorphic hooks (unified useTranslation) > should handle mixed async and non-async components in same file 1`] = `
234257
"import { useTranslation } from "@lingo.dev/_compiler/react";
235258
import { getServerTranslations } from "@lingo.dev/_compiler/react/server";

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

Lines changed: 56 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -2272,12 +2272,44 @@ export function ExplicitTranslate() {
22722272
});
22732273
});
22742274

2275-
describe("corner cases", () => {
2276-
it("should handle function expressions correctly", () => {
2275+
describe("fragments", () => {
2276+
it("should translate fragments", () => {
22772277
const code = `
2278-
export const Button = function() { return <button>Click me</button>; }
2279-
`;
2278+
export default function Home() {
2279+
const translatableText = <>Hello World</>;
2280+
return (
2281+
<main>
2282+
<div>
2283+
To translate it you have to wrap it into the {translatableText}
2284+
</div>
2285+
</main>
2286+
)
2287+
}
2288+
`;
2289+
const result = transformComponent({
2290+
code,
2291+
filePath: "src/FunctionExpression.tsx",
2292+
config,
2293+
});
2294+
2295+
expect(result.code).toMatchSnapshot();
2296+
});
2297+
2298+
it("should translate both <> and <Fragment>", () => {
2299+
const code = `
2300+
export default function Home() {
2301+
const translatableText = <>
2302+
<>These two pieces of text.</>
2303+
<Fragment>Considered separate, because no one in the world should write this code.</Fragment>
2304+
</>;
22802305
2306+
return (
2307+
<div>
2308+
{translatableText}
2309+
</div>
2310+
)
2311+
}
2312+
`;
22812313
const result = transformComponent({
22822314
code,
22832315
filePath: "src/FunctionExpression.tsx",
@@ -2290,7 +2322,6 @@ export const Button = function() { return <button>Click me</button>; }
22902322
it("should handle separate fragments used in expressions correctly", () => {
22912323
const code = `
22922324
export default function Home() {
2293-
const translatableText = <>Hello World</>;
22942325
const translatableMixedContextFragment = (
22952326
<>
22962327
<b>Mixed</b> content <i>fragment</i>
@@ -2299,13 +2330,10 @@ export default function Home() {
22992330
23002331
return (
23012332
<main>
2302-
<div>
2303-
To translate it you have to wrap it into the {translatableText}
2304-
</div>
2305-
<div>
2306-
Content that has text and other tags inside will br translated as a
2307-
single entity: {translatableMixedContextFragment}
2308-
</div>
2333+
<div>
2334+
Content that has text and other tags inside will br translated as a
2335+
single entity: {translatableMixedContextFragment}
2336+
</div>
23092337
</main>
23102338
)
23112339
}
@@ -2320,4 +2348,20 @@ export default function Home() {
23202348
expect(result.code).toMatchSnapshot();
23212349
});
23222350
});
2351+
2352+
describe("corner cases", () => {
2353+
it("should handle function expressions correctly", () => {
2354+
const code = `
2355+
export const Button = function() { return <button>Click me</button>; }
2356+
`;
2357+
2358+
const result = transformComponent({
2359+
code,
2360+
filePath: "src/FunctionExpression.tsx",
2361+
config,
2362+
});
2363+
2364+
expect(result.code).toMatchSnapshot();
2365+
});
2366+
});
23232367
});

cmp/compiler/src/plugin/transform/visitors.ts

Lines changed: 17 additions & 23 deletions
Original file line numberDiff line numberDiff line change
@@ -39,6 +39,8 @@ export interface VisitorsInternalState extends VisitorsSharedState {
3939

4040
const root = "@lingo.dev/_compiler";
4141

42+
const SKIP_ATTRIBUTE = "data-lingo-skip";
43+
4244
// ============================================================================
4345
// TEXT NORMALIZATION UTILITIES
4446
// ============================================================================
@@ -242,8 +244,7 @@ function shouldSkipTranslation(element: t.JSXElement | t.JSXFragment): boolean {
242244
}
243245
}
244246

245-
// Check for data-lingo-skip attribute (presence is enough)
246-
if (attr.name.name === "data-lingo-skip") {
247+
if (attr.name.name === SKIP_ATTRIBUTE) {
247248
return true;
248249
}
249250
}
@@ -1269,36 +1270,29 @@ function handleComponentFunction(
12691270
}
12701271

12711272
const componentVisitors = {
1272-
FunctionDeclaration: {
1273-
enter(path: NodePath<t.FunctionDeclaration>) {
1274-
handleComponentFunction(path, this.visitorState);
1275-
},
1273+
// 1. Handle nested components. FunctionDeclaration|FunctionExpression|ArrowFunctionExpression unfortunately doesn't give the correct type signature for the path.
1274+
FunctionDeclaration(path: NodePath<t.FunctionDeclaration>) {
1275+
handleComponentFunction(path, this.visitorState);
12761276
},
1277-
1278-
ArrowFunctionExpression: {
1279-
enter(path: NodePath<t.ArrowFunctionExpression>) {
1280-
handleComponentFunction(path, this.visitorState);
1281-
},
1277+
ArrowFunctionExpression(path: NodePath<t.ArrowFunctionExpression>) {
1278+
handleComponentFunction(path, this.visitorState);
12821279
},
1283-
1284-
FunctionExpression: {
1285-
enter(path: NodePath<t.FunctionExpression>) {
1286-
handleComponentFunction(path, this.visitorState);
1287-
},
1280+
FunctionExpression(path: NodePath<t.FunctionExpression>) {
1281+
handleComponentFunction(path, this.visitorState);
12881282
},
1289-
1283+
// 2. Fragments are separate from other JSX elements. Both <> and <Fragment> are the same AST node type.
12901284
JSXFragment(path: NodePath<t.JSXFragment>) {
1291-
path.skip();
1292-
1285+
// No attributes to translate on the fragment, so we only check if it has mixed content. If it doesn't, go ahead and its children will be checked.
1286+
// We also do not check for translation skip, because fragments are mostly used to make the bare text translatable.
1287+
// But if we want to support <Fragment data-lingo-skip>Text</Fragment> we should do it here.
12931288
if (hasMixedContent(path.node)) {
12941289
transformMixedJSXElement(path, this.visitorState);
12951290
// Skip traversing children since we've already processed them
12961291
path.skip();
12971292
}
12981293
},
12991294

1300-
// Transform JSX elements with mixed content (text + expressions + nested elements)
1301-
// This runs BEFORE JSXText visitor due to traversal order
1295+
// Transform JSX elements with mixed content (text + expressions or nested elements)
13021296
JSXElement(path: NodePath<t.JSXElement>) {
13031297
translateAttributes(path.node, this.visitorState);
13041298

@@ -1313,8 +1307,8 @@ const componentVisitors = {
13131307
}
13141308
},
13151309

1316-
// Transform JSX text nodes - finds nearest component ancestor
1317-
// This only runs for simple text nodes (not part of mixed content)
1310+
// Transform JSX text nodes
1311+
// This only runs for simple text nodes, both inside fragments and elements. Mixed content is handled separately.
13181312
JSXText(path: NodePath<t.JSXText>) {
13191313
transformJSXText(path, this.visitorState);
13201314
},

0 commit comments

Comments
 (0)