@verbaly/compiler 0.8.0 → 0.10.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 +19 -0
- package/dist/claude-RQATA6OI.js +63 -0
- package/dist/claude-RQATA6OI.js.map +1 -0
- package/dist/cli.js +472 -7
- package/dist/cli.js.map +1 -1
- package/dist/index.d.ts +64 -1
- package/dist/index.js +462 -3
- package/dist/index.js.map +1 -1
- package/package.json +7 -2
package/dist/index.js
CHANGED
|
@@ -471,7 +471,8 @@ function resolveConfig(config = {}) {
|
|
|
471
471
|
sourceLocale,
|
|
472
472
|
locales: [...locales],
|
|
473
473
|
include: config.include ?? ["src/**/*.{js,jsx,ts,tsx,mjs,mts}"],
|
|
474
|
-
exclude: config.exclude ?? ["**/node_modules/**", "**/dist/**"]
|
|
474
|
+
exclude: config.exclude ?? ["**/node_modules/**", "**/dist/**"],
|
|
475
|
+
translate: config.translate ?? {}
|
|
475
476
|
};
|
|
476
477
|
}
|
|
477
478
|
async function loadConfigFile(root) {
|
|
@@ -614,12 +615,403 @@ function pruneCatalogs(cfg, catalogs, registry) {
|
|
|
614
615
|
return removed;
|
|
615
616
|
}
|
|
616
617
|
|
|
617
|
-
// src/
|
|
618
|
+
// src/providers/claude.ts
|
|
619
|
+
var DEFAULT_MODEL = "claude-sonnet-5";
|
|
620
|
+
var SYSTEM = `You translate UI strings for the Verbaly i18n library.
|
|
621
|
+
Rules:
|
|
622
|
+
- Translate only the human-readable text, naturally for the target locale.
|
|
623
|
+
- Preserve verbatim: placeholders like {name}, format specs like {price:currency/EUR}, variant blocks like {count | one: ... | other: # ...} (translate only the text inside each variant, keep keys and # as-is), ICU syntax, named tags like <em>...</em> and escapes {{ }} || ##.
|
|
624
|
+
- Keys are opaque identifiers: return exactly the same keys, never translate or rename them.`;
|
|
625
|
+
function claudeProvider(options = {}) {
|
|
626
|
+
return async (request) => {
|
|
627
|
+
const Anthropic = await loadSdk();
|
|
628
|
+
const client = new Anthropic(options.apiKey ? { apiKey: options.apiKey } : {});
|
|
629
|
+
const response = await client.messages.create({
|
|
630
|
+
model: options.model ?? DEFAULT_MODEL,
|
|
631
|
+
max_tokens: options.maxTokens ?? 16e3,
|
|
632
|
+
thinking: { type: "disabled" },
|
|
633
|
+
system: SYSTEM,
|
|
634
|
+
messages: [{ role: "user", content: buildPrompt(request) }],
|
|
635
|
+
output_config: { format: batchFormat(request) }
|
|
636
|
+
});
|
|
637
|
+
const text = response.content.find((block) => block.type === "text")?.text ?? "{}";
|
|
638
|
+
return JSON.parse(text);
|
|
639
|
+
};
|
|
640
|
+
}
|
|
641
|
+
function buildPrompt(request) {
|
|
642
|
+
return `Translate each value from "${request.sourceLocale}" to "${request.targetLocale}". Return a JSON object with the same keys and translated values.
|
|
643
|
+
|
|
644
|
+
` + JSON.stringify(request.messages, null, 2);
|
|
645
|
+
}
|
|
646
|
+
function batchFormat(request) {
|
|
647
|
+
const keys = Object.keys(request.messages);
|
|
648
|
+
return {
|
|
649
|
+
type: "json_schema",
|
|
650
|
+
schema: {
|
|
651
|
+
type: "object",
|
|
652
|
+
properties: Object.fromEntries(keys.map((key) => [key, { type: "string" }])),
|
|
653
|
+
required: keys,
|
|
654
|
+
additionalProperties: false
|
|
655
|
+
}
|
|
656
|
+
};
|
|
657
|
+
}
|
|
658
|
+
async function loadSdk() {
|
|
659
|
+
try {
|
|
660
|
+
const mod = await import("@anthropic-ai/sdk");
|
|
661
|
+
return mod.default;
|
|
662
|
+
} catch (error) {
|
|
663
|
+
if (isModuleNotFound2(error, "@anthropic-ai/sdk")) {
|
|
664
|
+
throw new Error(
|
|
665
|
+
"[verbaly] the claude translate provider needs @anthropic-ai/sdk \u2014 install it as a dev dependency (e.g. `pnpm add -D @anthropic-ai/sdk` / `npm i -D @anthropic-ai/sdk`) and set ANTHROPIC_API_KEY",
|
|
666
|
+
{ cause: error }
|
|
667
|
+
);
|
|
668
|
+
}
|
|
669
|
+
throw error;
|
|
670
|
+
}
|
|
671
|
+
}
|
|
672
|
+
function isModuleNotFound2(error, name) {
|
|
673
|
+
return error instanceof Error && error.code === "ERR_MODULE_NOT_FOUND" && error.message.includes(name);
|
|
674
|
+
}
|
|
675
|
+
|
|
676
|
+
// src/pseudo.ts
|
|
677
|
+
var PSEUDO_LOCALE = "en-XA";
|
|
678
|
+
var ACCENTS = {
|
|
679
|
+
a: "\xE1",
|
|
680
|
+
b: "\u0180",
|
|
681
|
+
c: "\xE7",
|
|
682
|
+
d: "\u0111",
|
|
683
|
+
e: "\xE9",
|
|
684
|
+
f: "\u0192",
|
|
685
|
+
g: "\u011F",
|
|
686
|
+
h: "\u0125",
|
|
687
|
+
i: "\xED",
|
|
688
|
+
j: "\u0135",
|
|
689
|
+
k: "\u0137",
|
|
690
|
+
l: "\u013A",
|
|
691
|
+
m: "\u0271",
|
|
692
|
+
n: "\xF1",
|
|
693
|
+
o: "\xF3",
|
|
694
|
+
p: "\xFE",
|
|
695
|
+
q: "\u01EB",
|
|
696
|
+
r: "\u0155",
|
|
697
|
+
s: "\u0161",
|
|
698
|
+
t: "\u0163",
|
|
699
|
+
u: "\xFA",
|
|
700
|
+
v: "\u1E7D",
|
|
701
|
+
w: "\u0175",
|
|
702
|
+
x: "\u1E8B",
|
|
703
|
+
y: "\xFD",
|
|
704
|
+
z: "\u017E",
|
|
705
|
+
A: "\xC1",
|
|
706
|
+
B: "\u0181",
|
|
707
|
+
C: "\xC7",
|
|
708
|
+
D: "\u0110",
|
|
709
|
+
E: "\xC9",
|
|
710
|
+
F: "\u0191",
|
|
711
|
+
G: "\u011E",
|
|
712
|
+
H: "\u0124",
|
|
713
|
+
I: "\xCD",
|
|
714
|
+
J: "\u0134",
|
|
715
|
+
K: "\u0136",
|
|
716
|
+
L: "\u0139",
|
|
717
|
+
M: "\u1E3E",
|
|
718
|
+
N: "\xD1",
|
|
719
|
+
O: "\xD3",
|
|
720
|
+
P: "\xDE",
|
|
721
|
+
Q: "\u01EA",
|
|
722
|
+
R: "\u0154",
|
|
723
|
+
S: "\u0160",
|
|
724
|
+
T: "\u0162",
|
|
725
|
+
U: "\xDA",
|
|
726
|
+
V: "\u1E7C",
|
|
727
|
+
W: "\u0174",
|
|
728
|
+
X: "\u1E8C",
|
|
729
|
+
Y: "\xDD",
|
|
730
|
+
Z: "\u017D"
|
|
731
|
+
};
|
|
732
|
+
var TAG_AT = /<\/?[a-zA-Z][\w-]*\/?>/y;
|
|
733
|
+
function pseudoLocalize(message) {
|
|
734
|
+
let out = "";
|
|
735
|
+
let letters = 0;
|
|
736
|
+
let i = 0;
|
|
737
|
+
while (i < message.length) {
|
|
738
|
+
const two = message.slice(i, i + 2);
|
|
739
|
+
if (two === "{{" || two === "}}" || two === "||" || two === "##") {
|
|
740
|
+
out += two;
|
|
741
|
+
i += 2;
|
|
742
|
+
continue;
|
|
743
|
+
}
|
|
744
|
+
const ch = message[i];
|
|
745
|
+
if (ch === "{") {
|
|
746
|
+
const end = matchBrace(message, i);
|
|
747
|
+
out += message.slice(i, end);
|
|
748
|
+
i = end;
|
|
749
|
+
continue;
|
|
750
|
+
}
|
|
751
|
+
if (ch === "<") {
|
|
752
|
+
TAG_AT.lastIndex = i;
|
|
753
|
+
const m = TAG_AT.exec(message);
|
|
754
|
+
if (m) {
|
|
755
|
+
out += m[0];
|
|
756
|
+
i += m[0].length;
|
|
757
|
+
continue;
|
|
758
|
+
}
|
|
759
|
+
}
|
|
760
|
+
const mapped = ACCENTS[ch];
|
|
761
|
+
if (mapped) {
|
|
762
|
+
out += mapped;
|
|
763
|
+
letters += 1;
|
|
764
|
+
} else {
|
|
765
|
+
out += ch;
|
|
766
|
+
}
|
|
767
|
+
i += 1;
|
|
768
|
+
}
|
|
769
|
+
const pad = "~".repeat(Math.ceil(letters / 3));
|
|
770
|
+
return `\u27E6${out}${pad ? " " + pad : ""}\u27E7`;
|
|
771
|
+
}
|
|
772
|
+
function matchBrace(message, start) {
|
|
773
|
+
let depth = 0;
|
|
774
|
+
for (let i = start; i < message.length; i++) {
|
|
775
|
+
const two = message.slice(i, i + 2);
|
|
776
|
+
if (two === "{{" || two === "}}") {
|
|
777
|
+
i += 1;
|
|
778
|
+
continue;
|
|
779
|
+
}
|
|
780
|
+
const ch = message[i];
|
|
781
|
+
if (ch === "{") depth += 1;
|
|
782
|
+
else if (ch === "}") {
|
|
783
|
+
depth -= 1;
|
|
784
|
+
if (depth === 0) return i + 1;
|
|
785
|
+
}
|
|
786
|
+
}
|
|
787
|
+
return message.length;
|
|
788
|
+
}
|
|
789
|
+
function pseudoCatalogs(cfg, catalogs, locale = PSEUDO_LOCALE) {
|
|
790
|
+
const source = catalogs[cfg.sourceLocale] ?? {};
|
|
791
|
+
const target = {};
|
|
792
|
+
for (const [key, msg] of Object.entries(source)) {
|
|
793
|
+
target[key] = msg ? pseudoLocalize(msg) : "";
|
|
794
|
+
}
|
|
795
|
+
catalogs[locale] = target;
|
|
796
|
+
return Object.keys(target).filter((key) => target[key]);
|
|
797
|
+
}
|
|
798
|
+
|
|
799
|
+
// src/render.ts
|
|
800
|
+
import { mkdirSync as mkdirSync2, readFileSync as readFileSync4, writeFileSync as writeFileSync3 } from "fs";
|
|
801
|
+
import { dirname, join as join4, relative } from "path";
|
|
618
802
|
import MagicString from "magic-string";
|
|
803
|
+
import { glob as glob2 } from "tinyglobby";
|
|
804
|
+
import { createVerbaly, parseTags, RICH_TAGS } from "verbaly";
|
|
805
|
+
var VOID_TAGS = /* @__PURE__ */ new Set([
|
|
806
|
+
"area",
|
|
807
|
+
"base",
|
|
808
|
+
"br",
|
|
809
|
+
"col",
|
|
810
|
+
"embed",
|
|
811
|
+
"hr",
|
|
812
|
+
"img",
|
|
813
|
+
"input",
|
|
814
|
+
"link",
|
|
815
|
+
"meta",
|
|
816
|
+
"param",
|
|
817
|
+
"source",
|
|
818
|
+
"track",
|
|
819
|
+
"wbr"
|
|
820
|
+
]);
|
|
821
|
+
var START_TAG = /<([a-zA-Z][a-zA-Z0-9-]*)((?:"[^"]*"|'[^']*'|[^"'>])*)>/g;
|
|
822
|
+
var ATTR = /([^\s=/"'<>]+)(?:\s*=\s*(?:"([^"]*)"|'([^']*)'|([^\s>]+)))?/g;
|
|
823
|
+
function renderHtml(html, options) {
|
|
824
|
+
const attr = options.attribute ?? "data-verbaly";
|
|
825
|
+
const argsAttr = `${attr}-args`;
|
|
826
|
+
const attrsAttr = `${attr}-attr`;
|
|
827
|
+
const richAttr = `${attr}-rich`;
|
|
828
|
+
const richTags = new Set(options.richTags ?? RICH_TAGS);
|
|
829
|
+
const sourceLocale = options.sourceLocale ?? "en";
|
|
830
|
+
const messages = {};
|
|
831
|
+
for (const [locale, catalog] of Object.entries(options.catalogs)) {
|
|
832
|
+
const clean = {};
|
|
833
|
+
for (const [key, msg] of Object.entries(catalog)) if (msg) clean[key] = msg;
|
|
834
|
+
messages[locale] = clean;
|
|
835
|
+
}
|
|
836
|
+
const v = createVerbaly({ locale: options.locale, fallback: sourceLocale, messages });
|
|
837
|
+
const t = v.t;
|
|
838
|
+
const ms = new MagicString(html);
|
|
839
|
+
const missing = /* @__PURE__ */ new Set();
|
|
840
|
+
const skip = protectedRanges(html);
|
|
841
|
+
const inSkip = (index) => skip.some(([from, to]) => index >= from && index < to);
|
|
842
|
+
START_TAG.lastIndex = 0;
|
|
843
|
+
let m;
|
|
844
|
+
while ((m = START_TAG.exec(html)) !== null) {
|
|
845
|
+
if (inSkip(m.index)) continue;
|
|
846
|
+
const [full, rawName, attrChunk] = m;
|
|
847
|
+
const tagName = rawName.toLowerCase();
|
|
848
|
+
const openEnd = m.index + full.length;
|
|
849
|
+
const chunkStart = m.index + 1 + rawName.length;
|
|
850
|
+
if (tagName === "html" && options.setLang !== false) {
|
|
851
|
+
setAttribute(ms, html, chunkStart, openEnd, attrChunk, "lang", options.locale);
|
|
852
|
+
continue;
|
|
853
|
+
}
|
|
854
|
+
const attrs = parseAttrs(attrChunk);
|
|
855
|
+
const key = attrs.get(attr);
|
|
856
|
+
const attrMapRaw = attrs.get(attrsAttr);
|
|
857
|
+
if (key === void 0 && attrMapRaw === void 0) continue;
|
|
858
|
+
const args = parseArgs(attrs.get(argsAttr));
|
|
859
|
+
if (key) {
|
|
860
|
+
if (!v.has(key)) {
|
|
861
|
+
missing.add(key);
|
|
862
|
+
} else if (!VOID_TAGS.has(tagName) && !attrChunk.trimEnd().endsWith("/")) {
|
|
863
|
+
const close = findClose(html, tagName, openEnd, inSkip);
|
|
864
|
+
if (close) {
|
|
865
|
+
const text = t(key, args);
|
|
866
|
+
const content = attrs.has(richAttr) ? richToHtml(parseTags(text), richTags) : escapeHtml(text);
|
|
867
|
+
if (html.slice(openEnd, close.contentEnd) !== content) {
|
|
868
|
+
if (openEnd === close.contentEnd) ms.appendLeft(openEnd, content);
|
|
869
|
+
else ms.overwrite(openEnd, close.contentEnd, content);
|
|
870
|
+
}
|
|
871
|
+
}
|
|
872
|
+
}
|
|
873
|
+
}
|
|
874
|
+
if (attrMapRaw !== void 0) {
|
|
875
|
+
const map = parseArgs(attrMapRaw);
|
|
876
|
+
if (map) {
|
|
877
|
+
for (const [name, attrKey] of Object.entries(map)) {
|
|
878
|
+
if (typeof attrKey !== "string" || name.toLowerCase().startsWith("on")) continue;
|
|
879
|
+
if (!v.has(attrKey)) {
|
|
880
|
+
missing.add(attrKey);
|
|
881
|
+
continue;
|
|
882
|
+
}
|
|
883
|
+
setAttribute(ms, html, chunkStart, openEnd, attrChunk, name, t(attrKey, args));
|
|
884
|
+
}
|
|
885
|
+
}
|
|
886
|
+
}
|
|
887
|
+
}
|
|
888
|
+
return { html: ms.toString(), missing: [...missing] };
|
|
889
|
+
}
|
|
890
|
+
async function renderSite(cfg, options = {}) {
|
|
891
|
+
const site = join4(cfg.root, options.site ?? "dist");
|
|
892
|
+
const locales = options.locales ?? cfg.locales;
|
|
893
|
+
const catalogs = loadCatalogs(cfg);
|
|
894
|
+
const files = await glob2("**/*.html", {
|
|
895
|
+
cwd: site,
|
|
896
|
+
absolute: true,
|
|
897
|
+
ignore: locales.map((locale) => `${locale}/**`)
|
|
898
|
+
});
|
|
899
|
+
const missing = {};
|
|
900
|
+
for (const file of files) {
|
|
901
|
+
const html = readFileSync4(file, "utf8");
|
|
902
|
+
const rel = relative(site, file);
|
|
903
|
+
for (const locale of locales) {
|
|
904
|
+
const result = renderHtml(html, {
|
|
905
|
+
locale,
|
|
906
|
+
catalogs,
|
|
907
|
+
sourceLocale: cfg.sourceLocale,
|
|
908
|
+
attribute: options.attribute,
|
|
909
|
+
richTags: options.richTags
|
|
910
|
+
});
|
|
911
|
+
for (const key of result.missing) {
|
|
912
|
+
const list = missing[locale] ??= [];
|
|
913
|
+
if (!list.includes(key)) list.push(key);
|
|
914
|
+
}
|
|
915
|
+
const out = locale === cfg.sourceLocale ? file : join4(site, locale, rel);
|
|
916
|
+
mkdirSync2(dirname(out), { recursive: true });
|
|
917
|
+
writeFileSync3(out, result.html);
|
|
918
|
+
}
|
|
919
|
+
}
|
|
920
|
+
return { files: files.length, locales: [...locales], missing };
|
|
921
|
+
}
|
|
922
|
+
function parseAttrs(chunk) {
|
|
923
|
+
const attrs = /* @__PURE__ */ new Map();
|
|
924
|
+
ATTR.lastIndex = 0;
|
|
925
|
+
let m;
|
|
926
|
+
while ((m = ATTR.exec(chunk)) !== null) {
|
|
927
|
+
attrs.set(m[1].toLowerCase(), m[2] ?? m[3] ?? m[4] ?? "");
|
|
928
|
+
}
|
|
929
|
+
return attrs;
|
|
930
|
+
}
|
|
931
|
+
function parseArgs(raw) {
|
|
932
|
+
if (!raw) return void 0;
|
|
933
|
+
try {
|
|
934
|
+
return JSON.parse(decodeEntities(raw));
|
|
935
|
+
} catch {
|
|
936
|
+
console.warn(`[verbaly] invalid args JSON: ${raw}`);
|
|
937
|
+
return void 0;
|
|
938
|
+
}
|
|
939
|
+
}
|
|
940
|
+
function protectedRanges(html) {
|
|
941
|
+
const ranges = [];
|
|
942
|
+
const re = /<!--[\s\S]*?-->|<script\b[\s\S]*?<\/script\s*>|<style\b[\s\S]*?<\/style\s*>/gi;
|
|
943
|
+
let m;
|
|
944
|
+
while ((m = re.exec(html)) !== null) {
|
|
945
|
+
const openEnd = m[0].startsWith("<!--") ? m.index : html.indexOf(">", m.index) + 1;
|
|
946
|
+
ranges.push([openEnd, m.index + m[0].length]);
|
|
947
|
+
}
|
|
948
|
+
return ranges;
|
|
949
|
+
}
|
|
950
|
+
function findClose(html, tagName, from, inSkip) {
|
|
951
|
+
const re = new RegExp(
|
|
952
|
+
`<${tagName}(?=[\\s/>])(?:"[^"]*"|'[^']*'|[^"'>])*>|</${tagName}\\s*>`,
|
|
953
|
+
"gi"
|
|
954
|
+
);
|
|
955
|
+
re.lastIndex = from;
|
|
956
|
+
let depth = 1;
|
|
957
|
+
let m;
|
|
958
|
+
while ((m = re.exec(html)) !== null) {
|
|
959
|
+
if (inSkip(m.index)) continue;
|
|
960
|
+
if (m[0][1] === "/") {
|
|
961
|
+
depth -= 1;
|
|
962
|
+
if (depth === 0) return { contentEnd: m.index };
|
|
963
|
+
} else if (!m[0].endsWith("/>")) {
|
|
964
|
+
depth += 1;
|
|
965
|
+
}
|
|
966
|
+
}
|
|
967
|
+
return null;
|
|
968
|
+
}
|
|
969
|
+
function setAttribute(ms, html, chunkStart, openEnd, attrChunk, name, value) {
|
|
970
|
+
const escaped = escapeAttr(value);
|
|
971
|
+
const existing = new RegExp(`(\\s${name}\\s*=\\s*)("[^"]*"|'[^']*'|[^\\s>]+)`, "i").exec(
|
|
972
|
+
attrChunk
|
|
973
|
+
);
|
|
974
|
+
if (existing) {
|
|
975
|
+
const valueStart = chunkStart + existing.index + existing[1].length;
|
|
976
|
+
const valueEnd = valueStart + existing[2].length;
|
|
977
|
+
if (html.slice(valueStart, valueEnd) !== `"${escaped}"`) {
|
|
978
|
+
ms.overwrite(valueStart, valueEnd, `"${escaped}"`);
|
|
979
|
+
}
|
|
980
|
+
return;
|
|
981
|
+
}
|
|
982
|
+
const selfClosing = html.slice(openEnd - 2, openEnd) === "/>";
|
|
983
|
+
const insertAt = openEnd - (selfClosing ? 2 : 1);
|
|
984
|
+
ms.appendLeft(insertAt, ` ${name}="${escaped}"`);
|
|
985
|
+
}
|
|
986
|
+
function richToHtml(nodes, allowed) {
|
|
987
|
+
let out = "";
|
|
988
|
+
for (const node of nodes) {
|
|
989
|
+
if (typeof node === "string") {
|
|
990
|
+
out += escapeHtml(node);
|
|
991
|
+
} else if (allowed.has(node.name)) {
|
|
992
|
+
out += `<${node.name}>${richToHtml(node.children, allowed)}</${node.name}>`;
|
|
993
|
+
} else {
|
|
994
|
+
out += richToHtml(node.children, allowed);
|
|
995
|
+
}
|
|
996
|
+
}
|
|
997
|
+
return out;
|
|
998
|
+
}
|
|
999
|
+
function escapeHtml(text) {
|
|
1000
|
+
return text.replace(/&/g, "&").replace(/</g, "<").replace(/>/g, ">");
|
|
1001
|
+
}
|
|
1002
|
+
function escapeAttr(text) {
|
|
1003
|
+
return escapeHtml(text).replace(/"/g, """);
|
|
1004
|
+
}
|
|
1005
|
+
function decodeEntities(text) {
|
|
1006
|
+
return text.replace(/"/g, '"').replace(/'/g, "'").replace(/</g, "<").replace(/>/g, ">").replace(/&/g, "&");
|
|
1007
|
+
}
|
|
1008
|
+
|
|
1009
|
+
// src/transform.ts
|
|
1010
|
+
import MagicString2 from "magic-string";
|
|
619
1011
|
function transformCode(code, file, analysis) {
|
|
620
1012
|
const { tagged } = analysis ?? analyze(code, file);
|
|
621
1013
|
if (tagged.length === 0) return null;
|
|
622
|
-
const s = new
|
|
1014
|
+
const s = new MagicString2(code);
|
|
623
1015
|
for (const msg of tagged) {
|
|
624
1016
|
const seen = /* @__PURE__ */ new Set();
|
|
625
1017
|
const entries = msg.params.filter((p) => !seen.has(p.name) && seen.add(p.name));
|
|
@@ -640,12 +1032,73 @@ function transformCode(code, file, analysis) {
|
|
|
640
1032
|
}
|
|
641
1033
|
return { code: s.toString(), map: s.generateMap({ hires: true }) };
|
|
642
1034
|
}
|
|
1035
|
+
|
|
1036
|
+
// src/translate.ts
|
|
1037
|
+
async function translateCatalogs(cfg, catalogs, provider, options = {}) {
|
|
1038
|
+
const batchSize = options.batchSize ?? 20;
|
|
1039
|
+
const targets = (options.locales ?? cfg.locales).filter(
|
|
1040
|
+
(locale) => locale !== cfg.sourceLocale && cfg.locales.includes(locale)
|
|
1041
|
+
);
|
|
1042
|
+
const source = catalogs[cfg.sourceLocale] ?? {};
|
|
1043
|
+
const result = { translated: {}, invalid: {}, pending: {} };
|
|
1044
|
+
for (const locale of targets) {
|
|
1045
|
+
const catalog = catalogs[locale] ??= {};
|
|
1046
|
+
const missing = Object.keys(source).filter((key) => source[key] && !catalog[key]);
|
|
1047
|
+
if (missing.length === 0) continue;
|
|
1048
|
+
if (options.dryRun) {
|
|
1049
|
+
result.pending[locale] = missing;
|
|
1050
|
+
continue;
|
|
1051
|
+
}
|
|
1052
|
+
for (let i = 0; i < missing.length; i += batchSize) {
|
|
1053
|
+
const keys = missing.slice(i, i + batchSize);
|
|
1054
|
+
const messages = Object.fromEntries(keys.map((key) => [key, source[key]]));
|
|
1055
|
+
const out = await provider({
|
|
1056
|
+
sourceLocale: cfg.sourceLocale,
|
|
1057
|
+
targetLocale: locale,
|
|
1058
|
+
messages
|
|
1059
|
+
});
|
|
1060
|
+
for (const key of keys) {
|
|
1061
|
+
const text = out[key];
|
|
1062
|
+
if (typeof text === "string" && text.trim() && structureMatches(source[key], text)) {
|
|
1063
|
+
catalog[key] = text;
|
|
1064
|
+
(result.translated[locale] ??= []).push(key);
|
|
1065
|
+
} else {
|
|
1066
|
+
(result.invalid[locale] ??= []).push(key);
|
|
1067
|
+
}
|
|
1068
|
+
}
|
|
1069
|
+
}
|
|
1070
|
+
}
|
|
1071
|
+
return result;
|
|
1072
|
+
}
|
|
1073
|
+
function structureMatches(source, translated) {
|
|
1074
|
+
return sameMembers(paramNames(source), paramNames(translated)) && sameMembers(tagTokens(source), tagTokens(translated));
|
|
1075
|
+
}
|
|
1076
|
+
function paramNames(message) {
|
|
1077
|
+
try {
|
|
1078
|
+
return [...collectParams(message).keys()].sort();
|
|
1079
|
+
} catch {
|
|
1080
|
+
return ["\0invalid"];
|
|
1081
|
+
}
|
|
1082
|
+
}
|
|
1083
|
+
var TAG = /<(\/?)([a-zA-Z][\w-]*)(\/?)>/g;
|
|
1084
|
+
function tagTokens(message) {
|
|
1085
|
+
const out = [];
|
|
1086
|
+
for (const match of message.matchAll(TAG)) {
|
|
1087
|
+
out.push(`${match[1]}${match[2]}${match[3]}`);
|
|
1088
|
+
}
|
|
1089
|
+
return out.sort();
|
|
1090
|
+
}
|
|
1091
|
+
function sameMembers(a, b) {
|
|
1092
|
+
return a.length === b.length && a.every((value, i) => value === b[i]);
|
|
1093
|
+
}
|
|
643
1094
|
export {
|
|
644
1095
|
MessageRegistry,
|
|
1096
|
+
PSEUDO_LOCALE,
|
|
645
1097
|
VIRTUAL_ID,
|
|
646
1098
|
analyze,
|
|
647
1099
|
catalogPath,
|
|
648
1100
|
check,
|
|
1101
|
+
claudeProvider,
|
|
649
1102
|
collectParams,
|
|
650
1103
|
extractProject,
|
|
651
1104
|
formatCheckResult,
|
|
@@ -656,13 +1109,19 @@ export {
|
|
|
656
1109
|
loadConfig,
|
|
657
1110
|
loadConfigFile,
|
|
658
1111
|
pruneCatalogs,
|
|
1112
|
+
pseudoCatalogs,
|
|
1113
|
+
pseudoLocalize,
|
|
659
1114
|
readCatalog,
|
|
1115
|
+
renderHtml,
|
|
660
1116
|
renderParamType,
|
|
1117
|
+
renderSite,
|
|
661
1118
|
resolveConfig,
|
|
662
1119
|
serializeCatalog,
|
|
663
1120
|
stableKey,
|
|
1121
|
+
structureMatches,
|
|
664
1122
|
syncCatalogs,
|
|
665
1123
|
transformCode,
|
|
1124
|
+
translateCatalogs,
|
|
666
1125
|
writeCatalog,
|
|
667
1126
|
writeDts
|
|
668
1127
|
};
|