mirror of
https://github.com/Shik3i/KoalaSync.git
synced 2026-07-26 12:08:15 +00:00
build(website): move locales translation utility to scripts/ and add validate-brand-names script
This commit is contained in:
@@ -0,0 +1,113 @@
|
||||
/**
|
||||
* KoalaSync Locale Translation Script
|
||||
*
|
||||
* This script is stored in `scripts/translate-locales-tool.cjs`
|
||||
* so future LLM agents can easily reuse and adapt it to translate new keys.
|
||||
*
|
||||
* Usage:
|
||||
* node scripts/translate-locales-tool.cjs
|
||||
*/
|
||||
|
||||
const fs = require('fs');
|
||||
const path = require('path');
|
||||
const http = require('https');
|
||||
|
||||
const localesDir = path.join(__dirname, '../website/locales');
|
||||
const enFile = path.join(localesDir, 'en.json');
|
||||
const enLocale = JSON.parse(fs.readFileSync(enFile, 'utf8'));
|
||||
|
||||
// Keys starting with these prefixes need to be translated.
|
||||
// Update this list when translating new pages or features.
|
||||
const prefixes = ['ALT_W2G_', 'ALT_SCENER_', 'ALT_KOSMI_', 'ALT_TWOSEVEN_'];
|
||||
|
||||
const keysToTranslate = Object.keys(enLocale).filter(key => {
|
||||
return prefixes.some(prefix => key.startsWith(prefix));
|
||||
});
|
||||
|
||||
console.log(`Found ${keysToTranslate.length} keys to translate.`);
|
||||
|
||||
function translate(text, targetLang) {
|
||||
return new Promise((resolve, reject) => {
|
||||
// We use Google's free translation endpoint which is highly robust for standard text segment translations
|
||||
const url = `https://translate.googleapis.com/translate_a/single?client=gtx&sl=en&tl=${targetLang}&dt=t&q=${encodeURIComponent(text)}`;
|
||||
http.get(url, (res) => {
|
||||
let data = '';
|
||||
res.on('data', chunk => data += chunk);
|
||||
res.on('end', () => {
|
||||
try {
|
||||
const parsed = JSON.parse(data);
|
||||
const translation = parsed[0].map(item => item[0]).join('');
|
||||
resolve(translation);
|
||||
} catch (_e) {
|
||||
reject(new Error(`Failed to parse response for target ${targetLang}: ${data}`));
|
||||
}
|
||||
});
|
||||
}).on('error', reject);
|
||||
});
|
||||
}
|
||||
|
||||
function sleep(ms) {
|
||||
return new Promise(resolve => setTimeout(resolve, ms));
|
||||
}
|
||||
|
||||
const fileToLangMap = {
|
||||
'es.json': 'es',
|
||||
'fr.json': 'fr',
|
||||
'it.json': 'it',
|
||||
'ja.json': 'ja',
|
||||
'ko.json': 'ko',
|
||||
'nl.json': 'nl',
|
||||
'pl.json': 'pl',
|
||||
'pt-BR.json': 'pt',
|
||||
'pt.json': 'pt',
|
||||
'ru.json': 'ru',
|
||||
'tr.json': 'tr',
|
||||
'uk.json': 'uk',
|
||||
'zh.json': 'zh-CN'
|
||||
};
|
||||
|
||||
async function run() {
|
||||
const files = fs.readdirSync(localesDir).filter(f => f.endsWith('.json') && f !== 'en.json' && f !== 'de.json');
|
||||
|
||||
for (const file of files) {
|
||||
const targetLang = fileToLangMap[file];
|
||||
if (!targetLang) {
|
||||
console.log(`Skipping unknown file mapping: ${file}`);
|
||||
continue;
|
||||
}
|
||||
|
||||
const filePath = path.join(localesDir, file);
|
||||
const content = JSON.parse(fs.readFileSync(filePath, 'utf8'));
|
||||
|
||||
console.log(`\nTranslating ${file} (${targetLang})...`);
|
||||
let translatedCount = 0;
|
||||
|
||||
for (const key of keysToTranslate) {
|
||||
const englishValue = enLocale[key];
|
||||
// Translate if missing or currently identical to English value (which means it's a fallback)
|
||||
if (!content[key] || content[key] === englishValue) {
|
||||
try {
|
||||
const translatedValue = await translate(englishValue, targetLang);
|
||||
content[key] = translatedValue;
|
||||
translatedCount++;
|
||||
console.log(` [${translatedCount}/${keysToTranslate.length}] Translated key: ${key}`);
|
||||
await sleep(200); // 200ms delay to prevent rate limits
|
||||
} catch (err) {
|
||||
console.error(` Error translating key ${key} to ${targetLang}:`, err.message);
|
||||
// Wait longer on error
|
||||
await sleep(2000);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Save back keeping exact same structure and format
|
||||
fs.writeFileSync(filePath, JSON.stringify(content, null, 2) + '\n', 'utf8');
|
||||
console.log(`Saved ${file} (${translatedCount} keys updated).`);
|
||||
}
|
||||
|
||||
console.log('\nAll translations completed successfully!');
|
||||
}
|
||||
|
||||
run().catch(err => {
|
||||
console.error('Translation script failed:', err);
|
||||
});
|
||||
@@ -0,0 +1,50 @@
|
||||
const fs = require('fs');
|
||||
const path = require('path');
|
||||
|
||||
const localesDir = path.join(__dirname, '../website/locales');
|
||||
const files = fs.readdirSync(localesDir).filter(f => f.endsWith('.json') && f !== 'en.json');
|
||||
|
||||
const brandTranslations = [
|
||||
'dos siete', 'dos-siete', 'deux sept', 'deux-sept', 'zwei sieben', 'zwei-sieben',
|
||||
'due sette', 'due-sette', 'dwa siedem', 'dwa-siedem', 'twee zeven', 'twee-zeven',
|
||||
'iki yedi', 'iki-yedi', 'два семь', 'два-семь', '二七', '二 七', '이칠', '이 칠',
|
||||
'koala sync', 'koala-sync', 'watch 2 gether', 'watch-2-gether', 'watch 2gether',
|
||||
'watch-2gether', 'jelly fin', 'jelly-fin'
|
||||
];
|
||||
|
||||
let failed = false;
|
||||
|
||||
for (const file of files) {
|
||||
const filePath = path.join(localesDir, file);
|
||||
const content = JSON.parse(fs.readFileSync(filePath, 'utf8'));
|
||||
|
||||
for (const [key, value] of Object.entries(content)) {
|
||||
if (typeof value !== 'string') continue;
|
||||
|
||||
// Check brand translations (case-insensitive)
|
||||
const lowerValue = value.toLowerCase();
|
||||
for (const pattern of brandTranslations) {
|
||||
if (lowerValue.includes(pattern)) {
|
||||
console.error(`Error in ${file} [key: ${key}]: Found translated brand pattern "${pattern}" in value: "${value}"`);
|
||||
failed = true;
|
||||
}
|
||||
}
|
||||
|
||||
// Check tag validation
|
||||
const tags = ['strong', 'em', 'span', 'b', 'i', 'p', 'a'];
|
||||
for (const tag of tags) {
|
||||
const openCount = (value.match(new RegExp(`<${tag}\\b[^>]*>`, 'g')) || []).length;
|
||||
const closeCount = (value.match(new RegExp(`</${tag}\\b[^>]*>`, 'g')) || []).length;
|
||||
if (openCount !== closeCount) {
|
||||
console.error(`Error in ${file} [key: ${key}]: Unbalanced tag <${tag}> (open: ${openCount}, close: ${closeCount}) in value: "${value}"`);
|
||||
failed = true;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if (!failed) {
|
||||
console.log("All brand names and HTML tags are perfectly valid and intact across all languages!");
|
||||
} else {
|
||||
process.exit(1);
|
||||
}
|
||||
Reference in New Issue
Block a user