xlf-sync 1.0.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/LICENSE +21 -0
- package/README.md +125 -0
- package/dist/cli.d.ts +1 -0
- package/dist/cli.js +820 -0
- package/dist/cli.js.map +1 -0
- package/package.json +48 -0
package/LICENSE
ADDED
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
MIT License
|
|
2
|
+
|
|
3
|
+
Copyright (c) 2026 Anastasios Theodosiou
|
|
4
|
+
|
|
5
|
+
Permission is hereby granted, free of charge, to any person obtaining a copy
|
|
6
|
+
of this software and associated documentation files (the "Software"), to deal
|
|
7
|
+
in the Software without restriction, including without limitation the rights
|
|
8
|
+
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
|
9
|
+
copies of the Software, and to permit persons to whom the Software is
|
|
10
|
+
furnished to do so, subject to the following conditions:
|
|
11
|
+
|
|
12
|
+
The above copyright notice and this permission notice shall be included in all
|
|
13
|
+
copies or substantial portions of the Software.
|
|
14
|
+
|
|
15
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
|
16
|
+
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
|
17
|
+
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
|
18
|
+
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
|
19
|
+
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
|
20
|
+
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
|
21
|
+
SOFTWARE.
|
package/README.md
ADDED
|
@@ -0,0 +1,125 @@
|
|
|
1
|
+
# xlf-sync
|
|
2
|
+
|
|
3
|
+
> **The definitive CLI tool for synchronizing Angular XLIFF (1.2 & 2.0) locale files.**
|
|
4
|
+
|
|
5
|
+
**`xlf-sync`** is a robust, production-ready utility designed to solve the persistent challenge of Angular i18n management: keeping your locale files (`messages.<locale>.xlf`) in perfect sync with your source file (`messages.xlf`), without data loss or corruption.
|
|
6
|
+
|
|
7
|
+
It is built to integrate seamlessly into professional workflows, supporting both local development and strict CI/CD pipelines.
|
|
8
|
+
|
|
9
|
+
---
|
|
10
|
+
|
|
11
|
+
## ✨ Key Features
|
|
12
|
+
|
|
13
|
+
- **🔄 Full Synchronization**: Automatically adds missing keys from `messages.xlf` to all your locale files.
|
|
14
|
+
- **🛡️ Data Safety**: Never overwrites existing translations. Your work is safe.
|
|
15
|
+
- **🧬 Multi-Version Support**: Seamlessly handles **XLIFF 1.2** and **2.0** in the same project. It auto-detects the version per file.
|
|
16
|
+
- **🧹 Auto-Sorting**: Reorders translations in locale files to match the source file's order, ensuring clean and readable diffs.
|
|
17
|
+
- **💀 Graveyard Mode**: Optionally moves obsolete keys to a separate "graveyard" file instead of deleting them, preserving historical work.
|
|
18
|
+
- **🤖 CI/CD Ready**: Dedicated `check` command with strict exit codes for your build pipelines.
|
|
19
|
+
- **✨ Pretty Printing**: Normalizes XML formatting across all files, eliminating "dirty" diffs from other tools.
|
|
20
|
+
|
|
21
|
+
---
|
|
22
|
+
|
|
23
|
+
## 📦 Installation
|
|
24
|
+
|
|
25
|
+
Install globally or as a dev dependency in your project:
|
|
26
|
+
|
|
27
|
+
```bash
|
|
28
|
+
npm install -D xlf-sync
|
|
29
|
+
```
|
|
30
|
+
|
|
31
|
+
Run via `npx` or add a script to your `package.json`:
|
|
32
|
+
|
|
33
|
+
```bash
|
|
34
|
+
npx xlf-sync --help
|
|
35
|
+
```
|
|
36
|
+
|
|
37
|
+
---
|
|
38
|
+
|
|
39
|
+
## 🚀 Usage
|
|
40
|
+
|
|
41
|
+
### 1. The `sync` Command
|
|
42
|
+
|
|
43
|
+
The core command to update your locale files. It reads the source file and updates all target locale files found by the glob pattern.
|
|
44
|
+
|
|
45
|
+
```bash
|
|
46
|
+
# Basic usage
|
|
47
|
+
npx xlf-sync sync \
|
|
48
|
+
--source src/locale/messages.xlf \
|
|
49
|
+
--locales "src/locale/messages.*.xlf"
|
|
50
|
+
```
|
|
51
|
+
|
|
52
|
+
#### Sync Options
|
|
53
|
+
|
|
54
|
+
| Option | Default | Description |
|
|
55
|
+
| :--- | :--- | :--- |
|
|
56
|
+
| `--source <path>` | `src/locale/messages.xlf` | Path to the source XLIFF file generated by `ng extract-i18n`. |
|
|
57
|
+
| `--locales <glob>` | `src/locale/messages.*.xlf` | Glob pattern to find your target locale files (e.g., `src/locale/*.xlf`). |
|
|
58
|
+
| `--new-target <mode>` | `todo` | **Strategy for new keys:**<br>• `todo`: Fills target with `TODO`.<br>• `empty`: Leaves target empty.<br>• `source`: Copies source text to target. |
|
|
59
|
+
| `--obsolete <mode>` | `mark` | **Strategy for removed source keys:**<br>• `mark`: Keeps key with `state="obsolete"` (or prefix).<br>• `delete`: Permanently removes the key.<br>• `graveyard`: Moves key to a separate file (see below). |
|
|
60
|
+
| `--graveyard-file <pattern>` | `src/locale/_obsolete.{locale}.xlf` | Pattern for the output "graveyard" file. Used only if `--obsolete graveyard`. `{locale}` is replaced dynamically. |
|
|
61
|
+
| `--fail-on-missing` | `false` | Exits with error (code 1) if any keys are missing translations. Useful if you want to enforce 100% translation coverage during sync. |
|
|
62
|
+
| `--dry-run` | `false` | Simulates the operation without writing changes to disk. |
|
|
63
|
+
|
|
64
|
+
---
|
|
65
|
+
|
|
66
|
+
### 2. The `check` Command
|
|
67
|
+
|
|
68
|
+
A read-only command designed for **Continuous Integration (CI)** pipelines. It verifies the state of your translations without modifying any files.
|
|
69
|
+
|
|
70
|
+
```bash
|
|
71
|
+
# Check if files are in sync
|
|
72
|
+
npx xlf-sync check --fail-on-missing
|
|
73
|
+
```
|
|
74
|
+
|
|
75
|
+
#### Check Options
|
|
76
|
+
|
|
77
|
+
| Option | Default | Description |
|
|
78
|
+
| :--- | :--- | :--- |
|
|
79
|
+
| `--source <path>` | `src/locale/messages.xlf` | Path to the source XLIFF file. |
|
|
80
|
+
| `--locales <glob>` | `src/locale/messages.*.xlf` | Glob pattern for target locale files. |
|
|
81
|
+
| `--verbose` | `false` | Lists exactly which keys are missing or obsolete for each locale. |
|
|
82
|
+
| `--fail-on-missing` | `false` | **CI Failure Condition**: Fail if any translation targets are missing or empty. |
|
|
83
|
+
| `--fail-on-obsolete` | `false` | **CI Failure Condition**: Fail if obsolete keys exist in locale files. |
|
|
84
|
+
| `--fail-on-added` | `false` | **CI Failure Condition**: Fail if new keys exist in source that haven't been synced to locales yet. |
|
|
85
|
+
|
|
86
|
+
---
|
|
87
|
+
|
|
88
|
+
## 📚 Advanced Workflows
|
|
89
|
+
|
|
90
|
+
### Keeping Files Clean (Graveyard Mode)
|
|
91
|
+
|
|
92
|
+
For large, long-lived projects, we recommend the **Graveyard Strategy**. Instead of cluttering your main files with obsolete keys or deleting them immediately (and losing work), this mode moves them to a separate file.
|
|
93
|
+
|
|
94
|
+
```bash
|
|
95
|
+
npx xlf-sync sync --obsolete graveyard
|
|
96
|
+
```
|
|
97
|
+
|
|
98
|
+
**Result:**
|
|
99
|
+
- `messages.fr.xlf`: Contains **only** active, valid translations.
|
|
100
|
+
- `_obsolete.fr.xlf`: Contains all retired keys. You can restore them later if needed.
|
|
101
|
+
|
|
102
|
+
### Standard CI/CD Pipeline
|
|
103
|
+
|
|
104
|
+
Add this step to your Pull Request validation workflow to ensure no developer merges broken translations:
|
|
105
|
+
|
|
106
|
+
```yaml
|
|
107
|
+
- name: Verify i18n Sync
|
|
108
|
+
run: npx xlf-sync check --fail-on-missing --fail-on-added --verbose
|
|
109
|
+
```
|
|
110
|
+
|
|
111
|
+
---
|
|
112
|
+
|
|
113
|
+
## 🛠️ Supported Formats
|
|
114
|
+
|
|
115
|
+
| Format | Support Level |
|
|
116
|
+
| :--- | :--- |
|
|
117
|
+
| **XLIFF 1.2** | Legacy Angular projects. Supported fully. |
|
|
118
|
+
| **XLIFF 2.0** | Modern Angular default. Supported fully. |
|
|
119
|
+
| **Hybrid** | Mixed version projects are detecting and handled automatically per file. |
|
|
120
|
+
|
|
121
|
+
---
|
|
122
|
+
|
|
123
|
+
## 📄 License
|
|
124
|
+
|
|
125
|
+
MIT © Anastasios Theodosiou
|
package/dist/cli.d.ts
ADDED
|
@@ -0,0 +1 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
package/dist/cli.js
ADDED
|
@@ -0,0 +1,820 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
|
|
3
|
+
// src/cli.ts
|
|
4
|
+
import { Command } from "commander";
|
|
5
|
+
|
|
6
|
+
// src/commands/check.ts
|
|
7
|
+
import ora from "ora";
|
|
8
|
+
|
|
9
|
+
// src/ui/console.ts
|
|
10
|
+
import chalk from "chalk";
|
|
11
|
+
import logSymbols from "log-symbols";
|
|
12
|
+
import boxen from "boxen";
|
|
13
|
+
var ui = {
|
|
14
|
+
info: (msg) => console.log(chalk.cyan(msg)),
|
|
15
|
+
success: (msg) => console.log(`${logSymbols.success} ${chalk.green(msg)}`),
|
|
16
|
+
warn: (msg) => console.log(`${logSymbols.warning} ${chalk.yellow(msg)}`),
|
|
17
|
+
error: (msg) => console.error(`${logSymbols.error} ${chalk.red(msg)}`),
|
|
18
|
+
headerBox: (title, subtitle) => console.log(
|
|
19
|
+
boxen(
|
|
20
|
+
`${chalk.bold(title)}${subtitle ? `
|
|
21
|
+
${chalk.dim(subtitle)}` : ""}`,
|
|
22
|
+
{ padding: 1, borderStyle: "round" }
|
|
23
|
+
)
|
|
24
|
+
)
|
|
25
|
+
};
|
|
26
|
+
|
|
27
|
+
// src/core/discover.ts
|
|
28
|
+
import fg from "fast-glob";
|
|
29
|
+
import { promises as fs } from "fs";
|
|
30
|
+
import path from "path";
|
|
31
|
+
function extractLocaleFromFilename(filePath) {
|
|
32
|
+
const base = path.basename(filePath);
|
|
33
|
+
const m = base.match(/^messages\.([a-z]{2}(?:-[A-Z]{2})?)\.xlf$/);
|
|
34
|
+
return m?.[1] ?? null;
|
|
35
|
+
}
|
|
36
|
+
async function discoverFiles(opts) {
|
|
37
|
+
await fs.access(opts.sourcePath);
|
|
38
|
+
const matches = await fg(opts.localesGlob, { onlyFiles: true, unique: true });
|
|
39
|
+
const localeFiles = [];
|
|
40
|
+
for (const filePath of matches) {
|
|
41
|
+
if (path.resolve(filePath) === path.resolve(opts.sourcePath)) continue;
|
|
42
|
+
const locale = extractLocaleFromFilename(filePath);
|
|
43
|
+
if (!locale) continue;
|
|
44
|
+
localeFiles.push({ locale, filePath });
|
|
45
|
+
}
|
|
46
|
+
localeFiles.sort((a, b) => a.locale.localeCompare(b.locale));
|
|
47
|
+
const seen = /* @__PURE__ */ new Map();
|
|
48
|
+
for (const lf of localeFiles) {
|
|
49
|
+
if (seen.has(lf.locale)) {
|
|
50
|
+
const prev = seen.get(lf.locale);
|
|
51
|
+
throw new Error(
|
|
52
|
+
`Duplicate locale "${lf.locale}" detected:
|
|
53
|
+
- ${prev}
|
|
54
|
+
- ${lf.filePath}
|
|
55
|
+
Fix by removing one file or adjusting --locales glob.`
|
|
56
|
+
);
|
|
57
|
+
}
|
|
58
|
+
seen.set(lf.locale, lf.filePath);
|
|
59
|
+
}
|
|
60
|
+
return { sourcePath: opts.sourcePath, localeFiles };
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
// src/commands/check.ts
|
|
64
|
+
import { readFile } from "fs/promises";
|
|
65
|
+
|
|
66
|
+
// src/core/xlf/index.ts
|
|
67
|
+
import { XMLParser } from "fast-xml-parser";
|
|
68
|
+
|
|
69
|
+
// src/core/xlf/v12.ts
|
|
70
|
+
function asArray(v) {
|
|
71
|
+
if (!v) return [];
|
|
72
|
+
return Array.isArray(v) ? v : [v];
|
|
73
|
+
}
|
|
74
|
+
function parseV12(doc) {
|
|
75
|
+
const entries = /* @__PURE__ */ new Map();
|
|
76
|
+
const xliff = doc.xliff;
|
|
77
|
+
const file = xliff.file;
|
|
78
|
+
const locale = file?.["@_target-language"];
|
|
79
|
+
const body = file?.body;
|
|
80
|
+
if (!body) throw new Error("Invalid XLF 1.2: missing <body>");
|
|
81
|
+
const transUnits = asArray(body["trans-unit"]);
|
|
82
|
+
for (const tu of transUnits) {
|
|
83
|
+
const id = tu?.["@_id"];
|
|
84
|
+
if (!id) continue;
|
|
85
|
+
const source = tu.source ?? "";
|
|
86
|
+
const target = tu.target;
|
|
87
|
+
entries.set(id, {
|
|
88
|
+
key: id,
|
|
89
|
+
sourceXml: toXmlText(source),
|
|
90
|
+
targetXml: target !== void 0 ? toXmlText(target) : void 0
|
|
91
|
+
});
|
|
92
|
+
}
|
|
93
|
+
return {
|
|
94
|
+
version: "1.2",
|
|
95
|
+
locale,
|
|
96
|
+
entries,
|
|
97
|
+
raw: doc
|
|
98
|
+
};
|
|
99
|
+
}
|
|
100
|
+
function toXmlText(v) {
|
|
101
|
+
if (v === null || v === void 0) return "";
|
|
102
|
+
if (typeof v === "string") return v;
|
|
103
|
+
if (typeof v === "object") {
|
|
104
|
+
if (typeof v["#text"] === "string") return v["#text"];
|
|
105
|
+
}
|
|
106
|
+
return String(v);
|
|
107
|
+
}
|
|
108
|
+
|
|
109
|
+
// src/core/xlf/v20.ts
|
|
110
|
+
function asArray2(v) {
|
|
111
|
+
if (!v) return [];
|
|
112
|
+
return Array.isArray(v) ? v : [v];
|
|
113
|
+
}
|
|
114
|
+
function parseV20(doc) {
|
|
115
|
+
const entries = /* @__PURE__ */ new Map();
|
|
116
|
+
const xliff = doc.xliff;
|
|
117
|
+
const locale = xliff?.["@_trgLang"];
|
|
118
|
+
const file = xliff.file;
|
|
119
|
+
if (!file) throw new Error("Invalid XLF 2.0: missing <file>");
|
|
120
|
+
const units = asArray2(file.unit);
|
|
121
|
+
for (const unit of units) {
|
|
122
|
+
const unitId = unit?.["@_id"];
|
|
123
|
+
if (!unitId) continue;
|
|
124
|
+
const segments = asArray2(unit.segment);
|
|
125
|
+
segments.forEach((seg, idx) => {
|
|
126
|
+
const source = seg?.source ?? "";
|
|
127
|
+
const target = seg?.target;
|
|
128
|
+
const key = segments.length > 1 ? `${unitId}:${idx}` : unitId;
|
|
129
|
+
entries.set(key, {
|
|
130
|
+
key,
|
|
131
|
+
sourceXml: toXmlText2(source),
|
|
132
|
+
targetXml: target !== void 0 ? toXmlText2(target) : void 0
|
|
133
|
+
});
|
|
134
|
+
});
|
|
135
|
+
}
|
|
136
|
+
return {
|
|
137
|
+
version: "2.0",
|
|
138
|
+
locale,
|
|
139
|
+
entries,
|
|
140
|
+
raw: doc
|
|
141
|
+
};
|
|
142
|
+
}
|
|
143
|
+
function toXmlText2(v) {
|
|
144
|
+
if (v === null || v === void 0) return "";
|
|
145
|
+
if (typeof v === "string") return v;
|
|
146
|
+
if (typeof v === "object") {
|
|
147
|
+
if (typeof v["#text"] === "string") return v["#text"];
|
|
148
|
+
}
|
|
149
|
+
return String(v);
|
|
150
|
+
}
|
|
151
|
+
|
|
152
|
+
// src/core/xlf/write-v12.ts
|
|
153
|
+
function normalizeText(v) {
|
|
154
|
+
if (v == null) return "";
|
|
155
|
+
if (typeof v === "string") return v;
|
|
156
|
+
if (typeof v === "object") {
|
|
157
|
+
if (typeof v["#text"] === "string") return v["#text"];
|
|
158
|
+
if (typeof v.text === "string") return v.text;
|
|
159
|
+
if (Array.isArray(v)) {
|
|
160
|
+
return v.map(normalizeText).join("");
|
|
161
|
+
}
|
|
162
|
+
}
|
|
163
|
+
return "";
|
|
164
|
+
}
|
|
165
|
+
function writeV12(rawDoc, merged, obsoleteKeys, opts) {
|
|
166
|
+
const xliff = rawDoc.xliff;
|
|
167
|
+
const file = xliff.file;
|
|
168
|
+
const body = file.body;
|
|
169
|
+
const transUnits = [];
|
|
170
|
+
for (const entry of merged.values()) {
|
|
171
|
+
const tu = {
|
|
172
|
+
"@_id": normalizeText(entry.key),
|
|
173
|
+
source: normalizeText(entry.sourceXml)
|
|
174
|
+
};
|
|
175
|
+
if (entry.targetXml !== void 0) {
|
|
176
|
+
tu.target = normalizeText(entry.targetXml);
|
|
177
|
+
}
|
|
178
|
+
transUnits.push(tu);
|
|
179
|
+
}
|
|
180
|
+
if (opts.obsolete === "mark") {
|
|
181
|
+
const originalUnits = body["trans-unit"] ?? [];
|
|
182
|
+
for (const key of obsoleteKeys) {
|
|
183
|
+
const original = originalUnits.find((u) => u["@_id"] === key);
|
|
184
|
+
if (!original) continue;
|
|
185
|
+
transUnits.push({
|
|
186
|
+
"@_id": normalizeText(key),
|
|
187
|
+
source: normalizeText(original.source),
|
|
188
|
+
target: `__OBSOLETE__${normalizeText(original.target)}`,
|
|
189
|
+
note: "Marked obsolete by xlf-sync"
|
|
190
|
+
});
|
|
191
|
+
}
|
|
192
|
+
}
|
|
193
|
+
body["trans-unit"] = transUnits;
|
|
194
|
+
return toXmlV12(rawDoc);
|
|
195
|
+
}
|
|
196
|
+
function escapeXml(s) {
|
|
197
|
+
return s.replaceAll("&", "&").replaceAll("<", "<").replaceAll(">", ">").replaceAll('"', """).replaceAll("'", "'");
|
|
198
|
+
}
|
|
199
|
+
function toXmlV12(doc) {
|
|
200
|
+
const xliff = doc.xliff;
|
|
201
|
+
const file = xliff.file;
|
|
202
|
+
const body = file.body;
|
|
203
|
+
const headerAttrs = `version="${escapeXml(normalizeText(xliff["@_version"] ?? "1.2"))}"`;
|
|
204
|
+
const fileAttrs = [];
|
|
205
|
+
for (const [k, v] of Object.entries(file)) {
|
|
206
|
+
if (k.startsWith("@_")) {
|
|
207
|
+
fileAttrs.push(`${k.slice(2)}="${escapeXml(normalizeText(v))}"`);
|
|
208
|
+
}
|
|
209
|
+
}
|
|
210
|
+
const units = Array.isArray(body["trans-unit"]) ? body["trans-unit"] : [];
|
|
211
|
+
const unitsXml = units.map((tu) => {
|
|
212
|
+
const id = escapeXml(normalizeText(tu["@_id"]));
|
|
213
|
+
const source = escapeXml(normalizeText(tu.source));
|
|
214
|
+
let targetXml = "";
|
|
215
|
+
const targetRaw = tu.target;
|
|
216
|
+
if (typeof targetRaw === "string") {
|
|
217
|
+
if (targetRaw.startsWith("__OBSOLETE__")) {
|
|
218
|
+
const text = targetRaw.replace("__OBSOLETE__", "");
|
|
219
|
+
targetXml = `<target state="obsolete">${escapeXml(normalizeText(text))}</target>`;
|
|
220
|
+
} else {
|
|
221
|
+
targetXml = `<target>${escapeXml(normalizeText(targetRaw))}</target>`;
|
|
222
|
+
}
|
|
223
|
+
}
|
|
224
|
+
const noteXml = tu.note ? `<note>${escapeXml(normalizeText(tu.note))}</note>` : "";
|
|
225
|
+
return ` <trans-unit id="${id}">
|
|
226
|
+
<source>${source}</source>
|
|
227
|
+
` + (targetXml ? ` ${targetXml}
|
|
228
|
+
` : "") + (noteXml ? ` ${noteXml}
|
|
229
|
+
` : "") + ` </trans-unit>`;
|
|
230
|
+
}).join("\n\n");
|
|
231
|
+
return `<?xml version="1.0" encoding="UTF-8" ?>
|
|
232
|
+
<xliff ${headerAttrs}>
|
|
233
|
+
<file ${fileAttrs.join(" ")}>
|
|
234
|
+
<body>
|
|
235
|
+
${unitsXml}
|
|
236
|
+
</body>
|
|
237
|
+
</file>
|
|
238
|
+
</xliff>
|
|
239
|
+
`;
|
|
240
|
+
}
|
|
241
|
+
|
|
242
|
+
// src/core/xlf/write-v20.ts
|
|
243
|
+
function writeV20(rawDoc, merged, obsoleteKeys, opts) {
|
|
244
|
+
const xliff = rawDoc.xliff;
|
|
245
|
+
const file = xliff.file;
|
|
246
|
+
const units = [];
|
|
247
|
+
for (const entry of merged.values()) {
|
|
248
|
+
const unit = {
|
|
249
|
+
"@_id": entry.key,
|
|
250
|
+
segment: {
|
|
251
|
+
source: entry.sourceXml ?? ""
|
|
252
|
+
}
|
|
253
|
+
};
|
|
254
|
+
if (entry.targetXml !== void 0) {
|
|
255
|
+
unit.segment.target = entry.targetXml;
|
|
256
|
+
}
|
|
257
|
+
units.push(unit);
|
|
258
|
+
}
|
|
259
|
+
if (opts.obsolete === "mark") {
|
|
260
|
+
const originalUnits = file.unit ?? [];
|
|
261
|
+
for (const key of obsoleteKeys) {
|
|
262
|
+
const original = originalUnits.find((u) => u["@_id"] === key);
|
|
263
|
+
if (!original) continue;
|
|
264
|
+
const seg = original.segment ?? {};
|
|
265
|
+
units.push({
|
|
266
|
+
"@_id": key,
|
|
267
|
+
segment: {
|
|
268
|
+
source: seg.source ?? "",
|
|
269
|
+
target: `__OBSOLETE__${seg.target ?? ""}`
|
|
270
|
+
}
|
|
271
|
+
});
|
|
272
|
+
}
|
|
273
|
+
}
|
|
274
|
+
file.unit = units;
|
|
275
|
+
return toXmlV20(rawDoc);
|
|
276
|
+
}
|
|
277
|
+
function escapeXml2(s) {
|
|
278
|
+
return s.replaceAll("&", "&").replaceAll("<", "<").replaceAll(">", ">").replaceAll('"', """).replaceAll("'", "'");
|
|
279
|
+
}
|
|
280
|
+
function toXmlV20(doc) {
|
|
281
|
+
const xliff = doc.xliff;
|
|
282
|
+
const file = xliff.file;
|
|
283
|
+
const xliffAttrs = [];
|
|
284
|
+
for (const [k, v] of Object.entries(xliff)) {
|
|
285
|
+
if (k.startsWith("@_")) {
|
|
286
|
+
xliffAttrs.push(`${k.slice(2)}="${escapeXml2(String(v))}"`);
|
|
287
|
+
}
|
|
288
|
+
}
|
|
289
|
+
const fileAttrs = [];
|
|
290
|
+
for (const [k, v] of Object.entries(file)) {
|
|
291
|
+
if (k.startsWith("@_")) {
|
|
292
|
+
fileAttrs.push(`${k.slice(2)}="${escapeXml2(String(v))}"`);
|
|
293
|
+
}
|
|
294
|
+
}
|
|
295
|
+
const units = Array.isArray(file.unit) ? file.unit : [];
|
|
296
|
+
const unitsXml = units.map((u) => {
|
|
297
|
+
const id = escapeXml2(String(u["@_id"]));
|
|
298
|
+
const seg = u.segment ?? {};
|
|
299
|
+
const source = escapeXml2(String(seg.source ?? ""));
|
|
300
|
+
let targetXml = "";
|
|
301
|
+
if (typeof seg.target === "string") {
|
|
302
|
+
if (seg.target.startsWith("__OBSOLETE__")) {
|
|
303
|
+
const text = seg.target.replace("__OBSOLETE__", "");
|
|
304
|
+
targetXml = `<target state="obsolete">${escapeXml2(text)}</target>`;
|
|
305
|
+
} else {
|
|
306
|
+
targetXml = `<target>${escapeXml2(seg.target)}</target>`;
|
|
307
|
+
}
|
|
308
|
+
}
|
|
309
|
+
return ` <unit id="${id}">
|
|
310
|
+
<segment>
|
|
311
|
+
<source>${source}</source>
|
|
312
|
+
` + (targetXml ? ` ${targetXml}
|
|
313
|
+
` : "") + ` </segment>
|
|
314
|
+
</unit>`;
|
|
315
|
+
}).join("\n\n");
|
|
316
|
+
return `<?xml version="1.0" encoding="UTF-8"?>
|
|
317
|
+
<xliff ${xliffAttrs.join(" ")}>
|
|
318
|
+
<file ${fileAttrs.join(" ")}>
|
|
319
|
+
${unitsXml}
|
|
320
|
+
</file>
|
|
321
|
+
</xliff>
|
|
322
|
+
`;
|
|
323
|
+
}
|
|
324
|
+
|
|
325
|
+
// src/core/xlf/index.ts
|
|
326
|
+
var parser = new XMLParser({
|
|
327
|
+
ignoreAttributes: false,
|
|
328
|
+
attributeNamePrefix: "@_",
|
|
329
|
+
preserveOrder: false
|
|
330
|
+
});
|
|
331
|
+
function parseXlf(xml) {
|
|
332
|
+
const doc = parser.parse(xml);
|
|
333
|
+
const xliff = doc?.xliff;
|
|
334
|
+
if (!xliff) throw new Error("Invalid XLF: missing <xliff>");
|
|
335
|
+
const version = xliff["@_version"];
|
|
336
|
+
if (version === "1.2") return parseV12(doc);
|
|
337
|
+
if (version === "2.0") return parseV20(doc);
|
|
338
|
+
throw new Error(`Unsupported XLIFF version: ${version}`);
|
|
339
|
+
}
|
|
340
|
+
function writeXlf(parsed, merged, obsoleteKeys, opts) {
|
|
341
|
+
if (parsed.version === "1.2") return writeV12(parsed.raw, merged, obsoleteKeys, opts);
|
|
342
|
+
return writeV20(parsed.raw, merged, obsoleteKeys, opts);
|
|
343
|
+
}
|
|
344
|
+
|
|
345
|
+
// src/core/sync.ts
|
|
346
|
+
function syncLocale(source, locale, opts) {
|
|
347
|
+
const merged = /* @__PURE__ */ new Map();
|
|
348
|
+
const addedKeys = [];
|
|
349
|
+
const obsoleteKeys = [];
|
|
350
|
+
const keptKeys = [];
|
|
351
|
+
const missingTargets = [];
|
|
352
|
+
for (const [key, srcEntry] of source.entries()) {
|
|
353
|
+
const locEntry = locale.get(key);
|
|
354
|
+
if (locEntry) {
|
|
355
|
+
const targetXml = locEntry.targetXml;
|
|
356
|
+
merged.set(key, {
|
|
357
|
+
key,
|
|
358
|
+
sourceXml: srcEntry.sourceXml,
|
|
359
|
+
targetXml
|
|
360
|
+
});
|
|
361
|
+
keptKeys.push(key);
|
|
362
|
+
if (!targetXml || targetXml.trim() === "") missingTargets.push(key);
|
|
363
|
+
} else {
|
|
364
|
+
const targetXml = makeNewTarget(srcEntry.sourceXml, opts.newTarget);
|
|
365
|
+
merged.set(key, {
|
|
366
|
+
key,
|
|
367
|
+
sourceXml: srcEntry.sourceXml,
|
|
368
|
+
targetXml
|
|
369
|
+
});
|
|
370
|
+
addedKeys.push(key);
|
|
371
|
+
if (!targetXml || targetXml.trim() === "") missingTargets.push(key);
|
|
372
|
+
}
|
|
373
|
+
}
|
|
374
|
+
for (const key of locale.keys()) {
|
|
375
|
+
if (!source.has(key)) {
|
|
376
|
+
obsoleteKeys.push(key);
|
|
377
|
+
}
|
|
378
|
+
}
|
|
379
|
+
return { merged, addedKeys, obsoleteKeys, keptKeys, missingTargets };
|
|
380
|
+
}
|
|
381
|
+
function makeNewTarget(sourceXml, mode) {
|
|
382
|
+
if (mode === "empty") return void 0;
|
|
383
|
+
if (mode === "source") return sourceXml;
|
|
384
|
+
return "TODO";
|
|
385
|
+
}
|
|
386
|
+
|
|
387
|
+
// src/ui/table.ts
|
|
388
|
+
import Table from "cli-table3";
|
|
389
|
+
import chalk2 from "chalk";
|
|
390
|
+
function renderSummaryTable(rows) {
|
|
391
|
+
const table = new Table({
|
|
392
|
+
head: [
|
|
393
|
+
chalk2.bold("Locale"),
|
|
394
|
+
chalk2.bold("XLF"),
|
|
395
|
+
chalk2.bold("Source"),
|
|
396
|
+
chalk2.bold("Locale"),
|
|
397
|
+
chalk2.bold("Add"),
|
|
398
|
+
chalk2.bold("Obsolete"),
|
|
399
|
+
chalk2.bold("Missing targets")
|
|
400
|
+
]
|
|
401
|
+
});
|
|
402
|
+
for (const r of rows) {
|
|
403
|
+
table.push([
|
|
404
|
+
r.locale,
|
|
405
|
+
r.version,
|
|
406
|
+
r.sourceKeys,
|
|
407
|
+
r.localeKeys,
|
|
408
|
+
r.added === 0 ? chalk2.dim("0") : chalk2.yellow(String(r.added)),
|
|
409
|
+
r.obsolete === 0 ? chalk2.dim("0") : chalk2.red(String(r.obsolete)),
|
|
410
|
+
r.missingTargets === 0 ? chalk2.dim("0") : chalk2.yellow(String(r.missingTargets))
|
|
411
|
+
]);
|
|
412
|
+
}
|
|
413
|
+
console.log(table.toString());
|
|
414
|
+
}
|
|
415
|
+
function renderReportTable(rows) {
|
|
416
|
+
const table = new Table({
|
|
417
|
+
head: [
|
|
418
|
+
chalk2.bold("Locale"),
|
|
419
|
+
chalk2.bold("XLF"),
|
|
420
|
+
chalk2.bold("Keys"),
|
|
421
|
+
chalk2.bold("Translated"),
|
|
422
|
+
chalk2.bold("Pending"),
|
|
423
|
+
chalk2.bold("% Cov"),
|
|
424
|
+
chalk2.bold("Words")
|
|
425
|
+
]
|
|
426
|
+
});
|
|
427
|
+
for (const r of rows) {
|
|
428
|
+
const covColor = r.coverage === 100 ? chalk2.green : r.coverage >= 80 ? chalk2.yellow : chalk2.red;
|
|
429
|
+
table.push([
|
|
430
|
+
r.locale,
|
|
431
|
+
r.version,
|
|
432
|
+
r.total,
|
|
433
|
+
r.done,
|
|
434
|
+
r.todo === 0 ? chalk2.dim("0") : chalk2.yellow(String(r.todo)),
|
|
435
|
+
covColor(`${r.coverage.toFixed(1)}%`),
|
|
436
|
+
r.words
|
|
437
|
+
]);
|
|
438
|
+
}
|
|
439
|
+
console.log(table.toString());
|
|
440
|
+
}
|
|
441
|
+
|
|
442
|
+
// src/ui/banner.ts
|
|
443
|
+
import figlet from "figlet";
|
|
444
|
+
import chalk3 from "chalk";
|
|
445
|
+
|
|
446
|
+
// package.json
|
|
447
|
+
var package_default = {
|
|
448
|
+
name: "xlf-sync",
|
|
449
|
+
version: "0.1.0",
|
|
450
|
+
description: "Sync Angular XLIFF (1.2 & 2.0) locale files with messages.xlf",
|
|
451
|
+
type: "module",
|
|
452
|
+
bin: {
|
|
453
|
+
"xlf-sync": "dist/cli.js"
|
|
454
|
+
},
|
|
455
|
+
files: [
|
|
456
|
+
"dist"
|
|
457
|
+
],
|
|
458
|
+
scripts: {
|
|
459
|
+
build: "tsup",
|
|
460
|
+
dev: "node --enable-source-maps dist/cli.js",
|
|
461
|
+
test: "vitest run",
|
|
462
|
+
"test:watch": "vitest",
|
|
463
|
+
lint: "echo 'No linter configured'"
|
|
464
|
+
},
|
|
465
|
+
keywords: [
|
|
466
|
+
"angular",
|
|
467
|
+
"i18n",
|
|
468
|
+
"xliff",
|
|
469
|
+
"xlf",
|
|
470
|
+
"translation",
|
|
471
|
+
"sync",
|
|
472
|
+
"locale",
|
|
473
|
+
"merge"
|
|
474
|
+
],
|
|
475
|
+
author: "Anastasios Theodosiou",
|
|
476
|
+
license: "MIT",
|
|
477
|
+
dependencies: {
|
|
478
|
+
boxen: "^8.0.1",
|
|
479
|
+
chalk: "^5.6.2",
|
|
480
|
+
"cli-table3": "^0.6.5",
|
|
481
|
+
commander: "^14.0.2",
|
|
482
|
+
"fast-glob": "^3.3.3",
|
|
483
|
+
"fast-xml-parser": "^5.3.3",
|
|
484
|
+
figlet: "^1.10.0",
|
|
485
|
+
"log-symbols": "^7.0.1",
|
|
486
|
+
ora: "^9.1.0"
|
|
487
|
+
},
|
|
488
|
+
devDependencies: {
|
|
489
|
+
"@types/node": "^25.0.10",
|
|
490
|
+
tsup: "^8.5.1",
|
|
491
|
+
typescript: "^5.9.3",
|
|
492
|
+
vitest: "^4.0.18"
|
|
493
|
+
}
|
|
494
|
+
};
|
|
495
|
+
|
|
496
|
+
// src/ui/banner.ts
|
|
497
|
+
function renderBanner(command) {
|
|
498
|
+
const logo = figlet.textSync("XLF-SYNC", {
|
|
499
|
+
font: "Standard",
|
|
500
|
+
// μπορείς να αλλάξεις font
|
|
501
|
+
horizontalLayout: "default",
|
|
502
|
+
verticalLayout: "default"
|
|
503
|
+
});
|
|
504
|
+
console.log(chalk3.cyanBright(logo));
|
|
505
|
+
console.log(
|
|
506
|
+
chalk3.bold.white(
|
|
507
|
+
`XLF-SYNC v${package_default.version}${command ? ` [${command}]` : ""}`
|
|
508
|
+
)
|
|
509
|
+
);
|
|
510
|
+
console.log(chalk3.gray("Sync & validate Angular XLIFF files"));
|
|
511
|
+
console.log(chalk3.gray("Author: Anastasios Theodosiou\n"));
|
|
512
|
+
}
|
|
513
|
+
|
|
514
|
+
// src/commands/check.ts
|
|
515
|
+
function registerCheckCommand(program2) {
|
|
516
|
+
program2.command("check").description("Check if locale XLF files are in sync (CI-friendly)").option("--source <path>", "Path to source messages.xlf", "src/locale/messages.xlf").option("--locales <glob>", "Glob for locale files", "src/locale/messages.*.xlf").option("--fail-on-missing", "Exit non-zero if missing targets exist", false).option("--fail-on-obsolete", "Exit non-zero if obsolete keys exist", false).option("--fail-on-added", "Exit non-zero if new keys would be added", false).option("--new-target <mode>", "todo | empty | source (used for diff only)", "todo").option("--verbose", "Print missing keys per locale", false).action(async (opts) => {
|
|
517
|
+
renderBanner("check");
|
|
518
|
+
const spinner = ora("Checking...").start();
|
|
519
|
+
try {
|
|
520
|
+
const res = await discoverFiles({
|
|
521
|
+
sourcePath: opts.source,
|
|
522
|
+
localesGlob: opts.locales
|
|
523
|
+
});
|
|
524
|
+
const sourceXml = await readFile(res.sourcePath, "utf-8");
|
|
525
|
+
const sourceParsed = parseXlf(sourceXml);
|
|
526
|
+
const rows = [];
|
|
527
|
+
const missingKeysByLocale = {};
|
|
528
|
+
let hasMissing = false;
|
|
529
|
+
let hasObsolete = false;
|
|
530
|
+
let hasAdded = false;
|
|
531
|
+
for (const lf of res.localeFiles) {
|
|
532
|
+
const xml = await readFile(lf.filePath, "utf-8");
|
|
533
|
+
const parsed = parseXlf(xml);
|
|
534
|
+
const diff = syncLocale(sourceParsed.entries, parsed.entries, {
|
|
535
|
+
newTarget: opts.newTarget,
|
|
536
|
+
// For check: we only want detection, not modifications
|
|
537
|
+
obsolete: "delete"
|
|
538
|
+
});
|
|
539
|
+
const missingTargets = diff.missingTargets.length;
|
|
540
|
+
const obsolete = diff.obsoleteKeys.length;
|
|
541
|
+
const added = diff.addedKeys.length;
|
|
542
|
+
if (missingTargets > 0) {
|
|
543
|
+
hasMissing = true;
|
|
544
|
+
missingKeysByLocale[lf.locale] = diff.missingTargets.slice();
|
|
545
|
+
}
|
|
546
|
+
if (obsolete > 0) hasObsolete = true;
|
|
547
|
+
if (added > 0) hasAdded = true;
|
|
548
|
+
rows.push({
|
|
549
|
+
locale: lf.locale,
|
|
550
|
+
version: parsed.version,
|
|
551
|
+
sourceKeys: sourceParsed.entries.size,
|
|
552
|
+
localeKeys: parsed.entries.size,
|
|
553
|
+
added,
|
|
554
|
+
obsolete,
|
|
555
|
+
missingTargets
|
|
556
|
+
});
|
|
557
|
+
}
|
|
558
|
+
spinner.stop();
|
|
559
|
+
renderSummaryTable(rows);
|
|
560
|
+
if (opts.verbose) {
|
|
561
|
+
const locales = Object.keys(missingKeysByLocale);
|
|
562
|
+
if (locales.length === 0) {
|
|
563
|
+
ui.success("No missing targets.");
|
|
564
|
+
} else {
|
|
565
|
+
ui.info("Missing targets:");
|
|
566
|
+
for (const locale of locales) {
|
|
567
|
+
ui.info(`- ${locale}:`);
|
|
568
|
+
for (const key of missingKeysByLocale[locale]) {
|
|
569
|
+
ui.info(` \u2022 ${key}`);
|
|
570
|
+
}
|
|
571
|
+
}
|
|
572
|
+
}
|
|
573
|
+
}
|
|
574
|
+
const reasons = [];
|
|
575
|
+
if (opts.failOnMissing && hasMissing) reasons.push("missing targets");
|
|
576
|
+
if (opts.failOnObsolete && hasObsolete) reasons.push("obsolete keys");
|
|
577
|
+
if (opts.failOnAdded && hasAdded) reasons.push("new keys need adding");
|
|
578
|
+
if (reasons.length > 0) {
|
|
579
|
+
ui.error(`Check failed: ${reasons.join(", ")}`);
|
|
580
|
+
process.exitCode = 1;
|
|
581
|
+
} else {
|
|
582
|
+
ui.success("Check OK");
|
|
583
|
+
}
|
|
584
|
+
} catch (e) {
|
|
585
|
+
spinner.fail("Failed");
|
|
586
|
+
ui.error(e?.message ?? String(e));
|
|
587
|
+
process.exitCode = 1;
|
|
588
|
+
}
|
|
589
|
+
});
|
|
590
|
+
}
|
|
591
|
+
|
|
592
|
+
// src/commands/report.ts
|
|
593
|
+
import ora2 from "ora";
|
|
594
|
+
import { readFile as readFile2 } from "fs/promises";
|
|
595
|
+
function isUntranslated(target) {
|
|
596
|
+
if (!target) return true;
|
|
597
|
+
const t = target.trim();
|
|
598
|
+
return t === "" || t.toUpperCase() === "TODO";
|
|
599
|
+
}
|
|
600
|
+
function countWords(text) {
|
|
601
|
+
if (!text) return 0;
|
|
602
|
+
return text.trim().split(/\s+/).length;
|
|
603
|
+
}
|
|
604
|
+
function registerReportCommand(program2) {
|
|
605
|
+
program2.command("report").description("Generate translation statistics report").option("--source <path>", "Path to source messages.xlf", "src/locale/messages.xlf").option("--locales <glob>", "Glob for locale files", "src/locale/messages.*.xlf").action(async (opts) => {
|
|
606
|
+
renderBanner("report");
|
|
607
|
+
const spinner = ora2("Scanning files...").start();
|
|
608
|
+
try {
|
|
609
|
+
const res = await discoverFiles({
|
|
610
|
+
sourcePath: opts.source,
|
|
611
|
+
localesGlob: opts.locales
|
|
612
|
+
});
|
|
613
|
+
const rows = [];
|
|
614
|
+
for (const lf of res.localeFiles) {
|
|
615
|
+
const xml = await readFile2(lf.filePath, "utf-8");
|
|
616
|
+
const parsed = parseXlf(xml);
|
|
617
|
+
let total = 0;
|
|
618
|
+
let todo = 0;
|
|
619
|
+
let words = 0;
|
|
620
|
+
for (const entry of parsed.entries.values()) {
|
|
621
|
+
total++;
|
|
622
|
+
if (isUntranslated(entry.targetXml)) {
|
|
623
|
+
todo++;
|
|
624
|
+
} else {
|
|
625
|
+
words += countWords(entry.targetXml);
|
|
626
|
+
}
|
|
627
|
+
}
|
|
628
|
+
const done = total - todo;
|
|
629
|
+
const coverage = total > 0 ? done / total * 100 : 100;
|
|
630
|
+
rows.push({
|
|
631
|
+
locale: lf.locale,
|
|
632
|
+
version: parsed.version,
|
|
633
|
+
total,
|
|
634
|
+
done,
|
|
635
|
+
todo,
|
|
636
|
+
coverage,
|
|
637
|
+
words
|
|
638
|
+
});
|
|
639
|
+
}
|
|
640
|
+
spinner.stop();
|
|
641
|
+
if (rows.length === 0) {
|
|
642
|
+
ui.warn("No locale files found.");
|
|
643
|
+
} else {
|
|
644
|
+
renderReportTable(rows);
|
|
645
|
+
}
|
|
646
|
+
} catch (e) {
|
|
647
|
+
spinner.fail("Failed");
|
|
648
|
+
ui.error(e?.message ?? String(e));
|
|
649
|
+
process.exitCode = 1;
|
|
650
|
+
}
|
|
651
|
+
});
|
|
652
|
+
}
|
|
653
|
+
|
|
654
|
+
// src/commands/sync.ts
|
|
655
|
+
import ora3 from "ora";
|
|
656
|
+
import { readFile as readFile3, writeFile } from "fs/promises";
|
|
657
|
+
|
|
658
|
+
// src/core/graveyard.ts
|
|
659
|
+
function normalizeText2(v) {
|
|
660
|
+
if (v == null) return "";
|
|
661
|
+
if (typeof v === "string") return v;
|
|
662
|
+
if (typeof v === "object") {
|
|
663
|
+
if (typeof v["#text"] === "string") return v["#text"];
|
|
664
|
+
if (typeof v.text === "string") return v.text;
|
|
665
|
+
if (Array.isArray(v)) return v.map(normalizeText2).join("");
|
|
666
|
+
}
|
|
667
|
+
return "";
|
|
668
|
+
}
|
|
669
|
+
function buildGraveyardEntries(parsed, obsoleteKeys) {
|
|
670
|
+
const out = /* @__PURE__ */ new Map();
|
|
671
|
+
if (obsoleteKeys.length === 0) return out;
|
|
672
|
+
if (parsed.version === "1.2") {
|
|
673
|
+
const body = parsed.raw?.xliff?.file?.body;
|
|
674
|
+
const units2 = body?.["trans-unit"] ?? [];
|
|
675
|
+
for (const key of obsoleteKeys) {
|
|
676
|
+
const u = units2.find((x) => x?.["@_id"] === key);
|
|
677
|
+
if (!u) continue;
|
|
678
|
+
const source = normalizeText2(u.source);
|
|
679
|
+
const target = normalizeText2(u.target);
|
|
680
|
+
out.set(key, {
|
|
681
|
+
key,
|
|
682
|
+
sourceXml: source,
|
|
683
|
+
targetXml: `__OBSOLETE__${target}`
|
|
684
|
+
});
|
|
685
|
+
}
|
|
686
|
+
return out;
|
|
687
|
+
}
|
|
688
|
+
const file = parsed.raw?.xliff?.file;
|
|
689
|
+
const units = file?.unit ?? [];
|
|
690
|
+
for (const key of obsoleteKeys) {
|
|
691
|
+
const u = units.find((x) => x?.["@_id"] === key);
|
|
692
|
+
if (!u) continue;
|
|
693
|
+
const seg = u.segment ?? {};
|
|
694
|
+
const source = normalizeText2(seg.source);
|
|
695
|
+
const target = normalizeText2(seg.target);
|
|
696
|
+
out.set(key, {
|
|
697
|
+
key,
|
|
698
|
+
sourceXml: source,
|
|
699
|
+
targetXml: `__OBSOLETE__${target}`
|
|
700
|
+
});
|
|
701
|
+
}
|
|
702
|
+
return out;
|
|
703
|
+
}
|
|
704
|
+
|
|
705
|
+
// src/commands/sync.ts
|
|
706
|
+
function resolveGraveyardPath(pattern, locale) {
|
|
707
|
+
return pattern.replaceAll("{locale}", locale);
|
|
708
|
+
}
|
|
709
|
+
function registerSyncCommand(program2) {
|
|
710
|
+
program2.command("sync").description("Sync locale XLF files with the source messages.xlf").option("--source <path>", "Path to source messages.xlf", "src/locale/messages.xlf").option("--locales <glob>", "Glob for locale files", "src/locale/messages.*.xlf").option("--dry-run", "Do not write files, only report changes", false).option("--new-target <mode>", "todo | empty | source", "todo").option("--obsolete <mode>", "delete | mark | graveyard", "mark").option("--fail-on-missing", "Fail if missing targets exist (no files written)", false).option(
|
|
711
|
+
"--graveyard-file <path>",
|
|
712
|
+
"Graveyard output path pattern",
|
|
713
|
+
"src/locale/_obsolete.{locale}.xlf"
|
|
714
|
+
).action(async (opts) => {
|
|
715
|
+
renderBanner("sync");
|
|
716
|
+
const spinner = ora3("Scanning files...").start();
|
|
717
|
+
try {
|
|
718
|
+
const res = await discoverFiles({
|
|
719
|
+
sourcePath: opts.source,
|
|
720
|
+
localesGlob: opts.locales
|
|
721
|
+
});
|
|
722
|
+
spinner.succeed(`Found ${res.localeFiles.length} locale file(s)`);
|
|
723
|
+
for (const lf of res.localeFiles) {
|
|
724
|
+
ui.info(`- ${lf.locale}: ${lf.filePath}`);
|
|
725
|
+
}
|
|
726
|
+
const sourceXml = await readFile3(res.sourcePath, "utf-8");
|
|
727
|
+
const sourceParsed = parseXlf(sourceXml);
|
|
728
|
+
const rows = [];
|
|
729
|
+
const plans = [];
|
|
730
|
+
let hasMissing = false;
|
|
731
|
+
for (const lf of res.localeFiles) {
|
|
732
|
+
const localeXml = await readFile3(lf.filePath, "utf-8");
|
|
733
|
+
const parsed = parseXlf(localeXml);
|
|
734
|
+
const diff = syncLocale(sourceParsed.entries, parsed.entries, {
|
|
735
|
+
newTarget: opts.newTarget,
|
|
736
|
+
obsolete: opts.obsolete
|
|
737
|
+
});
|
|
738
|
+
if (diff.missingTargets.length > 0) hasMissing = true;
|
|
739
|
+
rows.push({
|
|
740
|
+
locale: lf.locale,
|
|
741
|
+
version: parsed.version,
|
|
742
|
+
sourceKeys: sourceParsed.entries.size,
|
|
743
|
+
localeKeys: parsed.entries.size,
|
|
744
|
+
added: diff.addedKeys.length,
|
|
745
|
+
obsolete: diff.obsoleteKeys.length,
|
|
746
|
+
missingTargets: diff.missingTargets.length
|
|
747
|
+
});
|
|
748
|
+
const mainObsoleteKeys = opts.obsolete === "mark" ? diff.obsoleteKeys : [];
|
|
749
|
+
const mainParsedClone = {
|
|
750
|
+
...parsed,
|
|
751
|
+
raw: structuredClone(parsed.raw)
|
|
752
|
+
};
|
|
753
|
+
const mainOutputXml = writeXlf(mainParsedClone, diff.merged, mainObsoleteKeys, {
|
|
754
|
+
newTarget: opts.newTarget,
|
|
755
|
+
// if graveyard, main behaves like delete (keeps file clean)
|
|
756
|
+
obsolete: opts.obsolete === "graveyard" ? "delete" : opts.obsolete
|
|
757
|
+
});
|
|
758
|
+
let graveyardOutputXml;
|
|
759
|
+
let graveyardPath;
|
|
760
|
+
if (opts.obsolete === "graveyard" && diff.obsoleteKeys.length > 0) {
|
|
761
|
+
const graveyardEntries = buildGraveyardEntries(parsed, diff.obsoleteKeys);
|
|
762
|
+
if (graveyardEntries.size > 0) {
|
|
763
|
+
graveyardPath = resolveGraveyardPath(opts.graveyardFile, lf.locale);
|
|
764
|
+
const graveParsedClone = {
|
|
765
|
+
...parsed,
|
|
766
|
+
raw: structuredClone(parsed.raw)
|
|
767
|
+
};
|
|
768
|
+
graveyardOutputXml = writeXlf(graveParsedClone, graveyardEntries, [], {
|
|
769
|
+
newTarget: opts.newTarget,
|
|
770
|
+
obsolete: "delete"
|
|
771
|
+
});
|
|
772
|
+
}
|
|
773
|
+
}
|
|
774
|
+
plans.push({
|
|
775
|
+
lf,
|
|
776
|
+
mainOutputXml,
|
|
777
|
+
graveyardOutputXml,
|
|
778
|
+
graveyardPath
|
|
779
|
+
});
|
|
780
|
+
}
|
|
781
|
+
spinner.stop();
|
|
782
|
+
renderSummaryTable(rows);
|
|
783
|
+
if (opts.failOnMissing && hasMissing) {
|
|
784
|
+
ui.error(
|
|
785
|
+
"Sync failed: missing targets. Fix translations or choose a different --new-target strategy."
|
|
786
|
+
);
|
|
787
|
+
process.exitCode = 1;
|
|
788
|
+
return;
|
|
789
|
+
}
|
|
790
|
+
if (!opts.dryRun) {
|
|
791
|
+
for (const p of plans) {
|
|
792
|
+
await writeFile(p.lf.filePath, p.mainOutputXml, "utf-8");
|
|
793
|
+
if (opts.obsolete === "graveyard" && p.graveyardOutputXml && p.graveyardPath) {
|
|
794
|
+
await writeFile(p.graveyardPath, p.graveyardOutputXml, "utf-8");
|
|
795
|
+
}
|
|
796
|
+
}
|
|
797
|
+
}
|
|
798
|
+
if (opts.dryRun) {
|
|
799
|
+
ui.success("Diff OK (dry-run)");
|
|
800
|
+
} else {
|
|
801
|
+
ui.success(
|
|
802
|
+
opts.obsolete === "graveyard" ? "Sync OK (files updated + graveyard written)" : "Sync OK (files updated)"
|
|
803
|
+
);
|
|
804
|
+
}
|
|
805
|
+
} catch (e) {
|
|
806
|
+
spinner.fail("Failed");
|
|
807
|
+
ui.error(e?.message ?? String(e));
|
|
808
|
+
process.exitCode = 1;
|
|
809
|
+
}
|
|
810
|
+
});
|
|
811
|
+
}
|
|
812
|
+
|
|
813
|
+
// src/cli.ts
|
|
814
|
+
var program = new Command();
|
|
815
|
+
program.name("xlf-sync").description("Sync Angular XLIFF (1.2 & 2.0) locale files with messages.xlf").version("0.1.0");
|
|
816
|
+
registerSyncCommand(program);
|
|
817
|
+
registerCheckCommand(program);
|
|
818
|
+
registerReportCommand(program);
|
|
819
|
+
program.parse(process.argv);
|
|
820
|
+
//# sourceMappingURL=cli.js.map
|
package/dist/cli.js.map
ADDED
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"sources":["../src/cli.ts","../src/commands/check.ts","../src/ui/console.ts","../src/core/discover.ts","../src/core/xlf/index.ts","../src/core/xlf/v12.ts","../src/core/xlf/v20.ts","../src/core/xlf/write-v12.ts","../src/core/xlf/write-v20.ts","../src/core/sync.ts","../src/ui/table.ts","../src/ui/banner.ts","../package.json","../src/commands/report.ts","../src/commands/sync.ts","../src/core/graveyard.ts"],"sourcesContent":["#!/usr/bin/env node\r\nimport { Command } from \"commander\";\r\nimport { registerCheckCommand } from \"./commands/check\";\r\nimport { registerReportCommand } from \"./commands/report\";\r\nimport { registerSyncCommand } from \"./commands/sync\";\r\n\r\nconst program = new Command();\r\n\r\nprogram\r\n .name(\"xlf-sync\")\r\n .description(\"Sync Angular XLIFF (1.2 & 2.0) locale files with messages.xlf\")\r\n .version(\"0.1.0\");\r\n\r\nregisterSyncCommand(program);\r\nregisterCheckCommand(program);\r\nregisterReportCommand(program);\r\n\r\nprogram.parse(process.argv);\r\n\r\n\r\n// https://chatgpt.com/c/6978c595-664c-8328-9756-4a51d628277b","import { Command } from \"commander\";\r\nimport ora from \"ora\";\r\nimport { ui } from \"../ui/console.js\";\r\nimport { discoverFiles } from \"../core/discover.js\";\r\nimport { readFile } from \"node:fs/promises\";\r\nimport { parseXlf } from \"../core/xlf/index.js\";\r\nimport { syncLocale } from \"../core/sync.js\";\r\nimport { renderSummaryTable } from \"../ui/table.js\";\r\nimport { renderBanner } from \"../ui/banner.js\";\r\n\r\nexport function registerCheckCommand(program: Command) {\r\n program\r\n .command(\"check\")\r\n .description(\"Check if locale XLF files are in sync (CI-friendly)\")\r\n .option(\"--source <path>\", \"Path to source messages.xlf\", \"src/locale/messages.xlf\")\r\n .option(\"--locales <glob>\", \"Glob for locale files\", \"src/locale/messages.*.xlf\")\r\n .option(\"--fail-on-missing\", \"Exit non-zero if missing targets exist\", false)\r\n .option(\"--fail-on-obsolete\", \"Exit non-zero if obsolete keys exist\", false)\r\n .option(\"--fail-on-added\", \"Exit non-zero if new keys would be added\", false)\r\n .option(\"--new-target <mode>\", \"todo | empty | source (used for diff only)\", \"todo\")\r\n .option(\"--verbose\", \"Print missing keys per locale\", false)\r\n .action(async (opts) => {\r\n renderBanner(\"check\");\r\n\r\n const spinner = ora(\"Checking...\").start();\r\n\r\n try {\r\n const res = await discoverFiles({\r\n sourcePath: opts.source,\r\n localesGlob: opts.locales,\r\n });\r\n\r\n const sourceXml = await readFile(res.sourcePath, \"utf-8\");\r\n const sourceParsed = parseXlf(sourceXml);\r\n\r\n const rows: any[] = [];\r\n const missingKeysByLocale: Record<string, string[]> = {};\r\n\r\n let hasMissing = false;\r\n let hasObsolete = false;\r\n let hasAdded = false;\r\n\r\n for (const lf of res.localeFiles) {\r\n const xml = await readFile(lf.filePath, \"utf-8\");\r\n const parsed = parseXlf(xml);\r\n\r\n const diff = syncLocale(sourceParsed.entries, parsed.entries, {\r\n newTarget: opts.newTarget,\r\n // For check: we only want detection, not modifications\r\n obsolete: \"delete\",\r\n });\r\n\r\n const missingTargets = diff.missingTargets.length;\r\n const obsolete = diff.obsoleteKeys.length;\r\n const added = diff.addedKeys.length;\r\n\r\n if (missingTargets > 0) {\r\n hasMissing = true;\r\n missingKeysByLocale[lf.locale] = diff.missingTargets.slice();\r\n }\r\n if (obsolete > 0) hasObsolete = true;\r\n if (added > 0) hasAdded = true;\r\n\r\n rows.push({\r\n locale: lf.locale,\r\n version: parsed.version,\r\n sourceKeys: sourceParsed.entries.size,\r\n localeKeys: parsed.entries.size,\r\n added,\r\n obsolete,\r\n missingTargets,\r\n });\r\n }\r\n\r\n spinner.stop();\r\n renderSummaryTable(rows);\r\n\r\n if (opts.verbose) {\r\n const locales = Object.keys(missingKeysByLocale);\r\n if (locales.length === 0) {\r\n ui.success(\"No missing targets.\");\r\n } else {\r\n ui.info(\"Missing targets:\");\r\n for (const locale of locales) {\r\n ui.info(`- ${locale}:`);\r\n for (const key of missingKeysByLocale[locale]) {\r\n ui.info(` • ${key}`);\r\n }\r\n }\r\n }\r\n }\r\n\r\n const reasons: string[] = [];\r\n if (opts.failOnMissing && hasMissing) reasons.push(\"missing targets\");\r\n if (opts.failOnObsolete && hasObsolete) reasons.push(\"obsolete keys\");\r\n if (opts.failOnAdded && hasAdded) reasons.push(\"new keys need adding\");\r\n\r\n if (reasons.length > 0) {\r\n ui.error(`Check failed: ${reasons.join(\", \")}`);\r\n process.exitCode = 1;\r\n } else {\r\n ui.success(\"Check OK\");\r\n }\r\n } catch (e: any) {\r\n spinner.fail(\"Failed\");\r\n ui.error(e?.message ?? String(e));\r\n process.exitCode = 1;\r\n }\r\n });\r\n}\r\n","import chalk from \"chalk\";\r\nimport logSymbols from \"log-symbols\";\r\nimport boxen from \"boxen\";\r\n\r\nexport const ui = {\r\n info: (msg: string) => console.log(chalk.cyan(msg)),\r\n success: (msg: string) => console.log(`${logSymbols.success} ${chalk.green(msg)}`),\r\n warn: (msg: string) => console.log(`${logSymbols.warning} ${chalk.yellow(msg)}`),\r\n error: (msg: string) => console.error(`${logSymbols.error} ${chalk.red(msg)}`),\r\n headerBox: (title: string, subtitle?: string) =>\r\n console.log(\r\n boxen(\r\n `${chalk.bold(title)}${subtitle ? `\\n${chalk.dim(subtitle)}` : \"\"}`,\r\n { padding: 1, borderStyle: \"round\" }\r\n )\r\n ),\r\n};\r\n","import fg from \"fast-glob\";\r\nimport { promises as fs } from \"node:fs\";\r\nimport path from \"node:path\";\r\n\r\nexport interface DiscoverOptions {\r\n sourcePath: string;\r\n localesGlob: string;\r\n}\r\n\r\nexport interface LocaleFile {\r\n locale: string; // e.g. \"el\" or \"el-GR\"\r\n filePath: string;\r\n}\r\n\r\nexport interface DiscoverResult {\r\n sourcePath: string;\r\n localeFiles: LocaleFile[];\r\n}\r\n\r\nfunction extractLocaleFromFilename(filePath: string): string | null {\r\n // supports:\r\n // messages.el.xlf\r\n // messages.el-GR.xlf\r\n // messages.fr-CA.xlf\r\n const base = path.basename(filePath);\r\n const m = base.match(/^messages\\.([a-z]{2}(?:-[A-Z]{2})?)\\.xlf$/);\r\n return m?.[1] ?? null;\r\n}\r\n\r\nexport async function discoverFiles(opts: DiscoverOptions): Promise<DiscoverResult> {\r\n // validate source exists\r\n await fs.access(opts.sourcePath);\r\n\r\n const matches = await fg(opts.localesGlob, { onlyFiles: true, unique: true });\r\n\r\n const localeFiles: LocaleFile[] = [];\r\n for (const filePath of matches) {\r\n // ignore the source file if glob accidentally matches it\r\n if (path.resolve(filePath) === path.resolve(opts.sourcePath)) continue;\r\n\r\n const locale = extractLocaleFromFilename(filePath);\r\n if (!locale) continue;\r\n\r\n localeFiles.push({ locale, filePath });\r\n }\r\n\r\n // sort for stable output\r\n localeFiles.sort((a, b) => a.locale.localeCompare(b.locale));\r\n\r\n // dedupe locale collisions (same locale appears twice)\r\n const seen = new Map<string, string>();\r\n for (const lf of localeFiles) {\r\n if (seen.has(lf.locale)) {\r\n const prev = seen.get(lf.locale)!;\r\n throw new Error(\r\n `Duplicate locale \"${lf.locale}\" detected:\\n- ${prev}\\n- ${lf.filePath}\\n` +\r\n `Fix by removing one file or adjusting --locales glob.`\r\n );\r\n }\r\n seen.set(lf.locale, lf.filePath);\r\n }\r\n\r\n return { sourcePath: opts.sourcePath, localeFiles };\r\n}\r\n","import { XMLParser } from \"fast-xml-parser\";\r\nimport { ParsedXlf, MessageEntry, WriteOptions } from \"../../types/model.js\";\r\nimport { parseV12 } from \"./v12.js\";\r\nimport { parseV20 } from \"./v20.js\";\r\nimport { writeV12 } from \"./write-v12.js\";\r\nimport { writeV20 } from \"./write-v20.js\";\r\n\r\nconst parser = new XMLParser({\r\n ignoreAttributes: false,\r\n attributeNamePrefix: \"@_\",\r\n preserveOrder: false,\r\n});\r\n\r\nexport function parseXlf(xml: string): ParsedXlf {\r\n const doc = parser.parse(xml);\r\n\r\n const xliff = doc?.xliff;\r\n if (!xliff) throw new Error(\"Invalid XLF: missing <xliff>\");\r\n\r\n const version = xliff[\"@_version\"];\r\n if (version === \"1.2\") return parseV12(doc);\r\n if (version === \"2.0\") return parseV20(doc);\r\n\r\n throw new Error(`Unsupported XLIFF version: ${version}`);\r\n}\r\n\r\nexport function writeXlf(\r\n parsed: ParsedXlf,\r\n merged: Map<string, MessageEntry>,\r\n obsoleteKeys: string[],\r\n opts: WriteOptions\r\n): string {\r\n if (parsed.version === \"1.2\") return writeV12(parsed.raw, merged, obsoleteKeys, opts);\r\n return writeV20(parsed.raw, merged, obsoleteKeys, opts);\r\n}\r\n","import { ParsedXlf, MessageEntry } from \"../../types/model.js\";\r\n\r\nfunction asArray<T>(v: T | T[] | undefined | null): T[] {\r\n if (!v) return [];\r\n return Array.isArray(v) ? v : [v];\r\n}\r\n\r\nexport function parseV12(doc: any): ParsedXlf {\r\n const entries = new Map<string, MessageEntry>();\r\n\r\n const xliff = doc.xliff;\r\n const file = xliff.file;\r\n const locale = file?.[\"@_target-language\"]; // optional\r\n\r\n const body = file?.body;\r\n if (!body) throw new Error(\"Invalid XLF 1.2: missing <body>\");\r\n\r\n const transUnits = asArray(body[\"trans-unit\"]);\r\n for (const tu of transUnits) {\r\n const id = tu?.[\"@_id\"];\r\n if (!id) continue;\r\n\r\n const source = tu.source ?? \"\";\r\n const target = tu.target;\r\n\r\n entries.set(id, {\r\n key: id,\r\n sourceXml: toXmlText(source),\r\n targetXml: target !== undefined ? toXmlText(target) : undefined,\r\n });\r\n }\r\n\r\n return {\r\n version: \"1.2\",\r\n locale,\r\n entries,\r\n raw: doc,\r\n };\r\n}\r\n\r\n// MVP: keep it simple (text-only). We'll upgrade later for inline tags.\r\nfunction toXmlText(v: any): string {\r\n if (v === null || v === undefined) return \"\";\r\n if (typeof v === \"string\") return v;\r\n if (typeof v === \"object\") {\r\n if (typeof v[\"#text\"] === \"string\") return v[\"#text\"];\r\n }\r\n // fast-xml-parser can produce objects for mixed content; fallback:\r\n return String(v);\r\n}\r\n","import { ParsedXlf, MessageEntry } from \"../../types/model.js\";\r\n\r\nfunction asArray<T>(v: T | T[] | undefined | null): T[] {\r\n if (!v) return [];\r\n return Array.isArray(v) ? v : [v];\r\n}\r\n\r\nexport function parseV20(doc: any): ParsedXlf {\r\n const entries = new Map<string, MessageEntry>();\r\n\r\n const xliff = doc.xliff;\r\n const locale = xliff?.[\"@_trgLang\"]; // optional\r\n\r\n const file = xliff.file;\r\n if (!file) throw new Error(\"Invalid XLF 2.0: missing <file>\");\r\n\r\n const units = asArray(file.unit);\r\n for (const unit of units) {\r\n const unitId = unit?.[\"@_id\"];\r\n if (!unitId) continue;\r\n\r\n const segments = asArray(unit.segment);\r\n // Angular exports usually have one segment, but support many\r\n segments.forEach((seg, idx) => {\r\n const source = seg?.source ?? \"\";\r\n const target = seg?.target;\r\n\r\n const key = segments.length > 1 ? `${unitId}:${idx}` : unitId;\r\n\r\n entries.set(key, {\r\n key,\r\n sourceXml: toXmlText(source),\r\n targetXml: target !== undefined ? toXmlText(target) : undefined,\r\n });\r\n });\r\n }\r\n\r\n return {\r\n version: \"2.0\",\r\n locale,\r\n entries,\r\n raw: doc,\r\n };\r\n}\r\n\r\nfunction toXmlText(v: any): string {\r\n if (v === null || v === undefined) return \"\";\r\n if (typeof v === \"string\") return v;\r\n if (typeof v === \"object\") {\r\n if (typeof v[\"#text\"] === \"string\") return v[\"#text\"];\r\n // fallback for other usage, though mostly #text is the key\r\n }\r\n return String(v);\r\n}\r\n","import { MessageEntry } from \"../../types/model.js\";\r\n\r\nexport type NewTargetMode = \"todo\" | \"empty\" | \"source\";\r\nexport type ObsoleteMode = \"delete\" | \"mark\" | \"graveyard\";\r\n\r\nexport interface WriteOptions {\r\n newTarget: NewTargetMode;\r\n obsolete: ObsoleteMode;\r\n}\r\n\r\n/**\r\n * Defensive normalization:\r\n * - Ensures we never stringify objects into \"[object Object]\"\r\n * - Helps recover from previously \"dirty\" files.\r\n */\r\nfunction normalizeText(v: any): string {\r\n if (v == null) return \"\";\r\n if (typeof v === \"string\") return v;\r\n\r\n // If some upstream step accidentally produced a fast-xml-parser style object\r\n if (typeof v === \"object\") {\r\n if (typeof v[\"#text\"] === \"string\") return v[\"#text\"];\r\n if (typeof v.text === \"string\") return v.text;\r\n\r\n // Sometimes arrays happen in edge cases\r\n if (Array.isArray(v)) {\r\n return v.map(normalizeText).join(\"\");\r\n }\r\n }\r\n\r\n return \"\";\r\n}\r\n\r\nexport function writeV12(\r\n rawDoc: any,\r\n merged: Map<string, MessageEntry>,\r\n obsoleteKeys: string[],\r\n opts: WriteOptions\r\n): string {\r\n const xliff = rawDoc.xliff;\r\n const file = xliff.file;\r\n const body = file.body;\r\n\r\n // rebuild trans-units from merged (source-of-truth order)\r\n const transUnits: any[] = [];\r\n\r\n for (const entry of merged.values()) {\r\n const tu: any = {\r\n \"@_id\": normalizeText(entry.key),\r\n source: normalizeText(entry.sourceXml),\r\n };\r\n\r\n if (entry.targetXml !== undefined) {\r\n tu.target = normalizeText(entry.targetXml);\r\n }\r\n\r\n transUnits.push(tu);\r\n }\r\n\r\n // OBSOLETE MARK (safe, string-only, recovery-friendly)\r\n if (opts.obsolete === \"mark\") {\r\n const originalUnits: any[] = body[\"trans-unit\"] ?? [];\r\n\r\n for (const key of obsoleteKeys) {\r\n const original = originalUnits.find((u) => u[\"@_id\"] === key);\r\n if (!original) continue;\r\n\r\n transUnits.push({\r\n \"@_id\": normalizeText(key),\r\n source: normalizeText(original.source),\r\n target: `__OBSOLETE__${normalizeText(original.target)}`,\r\n note: \"Marked obsolete by xlf-sync\",\r\n });\r\n }\r\n }\r\n\r\n // apply rebuilt units\r\n body[\"trans-unit\"] = transUnits;\r\n\r\n return toXmlV12(rawDoc);\r\n}\r\n\r\n/* =======================\r\n XML SERIALIZER (1.2)\r\n ======================= */\r\n\r\nfunction escapeXml(s: string) {\r\n return s\r\n .replaceAll(\"&\", \"&\")\r\n .replaceAll(\"<\", \"<\")\r\n .replaceAll(\">\", \">\")\r\n .replaceAll('\"', \""\")\r\n .replaceAll(\"'\", \"'\");\r\n}\r\n\r\nfunction toXmlV12(doc: any): string {\r\n const xliff = doc.xliff;\r\n const file = xliff.file;\r\n const body = file.body;\r\n\r\n const headerAttrs = `version=\"${escapeXml(normalizeText(xliff[\"@_version\"] ?? \"1.2\"))}\"`;\r\n\r\n const fileAttrs: string[] = [];\r\n for (const [k, v] of Object.entries(file)) {\r\n if (k.startsWith(\"@_\")) {\r\n fileAttrs.push(`${k.slice(2)}=\"${escapeXml(normalizeText(v))}\"`);\r\n }\r\n }\r\n\r\n const units: any[] = Array.isArray(body[\"trans-unit\"])\r\n ? body[\"trans-unit\"]\r\n : [];\r\n\r\n const unitsXml = units\r\n .map((tu) => {\r\n const id = escapeXml(normalizeText(tu[\"@_id\"]));\r\n const source = escapeXml(normalizeText(tu.source));\r\n\r\n let targetXml = \"\";\r\n const targetRaw = tu.target;\r\n\r\n if (typeof targetRaw === \"string\") {\r\n if (targetRaw.startsWith(\"__OBSOLETE__\")) {\r\n const text = targetRaw.replace(\"__OBSOLETE__\", \"\");\r\n targetXml = `<target state=\"obsolete\">${escapeXml(normalizeText(text))}</target>`;\r\n } else {\r\n targetXml = `<target>${escapeXml(normalizeText(targetRaw))}</target>`;\r\n }\r\n }\r\n\r\n const noteXml = tu.note\r\n ? `<note>${escapeXml(normalizeText(tu.note))}</note>`\r\n : \"\";\r\n\r\n return (\r\n ` <trans-unit id=\"${id}\">\\n` +\r\n ` <source>${source}</source>\\n` +\r\n (targetXml ? ` ${targetXml}\\n` : \"\") +\r\n (noteXml ? ` ${noteXml}\\n` : \"\") +\r\n ` </trans-unit>`\r\n );\r\n })\r\n .join(\"\\n\\n\");\r\n\r\n return (\r\n `<?xml version=\"1.0\" encoding=\"UTF-8\" ?>\\n` +\r\n `<xliff ${headerAttrs}>\\n` +\r\n ` <file ${fileAttrs.join(\" \")}>\\n` +\r\n ` <body>\\n` +\r\n `${unitsXml}\\n` +\r\n ` </body>\\n` +\r\n ` </file>\\n` +\r\n `</xliff>\\n`\r\n );\r\n}\r\n","import { MessageEntry } from \"../../types/model.js\";\r\n\r\nexport type NewTargetMode = \"todo\" | \"empty\" | \"source\";\r\nexport type ObsoleteMode = \"delete\" | \"mark\" | \"graveyard\";\r\n\r\nexport interface WriteOptions {\r\n newTarget: NewTargetMode;\r\n obsolete: ObsoleteMode;\r\n}\r\n\r\nexport function writeV20(\r\n rawDoc: any,\r\n merged: Map<string, MessageEntry>,\r\n obsoleteKeys: string[],\r\n opts: WriteOptions\r\n): string {\r\n const xliff = rawDoc.xliff;\r\n const file = xliff.file;\r\n\r\n // Build units from merged (source-of-truth order)\r\n const units: any[] = [];\r\n\r\n for (const entry of merged.values()) {\r\n const unit: any = {\r\n \"@_id\": entry.key,\r\n segment: {\r\n source: entry.sourceXml ?? \"\",\r\n },\r\n };\r\n\r\n if (entry.targetXml !== undefined) {\r\n unit.segment.target = entry.targetXml;\r\n }\r\n\r\n units.push(unit);\r\n }\r\n\r\n // OBSOLETE MARK (safe, string-only)\r\n if (opts.obsolete === \"mark\") {\r\n const originalUnits: any[] = file.unit ?? [];\r\n\r\n for (const key of obsoleteKeys) {\r\n const original = originalUnits.find((u) => u[\"@_id\"] === key);\r\n if (!original) continue;\r\n\r\n const seg = original.segment ?? {};\r\n\r\n units.push({\r\n \"@_id\": key,\r\n segment: {\r\n source: seg.source ?? \"\",\r\n target: `__OBSOLETE__${seg.target ?? \"\"}`,\r\n },\r\n });\r\n }\r\n }\r\n\r\n // apply rebuilt units\r\n file.unit = units;\r\n\r\n return toXmlV20(rawDoc);\r\n}\r\n\r\n/* =======================\r\n XML SERIALIZER (2.0)\r\n ======================= */\r\n\r\nfunction escapeXml(s: string) {\r\n return s\r\n .replaceAll(\"&\", \"&\")\r\n .replaceAll(\"<\", \"<\")\r\n .replaceAll(\">\", \">\")\r\n .replaceAll('\"', \""\")\r\n .replaceAll(\"'\", \"'\");\r\n}\r\n\r\nfunction toXmlV20(doc: any): string {\r\n const xliff = doc.xliff;\r\n const file = xliff.file;\r\n\r\n const xliffAttrs: string[] = [];\r\n for (const [k, v] of Object.entries(xliff)) {\r\n if (k.startsWith(\"@_\")) {\r\n xliffAttrs.push(`${k.slice(2)}=\"${escapeXml(String(v))}\"`);\r\n }\r\n }\r\n\r\n const fileAttrs: string[] = [];\r\n for (const [k, v] of Object.entries(file)) {\r\n if (k.startsWith(\"@_\")) {\r\n fileAttrs.push(`${k.slice(2)}=\"${escapeXml(String(v))}\"`);\r\n }\r\n }\r\n\r\n const units: any[] = Array.isArray(file.unit) ? file.unit : [];\r\n\r\n const unitsXml = units\r\n .map((u) => {\r\n const id = escapeXml(String(u[\"@_id\"]));\r\n const seg = u.segment ?? {};\r\n const source = escapeXml(String(seg.source ?? \"\"));\r\n\r\n let targetXml = \"\";\r\n if (typeof seg.target === \"string\") {\r\n if (seg.target.startsWith(\"__OBSOLETE__\")) {\r\n const text = seg.target.replace(\"__OBSOLETE__\", \"\");\r\n targetXml = `<target state=\"obsolete\">${escapeXml(text)}</target>`;\r\n } else {\r\n targetXml = `<target>${escapeXml(seg.target)}</target>`;\r\n }\r\n }\r\n\r\n return (\r\n ` <unit id=\"${id}\">\\n` +\r\n ` <segment>\\n` +\r\n ` <source>${source}</source>\\n` +\r\n (targetXml ? ` ${targetXml}\\n` : \"\") +\r\n ` </segment>\\n` +\r\n ` </unit>`\r\n );\r\n })\r\n .join(\"\\n\\n\");\r\n\r\n return (\r\n `<?xml version=\"1.0\" encoding=\"UTF-8\"?>\\n` +\r\n `<xliff ${xliffAttrs.join(\" \")}>\\n` +\r\n ` <file ${fileAttrs.join(\" \")}>\\n` +\r\n `${unitsXml}\\n` +\r\n ` </file>\\n` +\r\n `</xliff>\\n`\r\n );\r\n}\r\n","import { MessageEntry } from \"../types/model.js\";\r\n\r\nexport type NewTargetMode = \"todo\" | \"empty\" | \"source\";\r\nexport type ObsoleteMode = \"delete\" | \"mark\" | \"graveyard\";\r\n\r\nexport interface SyncOptions {\r\n newTarget: NewTargetMode;\r\n obsolete: ObsoleteMode;\r\n}\r\n\r\nexport interface SyncResult {\r\n merged: Map<string, MessageEntry>;\r\n addedKeys: string[];\r\n obsoleteKeys: string[];\r\n keptKeys: string[];\r\n missingTargets: string[]; // keys that exist but have no target\r\n}\r\n\r\nexport function syncLocale(\r\n source: Map<string, MessageEntry>,\r\n locale: Map<string, MessageEntry>,\r\n opts: SyncOptions\r\n): SyncResult {\r\n const merged = new Map<string, MessageEntry>();\r\n\r\n const addedKeys: string[] = [];\r\n const obsoleteKeys: string[] = [];\r\n const keptKeys: string[] = [];\r\n const missingTargets: string[] = [];\r\n\r\n // 1) Build merged in the exact order of source\r\n for (const [key, srcEntry] of source.entries()) {\r\n const locEntry = locale.get(key);\r\n\r\n if (locEntry) {\r\n // keep existing translation if present\r\n const targetXml = locEntry.targetXml;\r\n merged.set(key, {\r\n key,\r\n sourceXml: srcEntry.sourceXml,\r\n targetXml,\r\n });\r\n keptKeys.push(key);\r\n if (!targetXml || targetXml.trim() === \"\") missingTargets.push(key);\r\n } else {\r\n // add new entry\r\n const targetXml = makeNewTarget(srcEntry.sourceXml, opts.newTarget);\r\n merged.set(key, {\r\n key,\r\n sourceXml: srcEntry.sourceXml,\r\n targetXml,\r\n });\r\n addedKeys.push(key);\r\n if (!targetXml || targetXml.trim() === \"\") missingTargets.push(key);\r\n }\r\n }\r\n\r\n // 2) Find obsolete keys (present in locale, not in source)\r\n for (const key of locale.keys()) {\r\n if (!source.has(key)) {\r\n obsoleteKeys.push(key);\r\n // for now we don't apply obsolete policy into merged (next step)\r\n }\r\n }\r\n\r\n return { merged, addedKeys, obsoleteKeys, keptKeys, missingTargets };\r\n}\r\n\r\nfunction makeNewTarget(sourceXml: string, mode: NewTargetMode): string | undefined {\r\n if (mode === \"empty\") return undefined;\r\n if (mode === \"source\") return sourceXml;\r\n // todo\r\n return \"TODO\";\r\n}\r\n","import Table from \"cli-table3\";\r\nimport chalk from \"chalk\";\r\n\r\nexport interface LocaleRow {\r\n locale: string;\r\n version: string;\r\n sourceKeys: number;\r\n localeKeys: number;\r\n added: number;\r\n obsolete: number;\r\n missingTargets: number;\r\n}\r\n\r\nexport interface ReportRow {\r\n locale: string;\r\n version: string;\r\n total: number;\r\n done: number;\r\n todo: number;\r\n coverage: number;\r\n words: number;\r\n}\r\n\r\nexport function renderSummaryTable(rows: LocaleRow[]) {\r\n const table = new Table({\r\n head: [\r\n chalk.bold(\"Locale\"),\r\n chalk.bold(\"XLF\"),\r\n chalk.bold(\"Source\"),\r\n chalk.bold(\"Locale\"),\r\n chalk.bold(\"Add\"),\r\n chalk.bold(\"Obsolete\"),\r\n chalk.bold(\"Missing targets\"),\r\n ],\r\n });\r\n\r\n for (const r of rows) {\r\n table.push([\r\n r.locale,\r\n r.version,\r\n r.sourceKeys,\r\n r.localeKeys,\r\n r.added === 0 ? chalk.dim(\"0\") : chalk.yellow(String(r.added)),\r\n r.obsolete === 0 ? chalk.dim(\"0\") : chalk.red(String(r.obsolete)),\r\n r.missingTargets === 0 ? chalk.dim(\"0\") : chalk.yellow(String(r.missingTargets)),\r\n ]);\r\n }\r\n\r\n console.log(table.toString());\r\n}\r\n\r\nexport function renderReportTable(rows: ReportRow[]) {\r\n const table = new Table({\r\n head: [\r\n chalk.bold(\"Locale\"),\r\n chalk.bold(\"XLF\"),\r\n chalk.bold(\"Keys\"),\r\n chalk.bold(\"Translated\"),\r\n chalk.bold(\"Pending\"),\r\n chalk.bold(\"% Cov\"),\r\n chalk.bold(\"Words\"),\r\n ],\r\n });\r\n\r\n for (const r of rows) {\r\n const covColor =\r\n r.coverage === 100\r\n ? chalk.green\r\n : r.coverage >= 80\r\n ? chalk.yellow\r\n : chalk.red;\r\n\r\n table.push([\r\n r.locale,\r\n r.version,\r\n r.total,\r\n r.done,\r\n r.todo === 0 ? chalk.dim(\"0\") : chalk.yellow(String(r.todo)),\r\n covColor(`${r.coverage.toFixed(1)}%`),\r\n r.words,\r\n ]);\r\n }\r\n\r\n console.log(table.toString());\r\n}\r\n","import figlet from \"figlet\";\r\nimport chalk from \"chalk\";\r\nimport pkg from \"../../package.json\";\r\n\r\nexport function renderBanner(command?: string) {\r\n const logo = figlet.textSync(\"XLF-SYNC\", {\r\n font: \"Standard\", // μπορείς να αλλάξεις font\r\n horizontalLayout: \"default\",\r\n verticalLayout: \"default\",\r\n });\r\n\r\n console.log(chalk.cyanBright(logo));\r\n\r\n console.log(\r\n chalk.bold.white(\r\n `XLF-SYNC v${pkg.version}${command ? ` [${command}]` : \"\"}`\r\n )\r\n );\r\n\r\n console.log(chalk.gray(\"Sync & validate Angular XLIFF files\"));\r\n console.log(chalk.gray(\"Author: Anastasios Theodosiou\\n\"));\r\n}\r\n","{\n \"name\": \"xlf-sync\",\n \"version\": \"0.1.0\",\n \"description\": \"Sync Angular XLIFF (1.2 & 2.0) locale files with messages.xlf\",\n \"type\": \"module\",\n \"bin\": {\n \"xlf-sync\": \"dist/cli.js\"\n },\n \"files\": [\n \"dist\"\n ],\n \"scripts\": {\n \"build\": \"tsup\",\n \"dev\": \"node --enable-source-maps dist/cli.js\",\n \"test\": \"vitest run\",\n \"test:watch\": \"vitest\",\n \"lint\": \"echo 'No linter configured'\"\n },\n \"keywords\": [\n \"angular\",\n \"i18n\",\n \"xliff\",\n \"xlf\",\n \"translation\",\n \"sync\",\n \"locale\",\n \"merge\"\n ],\n \"author\": \"Anastasios Theodosiou\",\n \"license\": \"MIT\",\n \"dependencies\": {\n \"boxen\": \"^8.0.1\",\n \"chalk\": \"^5.6.2\",\n \"cli-table3\": \"^0.6.5\",\n \"commander\": \"^14.0.2\",\n \"fast-glob\": \"^3.3.3\",\n \"fast-xml-parser\": \"^5.3.3\",\n \"figlet\": \"^1.10.0\",\n \"log-symbols\": \"^7.0.1\",\n \"ora\": \"^9.1.0\"\n },\n \"devDependencies\": {\n \"@types/node\": \"^25.0.10\",\n \"tsup\": \"^8.5.1\",\n \"typescript\": \"^5.9.3\",\n \"vitest\": \"^4.0.18\"\n }\n}","import { Command } from \"commander\";\r\nimport ora from \"ora\";\r\nimport { ui } from \"../ui/console.js\";\r\nimport { discoverFiles } from \"../core/discover.js\";\r\nimport { readFile } from \"node:fs/promises\";\r\nimport { parseXlf } from \"../core/xlf/index.js\";\r\nimport { renderReportTable, ReportRow } from \"../ui/table.js\";\r\nimport { renderBanner } from \"../ui/banner.js\";\r\n\r\nfunction isUntranslated(target: string | undefined): boolean {\r\n if (!target) return true;\r\n const t = target.trim();\r\n return t === \"\" || t.toUpperCase() === \"TODO\";\r\n}\r\n\r\nfunction countWords(text: string | undefined): number {\r\n if (!text) return 0;\r\n return text.trim().split(/\\s+/).length;\r\n}\r\n\r\nexport function registerReportCommand(program: Command) {\r\n program\r\n .command(\"report\")\r\n .description(\"Generate translation statistics report\")\r\n .option(\"--source <path>\", \"Path to source messages.xlf\", \"src/locale/messages.xlf\")\r\n .option(\"--locales <glob>\", \"Glob for locale files\", \"src/locale/messages.*.xlf\")\r\n .action(async (opts) => {\r\n renderBanner(\"report\");\r\n\r\n const spinner = ora(\"Scanning files...\").start();\r\n\r\n try {\r\n // 1) Discover files\r\n const res = await discoverFiles({\r\n sourcePath: opts.source,\r\n localesGlob: opts.locales,\r\n });\r\n\r\n const rows: ReportRow[] = [];\r\n\r\n // 2) Parse each locale file\r\n for (const lf of res.localeFiles) {\r\n const xml = await readFile(lf.filePath, \"utf-8\");\r\n const parsed = parseXlf(xml);\r\n\r\n let total = 0;\r\n let todo = 0;\r\n let words = 0;\r\n\r\n for (const entry of parsed.entries.values()) {\r\n total++;\r\n\r\n if (isUntranslated(entry.targetXml)) {\r\n todo++;\r\n } else {\r\n // count words only for done translations\r\n words += countWords(entry.targetXml);\r\n }\r\n }\r\n\r\n const done = total - todo;\r\n const coverage = total > 0 ? (done / total) * 100 : 100;\r\n\r\n rows.push({\r\n locale: lf.locale,\r\n version: parsed.version,\r\n total,\r\n done,\r\n todo,\r\n coverage,\r\n words,\r\n });\r\n }\r\n\r\n spinner.stop();\r\n\r\n // 3) Render table\r\n if (rows.length === 0) {\r\n ui.warn(\"No locale files found.\");\r\n } else {\r\n renderReportTable(rows);\r\n }\r\n } catch (e: any) {\r\n spinner.fail(\"Failed\");\r\n ui.error(e?.message ?? String(e));\r\n process.exitCode = 1;\r\n }\r\n });\r\n}\r\n","import { Command } from \"commander\";\r\nimport ora from \"ora\";\r\nimport { ui } from \"../ui/console.js\";\r\nimport { discoverFiles } from \"../core/discover.js\";\r\nimport { readFile, writeFile } from \"node:fs/promises\";\r\nimport { parseXlf, writeXlf } from \"../core/xlf/index.js\";\r\nimport { syncLocale } from \"../core/sync.js\";\r\nimport { renderSummaryTable } from \"../ui/table.js\";\r\nimport { buildGraveyardEntries } from \"../core/graveyard.js\";\r\nimport { renderBanner } from \"../ui/banner.js\";\r\n\r\nfunction resolveGraveyardPath(pattern: string, locale: string) {\r\n return pattern.replaceAll(\"{locale}\", locale);\r\n}\r\n\r\ntype Plan = {\r\n lf: { locale: string; filePath: string };\r\n mainOutputXml: string;\r\n graveyardOutputXml?: string;\r\n graveyardPath?: string;\r\n};\r\n\r\nexport function registerSyncCommand(program: Command) {\r\n program\r\n .command(\"sync\")\r\n .description(\"Sync locale XLF files with the source messages.xlf\")\r\n .option(\"--source <path>\", \"Path to source messages.xlf\", \"src/locale/messages.xlf\")\r\n .option(\"--locales <glob>\", \"Glob for locale files\", \"src/locale/messages.*.xlf\")\r\n .option(\"--dry-run\", \"Do not write files, only report changes\", false)\r\n .option(\"--new-target <mode>\", \"todo | empty | source\", \"todo\")\r\n .option(\"--obsolete <mode>\", \"delete | mark | graveyard\", \"mark\")\r\n .option(\"--fail-on-missing\", \"Fail if missing targets exist (no files written)\", false)\r\n .option(\r\n \"--graveyard-file <path>\",\r\n \"Graveyard output path pattern\",\r\n \"src/locale/_obsolete.{locale}.xlf\"\r\n )\r\n .action(async (opts) => {\r\n renderBanner(\"sync\");\r\n\r\n const spinner = ora(\"Scanning files...\").start();\r\n\r\n try {\r\n // 1️⃣ Discover source + locale files\r\n const res = await discoverFiles({\r\n sourcePath: opts.source,\r\n localesGlob: opts.locales,\r\n });\r\n\r\n spinner.succeed(`Found ${res.localeFiles.length} locale file(s)`);\r\n\r\n for (const lf of res.localeFiles) {\r\n ui.info(`- ${lf.locale}: ${lf.filePath}`);\r\n }\r\n\r\n // 2️⃣ Parse source file\r\n const sourceXml = await readFile(res.sourcePath, \"utf-8\");\r\n const sourceParsed = parseXlf(sourceXml);\r\n\r\n const rows: {\r\n locale: string;\r\n version: string;\r\n sourceKeys: number;\r\n localeKeys: number;\r\n added: number;\r\n obsolete: number;\r\n missingTargets: number;\r\n }[] = [];\r\n\r\n const plans: Plan[] = [];\r\n let hasMissing = false;\r\n\r\n // 3️⃣ PASS 1: compute diffs + prepare outputs (NO WRITES)\r\n for (const lf of res.localeFiles) {\r\n const localeXml = await readFile(lf.filePath, \"utf-8\");\r\n const parsed = parseXlf(localeXml);\r\n\r\n const diff = syncLocale(sourceParsed.entries, parsed.entries, {\r\n newTarget: opts.newTarget,\r\n obsolete: opts.obsolete,\r\n });\r\n\r\n if (diff.missingTargets.length > 0) hasMissing = true;\r\n\r\n rows.push({\r\n locale: lf.locale,\r\n version: parsed.version,\r\n sourceKeys: sourceParsed.entries.size,\r\n localeKeys: parsed.entries.size,\r\n added: diff.addedKeys.length,\r\n obsolete: diff.obsoleteKeys.length,\r\n missingTargets: diff.missingTargets.length,\r\n });\r\n\r\n // MAIN OUTPUT\r\n const mainObsoleteKeys = opts.obsolete === \"mark\" ? diff.obsoleteKeys : [];\r\n\r\n const mainParsedClone = {\r\n ...parsed,\r\n raw: structuredClone(parsed.raw),\r\n };\r\n\r\n const mainOutputXml = writeXlf(mainParsedClone, diff.merged, mainObsoleteKeys, {\r\n newTarget: opts.newTarget,\r\n // if graveyard, main behaves like delete (keeps file clean)\r\n obsolete: opts.obsolete === \"graveyard\" ? \"delete\" : opts.obsolete,\r\n });\r\n\r\n // GRAVEYARD OUTPUT (optional)\r\n let graveyardOutputXml: string | undefined;\r\n let graveyardPath: string | undefined;\r\n\r\n if (opts.obsolete === \"graveyard\" && diff.obsoleteKeys.length > 0) {\r\n const graveyardEntries = buildGraveyardEntries(parsed, diff.obsoleteKeys);\r\n\r\n if (graveyardEntries.size > 0) {\r\n graveyardPath = resolveGraveyardPath(opts.graveyardFile, lf.locale);\r\n\r\n const graveParsedClone = {\r\n ...parsed,\r\n raw: structuredClone(parsed.raw),\r\n };\r\n\r\n graveyardOutputXml = writeXlf(graveParsedClone, graveyardEntries, [], {\r\n newTarget: opts.newTarget,\r\n obsolete: \"delete\",\r\n });\r\n }\r\n }\r\n\r\n plans.push({\r\n lf,\r\n mainOutputXml,\r\n graveyardOutputXml,\r\n graveyardPath,\r\n });\r\n }\r\n\r\n spinner.stop();\r\n\r\n // 4️⃣ Render summary table\r\n renderSummaryTable(rows);\r\n\r\n // 5️⃣ FAIL GATE (prevents partial writes)\r\n if (opts.failOnMissing && hasMissing) {\r\n ui.error(\r\n \"Sync failed: missing targets. Fix translations or choose a different --new-target strategy.\"\r\n );\r\n process.exitCode = 1;\r\n return;\r\n }\r\n\r\n // 6️⃣ PASS 2: write outputs\r\n if (!opts.dryRun) {\r\n for (const p of plans) {\r\n await writeFile(p.lf.filePath, p.mainOutputXml, \"utf-8\");\r\n\r\n if (opts.obsolete === \"graveyard\" && p.graveyardOutputXml && p.graveyardPath) {\r\n await writeFile(p.graveyardPath, p.graveyardOutputXml, \"utf-8\");\r\n }\r\n }\r\n }\r\n\r\n if (opts.dryRun) {\r\n ui.success(\"Diff OK (dry-run)\");\r\n } else {\r\n ui.success(\r\n opts.obsolete === \"graveyard\"\r\n ? \"Sync OK (files updated + graveyard written)\"\r\n : \"Sync OK (files updated)\"\r\n );\r\n }\r\n } catch (e: any) {\r\n spinner.fail(\"Failed\");\r\n ui.error(e?.message ?? String(e));\r\n process.exitCode = 1;\r\n }\r\n });\r\n}\r\n","import { ParsedXlf, MessageEntry } from \"../types/model.js\";\r\n\r\nfunction normalizeText(v: any): string {\r\n if (v == null) return \"\";\r\n if (typeof v === \"string\") return v;\r\n if (typeof v === \"object\") {\r\n if (typeof v[\"#text\"] === \"string\") return v[\"#text\"];\r\n if (typeof v.text === \"string\") return v.text;\r\n if (Array.isArray(v)) return v.map(normalizeText).join(\"\");\r\n }\r\n return \"\";\r\n}\r\n\r\n/**\r\n * Extracts obsolete entries from the *original parsed raw doc* and returns them as MessageEntry map.\r\n * Targets are prefixed with \"__OBSOLETE__\" so writers emit state=\"obsolete\".\r\n */\r\nexport function buildGraveyardEntries(parsed: ParsedXlf, obsoleteKeys: string[]): Map<string, MessageEntry> {\r\n const out = new Map<string, MessageEntry>();\r\n\r\n if (obsoleteKeys.length === 0) return out;\r\n\r\n if (parsed.version === \"1.2\") {\r\n const body = parsed.raw?.xliff?.file?.body;\r\n const units: any[] = body?.[\"trans-unit\"] ?? [];\r\n\r\n for (const key of obsoleteKeys) {\r\n const u = units.find((x) => x?.[\"@_id\"] === key);\r\n if (!u) continue;\r\n\r\n const source = normalizeText(u.source);\r\n const target = normalizeText(u.target);\r\n\r\n out.set(key, {\r\n key,\r\n sourceXml: source,\r\n targetXml: `__OBSOLETE__${target}`,\r\n });\r\n }\r\n\r\n return out;\r\n }\r\n\r\n // 2.0\r\n const file = parsed.raw?.xliff?.file;\r\n const units: any[] = file?.unit ?? [];\r\n\r\n for (const key of obsoleteKeys) {\r\n const u = units.find((x) => x?.[\"@_id\"] === key);\r\n if (!u) continue;\r\n\r\n const seg = u.segment ?? {};\r\n const source = normalizeText(seg.source);\r\n const target = normalizeText(seg.target);\r\n\r\n out.set(key, {\r\n key,\r\n sourceXml: source,\r\n targetXml: `__OBSOLETE__${target}`,\r\n });\r\n }\r\n\r\n return out;\r\n}\r\n"],"mappings":"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","names":["asArray","toXmlText","escapeXml","chalk","chalk","chalk","program","ora","readFile","program","ora","readFile","ora","readFile","normalizeText","units","program","ora","readFile"]}
|
package/package.json
ADDED
|
@@ -0,0 +1,48 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "xlf-sync",
|
|
3
|
+
"version": "1.0.0",
|
|
4
|
+
"description": "Sync Angular XLIFF (1.2 & 2.0) locale files with messages.xlf",
|
|
5
|
+
"type": "module",
|
|
6
|
+
"bin": {
|
|
7
|
+
"xlf-sync": "dist/cli.js"
|
|
8
|
+
},
|
|
9
|
+
"files": [
|
|
10
|
+
"dist"
|
|
11
|
+
],
|
|
12
|
+
"scripts": {
|
|
13
|
+
"build": "tsup",
|
|
14
|
+
"dev": "node --enable-source-maps dist/cli.js",
|
|
15
|
+
"test": "vitest run",
|
|
16
|
+
"test:watch": "vitest",
|
|
17
|
+
"lint": "echo 'No linter configured'"
|
|
18
|
+
},
|
|
19
|
+
"keywords": [
|
|
20
|
+
"angular",
|
|
21
|
+
"i18n",
|
|
22
|
+
"xliff",
|
|
23
|
+
"xlf",
|
|
24
|
+
"translation",
|
|
25
|
+
"sync",
|
|
26
|
+
"locale",
|
|
27
|
+
"merge"
|
|
28
|
+
],
|
|
29
|
+
"author": "Anastasios Theodosiou",
|
|
30
|
+
"license": "MIT",
|
|
31
|
+
"dependencies": {
|
|
32
|
+
"boxen": "^8.0.1",
|
|
33
|
+
"chalk": "^5.6.2",
|
|
34
|
+
"cli-table3": "^0.6.5",
|
|
35
|
+
"commander": "^14.0.2",
|
|
36
|
+
"fast-glob": "^3.3.3",
|
|
37
|
+
"fast-xml-parser": "^5.3.3",
|
|
38
|
+
"figlet": "^1.10.0",
|
|
39
|
+
"log-symbols": "^7.0.1",
|
|
40
|
+
"ora": "^9.1.0"
|
|
41
|
+
},
|
|
42
|
+
"devDependencies": {
|
|
43
|
+
"@types/node": "^25.0.10",
|
|
44
|
+
"tsup": "^8.5.1",
|
|
45
|
+
"typescript": "^5.9.3",
|
|
46
|
+
"vitest": "^4.0.18"
|
|
47
|
+
}
|
|
48
|
+
}
|