Files
SnapOtter/scripts/generate-conversion-i18n.mjs
SnapOtterandGitHub 63a03d26f2 feat: pipeline templates, analytics opt-out, 83 conversion presets, positioning + e2e modernization
Lands five integrated branches: pipeline templates (#355), analytics opt-out (#354), 83 conversion presets bringing the catalog to 240 tools (#356), self-hosted positioning (#353), and e2e modernization (#351).

Integration fixes: aligned stale web analytics tests with the opt-out/allow-list model, closed 3 CodeQL incomplete-sanitization alerts in the i18n generator, resolved settings/index/docs/format-matrix conflicts, and corrected tool counts to 240.
2026-06-28 18:57:53 +08:00

186 lines
6.4 KiB
JavaScript
Raw Permalink Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
import { readFileSync, writeFileSync } from "node:fs";
import { dirname, join } from "node:path";
import { fileURLToPath } from "node:url";
import { CONVERSION_PRESETS } from "../packages/shared/src/conversion-presets.ts";
const __dirname = dirname(fileURLToPath(import.meta.url));
const I18N_DIR = join(__dirname, "../packages/shared/src/i18n");
const BEGIN = " // BEGIN conversion-presets (generated by scripts/generate-conversion-i18n.mjs)";
const END = " // END conversion-presets";
// Per-locale phrasing: name template and description template. {from}/{to} are format labels (kept as-is).
const L = {
en: {
name: (f, t) => `${f} to ${t}`,
desc: (f, t) => `Convert ${f} to ${t} online. Fast, free, private, no signup.`,
},
de: {
name: (f, t) => `${f} in ${t} umwandeln`,
desc: (f, t) => `${f} online in ${t} umwandeln. Schnell, kostenlos und privat.`,
},
es: {
name: (f, t) => `${f} a ${t}`,
desc: (f, t) => `Convierte ${f} a ${t} en línea. Rápido, gratis y privado.`,
},
fr: {
name: (f, t) => `${f} en ${t}`,
desc: (f, t) => `Convertir ${f} en ${t} en ligne. Rapide, gratuit et privé.`,
},
it: {
name: (f, t) => `Da ${f} a ${t}`,
desc: (f, t) => `Converti ${f} in ${t} online. Veloce, gratuito e privato.`,
},
"pt-BR": {
name: (f, t) => `${f} para ${t}`,
desc: (f, t) => `Converta ${f} para ${t} online. Rápido, grátis e privado.`,
},
nl: {
name: (f, t) => `${f} naar ${t}`,
desc: (f, t) => `Zet ${f} online om naar ${t}. Snel, gratis en privé.`,
},
sv: {
name: (f, t) => `${f} till ${t}`,
desc: (f, t) => `Konvertera ${f} till ${t} online. Snabbt, gratis och privat.`,
},
pl: {
name: (f, t) => `${f} na ${t}`,
desc: (f, t) => `Konwertuj ${f} na ${t} online. Szybko, za darmo i prywatnie.`,
},
ru: {
name: (f, t) => `${f} в ${t}`,
desc: (f, t) => `Конвертируйте ${f} в ${t} онлайн. Быстро, бесплатно и приватно.`,
},
uk: {
name: (f, t) => `${f} у ${t}`,
desc: (f, t) => `Конвертуйте ${f} у ${t} онлайн. Швидко, безкоштовно та приватно.`,
},
tr: {
name: (f, t) => `${f} - ${t}`,
desc: (f, t) => `${f} dosyasını çevrimiçi ${t} biçimine dönüştürün. Hızlı, ücretsiz ve gizli.`,
},
ar: {
name: (f, t) => `${f} إلى ${t}`,
desc: (f, t) => `حوّل ${f} إلى ${t} عبر الإنترنت. سريع ومجاني وخاص.`,
},
hi: {
name: (f, t) => `${f} से ${t}`,
desc: (f, t) => `${f} को ${t} में ऑनलाइन बदलें। तेज़, मुफ़्त और निजी।`,
},
vi: {
name: (f, t) => `${f} sang ${t}`,
desc: (f, t) => `Chuyển đổi ${f} sang ${t} trực tuyến. Nhanh, miễn phí và riêng tư.`,
},
id: {
name: (f, t) => `${f} ke ${t}`,
desc: (f, t) => `Konversi ${f} ke ${t} online. Cepat, gratis, dan privat.`,
},
th: {
name: (f, t) => `${f} เป็น ${t}`,
desc: (f, t) => `แปลง ${f} เป็น ${t} ออนไลน์ รวดเร็ว ฟรี และเป็นส่วนตัว`,
},
ja: {
name: (f, t) => `${f}${t}に変換`,
desc: (f, t) => `${f}${t}にオンラインで変換。高速、無料、プライベート。`,
},
ko: {
name: (f, t) => `${f}을(를) ${t}(으)로`,
desc: (f, t) => `${f}을(를) ${t}(으)로 온라인 변환. 빠르고 무료이며 비공개입니다.`,
},
"zh-CN": {
name: (f, t) => `${f}${t}`,
desc: (f, t) => `在线将 ${f} 转换为 ${t}。快速、免费、私密。`,
},
"zh-TW": {
name: (f, t) => `${f}${t}`,
desc: (f, t) => `線上將 ${f} 轉換為 ${t}。快速、免費、私密。`,
},
};
// Localized "Extract Text from Image (OCR)" per locale (keep the "(OCR)" token literal).
const OCR_NAMES = {
en: "Extract Text from Image (OCR)",
de: "Text aus Bild extrahieren (OCR)",
es: "Extraer texto de imagen (OCR)",
fr: "Extraire le texte d'une image (OCR)",
it: "Estrai testo da immagine (OCR)",
"pt-BR": "Extrair texto de imagem (OCR)",
nl: "Tekst uit afbeelding halen (OCR)",
sv: "Extrahera text från bild (OCR)",
pl: "Wyodrębnij tekst z obrazu (OCR)",
ru: "Извлечь текст из изображения (OCR)",
uk: "Витягти текст із зображення (OCR)",
tr: "Görüntüden Metin Çıkarma (OCR)",
ar: "استخراج النص من صورة (OCR)",
hi: "छवि से टेक्स्ट निकालें (OCR)",
vi: "Trích xuất văn bản từ ảnh (OCR)",
id: "Ekstrak Teks dari Gambar (OCR)",
th: "แยกข้อความจากรูปภาพ (OCR)",
ja: "画像から文字を抽出 (OCR)",
ko: "이미지에서 텍스트 추출 (OCR)",
"zh-CN": "从图片提取文字 (OCR)",
"zh-TW": "從圖片擷取文字 (OCR)",
};
function block(locale) {
const tpl = L[locale] ?? L.en;
const lines = CONVERSION_PRESETS.map((p) => {
const name = tpl.name(p.from, p.to).replace(/[\\"]/g, "\\$&");
const desc = tpl.desc(p.from, p.to).replace(/[\\"]/g, "\\$&");
return ` "${p.id}": { name: "${name}", description: "${desc}" },`;
});
return [BEGIN, ...lines, END].join("\n");
}
function escapeRe(s) {
return s.replace(/[.*+?^${}()|[\]\\]/g, "\\$&");
}
/** Rename the display tools.ocr.name (the first `ocr:` block, the one whose first key is `name`). */
function renameOcr(src, locale) {
const name = (OCR_NAMES[locale] ?? OCR_NAMES.en).replace(/[\\"]/g, "\\$&");
return src.replace(/(ocr:\s*\{\s*name:\s*")(?:[^"\\]|\\.)*(")/, `$1${name}$2`);
}
function injectInto(file, locale) {
let src = readFileSync(file, "utf8");
const gen = block(locale);
if (src.includes(BEGIN)) {
src = src.replace(new RegExp(`${escapeRe(BEGIN)}[\\s\\S]*?${escapeRe(END)}`), gen);
} else {
// Insert right after the `tools: {` line.
src = src.replace(/(\n\s*tools:\s*\{\n)/, `$1${gen}\n`);
}
src = renameOcr(src, locale);
writeFileSync(file, src);
}
const LOCALES = [
"en",
"zh-CN",
"zh-TW",
"ja",
"ko",
"es",
"fr",
"it",
"pt-BR",
"de",
"nl",
"sv",
"ru",
"pl",
"uk",
"ar",
"tr",
"hi",
"vi",
"id",
"th",
];
for (const code of LOCALES) {
injectInto(join(I18N_DIR, `${code}.ts`), code);
}
console.log(
`Injected ${CONVERSION_PRESETS.length} preset entries into ${LOCALES.length} locales; renamed tools.ocr.name`,
);