Skip to content

Commit 7d68372

Browse files
committed
feat: pluralization
1 parent a3a01c6 commit 7d68372

31 files changed

Lines changed: 1495 additions & 345 deletions

cmp/compiler/package.json

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -152,10 +152,12 @@
152152
"@babel/preset-typescript": "^7.26.0",
153153
"@babel/traverse": "^7.28.5",
154154
"@babel/types": "^7.28.5",
155+
"@formatjs/icu-messageformat-parser": "^2.11.4",
155156
"@openrouter/ai-sdk-provider": "^0.7.1",
156157
"ai": "^4.3.19",
157158
"dotenv": "^16.4.5",
158159
"fast-xml-parser": "^5.0.8",
160+
"intl-messageformat": "^10.7.18",
159161
"lingo.dev": "^0.117.0",
160162
"lodash": "^4.17.21",
161163
"ollama-ai-provider": "^1.2.0",

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

Lines changed: 4 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -446,8 +446,8 @@ export default function Spaced() {
446446
exports[`transformComponent > mixed content (rich text) > should keep text separated by void elements as the single translation 1`] = `
447447
"import { useTranslation } from "@lingo.dev/_compiler/react";
448448
export default function Links() {
449-
const t = useTranslation(["35a60a2a9411"]);
450-
return <p>{t("35a60a2a9411", "Click <br0/>there", {
449+
const t = useTranslation(["a935cf526185"]);
450+
return <p>{t("a935cf526185", "Click <br0></br0>there", {
451451
br0: () => <br />
452452
})}</p>;
453453
}"
@@ -665,9 +665,9 @@ export function Card() {
665665
exports[`transformComponent > skip translation > should skip translation for <code> elements 1`] = `
666666
"import { useTranslation } from "@lingo.dev/_compiler/react";
667667
export function Example() {
668-
const t = useTranslation(["89b67cab5fe3"]);
668+
const t = useTranslation(["fc543267bd22"]);
669669
return <div>
670-
<p>{t("89b67cab5fe3", "Install using <code0/> command", {
670+
<p>{t("fc543267bd22", "Install using <code0></code0> command", {
671671
code0: () => <code>npm install package</code>
672672
})}</p>
673673
</div>;

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

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -81,11 +81,14 @@ export function transformComponent({
8181
ast,
8282
{
8383
sourceMaps: true,
84+
// TODO (AleksandrSl 05/12/2025): Why is it false?
8485
retainLines: false,
8586
},
8687
code,
8788
);
8889

90+
logger.debug(`Transformed ${filePath}. Code: ${output.code}`);
91+
8992
return {
9093
code: output.code,
9194
map: output.map,

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

Lines changed: 5 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -1,8 +1,8 @@
11
/**
22
* Keep mutations to the current node and children. e.g. do not mutate program inserting imports when processing the component. This should be done in the end.
33
*/
4-
import * as t from "@babel/types";
54
import type { VariableDeclaration } from "@babel/types";
5+
import * as t from "@babel/types";
66
import type { NodePath, TraverseOptions } from "@babel/traverse";
77
import type { ComponentType, LingoConfig, TranslationEntry } from "../../types";
88
import { generateTranslationHash } from "../../utils/hash";
@@ -504,7 +504,8 @@ function serializeJSXChildren(
504504
components.set(`${tagName}_${nestedTag}`, nestedElement);
505505
}
506506
} else {
507-
text += `<${tagName}/>`;
507+
// There is no support for self-closing tags in format js, so we should generate empty ones.
508+
text += `<${tagName}></${tagName}>`;
508509
child.extra = {
509510
...child.extra,
510511
shouldTranslate: false,
@@ -551,7 +552,8 @@ function createRichTextTranslationCall(
551552
for (const [tagName, element] of components) {
552553
const renderFn =
553554
element.extra?.shouldTranslate === false
554-
? t.arrowFunctionExpression([], element)
555+
? // Even when doing: tagName: () => <Element />, we need a function for formatjs to work.
556+
t.arrowFunctionExpression([], element)
555557
: // Create: tagName: (chunks) => <Element>{chunks}</Element>
556558
t.arrowFunctionExpression(
557559
// TODO (AleksandrSl 28/11/2025): Use content instead of the chunks later

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

Lines changed: 0 additions & 25 deletions
This file was deleted.

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

Lines changed: 124 additions & 32 deletions
Original file line numberDiff line numberDiff line change
@@ -2,76 +2,132 @@
22

33
exports[`renderRichText > should handle nested components inside component tags 1`] = `
44
[
5-
"You have ",
6-
<React.Fragment>
5+
<React.Fragment
6+
data-key="0"
7+
>
8+
You have
9+
</React.Fragment>,
10+
<React.Fragment
11+
data-key="1"
12+
>
713
<strong>
814
10
915
</strong>
1016
</React.Fragment>,
11-
" items",
17+
<React.Fragment
18+
data-key="2"
19+
>
20+
items
21+
</React.Fragment>,
1222
]
1323
`;
1424

1525
exports[`renderRichText > should handle the page.tsx example case 1`] = `
1626
[
17-
"Looking for a starting points or more instructions? Head over to ",
18-
<React.Fragment>
27+
<React.Fragment
28+
data-key="0"
29+
>
30+
Looking for a starting points or more instructions? Head over to
31+
</React.Fragment>,
32+
<React.Fragment
33+
data-key="1"
34+
>
1935
<a
2036
className="font-medium"
2137
href="https://vercel.com/templates"
2238
>
2339
Templates
2440
</a>
2541
</React.Fragment>,
26-
" or the ",
27-
<React.Fragment>
42+
<React.Fragment
43+
data-key="2"
44+
>
45+
or the
46+
</React.Fragment>,
47+
<React.Fragment
48+
data-key="3"
49+
>
2850
<a
2951
className="font-medium"
3052
href="https://nextjs.org/learn"
3153
>
3254
Learning
3355
</a>
3456
</React.Fragment>,
35-
" center.",
57+
<React.Fragment
58+
data-key="4"
59+
>
60+
center.
61+
</React.Fragment>,
3662
]
3763
`;
3864

3965
exports[`renderRichText > should handle whitespace around component tags 1`] = `
4066
[
41-
"Looking for help? Head over to ",
42-
<React.Fragment>
67+
<React.Fragment
68+
data-key="0"
69+
>
70+
Looking for help? Head over to
71+
</React.Fragment>,
72+
<React.Fragment
73+
data-key="1"
74+
>
4375
<a>
4476
Templates
4577
</a>
4678
</React.Fragment>,
47-
" or the ",
48-
<React.Fragment>
79+
<React.Fragment
80+
data-key="2"
81+
>
82+
or the
83+
</React.Fragment>,
84+
<React.Fragment
85+
data-key="3"
86+
>
4987
<a>
5088
Learning
5189
</a>
5290
</React.Fragment>,
53-
" center.",
91+
<React.Fragment
92+
data-key="4"
93+
>
94+
center.
95+
</React.Fragment>,
5496
]
5597
`;
5698

5799
exports[`renderRichText > should render complex mixed content with variables and components 1`] = `
58100
[
59-
"Hello ",
60-
"Alice",
61-
", you have ",
62-
<React.Fragment>
101+
<React.Fragment
102+
data-key="0"
103+
>
104+
Hello Alice, you have
105+
</React.Fragment>,
106+
<React.Fragment
107+
data-key="1"
108+
>
63109
<strong>
64110
5
65111
</strong>
66112
</React.Fragment>,
67-
" messages",
113+
<React.Fragment
114+
data-key="2"
115+
>
116+
messages
117+
</React.Fragment>,
68118
]
69119
`;
70120

71121
exports[`renderRichText > should render component placeholders with JSX 1`] = `
72122
[
73-
"Click ",
74-
<React.Fragment>
123+
<React.Fragment
124+
data-key="0"
125+
>
126+
Click
127+
</React.Fragment>,
128+
<React.Fragment
129+
data-key="1"
130+
>
75131
<a
76132
href="/home"
77133
>
@@ -83,16 +139,30 @@ exports[`renderRichText > should render component placeholders with JSX 1`] = `
83139

84140
exports[`renderRichText > should render fragments with expressions 1`] = `
85141
[
86-
"Content that has text and other tags inside will br translated as a single entity: ",
87-
<React.Fragment>
142+
<React.Fragment
143+
data-key="0"
144+
>
145+
Content that has text and other tags inside will be translated as a single entity:
146+
</React.Fragment>,
147+
<React.Fragment
148+
data-key="1"
149+
>
88150
<React.Fragment>
89-
<React.Fragment>
151+
<React.Fragment
152+
data-key="0"
153+
>
90154
<b>
91155
Mixed
92156
</b>
93157
</React.Fragment>
94-
content
95-
<React.Fragment>
158+
<React.Fragment
159+
data-key="1"
160+
>
161+
content
162+
</React.Fragment>
163+
<React.Fragment
164+
data-key="2"
165+
>
96166
<i>
97167
fragment
98168
</i>
@@ -104,16 +174,28 @@ exports[`renderRichText > should render fragments with expressions 1`] = `
104174

105175
exports[`renderRichText > should render multiple same-type components 1`] = `
106176
[
107-
"Click ",
108-
<React.Fragment>
177+
<React.Fragment
178+
data-key="0"
179+
>
180+
Click
181+
</React.Fragment>,
182+
<React.Fragment
183+
data-key="1"
184+
>
109185
<a
110186
href="/home"
111187
>
112188
here
113189
</a>
114190
</React.Fragment>,
115-
" or ",
116-
<React.Fragment>
191+
<React.Fragment
192+
data-key="2"
193+
>
194+
or
195+
</React.Fragment>,
196+
<React.Fragment
197+
data-key="3"
198+
>
117199
<a
118200
href="/about"
119201
>
@@ -125,12 +207,22 @@ exports[`renderRichText > should render multiple same-type components 1`] = `
125207

126208
exports[`renderRichText > should render untranslatable content as is 1`] = `
127209
[
128-
"Install using ",
129-
<React.Fragment>
210+
<React.Fragment
211+
data-key="0"
212+
>
213+
Install using
214+
</React.Fragment>,
215+
<React.Fragment
216+
data-key="1"
217+
>
130218
<code>
131219
npm install package
132220
</code>
133221
</React.Fragment>,
134-
" command",
222+
<React.Fragment
223+
data-key="2"
224+
>
225+
command
226+
</React.Fragment>,
135227
]
136228
`;

0 commit comments

Comments
 (0)