Skip to content

Commit 30faa6d

Browse files
fix(cli): reimplemented xliff (#1000)
* fix(cli): reimplemented xliff * chore: flat loader * chore: add changeset
1 parent 83db104 commit 30faa6d

File tree

8 files changed

+816
-16
lines changed

8 files changed

+816
-16
lines changed

.changeset/seven-ties-grow.md

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,5 @@
1+
---
2+
"lingo.dev": patch
3+
---
4+
5+
xliff 1.2 implementation

packages/cli/demo/xliff/en.xliff

Lines changed: 35 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,35 @@
1+
<?xml version="1.0" encoding="utf-8"?>
2+
<xliff xmlns="urn:oasis:names:tc:xliff:document:2.0" version="2.0" srcLang="en-US">
3+
<file id="core">
4+
<unit id="hello" resname="hello">
5+
<segment><source>Hello</source></segment>
6+
</unit>
7+
8+
<unit id="bye" resname="bye">
9+
<segment><source>Good-bye</source><target state="translated">Adiós</target></segment>
10+
</unit>
11+
12+
<unit id="expr" resname="expr">
13+
<segment><source><![CDATA[5 < 7 & 8 > 3]]></source></segment>
14+
</unit>
15+
16+
<unit id="dupA" resname="dup_key"><segment><source>A</source></segment></unit>
17+
<unit id="dupB" resname="dup_key"><segment><source>B</source></segment></unit>
18+
19+
<group id="settings">
20+
<group id="cta">
21+
<unit id="ctaTitle" resname="CTA/Title"><segment><source>Try it now</source></segment></unit>
22+
<unit id="ctaBody" resname="CTA/Body"><segment><source>The best app ever.</source></segment></unit>
23+
</group>
24+
</group>
25+
26+
<unit id="skeleton" resname="skeleton"/>
27+
</file>
28+
29+
<file id="marketing">
30+
<unit id="heroTitle" resname="Hero/Title"><segment><source>The future of translation</source></segment></unit>
31+
<unit id="heroSubtitle" resname="Hero/Subtitle">
32+
<segment><source>Say it once, localise everywhere.</source><target state="new"/></segment>
33+
</unit>
34+
</file>
35+
</xliff>

packages/cli/demo/xliff/es.xliff

Lines changed: 63 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,63 @@
1+
<?xml version="1.0" encoding="utf-8"?>
2+
<xliff xmlns="urn:oasis:names:tc:xliff:document:2.0" version="2.0" srcLang="en-US">
3+
<file id="core">
4+
<unit id="hello" resname="hello">
5+
<segment>
6+
<source>Hola</source>
7+
</segment>
8+
</unit>
9+
<unit id="bye" resname="bye">
10+
<segment>
11+
<source>Adiós</source>
12+
<target state="translated">Adiós</target>
13+
</segment>
14+
</unit>
15+
<unit id="expr" resname="expr">
16+
<segment>
17+
<source><![CDATA[5 < 7 & 8 > 3]]></source>
18+
</segment>
19+
</unit>
20+
<unit id="dupA" resname="dup_key">
21+
<segment>
22+
<source>A</source>
23+
</segment>
24+
</unit>
25+
<unit id="dupB" resname="dup_key">
26+
<segment>
27+
<source>B</source>
28+
</segment>
29+
</unit>
30+
<group id="settings">
31+
<group id="cta">
32+
<unit id="ctaTitle" resname="CTA/Title">
33+
<segment>
34+
<source>Pruébalo ahora</source>
35+
</segment>
36+
</unit>
37+
<unit id="ctaBody" resname="CTA/Body">
38+
<segment>
39+
<source>La mejor aplicación de todos los tiempos.</source>
40+
</segment>
41+
</unit>
42+
</group>
43+
</group>
44+
<unit id="skeleton" resname="skeleton">
45+
<segment xmlns="">
46+
<source>esqueleto</source>
47+
</segment>
48+
</unit>
49+
</file>
50+
<file id="marketing">
51+
<unit id="heroTitle" resname="Hero/Title">
52+
<segment>
53+
<source>El futuro de la traducción</source>
54+
</segment>
55+
</unit>
56+
<unit id="heroSubtitle" resname="Hero/Subtitle">
57+
<segment>
58+
<source>Dilo una vez, localiza en todas partes.</source>
59+
<target state="new"></target>
60+
</segment>
61+
</unit>
62+
</file>
63+
</xliff>

packages/cli/i18n.json

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -5,6 +5,9 @@
55
"targets": ["ru", "es"]
66
},
77
"buckets": {
8+
"xliff": {
9+
"include": ["demo/xliff/[locale].xliff"]
10+
},
811
"android": {
912
"include": ["demo/android/[locale].xml"]
1013
},

packages/cli/i18n.lock

Lines changed: 12 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -359,3 +359,15 @@ checksums:
359359
days_until_event/other: 8860d8b849df0fc9c05a0105686c870a
360360
complex_format: 412e9a53af55fd61a80550c462dfcfcb
361361
date_format: 53fd72141572ffcdb3ebd8e5e6c4388d
362+
decd5fd51d99ea132f05fdfdb5d7eada:
363+
sourceLanguage: a422f993d9220fc3488259e5e0426e80
364+
resources/core/hello/source: f01e599cced8b7d7105329947b5096de
365+
resources/core/bye/source: 87fa86f9031290a9c591f91cff393930
366+
resources/core/expr/source: b96f42288414c31f9016040e21f7ff7f
367+
resources/core/dupA/source: 59a5c5f4cf42c5d5e87fdbd711c2aab7
368+
resources/core/dupB/source: f4d018cabf6eeab3f1c879c1d8eed6de
369+
resources/core/settings/groupUnits/cta/groupUnits/ctaTitle/source: 5a9263f67cc7632eb0e3938304b03cb8
370+
resources/core/settings/groupUnits/cta/groupUnits/ctaBody/source: ea0bce46ca0a7d95ea6be8ba24e10131
371+
resources/core/skeleton/source: 787b64864ffaef3e1c4b5657b3ce71b4
372+
resources/marketing/heroTitle/source: 7b843029e2f0de6821d39fd42595d9e8
373+
resources/marketing/heroSubtitle/source: aa6d22f1442eea0e9724d2f560329778

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

Lines changed: 12 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -2037,11 +2037,13 @@ Mundo!`;
20372037
</xliff>
20382038
`.trim();
20392039

2040+
// Keys must be encoded (e.g. / replaced with %2F)
20402041
const expectedOutput = {
2041-
"resources/namespace1/group/groupUnits/groupUnit/source": "Group",
2042-
"resources/namespace1/key.nested/source": "XLIFF Data Manager",
2043-
"resources/namespace1/key1/source": "Hello",
2044-
"resources/namespace1/key2/source":
2042+
"resources%2Fnamespace1%2Fgroup%2FgroupUnits%2FgroupUnit%2Fsource":
2043+
"Group",
2044+
"resources%2Fnamespace1%2Fkey.nested%2Fsource": "XLIFF Data Manager",
2045+
"resources%2Fnamespace1%2Fkey1%2Fsource": "Hello",
2046+
"resources%2Fnamespace1%2Fkey2%2Fsource":
20452047
"An application to manipulate and process XLIFF documents",
20462048
sourceLanguage: "en-US",
20472049
};
@@ -2088,12 +2090,14 @@ Mundo!`;
20882090
</file>
20892091
</xliff>
20902092
`.trim();
2093+
// Keys must be encoded (e.g. / replaced with %2F)
20912094
const payload = {
2092-
"resources/namespace1/group/groupUnits/groupUnit/source": "Grupo",
2093-
"resources/namespace1/key.nested/source":
2095+
"resources%2Fnamespace1%2Fgroup%2FgroupUnits%2FgroupUnit%2Fsource":
2096+
"Grupo",
2097+
"resources%2Fnamespace1%2Fkey.nested%2Fsource":
20942098
"Administrador de Datos XLIFF",
2095-
"resources/namespace1/key1/source": "Hola",
2096-
"resources/namespace1/key2/source":
2099+
"resources%2Fnamespace1%2Fkey1%2Fsource": "Hola",
2100+
"resources%2Fnamespace1%2Fkey2%2Fsource":
20972101
"Una aplicación para manipular y procesar documentos XLIFF",
20982102
sourceLanguage: "es-ES",
20992103
};
Lines changed: 107 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,107 @@
1+
import { describe, it, expect } from "vitest";
2+
import dedent from "dedent";
3+
import createXliffLoader from "./xliff";
4+
5+
function normalize(xml: string) {
6+
return xml.trim().replace(/\r?\n/g, "\n");
7+
}
8+
9+
describe("XLIFF loader", () => {
10+
it("round-trips a simple file without changes", async () => {
11+
const input = dedent`<?xml version="1.0" encoding="utf-8"?>
12+
<xliff xmlns="urn:oasis:names:tc:xliff:document:1.2" version="1.2">
13+
<file original="demo" source-language="en" target-language="en" datatype="plaintext">
14+
<body>
15+
<trans-unit id="hello" resname="hello">
16+
<source>Hello</source>
17+
<target state="translated">Hello</target>
18+
</trans-unit>
19+
</body>
20+
</file>
21+
</xliff>`;
22+
23+
const loader = createXliffLoader();
24+
loader.setDefaultLocale("en");
25+
26+
const data = await loader.pull("en", input);
27+
expect(data).toEqual({ hello: "Hello" });
28+
29+
// push back identical payload
30+
const output = await loader.push("en", data);
31+
expect(normalize(output)).toBe(normalize(input));
32+
});
33+
34+
it("handles duplicate resnames deterministically", async () => {
35+
const input = dedent`<?xml version="1.0" encoding="utf-8"?>
36+
<xliff xmlns="urn:oasis:names:tc:xliff:document:1.2" version="1.2">
37+
<file original="dup" source-language="en" target-language="en" datatype="plaintext">
38+
<body>
39+
<trans-unit id="a" resname="dup_key"><source>A</source><target>A</target></trans-unit>
40+
<trans-unit id="b" resname="dup_key"><source>B</source><target>B</target></trans-unit>
41+
</body>
42+
</file>
43+
</xliff>`;
44+
45+
const loader = createXliffLoader();
46+
loader.setDefaultLocale("en");
47+
const pulled = await loader.pull("en", input);
48+
expect(pulled).toEqual({
49+
dup_key: "A",
50+
"dup_key#b": "B",
51+
});
52+
53+
// translate and push
54+
const esPayload = {
55+
dup_key: "AA",
56+
"dup_key#b": "BB",
57+
} as const;
58+
59+
const esXml = await loader.push("es", esPayload);
60+
61+
// Pull from Spanish to verify the values were set correctly
62+
const loaderEs = createXliffLoader();
63+
loaderEs.setDefaultLocale("en");
64+
await loaderEs.pull("en", input); // pull original first
65+
const pullEs = await loaderEs.pull("es", esXml);
66+
67+
// Should get the translated values, not the original
68+
expect(pullEs).toEqual({
69+
dup_key: "AA",
70+
"dup_key#b": "BB",
71+
});
72+
});
73+
74+
it("wraps XML-sensitive target in CDATA", async () => {
75+
const input = dedent`<?xml version="1.0" encoding="utf-8"?>
76+
<xliff xmlns="urn:oasis:names:tc:xliff:document:1.2" version="1.2">
77+
<file original="cdata" source-language="en" target-language="en" datatype="plaintext">
78+
<body>
79+
<trans-unit id="expr" resname="expr"><source>5 &lt; 7</source><target>5 &lt; 7</target></trans-unit>
80+
</body>
81+
</file>
82+
</xliff>`;
83+
84+
const loader = createXliffLoader();
85+
loader.setDefaultLocale("en");
86+
await loader.pull("en", input);
87+
88+
const out = await loader.push("es", { expr: "5 < 7 & 8 > 3" });
89+
90+
expect(out.includes("<![CDATA[5 < 7 & 8 > 3]]>")).toBe(true);
91+
});
92+
93+
it("creates skeleton for missing locale", async () => {
94+
const loader = createXliffLoader();
95+
loader.setDefaultLocale("en");
96+
97+
// pulling default locale from scratch (empty)
98+
await loader.pull("en", "");
99+
100+
const payload = { key1: "Valor" };
101+
const esXml = await loader.push("es", payload);
102+
103+
// Ensure skeleton contains our translated value
104+
expect(esXml.includes("Valor")).toBe(true);
105+
expect(esXml.includes('target-language="es"'));
106+
});
107+
});

0 commit comments

Comments
 (0)