@verbaly/compiler 0.21.0 → 0.23.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 +27 -14
- package/dist/cli.js +214 -24
- package/dist/cli.js.map +1 -1
- package/dist/index.d.ts +26 -3
- package/dist/index.js +160 -5
- package/dist/index.js.map +1 -1
- package/package.json +2 -2
package/dist/index.d.ts
CHANGED
|
@@ -189,9 +189,12 @@ declare function doctor(cfg: ResolvedConfig): Promise<DoctorResult>;
|
|
|
189
189
|
//#endregion
|
|
190
190
|
//#region src/exchange.d.ts
|
|
191
191
|
type ExchangeFormat = 'xliff' | 'csv';
|
|
192
|
+
type MobileFormat = 'android-xml' | 'ios-strings';
|
|
193
|
+
type ExportFormat = ExchangeFormat | MobileFormat;
|
|
194
|
+
declare function isMobileFormat(format: ExportFormat): format is MobileFormat;
|
|
192
195
|
interface ExportOptions {
|
|
193
196
|
locales?: string[];
|
|
194
|
-
format?:
|
|
197
|
+
format?: ExportFormat;
|
|
195
198
|
out?: string;
|
|
196
199
|
missing?: boolean;
|
|
197
200
|
}
|
|
@@ -202,7 +205,7 @@ interface ExportedFile {
|
|
|
202
205
|
untranslated: number;
|
|
203
206
|
}
|
|
204
207
|
interface ExportResult {
|
|
205
|
-
format:
|
|
208
|
+
format: ExportFormat;
|
|
206
209
|
dir: string;
|
|
207
210
|
files: ExportedFile[];
|
|
208
211
|
}
|
|
@@ -309,5 +312,25 @@ interface RenderSiteResult {
|
|
|
309
312
|
}
|
|
310
313
|
declare function renderSite(cfg: ResolvedConfig, options?: RenderSiteOptions): Promise<RenderSiteResult>;
|
|
311
314
|
//#endregion
|
|
312
|
-
|
|
315
|
+
//#region src/status.d.ts
|
|
316
|
+
interface LocaleStatus {
|
|
317
|
+
locale: string;
|
|
318
|
+
translated: number;
|
|
319
|
+
total: number;
|
|
320
|
+
}
|
|
321
|
+
interface StatusResult {
|
|
322
|
+
messages: number;
|
|
323
|
+
source: string;
|
|
324
|
+
locales: LocaleStatus[];
|
|
325
|
+
}
|
|
326
|
+
declare function status(cfg: ResolvedConfig, catalogs: Catalogs, registry: MessageRegistry): StatusResult;
|
|
327
|
+
declare function formatStatusResult(result: StatusResult): string;
|
|
328
|
+
//#endregion
|
|
329
|
+
//#region src/watch.d.ts
|
|
330
|
+
interface WatchProjectOptions {
|
|
331
|
+
debounce?: number;
|
|
332
|
+
}
|
|
333
|
+
declare function watchProject(cfg: ResolvedConfig, run: () => Promise<void>, options?: WatchProjectOptions): () => void;
|
|
334
|
+
//#endregion
|
|
335
|
+
export { type Alternate, type Analysis, type AnalyzeOptions, type Catalog, type Catalogs, type CheckResult, type ClaudeProviderOptions, type DoctorEntry, type DoctorResult, type ExchangeFormat, type ExportFormat, type ExportOptions, type ExportResult, type ExportedFile, type ImportOptions, type ImportResult, type InitOptions, type InitResult, LOCALE_MODULE_PREFIX, type LocaleStatus, MessageRegistry, type MissingEntry, type MobileFormat, PSEUDO_LOCALE, type ParamType, type PluginOptions, RESOLVED_VIRTUAL_ID, type RenderConfig, type RenderHtmlOptions, type RenderHtmlResult, type RenderSiteOptions, type RenderSiteResult, type ResolvedConfig, type RuntimeModuleOptions, SFC_FILE_RE, SOURCE_FILE_RE, type StatusResult, type SyncResult, type TaggedMessage, type TaggedParam, type TransComponent, type TransformResult, type TranslateConfig, type TranslateOptions, type TranslateProvider, type TranslateRequest, type TranslateResult, type UnknownEntry, type UsedKey, VIRTUAL_ID, type VerbalyConfig, type WatchProjectOptions, analyze, analyzeFile, analyzeSfc, catalogPath, check, claudeProvider, collectParams, detectBundler, doctor, exportCatalogs, extractProject, findConfigFile, formatCheckResult, formatStatusResult, generateDts, generateLocaleModule, generateRuntimeModule, importCatalogs, init, isMobileFormat, isTransformTarget, loadCatalogs, loadConfig, loadConfigFile, loadVirtualModule, parseExchangeFile, pruneCatalogs, pseudoCatalogs, pseudoLocalize, readCatalog, renderHtml, renderParamType, renderSite, resolveConfig, resolveVirtualId, runBuildGate, serializeCatalog, stableKey, status, structureMatches, syncCatalogs, targetLocales, transformCode, transformSource, translateCatalogs, watchProject, writeCatalog, writeDts };
|
|
313
336
|
//# sourceMappingURL=index.d.ts.map
|
package/dist/index.js
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
import { parse } from "@babel/parser";
|
|
2
2
|
import { createHash } from "node:crypto";
|
|
3
|
-
import { existsSync, mkdirSync, readFileSync, readdirSync, rmSync, writeFileSync } from "node:fs";
|
|
3
|
+
import { existsSync, mkdirSync, readFileSync, readdirSync, rmSync, watch, writeFileSync } from "node:fs";
|
|
4
4
|
import { basename, dirname, join, relative, resolve } from "node:path";
|
|
5
5
|
import { RICH_TAGS, createVerbaly, parse as parse$1, parseTags, safeHref } from "verbaly";
|
|
6
6
|
import { pathToFileURL } from "node:url";
|
|
@@ -382,11 +382,17 @@ function catalogPath(cfg, locale) {
|
|
|
382
382
|
return join(cfg.dir, `${locale}.json`);
|
|
383
383
|
}
|
|
384
384
|
function readCatalog(cfg, locale) {
|
|
385
|
+
let content;
|
|
385
386
|
try {
|
|
386
|
-
|
|
387
|
+
content = readFileSync(catalogPath(cfg, locale), "utf8");
|
|
387
388
|
} catch {
|
|
388
389
|
return {};
|
|
389
390
|
}
|
|
391
|
+
try {
|
|
392
|
+
return JSON.parse(content.replace(/^\uFEFF/, ""));
|
|
393
|
+
} catch (error) {
|
|
394
|
+
throw new Error(`[verbaly] ${catalogPath(cfg, locale)} is not valid JSON, fix or delete the file`, { cause: error });
|
|
395
|
+
}
|
|
390
396
|
}
|
|
391
397
|
function loadCatalogs(cfg) {
|
|
392
398
|
const catalogs = {};
|
|
@@ -403,8 +409,13 @@ function serializeCatalog(catalog) {
|
|
|
403
409
|
}
|
|
404
410
|
function writeCatalog(cfg, locale, catalog) {
|
|
405
411
|
const serialized = serializeCatalog(catalog);
|
|
406
|
-
|
|
407
|
-
|
|
412
|
+
const path = catalogPath(cfg, locale);
|
|
413
|
+
try {
|
|
414
|
+
if (readFileSync(path, "utf8") === serialized) return serialized;
|
|
415
|
+
} catch {
|
|
416
|
+
mkdirSync(cfg.dir, { recursive: true });
|
|
417
|
+
}
|
|
418
|
+
writeFileSync(path, serialized);
|
|
408
419
|
return serialized;
|
|
409
420
|
}
|
|
410
421
|
//#endregion
|
|
@@ -1124,6 +1135,45 @@ function readDeps(root) {
|
|
|
1124
1135
|
}
|
|
1125
1136
|
}
|
|
1126
1137
|
//#endregion
|
|
1138
|
+
//#region src/mobile.ts
|
|
1139
|
+
function androidValuesDir(locale, sourceLocale) {
|
|
1140
|
+
if (locale === sourceLocale) return "values";
|
|
1141
|
+
const parts = locale.split("-");
|
|
1142
|
+
if (parts.length === 1) return `values-${locale}`;
|
|
1143
|
+
if (parts.length === 2 && /^[a-zA-Z]{2}$/.test(parts[1])) return `values-${parts[0]}-r${parts[1].toUpperCase()}`;
|
|
1144
|
+
return `values-b+${parts.join("+")}`;
|
|
1145
|
+
}
|
|
1146
|
+
function toAndroidXml(entries) {
|
|
1147
|
+
const names = /* @__PURE__ */ new Map();
|
|
1148
|
+
return [
|
|
1149
|
+
"<?xml version=\"1.0\" encoding=\"UTF-8\"?>",
|
|
1150
|
+
"<resources>",
|
|
1151
|
+
...entries.map(({ key, text }) => {
|
|
1152
|
+
const name = androidName(key);
|
|
1153
|
+
const clash = names.get(name);
|
|
1154
|
+
if (clash !== void 0) throw new Error(`[verbaly] android-xml: keys "${clash}" and "${key}" both become resource name "${name}", rename one of them.`);
|
|
1155
|
+
names.set(name, key);
|
|
1156
|
+
return ` <string name="${name}">${androidText(text)}</string>`;
|
|
1157
|
+
}),
|
|
1158
|
+
"</resources>",
|
|
1159
|
+
""
|
|
1160
|
+
].join("\n");
|
|
1161
|
+
}
|
|
1162
|
+
function toIosStrings(entries) {
|
|
1163
|
+
return [...entries.map(({ key, text }) => `"${iosText(key)}" = "${iosText(text)}";`), ""].join("\n");
|
|
1164
|
+
}
|
|
1165
|
+
function androidName(key) {
|
|
1166
|
+
const name = key.replace(/[^A-Za-z0-9_]/g, "_");
|
|
1167
|
+
return /^[0-9]/.test(name) ? `_${name}` : name;
|
|
1168
|
+
}
|
|
1169
|
+
function androidText(text) {
|
|
1170
|
+
const escaped = text.replace(/\\/g, "\\\\").replace(/'/g, "\\'").replace(/"/g, "\\\"").replace(/\n/g, "\\n").replace(/\t/g, "\\t").replace(/&/g, "&").replace(/</g, "<").replace(/>/g, ">");
|
|
1171
|
+
return /^[@?]/.test(escaped) ? `\\${escaped}` : escaped;
|
|
1172
|
+
}
|
|
1173
|
+
function iosText(text) {
|
|
1174
|
+
return text.replace(/\\/g, "\\\\").replace(/"/g, "\\\"").replace(/\n/g, "\\n").replace(/\t/g, "\\t");
|
|
1175
|
+
}
|
|
1176
|
+
//#endregion
|
|
1127
1177
|
//#region src/translate.ts
|
|
1128
1178
|
async function translateCatalogs(cfg, catalogs, provider, options = {}) {
|
|
1129
1179
|
const batchSize = options.batchSize ?? 20;
|
|
@@ -1182,11 +1232,15 @@ function sameMembers(a, b) {
|
|
|
1182
1232
|
}
|
|
1183
1233
|
//#endregion
|
|
1184
1234
|
//#region src/exchange.ts
|
|
1235
|
+
function isMobileFormat(format) {
|
|
1236
|
+
return format === "android-xml" || format === "ios-strings";
|
|
1237
|
+
}
|
|
1185
1238
|
function exportCatalogs(cfg, catalogs, options = {}) {
|
|
1186
1239
|
const format = options.format ?? "xliff";
|
|
1187
1240
|
const dir = resolve(cfg.root, options.out ?? "verbaly-export");
|
|
1188
1241
|
const source = catalogs[cfg.sourceLocale] ?? {};
|
|
1189
1242
|
const targets = targetLocales(cfg, options.locales);
|
|
1243
|
+
if (isMobileFormat(format)) return exportMobile(cfg, catalogs, format, dir, source, targets);
|
|
1190
1244
|
const files = [];
|
|
1191
1245
|
mkdirSync(dir, { recursive: true });
|
|
1192
1246
|
for (const locale of targets) {
|
|
@@ -1213,6 +1267,31 @@ function exportCatalogs(cfg, catalogs, options = {}) {
|
|
|
1213
1267
|
files
|
|
1214
1268
|
};
|
|
1215
1269
|
}
|
|
1270
|
+
function exportMobile(cfg, catalogs, format, dir, source, targets) {
|
|
1271
|
+
const keys = Object.keys(source).filter((key) => source[key]).sort();
|
|
1272
|
+
const files = [];
|
|
1273
|
+
for (const locale of [cfg.sourceLocale, ...targets]) {
|
|
1274
|
+
const catalog = locale === cfg.sourceLocale ? source : catalogs[locale] ?? {};
|
|
1275
|
+
const entries = keys.map((key) => ({
|
|
1276
|
+
key,
|
|
1277
|
+
text: catalog[key] ?? ""
|
|
1278
|
+
})).filter((entry) => entry.text);
|
|
1279
|
+
const path = join(dir, format === "android-xml" ? join(androidValuesDir(locale, cfg.sourceLocale), "strings.xml") : join(`${locale}.lproj`, "Localizable.strings"));
|
|
1280
|
+
mkdirSync(dirname(path), { recursive: true });
|
|
1281
|
+
writeFileSync(path, format === "android-xml" ? toAndroidXml(entries) : toIosStrings(entries));
|
|
1282
|
+
files.push({
|
|
1283
|
+
locale,
|
|
1284
|
+
path,
|
|
1285
|
+
total: entries.length,
|
|
1286
|
+
untranslated: keys.length - entries.length
|
|
1287
|
+
});
|
|
1288
|
+
}
|
|
1289
|
+
return {
|
|
1290
|
+
format,
|
|
1291
|
+
dir,
|
|
1292
|
+
files
|
|
1293
|
+
};
|
|
1294
|
+
}
|
|
1216
1295
|
function importCatalogs(cfg, catalogs, files, options = {}) {
|
|
1217
1296
|
const source = catalogs[cfg.sourceLocale] ?? {};
|
|
1218
1297
|
const result = {
|
|
@@ -1683,6 +1762,82 @@ function decodeEntities(text) {
|
|
|
1683
1762
|
return text.replace(/"/g, "\"").replace(/'/g, "'").replace(/</g, "<").replace(/>/g, ">").replace(/&/g, "&");
|
|
1684
1763
|
}
|
|
1685
1764
|
//#endregion
|
|
1686
|
-
|
|
1765
|
+
//#region src/status.ts
|
|
1766
|
+
function status(cfg, catalogs, registry) {
|
|
1767
|
+
const source = catalogs[cfg.sourceLocale] ?? {};
|
|
1768
|
+
const needed = /* @__PURE__ */ new Set([...registry.messages().keys(), ...Object.keys(source)]);
|
|
1769
|
+
const locales = [];
|
|
1770
|
+
for (const locale of cfg.locales) {
|
|
1771
|
+
if (locale === cfg.sourceLocale) continue;
|
|
1772
|
+
let translated = 0;
|
|
1773
|
+
for (const key of needed) if (catalogs[locale]?.[key]) translated += 1;
|
|
1774
|
+
locales.push({
|
|
1775
|
+
locale,
|
|
1776
|
+
translated,
|
|
1777
|
+
total: needed.size
|
|
1778
|
+
});
|
|
1779
|
+
}
|
|
1780
|
+
return {
|
|
1781
|
+
messages: needed.size,
|
|
1782
|
+
source: cfg.sourceLocale,
|
|
1783
|
+
locales
|
|
1784
|
+
};
|
|
1785
|
+
}
|
|
1786
|
+
function formatStatusResult(result) {
|
|
1787
|
+
const lines = [`[verbaly] ${result.messages} messages · source: ${result.source}`];
|
|
1788
|
+
if (result.locales.length === 0) {
|
|
1789
|
+
lines.push(" no target locales (add locales to your config)");
|
|
1790
|
+
return lines.join("\n");
|
|
1791
|
+
}
|
|
1792
|
+
for (const { locale, translated, total } of result.locales) {
|
|
1793
|
+
const pct = total === 0 ? 100 : Math.floor(translated / total * 100);
|
|
1794
|
+
const mark = translated === total ? " ✓" : "";
|
|
1795
|
+
lines.push(` ${locale}: ${translated}/${total} translated (${pct}%)${mark}`);
|
|
1796
|
+
}
|
|
1797
|
+
return lines.join("\n");
|
|
1798
|
+
}
|
|
1799
|
+
//#endregion
|
|
1800
|
+
//#region src/watch.ts
|
|
1801
|
+
function watchProject(cfg, run, options = {}) {
|
|
1802
|
+
const catalogDir = relative(cfg.root, cfg.dir).replaceAll("\\", "/");
|
|
1803
|
+
let timer;
|
|
1804
|
+
let running = false;
|
|
1805
|
+
let queued = false;
|
|
1806
|
+
async function refresh() {
|
|
1807
|
+
if (running) {
|
|
1808
|
+
queued = true;
|
|
1809
|
+
return;
|
|
1810
|
+
}
|
|
1811
|
+
running = true;
|
|
1812
|
+
try {
|
|
1813
|
+
await run();
|
|
1814
|
+
} catch (error) {
|
|
1815
|
+
console.warn("[verbaly] watch run failed:", error);
|
|
1816
|
+
} finally {
|
|
1817
|
+
running = false;
|
|
1818
|
+
if (queued) {
|
|
1819
|
+
queued = false;
|
|
1820
|
+
schedule();
|
|
1821
|
+
}
|
|
1822
|
+
}
|
|
1823
|
+
}
|
|
1824
|
+
function schedule() {
|
|
1825
|
+
clearTimeout(timer);
|
|
1826
|
+
timer = setTimeout(() => void refresh(), options.debounce ?? 150);
|
|
1827
|
+
}
|
|
1828
|
+
const watcher = watch(cfg.root, { recursive: true }, (_event, filename) => {
|
|
1829
|
+
if (!filename) return;
|
|
1830
|
+
const file = filename.replaceAll("\\", "/");
|
|
1831
|
+
if (file.includes("node_modules/") || file.endsWith(".d.ts")) return;
|
|
1832
|
+
if (catalogDir && file.startsWith(`${catalogDir}/`)) return;
|
|
1833
|
+
if (SOURCE_FILE_RE.test(file)) schedule();
|
|
1834
|
+
});
|
|
1835
|
+
return () => {
|
|
1836
|
+
clearTimeout(timer);
|
|
1837
|
+
watcher.close();
|
|
1838
|
+
};
|
|
1839
|
+
}
|
|
1840
|
+
//#endregion
|
|
1841
|
+
export { LOCALE_MODULE_PREFIX, MessageRegistry, PSEUDO_LOCALE, RESOLVED_VIRTUAL_ID, SFC_FILE_RE, SOURCE_FILE_RE, VIRTUAL_ID, analyze, analyzeFile, analyzeSfc, catalogPath, check, claudeProvider, collectParams, detectBundler, doctor, exportCatalogs, extractProject, findConfigFile, formatCheckResult, formatStatusResult, generateDts, generateLocaleModule, generateRuntimeModule, importCatalogs, init, isMobileFormat, isTransformTarget, loadCatalogs, loadConfig, loadConfigFile, loadVirtualModule, parseExchangeFile, pruneCatalogs, pseudoCatalogs, pseudoLocalize, readCatalog, renderHtml, renderParamType, renderSite, resolveConfig, resolveVirtualId, runBuildGate, serializeCatalog, stableKey, status, structureMatches, syncCatalogs, targetLocales, transformCode, transformSource, translateCatalogs, watchProject, writeCatalog, writeDts };
|
|
1687
1842
|
|
|
1688
1843
|
//# sourceMappingURL=index.js.map
|