Files
SnapOtter/scripts/i18n/lib/mask.mjs
T
SnapOtterandGitHub 00b651c9f8 feat(i18n): 21-language pipeline, landing/docs/API wiring, landing+API translations
Shared Claude Code translation pipeline (scripts/i18n, no API key) plus Astro/VitePress/Scalar i18n wiring. Landing and API reference translated into all 20 languages; docs i18n wiring + English source anchors. The translated docs markdown (apps/docs/<locale>/**, 3,620 files) follows in a companion PR because it exceeds GitHub's per-PR CI file limit.
2026-07-11 13:01:55 +08:00

67 lines
2.2 KiB
JavaScript

// scripts/i18n/lib/mask.mjs
// Ordered so multi-char structures are matched before their sub-parts.
// Each pattern captures the exact substring to hide from the translator.
const FENCE = /(^|\n)(```|~~~)[\s\S]*?\n\2/g; // fenced code blocks
const INLINE = /`[^`\n]+`/g; // inline code spans
// Link/image URL: mask the URL, keep the [text]. The URL may itself contain a
// single level of balanced parentheses (e.g. wikipedia .../Foo_(bar)); match
// runs of non-paren chars and optional (balanced) groups, then the closing paren.
const LINK_URL = /(!?\[[^\]]*\]\()((?:[^()]|\([^()]*\))*)(\))/g;
// {{double}} first so it is captured whole, then {single} interpolation.
const PLACEHOLDER = /\{\{[^{}\n]+\}\}|\{[^{}\n]+\}/g;
const TOKEN = (i) => `⸤I18N${i}⸥`; // uncommon delimiters, restored 1:1
const TOKEN_RE = /⸤I18N\d+⸥/g;
/**
* Replace protected substrings with opaque tokens.
* @param {string} text
* @returns {{ masked: string, tokens: string[] }}
*/
export function mask(text) {
const tokens = [];
const push = (value) => {
tokens.push(value);
return TOKEN(tokens.length - 1);
};
let out = String(text);
out = out.replace(FENCE, (m) => push(m));
out = out.replace(INLINE, (m) => push(m));
out = out.replace(LINK_URL, (_m, open, url, close) => `${open}${push(url)}${close}`);
out = out.replace(PLACEHOLDER, (m) => push(m));
return { masked: out, tokens };
}
/**
* Reinsert masked substrings by token index.
* @param {string} masked
* @param {string[]} tokens
* @returns {string}
*/
export function restore(masked, tokens) {
return String(masked).replace(TOKEN_RE, (t) => {
const m = t.match(/I18N(\d+)/);
const i = m ? Number(m[1]) : Number.NaN;
return tokens[i] ?? t;
});
}
/**
* Count structural elements, used by the validator to prove nothing was lost.
* @param {string} text
* @returns {{ fences: number, inlineCode: number, links: number, placeholders: number }}
*/
export function countStructures(text) {
const s = String(text);
const count = (re) => (s.match(re) || []).length;
return {
fences: count(FENCE),
inlineCode: count(INLINE),
links: count(LINK_URL),
placeholders: count(PLACEHOLDER),
};
}
export { TOKEN_RE };