mirror of
https://github.com/snapotter-hq/SnapOtter.git
synced 2026-08-03 07:46:42 +02:00
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.
186 lines
6.4 KiB
JavaScript
186 lines
6.4 KiB
JavaScript
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`,
|
||
);
|