@verbaly/compiler 0.26.0 → 0.28.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/README.md +4 -3
- package/dist/{claude-C-ETlqsW.js → claude-h9WA0ppg.js} +4 -2
- package/dist/claude-h9WA0ppg.js.map +1 -0
- package/dist/cli.js +461 -42
- package/dist/cli.js.map +1 -1
- package/dist/index.d.ts +15 -3
- package/dist/index.js +400 -32
- package/dist/index.js.map +1 -1
- package/package.json +2 -2
- package/dist/claude-C-ETlqsW.js.map +0 -1
package/dist/cli.js
CHANGED
|
@@ -331,7 +331,7 @@ function pseudoLocalize(message) {
|
|
|
331
331
|
}
|
|
332
332
|
const ch = message[i];
|
|
333
333
|
if (ch === "{") {
|
|
334
|
-
const end = matchBrace(message, i);
|
|
334
|
+
const end = matchBrace$1(message, i);
|
|
335
335
|
out += message.slice(i, end);
|
|
336
336
|
i = end;
|
|
337
337
|
continue;
|
|
@@ -355,7 +355,7 @@ function pseudoLocalize(message) {
|
|
|
355
355
|
const pad = "~".repeat(Math.ceil(letters / 3));
|
|
356
356
|
return `⟦${out}${pad ? " " + pad : ""}⟧`;
|
|
357
357
|
}
|
|
358
|
-
function matchBrace(message, start) {
|
|
358
|
+
function matchBrace$1(message, start) {
|
|
359
359
|
let depth = 0;
|
|
360
360
|
for (let i = start; i < message.length; i++) {
|
|
361
361
|
const two = message.slice(i, i + 2);
|
|
@@ -387,7 +387,7 @@ function resolveConfig(config = {}) {
|
|
|
387
387
|
const sourceLocale = config.sourceLocale ?? "en";
|
|
388
388
|
const locales = /* @__PURE__ */ new Set([sourceLocale, ...config.locales ?? []]);
|
|
389
389
|
if (existsSync(dir)) {
|
|
390
|
-
for (const file of readdirSync(dir)) if (file.endsWith(".json") && file !== `en-XA.json`) locales.add(file.slice(0, -5));
|
|
390
|
+
for (const file of readdirSync(dir)) if (file.endsWith(".json") && !file.startsWith(".") && file !== `en-XA.json`) locales.add(file.slice(0, -5));
|
|
391
391
|
}
|
|
392
392
|
return {
|
|
393
393
|
root,
|
|
@@ -1083,6 +1083,282 @@ function readDeps(root) {
|
|
|
1083
1083
|
}
|
|
1084
1084
|
}
|
|
1085
1085
|
//#endregion
|
|
1086
|
+
//#region src/drafts.ts
|
|
1087
|
+
const DRAFTS_FILE = ".verbaly-drafts.json";
|
|
1088
|
+
function draftsPath(cfg) {
|
|
1089
|
+
return join(cfg.dir, DRAFTS_FILE);
|
|
1090
|
+
}
|
|
1091
|
+
function loadDrafts(cfg) {
|
|
1092
|
+
let content;
|
|
1093
|
+
try {
|
|
1094
|
+
content = readFileSync(draftsPath(cfg), "utf8");
|
|
1095
|
+
} catch {
|
|
1096
|
+
return {};
|
|
1097
|
+
}
|
|
1098
|
+
try {
|
|
1099
|
+
return JSON.parse(content.replace(/^\uFEFF/, ""));
|
|
1100
|
+
} catch (error) {
|
|
1101
|
+
throw new Error(`[verbaly] ${draftsPath(cfg)} is not valid JSON, fix or delete the file`, { cause: error });
|
|
1102
|
+
}
|
|
1103
|
+
}
|
|
1104
|
+
function serializeDrafts(drafts) {
|
|
1105
|
+
const sorted = {};
|
|
1106
|
+
for (const locale of Object.keys(drafts).sort()) {
|
|
1107
|
+
const keys = [...new Set(drafts[locale])].sort();
|
|
1108
|
+
if (keys.length) sorted[locale] = keys;
|
|
1109
|
+
}
|
|
1110
|
+
return JSON.stringify(sorted, null, 2) + "\n";
|
|
1111
|
+
}
|
|
1112
|
+
function saveDrafts(cfg, drafts) {
|
|
1113
|
+
const serialized = serializeDrafts(drafts);
|
|
1114
|
+
const path = draftsPath(cfg);
|
|
1115
|
+
try {
|
|
1116
|
+
if (readFileSync(path, "utf8") === serialized) return;
|
|
1117
|
+
} catch {
|
|
1118
|
+
mkdirSync(cfg.dir, { recursive: true });
|
|
1119
|
+
}
|
|
1120
|
+
writeFileSync(path, serialized);
|
|
1121
|
+
}
|
|
1122
|
+
function markDrafts(drafts, locale, keys) {
|
|
1123
|
+
if (!keys.length) return;
|
|
1124
|
+
drafts[locale] = [.../* @__PURE__ */ new Set([...drafts[locale] ?? [], ...keys])];
|
|
1125
|
+
}
|
|
1126
|
+
function clearDrafts(drafts, locale, keys) {
|
|
1127
|
+
if (!drafts[locale]) return;
|
|
1128
|
+
if (!keys) {
|
|
1129
|
+
delete drafts[locale];
|
|
1130
|
+
return;
|
|
1131
|
+
}
|
|
1132
|
+
const drop = new Set(keys);
|
|
1133
|
+
drafts[locale] = drafts[locale].filter((key) => !drop.has(key));
|
|
1134
|
+
if (!drafts[locale].length) delete drafts[locale];
|
|
1135
|
+
}
|
|
1136
|
+
function effectiveDrafts(drafts, catalogs) {
|
|
1137
|
+
const out = {};
|
|
1138
|
+
for (const [locale, keys] of Object.entries(drafts)) {
|
|
1139
|
+
const catalog = catalogs[locale] ?? {};
|
|
1140
|
+
const live = keys.filter((key) => catalog[key]);
|
|
1141
|
+
if (live.length) out[locale] = live;
|
|
1142
|
+
}
|
|
1143
|
+
return out;
|
|
1144
|
+
}
|
|
1145
|
+
//#endregion
|
|
1146
|
+
//#region src/inline.ts
|
|
1147
|
+
const OPEN_TAG = /^<([a-zA-Z][a-zA-Z0-9-]*)\s*(\/?)>/;
|
|
1148
|
+
function messageToInline(text) {
|
|
1149
|
+
return renderParts(parseParts(text, 0).parts, /* @__PURE__ */ new Map());
|
|
1150
|
+
}
|
|
1151
|
+
function inlineToMessage(xml) {
|
|
1152
|
+
let out = "";
|
|
1153
|
+
let text = "";
|
|
1154
|
+
let i = 0;
|
|
1155
|
+
const flush = () => {
|
|
1156
|
+
out += unescapeXml(text);
|
|
1157
|
+
text = "";
|
|
1158
|
+
};
|
|
1159
|
+
while (i < xml.length) {
|
|
1160
|
+
if (xml[i] === "<") {
|
|
1161
|
+
const slice = xml.slice(i);
|
|
1162
|
+
const ph = /^<ph\b([^>]*?)\/\s*>/.exec(slice) ?? /^<ph\b([^>]*)>\s*<\/ph\s*>/.exec(slice);
|
|
1163
|
+
if (ph) {
|
|
1164
|
+
flush();
|
|
1165
|
+
out += attr(ph[1], "disp") ?? `{${attr(ph[1], "id") ?? "ph"}}`;
|
|
1166
|
+
i += ph[0].length;
|
|
1167
|
+
continue;
|
|
1168
|
+
}
|
|
1169
|
+
const pc = /^<pc\b([^>]*)>/.exec(slice);
|
|
1170
|
+
if (pc) {
|
|
1171
|
+
const close = findPcClose(xml, i + pc[0].length);
|
|
1172
|
+
if (close) {
|
|
1173
|
+
flush();
|
|
1174
|
+
const id = attr(pc[1], "id") ?? "pc";
|
|
1175
|
+
out += attr(pc[1], "dispStart") ?? `<${id}>`;
|
|
1176
|
+
out += inlineToMessage(xml.slice(i + pc[0].length, close.innerEnd));
|
|
1177
|
+
out += attr(pc[1], "dispEnd") ?? `</${id}>`;
|
|
1178
|
+
i = close.end;
|
|
1179
|
+
continue;
|
|
1180
|
+
}
|
|
1181
|
+
}
|
|
1182
|
+
}
|
|
1183
|
+
text += xml[i];
|
|
1184
|
+
i += 1;
|
|
1185
|
+
}
|
|
1186
|
+
flush();
|
|
1187
|
+
return out;
|
|
1188
|
+
}
|
|
1189
|
+
function escapeXml(text) {
|
|
1190
|
+
return text.replace(/&/g, "&").replace(/</g, "<").replace(/>/g, ">").replace(/"/g, """);
|
|
1191
|
+
}
|
|
1192
|
+
function unescapeXml(text) {
|
|
1193
|
+
return text.replace(/&(lt|gt|amp|quot|apos|#x?[0-9a-fA-F]+);/g, (_, entity) => {
|
|
1194
|
+
if (entity === "lt") return "<";
|
|
1195
|
+
if (entity === "gt") return ">";
|
|
1196
|
+
if (entity === "amp") return "&";
|
|
1197
|
+
if (entity === "quot") return "\"";
|
|
1198
|
+
if (entity === "apos") return "'";
|
|
1199
|
+
const code = entity.startsWith("#x") ? parseInt(entity.slice(2), 16) : parseInt(entity.slice(1), 10);
|
|
1200
|
+
return String.fromCodePoint(code);
|
|
1201
|
+
});
|
|
1202
|
+
}
|
|
1203
|
+
function parseParts(text, pos, closeTag) {
|
|
1204
|
+
const parts = [];
|
|
1205
|
+
let plain = "";
|
|
1206
|
+
const flush = () => {
|
|
1207
|
+
if (plain) parts.push({
|
|
1208
|
+
kind: "text",
|
|
1209
|
+
text: plain
|
|
1210
|
+
});
|
|
1211
|
+
plain = "";
|
|
1212
|
+
};
|
|
1213
|
+
let i = pos;
|
|
1214
|
+
while (i < text.length) {
|
|
1215
|
+
const pair = text[i] + (text[i + 1] ?? "");
|
|
1216
|
+
if (pair === "{{" || pair === "}}") {
|
|
1217
|
+
plain += pair;
|
|
1218
|
+
i += 2;
|
|
1219
|
+
continue;
|
|
1220
|
+
}
|
|
1221
|
+
if (text[i] === "{") {
|
|
1222
|
+
const end = matchBrace(text, i);
|
|
1223
|
+
if (end === -1) {
|
|
1224
|
+
plain += "{";
|
|
1225
|
+
i += 1;
|
|
1226
|
+
continue;
|
|
1227
|
+
}
|
|
1228
|
+
const src = text.slice(i, end + 1);
|
|
1229
|
+
if (hasTopLevelPipe(src)) plain += src;
|
|
1230
|
+
else {
|
|
1231
|
+
flush();
|
|
1232
|
+
parts.push({
|
|
1233
|
+
kind: "ph",
|
|
1234
|
+
name: paramName(src),
|
|
1235
|
+
src
|
|
1236
|
+
});
|
|
1237
|
+
}
|
|
1238
|
+
i = end + 1;
|
|
1239
|
+
continue;
|
|
1240
|
+
}
|
|
1241
|
+
if (text[i] === "<") {
|
|
1242
|
+
if (closeTag && text.startsWith(`</${closeTag}>`, i)) {
|
|
1243
|
+
flush();
|
|
1244
|
+
return {
|
|
1245
|
+
parts,
|
|
1246
|
+
end: i + closeTag.length + 3,
|
|
1247
|
+
closed: true
|
|
1248
|
+
};
|
|
1249
|
+
}
|
|
1250
|
+
const open = OPEN_TAG.exec(text.slice(i));
|
|
1251
|
+
if (open) {
|
|
1252
|
+
const [openSrc, name, selfClose] = open;
|
|
1253
|
+
if (selfClose) {
|
|
1254
|
+
flush();
|
|
1255
|
+
parts.push({
|
|
1256
|
+
kind: "ph",
|
|
1257
|
+
name: idSafe(name),
|
|
1258
|
+
src: openSrc
|
|
1259
|
+
});
|
|
1260
|
+
i += openSrc.length;
|
|
1261
|
+
continue;
|
|
1262
|
+
}
|
|
1263
|
+
const inner = parseParts(text, i + openSrc.length, name);
|
|
1264
|
+
if (inner.closed) {
|
|
1265
|
+
flush();
|
|
1266
|
+
parts.push({
|
|
1267
|
+
kind: "pc",
|
|
1268
|
+
name: idSafe(name),
|
|
1269
|
+
openSrc,
|
|
1270
|
+
closeSrc: `</${name}>`,
|
|
1271
|
+
children: inner.parts
|
|
1272
|
+
});
|
|
1273
|
+
i = inner.end;
|
|
1274
|
+
continue;
|
|
1275
|
+
}
|
|
1276
|
+
}
|
|
1277
|
+
plain += "<";
|
|
1278
|
+
i += 1;
|
|
1279
|
+
continue;
|
|
1280
|
+
}
|
|
1281
|
+
plain += text[i];
|
|
1282
|
+
i += 1;
|
|
1283
|
+
}
|
|
1284
|
+
flush();
|
|
1285
|
+
return {
|
|
1286
|
+
parts,
|
|
1287
|
+
end: i,
|
|
1288
|
+
closed: false
|
|
1289
|
+
};
|
|
1290
|
+
}
|
|
1291
|
+
function matchBrace(text, start) {
|
|
1292
|
+
let depth = 0;
|
|
1293
|
+
for (let i = start; i < text.length; i++) {
|
|
1294
|
+
const pair = text[i] + (text[i + 1] ?? "");
|
|
1295
|
+
if (pair === "{{" || pair === "}}") {
|
|
1296
|
+
i += 1;
|
|
1297
|
+
continue;
|
|
1298
|
+
}
|
|
1299
|
+
if (text[i] === "{") depth += 1;
|
|
1300
|
+
else if (text[i] === "}") {
|
|
1301
|
+
depth -= 1;
|
|
1302
|
+
if (depth === 0) return i;
|
|
1303
|
+
}
|
|
1304
|
+
}
|
|
1305
|
+
return -1;
|
|
1306
|
+
}
|
|
1307
|
+
function hasTopLevelPipe(src) {
|
|
1308
|
+
let depth = 0;
|
|
1309
|
+
for (let i = 0; i < src.length; i++) {
|
|
1310
|
+
const pair = src[i] + (src[i + 1] ?? "");
|
|
1311
|
+
if (pair === "{{" || pair === "}}" || pair === "||") {
|
|
1312
|
+
i += 1;
|
|
1313
|
+
continue;
|
|
1314
|
+
}
|
|
1315
|
+
if (src[i] === "{") depth += 1;
|
|
1316
|
+
else if (src[i] === "}") depth -= 1;
|
|
1317
|
+
else if (src[i] === "|" && depth === 1) return true;
|
|
1318
|
+
}
|
|
1319
|
+
return false;
|
|
1320
|
+
}
|
|
1321
|
+
function paramName(src) {
|
|
1322
|
+
const name = /^\{\s*([^{}|:\s]+)/.exec(src)?.[1];
|
|
1323
|
+
return name ? idSafe(name) : "ph";
|
|
1324
|
+
}
|
|
1325
|
+
function idSafe(name) {
|
|
1326
|
+
return name.replace(/[^a-zA-Z0-9._-]/g, "_") || "ph";
|
|
1327
|
+
}
|
|
1328
|
+
function renderParts(parts, taken) {
|
|
1329
|
+
let out = "";
|
|
1330
|
+
for (const part of parts) if (part.kind === "text") out += escapeXml(part.text);
|
|
1331
|
+
else if (part.kind === "ph") out += `<ph id="${uniqueId(part.name, taken)}" disp="${escapeXml(part.src)}"/>`;
|
|
1332
|
+
else {
|
|
1333
|
+
const id = uniqueId(part.name, taken);
|
|
1334
|
+
out += `<pc id="${id}" dispStart="${escapeXml(part.openSrc)}" dispEnd="${escapeXml(part.closeSrc)}">`;
|
|
1335
|
+
out += renderParts(part.children, taken);
|
|
1336
|
+
out += "</pc>";
|
|
1337
|
+
}
|
|
1338
|
+
return out;
|
|
1339
|
+
}
|
|
1340
|
+
function uniqueId(name, taken) {
|
|
1341
|
+
const count = taken.get(name) ?? 0;
|
|
1342
|
+
taken.set(name, count + 1);
|
|
1343
|
+
return count === 0 ? name : `${name}${count + 1}`;
|
|
1344
|
+
}
|
|
1345
|
+
function findPcClose(xml, from) {
|
|
1346
|
+
const tags = /<pc\b[^>]*>|<\/pc\s*>/g;
|
|
1347
|
+
tags.lastIndex = from;
|
|
1348
|
+
let depth = 1;
|
|
1349
|
+
for (let match = tags.exec(xml); match; match = tags.exec(xml)) {
|
|
1350
|
+
depth += match[0].startsWith("</") ? -1 : 1;
|
|
1351
|
+
if (depth === 0) return {
|
|
1352
|
+
innerEnd: match.index,
|
|
1353
|
+
end: tags.lastIndex
|
|
1354
|
+
};
|
|
1355
|
+
}
|
|
1356
|
+
}
|
|
1357
|
+
function attr(attrs, name) {
|
|
1358
|
+
const value = new RegExp(`\\b${name}\\s*=\\s*"([^"]*)"`).exec(attrs)?.[1];
|
|
1359
|
+
return value === void 0 ? void 0 : unescapeXml(value);
|
|
1360
|
+
}
|
|
1361
|
+
//#endregion
|
|
1086
1362
|
//#region src/mobile.ts
|
|
1087
1363
|
function androidValuesDir(locale, sourceLocale) {
|
|
1088
1364
|
if (locale === sourceLocale) return "values";
|
|
@@ -1122,6 +1398,97 @@ function iosText(text) {
|
|
|
1122
1398
|
return text.replace(/\\/g, "\\\\").replace(/"/g, "\\\"").replace(/\n/g, "\\n").replace(/\t/g, "\\t");
|
|
1123
1399
|
}
|
|
1124
1400
|
//#endregion
|
|
1401
|
+
//#region src/po.ts
|
|
1402
|
+
function toPo(sourceLocale, locale, entries) {
|
|
1403
|
+
return [
|
|
1404
|
+
[
|
|
1405
|
+
"msgid \"\"",
|
|
1406
|
+
"msgstr \"\"",
|
|
1407
|
+
`${poString("Project-Id-Version: verbaly\n")}`,
|
|
1408
|
+
`${poString("MIME-Version: 1.0\n")}`,
|
|
1409
|
+
`${poString("Content-Type: text/plain; charset=UTF-8\n")}`,
|
|
1410
|
+
`${poString("Content-Transfer-Encoding: 8bit\n")}`,
|
|
1411
|
+
`${poString(`Language: ${locale}\n`)}`,
|
|
1412
|
+
`${poString(`X-Source-Language: ${sourceLocale}\n`)}`
|
|
1413
|
+
].join("\n"),
|
|
1414
|
+
...entries.map(({ key, source, target, location }) => [
|
|
1415
|
+
...location?.length ? [`#: ${location.join(" ")}`] : [],
|
|
1416
|
+
`msgctxt ${poString(key)}`,
|
|
1417
|
+
`msgid ${poString(source)}`,
|
|
1418
|
+
`msgstr ${poString(target)}`
|
|
1419
|
+
].join("\n")),
|
|
1420
|
+
""
|
|
1421
|
+
].join("\n\n");
|
|
1422
|
+
}
|
|
1423
|
+
function parsePo(content) {
|
|
1424
|
+
const entries = {};
|
|
1425
|
+
let locale;
|
|
1426
|
+
let msgctxt;
|
|
1427
|
+
let msgid;
|
|
1428
|
+
let msgstr;
|
|
1429
|
+
let fuzzy = false;
|
|
1430
|
+
let field;
|
|
1431
|
+
const finish = () => {
|
|
1432
|
+
if (msgid === "" && msgctxt === void 0) {
|
|
1433
|
+
const lang = /^Language:[ \t]*(.+?)[ \t]*$/m.exec(msgstr ?? "")?.[1];
|
|
1434
|
+
if (lang) locale = lang;
|
|
1435
|
+
} else if (msgctxt !== void 0 && msgid !== void 0) entries[msgctxt] = fuzzy ? "" : msgstr ?? "";
|
|
1436
|
+
msgctxt = msgid = msgstr = field = void 0;
|
|
1437
|
+
fuzzy = false;
|
|
1438
|
+
};
|
|
1439
|
+
for (const raw of content.replace(/^\uFEFF/, "").split(/\r\n|[\r\n]/)) {
|
|
1440
|
+
const line = raw.trim();
|
|
1441
|
+
if (line === "") {
|
|
1442
|
+
finish();
|
|
1443
|
+
continue;
|
|
1444
|
+
}
|
|
1445
|
+
if (line.startsWith("#")) {
|
|
1446
|
+
if (/^#,.*\bfuzzy\b/.test(line)) fuzzy = true;
|
|
1447
|
+
continue;
|
|
1448
|
+
}
|
|
1449
|
+
const keyword = /^(msgctxt|msgid_plural|msgid|msgstr(?:\[\d+\])?)\s+(.*)$/.exec(line);
|
|
1450
|
+
if (keyword) {
|
|
1451
|
+
const [, name, rest] = keyword;
|
|
1452
|
+
const value = poUnquote(rest);
|
|
1453
|
+
if (value === void 0) continue;
|
|
1454
|
+
if (name === "msgctxt") {
|
|
1455
|
+
msgctxt = value;
|
|
1456
|
+
field = "msgctxt";
|
|
1457
|
+
} else if (name === "msgid") {
|
|
1458
|
+
msgid = value;
|
|
1459
|
+
field = "msgid";
|
|
1460
|
+
} else if (name === "msgstr" || name === "msgstr[0]") {
|
|
1461
|
+
msgstr = value;
|
|
1462
|
+
field = "msgstr";
|
|
1463
|
+
} else field = "other";
|
|
1464
|
+
continue;
|
|
1465
|
+
}
|
|
1466
|
+
const continuation = poUnquote(line);
|
|
1467
|
+
if (continuation === void 0 || field === void 0 || field === "other") continue;
|
|
1468
|
+
if (field === "msgctxt") msgctxt = (msgctxt ?? "") + continuation;
|
|
1469
|
+
else if (field === "msgid") msgid = (msgid ?? "") + continuation;
|
|
1470
|
+
else msgstr = (msgstr ?? "") + continuation;
|
|
1471
|
+
}
|
|
1472
|
+
finish();
|
|
1473
|
+
return {
|
|
1474
|
+
locale,
|
|
1475
|
+
entries
|
|
1476
|
+
};
|
|
1477
|
+
}
|
|
1478
|
+
function poString(text) {
|
|
1479
|
+
return `"${text.replace(/\\/g, "\\\\").replace(/"/g, "\\\"").replace(/\n/g, "\\n").replace(/\t/g, "\\t").replace(/\r/g, "\\r")}"`;
|
|
1480
|
+
}
|
|
1481
|
+
function poUnquote(text) {
|
|
1482
|
+
const match = /^"((?:[^"\\]|\\.)*)"$/.exec(text);
|
|
1483
|
+
if (!match) return void 0;
|
|
1484
|
+
return match[1].replace(/\\(.)/g, (_, ch) => {
|
|
1485
|
+
if (ch === "n") return "\n";
|
|
1486
|
+
if (ch === "t") return " ";
|
|
1487
|
+
if (ch === "r") return "\r";
|
|
1488
|
+
return ch;
|
|
1489
|
+
});
|
|
1490
|
+
}
|
|
1491
|
+
//#endregion
|
|
1125
1492
|
//#region src/translate.ts
|
|
1126
1493
|
async function translateCatalogs(cfg, catalogs, provider, options = {}) {
|
|
1127
1494
|
const batchSize = options.batchSize ?? 20;
|
|
@@ -1143,10 +1510,12 @@ async function translateCatalogs(cfg, catalogs, provider, options = {}) {
|
|
|
1143
1510
|
for (let i = 0; i < missing.length; i += batchSize) {
|
|
1144
1511
|
const keys = missing.slice(i, i + batchSize);
|
|
1145
1512
|
const messages = Object.fromEntries(keys.map((key) => [key, source[key]]));
|
|
1513
|
+
const origins = options.origins ? Object.fromEntries(keys.filter((key) => options.origins[key]).map((key) => [key, options.origins[key]])) : void 0;
|
|
1146
1514
|
const out = await provider({
|
|
1147
1515
|
sourceLocale: cfg.sourceLocale,
|
|
1148
1516
|
targetLocale: locale,
|
|
1149
|
-
messages
|
|
1517
|
+
messages,
|
|
1518
|
+
origins
|
|
1150
1519
|
});
|
|
1151
1520
|
for (const key of keys) {
|
|
1152
1521
|
const text = out[key];
|
|
@@ -1201,8 +1570,8 @@ function exportCatalogs(cfg, catalogs, options = {}) {
|
|
|
1201
1570
|
}));
|
|
1202
1571
|
const untranslated = all.filter((entry) => !entry.target);
|
|
1203
1572
|
const entries = options.missing ? untranslated : all;
|
|
1204
|
-
const path = join(dir, `${locale}.${format === "
|
|
1205
|
-
writeFileSync(path, format === "csv" ? toCsv(entries) : toXliff(cfg.sourceLocale, locale, entries));
|
|
1573
|
+
const path = join(dir, `${locale}.${format === "xliff" ? "xlf" : format}`);
|
|
1574
|
+
writeFileSync(path, format === "csv" ? toCsv(entries) : format === "po" ? toPo(cfg.sourceLocale, locale, entries) : toXliff(cfg.sourceLocale, locale, entries));
|
|
1206
1575
|
files.push({
|
|
1207
1576
|
locale,
|
|
1208
1577
|
path,
|
|
@@ -1290,7 +1659,14 @@ function parseExchangeFile(file, localeOverride) {
|
|
|
1290
1659
|
locale: localeOverride ?? basename(file).replace(/\.csv$/i, ""),
|
|
1291
1660
|
entries: parseCsv(content)
|
|
1292
1661
|
};
|
|
1293
|
-
|
|
1662
|
+
if (/\.po$/i.test(file)) {
|
|
1663
|
+
const parsed = parsePo(content);
|
|
1664
|
+
return {
|
|
1665
|
+
locale: localeOverride ?? parsed.locale ?? basename(file).replace(/\.po$/i, ""),
|
|
1666
|
+
entries: parsed.entries
|
|
1667
|
+
};
|
|
1668
|
+
}
|
|
1669
|
+
throw new Error(`[verbaly] ${file}: unsupported format, expected .xlf, .xliff, .csv or .po.`);
|
|
1294
1670
|
}
|
|
1295
1671
|
function toXliff(sourceLocale, locale, entries) {
|
|
1296
1672
|
const units = entries.map(({ key, source, target, location }) => [
|
|
@@ -1301,8 +1677,8 @@ function toXliff(sourceLocale, locale, entries) {
|
|
|
1301
1677
|
" </notes>"
|
|
1302
1678
|
] : [],
|
|
1303
1679
|
` <segment state="${target ? "translated" : "initial"}">`,
|
|
1304
|
-
` <source>${
|
|
1305
|
-
` <target>${
|
|
1680
|
+
` <source>${messageToInline(source)}</source>`,
|
|
1681
|
+
` <target>${messageToInline(target)}</target>`,
|
|
1306
1682
|
" </segment>",
|
|
1307
1683
|
" </unit>"
|
|
1308
1684
|
].join("\n")).join("\n");
|
|
@@ -1323,30 +1699,16 @@ function parseXliff(content) {
|
|
|
1323
1699
|
const id = /\bid\s*=\s*"([^"]*)"/.exec(match[1])?.[1];
|
|
1324
1700
|
if (!id) continue;
|
|
1325
1701
|
const target = /<target\b[^>]*>([\s\S]*?)<\/target>/.exec(match[2])?.[1];
|
|
1326
|
-
entries[unescapeXml(id)] = target === void 0 ? "" :
|
|
1702
|
+
entries[unescapeXml(id)] = target === void 0 ? "" : inlineToMessage(stripCdata(target));
|
|
1327
1703
|
}
|
|
1328
1704
|
return {
|
|
1329
1705
|
locale,
|
|
1330
1706
|
entries
|
|
1331
1707
|
};
|
|
1332
1708
|
}
|
|
1333
|
-
function escapeXml(text) {
|
|
1334
|
-
return text.replace(/&/g, "&").replace(/</g, "<").replace(/>/g, ">").replace(/"/g, """);
|
|
1335
|
-
}
|
|
1336
1709
|
function stripCdata(text) {
|
|
1337
1710
|
return text.replace(/<!\[CDATA\[([\s\S]*?)\]\]>/g, "$1");
|
|
1338
1711
|
}
|
|
1339
|
-
function unescapeXml(text) {
|
|
1340
|
-
return text.replace(/&(lt|gt|amp|quot|apos|#x?[0-9a-fA-F]+);/g, (_, entity) => {
|
|
1341
|
-
if (entity === "lt") return "<";
|
|
1342
|
-
if (entity === "gt") return ">";
|
|
1343
|
-
if (entity === "amp") return "&";
|
|
1344
|
-
if (entity === "quot") return "\"";
|
|
1345
|
-
if (entity === "apos") return "'";
|
|
1346
|
-
const code = entity.startsWith("#x") ? parseInt(entity.slice(2), 16) : parseInt(entity.slice(1), 10);
|
|
1347
|
-
return String.fromCodePoint(code);
|
|
1348
|
-
});
|
|
1349
|
-
}
|
|
1350
1712
|
function toCsv(entries) {
|
|
1351
1713
|
return [
|
|
1352
1714
|
"key,source,target,location",
|
|
@@ -1519,7 +1881,8 @@ async function renderSite(cfg, options = {}) {
|
|
|
1519
1881
|
const attribute = options.attribute ?? cfg.render.attribute;
|
|
1520
1882
|
const baseUrl = (options.baseUrl ?? cfg.render.baseUrl)?.replace(/\/+$/, "");
|
|
1521
1883
|
const wantHreflang = (options.hreflang ?? cfg.render.hreflang ?? true) && baseUrl !== void 0;
|
|
1522
|
-
const
|
|
1884
|
+
const sitemap = options.sitemap ?? cfg.render.sitemap ?? false;
|
|
1885
|
+
const wantSitemap = sitemap !== false && baseUrl !== void 0;
|
|
1523
1886
|
const clean = options.clean ?? cfg.render.clean ?? false;
|
|
1524
1887
|
const catalogs = loadCatalogs(cfg);
|
|
1525
1888
|
if (clean) {
|
|
@@ -1562,7 +1925,7 @@ async function renderSite(cfg, options = {}) {
|
|
|
1562
1925
|
writeFileSync(out, result.html);
|
|
1563
1926
|
}
|
|
1564
1927
|
}
|
|
1565
|
-
if (wantSitemap && urls.length) writeFileSync(join(site, typeof
|
|
1928
|
+
if (wantSitemap && urls.length) writeFileSync(join(site, typeof sitemap === "string" ? sitemap : "sitemap-i18n.xml"), buildSitemap(urls));
|
|
1566
1929
|
return {
|
|
1567
1930
|
files: files.length,
|
|
1568
1931
|
locales: [...locales],
|
|
@@ -1670,9 +2033,10 @@ function decodeEntities(text) {
|
|
|
1670
2033
|
}
|
|
1671
2034
|
//#endregion
|
|
1672
2035
|
//#region src/status.ts
|
|
1673
|
-
function status(cfg, catalogs, registry) {
|
|
2036
|
+
function status(cfg, catalogs, registry, drafts = {}) {
|
|
1674
2037
|
const source = catalogs[cfg.sourceLocale] ?? {};
|
|
1675
2038
|
const needed = /* @__PURE__ */ new Set([...registry.messages().keys(), ...Object.keys(source)]);
|
|
2039
|
+
const live = effectiveDrafts(drafts, catalogs);
|
|
1676
2040
|
const locales = [];
|
|
1677
2041
|
for (const locale of cfg.locales) {
|
|
1678
2042
|
if (locale === cfg.sourceLocale) continue;
|
|
@@ -1681,7 +2045,8 @@ function status(cfg, catalogs, registry) {
|
|
|
1681
2045
|
locales.push({
|
|
1682
2046
|
locale,
|
|
1683
2047
|
translated,
|
|
1684
|
-
total: needed.size
|
|
2048
|
+
total: needed.size,
|
|
2049
|
+
drafts: live[locale]?.length ?? 0
|
|
1685
2050
|
});
|
|
1686
2051
|
}
|
|
1687
2052
|
return {
|
|
@@ -1696,10 +2061,11 @@ function formatStatusResult(result) {
|
|
|
1696
2061
|
lines.push(" no target locales (add locales to your config)");
|
|
1697
2062
|
return lines.join("\n");
|
|
1698
2063
|
}
|
|
1699
|
-
for (const { locale, translated, total } of result.locales) {
|
|
2064
|
+
for (const { locale, translated, total, drafts } of result.locales) {
|
|
1700
2065
|
const pct = total === 0 ? 100 : Math.floor(translated / total * 100);
|
|
1701
2066
|
const mark = translated === total ? " ✓" : "";
|
|
1702
|
-
|
|
2067
|
+
const draftNote = drafts > 0 ? `, ${drafts} unreviewed` : "";
|
|
2068
|
+
lines.push(` ${locale}: ${translated}/${total} translated (${pct}%${draftNote})${mark}`);
|
|
1703
2069
|
}
|
|
1704
2070
|
return lines.join("\n");
|
|
1705
2071
|
}
|
|
@@ -1969,8 +2335,9 @@ Usage:
|
|
|
1969
2335
|
verbaly status translation coverage per locale, at a glance
|
|
1970
2336
|
verbaly check verify translations are complete (CI)
|
|
1971
2337
|
verbaly translate fill missing translations via a provider (default: claude)
|
|
1972
|
-
verbaly
|
|
1973
|
-
verbaly
|
|
2338
|
+
verbaly review list machine translations awaiting review (--approve marks them reviewed)
|
|
2339
|
+
verbaly export write translator files (XLIFF 2.0, CSV, gettext PO) or mobile resources (Android, iOS)
|
|
2340
|
+
verbaly import <files…> fill catalogs back from translated XLIFF/CSV/PO files
|
|
1974
2341
|
verbaly pseudo generate a pseudo-locale catalog for i18n QA (default: en-XA)
|
|
1975
2342
|
verbaly render pre-fill data-verbaly HTML per locale (SSG, kills the FOUC)
|
|
1976
2343
|
|
|
@@ -1983,10 +2350,12 @@ Options:
|
|
|
1983
2350
|
--watch keep extracting as source files change (extract)
|
|
1984
2351
|
--write apply the rewrites instead of only reporting (wrap)
|
|
1985
2352
|
--json machine-readable output (status)
|
|
2353
|
+
--drafts also fail on unreviewed machine translations (check)
|
|
2354
|
+
--approve mark listed drafts as reviewed (review)
|
|
1986
2355
|
--reporter <name> failure format: text (default) or github annotations (check)
|
|
1987
2356
|
--model <id> model override for the claude provider (translate)
|
|
1988
2357
|
--dry-run list what would happen, write nothing (translate, import, extract)
|
|
1989
|
-
--format <f> export format: xliff (default), csv, android-xml or ios-strings (export)
|
|
2358
|
+
--format <f> export format: xliff (default), csv, po, android-xml or ios-strings (export)
|
|
1990
2359
|
--out <path> export directory (export, default: verbaly-export)
|
|
1991
2360
|
--missing export only untranslated entries (export)
|
|
1992
2361
|
--overwrite replace existing translations on import (import)
|
|
@@ -2013,6 +2382,8 @@ async function runCli(args = process.argv.slice(2)) {
|
|
|
2013
2382
|
watch: { type: "boolean" },
|
|
2014
2383
|
write: { type: "boolean" },
|
|
2015
2384
|
json: { type: "boolean" },
|
|
2385
|
+
drafts: { type: "boolean" },
|
|
2386
|
+
approve: { type: "boolean" },
|
|
2016
2387
|
reporter: { type: "string" },
|
|
2017
2388
|
model: { type: "string" },
|
|
2018
2389
|
locale: { type: "string" },
|
|
@@ -2131,7 +2502,7 @@ async function runCli(args = process.argv.slice(2)) {
|
|
|
2131
2502
|
}
|
|
2132
2503
|
if (command === "status") {
|
|
2133
2504
|
const registry = await extractProject(cfg);
|
|
2134
|
-
const result = status(cfg, loadCatalogs(cfg), registry);
|
|
2505
|
+
const result = status(cfg, loadCatalogs(cfg), registry, loadDrafts(cfg));
|
|
2135
2506
|
console.log(values.json ? JSON.stringify(result) : formatStatusResult(result));
|
|
2136
2507
|
return;
|
|
2137
2508
|
}
|
|
@@ -2143,11 +2514,20 @@ async function runCli(args = process.argv.slice(2)) {
|
|
|
2143
2514
|
return;
|
|
2144
2515
|
}
|
|
2145
2516
|
const registry = await extractProject(cfg);
|
|
2146
|
-
const
|
|
2147
|
-
|
|
2517
|
+
const catalogs = loadCatalogs(cfg);
|
|
2518
|
+
const result = check(cfg, catalogs, registry);
|
|
2519
|
+
const unreviewed = values.drafts ? effectiveDrafts(loadDrafts(cfg), catalogs) : {};
|
|
2520
|
+
const draftKeys = Object.entries(unreviewed);
|
|
2521
|
+
if (result.ok && draftKeys.length === 0) {
|
|
2148
2522
|
console.log("[verbaly] all translations complete ✓");
|
|
2149
2523
|
return;
|
|
2150
2524
|
}
|
|
2525
|
+
if (result.ok && draftKeys.length > 0) {
|
|
2526
|
+
for (const [locale, keys] of draftKeys) console.error(` [${locale}] ${keys.length} unreviewed: ${keys.join(", ")}`);
|
|
2527
|
+
console.error("[verbaly] check failed: machine translations awaiting review (run verbaly review --approve)");
|
|
2528
|
+
process.exitCode = 1;
|
|
2529
|
+
return;
|
|
2530
|
+
}
|
|
2151
2531
|
if (reporter === "github") {
|
|
2152
2532
|
for (const line of githubCheckAnnotations(result, registry, cfg.root)) console.error(line);
|
|
2153
2533
|
console.error(`[verbaly] check failed: ${result.missing.length} missing, ${result.unknown.length} unknown`);
|
|
@@ -2160,7 +2540,8 @@ async function runCli(args = process.argv.slice(2)) {
|
|
|
2160
2540
|
const result = await translateCatalogs(cfg, catalogs, await resolveProvider(cfg, values.model), {
|
|
2161
2541
|
locales: values.locales?.split(","),
|
|
2162
2542
|
batchSize: cfg.translate.batchSize,
|
|
2163
|
-
dryRun: values["dry-run"]
|
|
2543
|
+
dryRun: values["dry-run"],
|
|
2544
|
+
origins: values["dry-run"] ? void 0 : await collectOrigins(cfg)
|
|
2164
2545
|
});
|
|
2165
2546
|
if (values["dry-run"]) {
|
|
2166
2547
|
const entries = Object.entries(result.pending);
|
|
@@ -2171,23 +2552,53 @@ async function runCli(args = process.argv.slice(2)) {
|
|
|
2171
2552
|
for (const [locale, keys] of entries) console.log(` ${locale}: ${keys.length} missing: ${keys.join(", ")}`);
|
|
2172
2553
|
return;
|
|
2173
2554
|
}
|
|
2555
|
+
const drafts = loadDrafts(cfg);
|
|
2174
2556
|
for (const locale of Object.keys(result.translated)) {
|
|
2175
2557
|
writeCatalog(cfg, locale, catalogs[locale] ?? {});
|
|
2176
|
-
|
|
2558
|
+
markDrafts(drafts, locale, result.translated[locale]);
|
|
2559
|
+
console.log(` ${locale}: +${result.translated[locale].length} translated (draft)`);
|
|
2177
2560
|
}
|
|
2561
|
+
if (Object.keys(result.translated).length > 0) saveDrafts(cfg, drafts);
|
|
2178
2562
|
for (const [locale, keys] of Object.entries(result.invalid)) console.warn(` ${locale}: ${keys.length} rejected (params/tags not preserved): ${keys.join(", ")}`);
|
|
2179
2563
|
if (Object.keys(result.translated).length === 0 && Object.keys(result.invalid).length === 0) console.log("[verbaly] nothing to translate ✓");
|
|
2180
2564
|
return;
|
|
2181
2565
|
}
|
|
2566
|
+
if (command === "review") {
|
|
2567
|
+
const catalogs = loadCatalogs(cfg);
|
|
2568
|
+
const drafts = loadDrafts(cfg);
|
|
2569
|
+
const live = effectiveDrafts(drafts, catalogs);
|
|
2570
|
+
const targets = values.locale ? { [values.locale]: live[values.locale] ?? [] } : live;
|
|
2571
|
+
const entries = Object.entries(targets).filter(([, keys]) => keys.length);
|
|
2572
|
+
if (entries.length === 0) {
|
|
2573
|
+
console.log("[verbaly] no machine translations awaiting review ✓");
|
|
2574
|
+
return;
|
|
2575
|
+
}
|
|
2576
|
+
if (values.approve) {
|
|
2577
|
+
let count = 0;
|
|
2578
|
+
for (const [locale, keys] of entries) {
|
|
2579
|
+
clearDrafts(drafts, locale, keys);
|
|
2580
|
+
count += keys.length;
|
|
2581
|
+
console.log(` ${locale}: ${keys.length} approved`);
|
|
2582
|
+
}
|
|
2583
|
+
saveDrafts(cfg, drafts);
|
|
2584
|
+
console.log(`[verbaly] ${count} translations marked reviewed ✓`);
|
|
2585
|
+
return;
|
|
2586
|
+
}
|
|
2587
|
+
const total = entries.reduce((sum, [, keys]) => sum + keys.length, 0);
|
|
2588
|
+
console.log(`[verbaly] ${total} machine translations awaiting review (--approve to accept)`);
|
|
2589
|
+
for (const [locale, keys] of entries) console.log(` ${locale}: ${keys.join(", ")}`);
|
|
2590
|
+
return;
|
|
2591
|
+
}
|
|
2182
2592
|
if (command === "export") {
|
|
2183
2593
|
const format = values.format ?? "xliff";
|
|
2184
2594
|
if (![
|
|
2185
2595
|
"xliff",
|
|
2186
2596
|
"csv",
|
|
2597
|
+
"po",
|
|
2187
2598
|
"android-xml",
|
|
2188
2599
|
"ios-strings"
|
|
2189
2600
|
].includes(format)) {
|
|
2190
|
-
console.error(`[verbaly] unknown format "${values.format}", use xliff, csv, android-xml or ios-strings`);
|
|
2601
|
+
console.error(`[verbaly] unknown format "${values.format}", use xliff, csv, po, android-xml or ios-strings`);
|
|
2191
2602
|
process.exitCode = 1;
|
|
2192
2603
|
return;
|
|
2193
2604
|
}
|
|
@@ -2225,11 +2636,18 @@ async function runCli(args = process.argv.slice(2)) {
|
|
|
2225
2636
|
overwrite: values.overwrite,
|
|
2226
2637
|
dryRun: values["dry-run"]
|
|
2227
2638
|
});
|
|
2639
|
+
const drafts = loadDrafts(cfg);
|
|
2640
|
+
let draftsChanged = false;
|
|
2228
2641
|
for (const [locale, keys] of Object.entries(result.imported)) {
|
|
2229
|
-
if (!values["dry-run"])
|
|
2642
|
+
if (!values["dry-run"]) {
|
|
2643
|
+
writeCatalog(cfg, locale, catalogs[locale] ?? {});
|
|
2644
|
+
clearDrafts(drafts, locale, keys);
|
|
2645
|
+
draftsChanged = true;
|
|
2646
|
+
}
|
|
2230
2647
|
const verb = values["dry-run"] ? "would import" : "imported";
|
|
2231
2648
|
console.log(` ${locale}: +${keys.length} ${verb}`);
|
|
2232
2649
|
}
|
|
2650
|
+
if (draftsChanged) saveDrafts(cfg, drafts);
|
|
2233
2651
|
for (const [locale, keys] of Object.entries(result.skipped)) console.log(` ${locale}: ${keys.length} already translated, kept (use --overwrite to replace)`);
|
|
2234
2652
|
for (const [locale, keys] of Object.entries(result.rejected)) console.warn(` ${locale}: ${keys.length} rejected (params/tags not preserved): ${keys.join(", ")}`);
|
|
2235
2653
|
for (const [locale, keys] of Object.entries(result.unknown)) console.warn(` ${locale}: ${keys.length} unknown keys ignored (not in the source catalog): ${keys.join(", ")}`);
|
|
@@ -2277,8 +2695,9 @@ const COMMAND_FLAGS = {
|
|
|
2277
2695
|
],
|
|
2278
2696
|
wrap: ["write"],
|
|
2279
2697
|
status: ["json"],
|
|
2280
|
-
check: ["reporter"],
|
|
2698
|
+
check: ["reporter", "drafts"],
|
|
2281
2699
|
translate: ["model", "dry-run"],
|
|
2700
|
+
review: ["approve", "locale"],
|
|
2282
2701
|
export: [
|
|
2283
2702
|
"format",
|
|
2284
2703
|
"out",
|
|
@@ -2317,7 +2736,7 @@ async function collectOrigins(cfg) {
|
|
|
2317
2736
|
async function resolveProvider(cfg, model) {
|
|
2318
2737
|
const configured = cfg.translate.provider;
|
|
2319
2738
|
if (typeof configured === "function") return configured;
|
|
2320
|
-
const { claudeProvider } = await import("./claude-
|
|
2739
|
+
const { claudeProvider } = await import("./claude-h9WA0ppg.js");
|
|
2321
2740
|
return claudeProvider({ model: model ?? cfg.translate.model });
|
|
2322
2741
|
}
|
|
2323
2742
|
//#endregion
|