import path from 'node:path'; import { Converter } from 'opencc-js'; import { root, jsonFile, resource, pool, writeJSON } from './lib.mjs'; const toSimplified = Converter({from:'hk',to:'cn'}); const toTraditional = Converter({from:'cn',to:'hk'}); export const hasHan = s => /[\u3400-\u9fff\u3040-\u30ff\uac00-\ud7af]/.test(s); const normalized = s => { const text=s.replace(/\s+/g,' ').trim();return /[\u3040-\u30ff\uac00-\ud7af]/.test(text)?text:toSimplified(text); }; const dictFile = path.join(root,'scripts','locales','en.json'); const overrides = { '云服务器':'Cloud Server','产品':'Products','产品中心':'Products','关于我们':'About Us','公司简介':'Company Profile','公司介绍':'Company Profile','联系我们':'Contact Us', '物理服务器':'Dedicated Server','独立服务器':'Dedicated Server','物理裸机':'Dedicated Server','裸金属服务器':'Bare Metal Server','负载均衡':'Load Balancer','高防CDN':'Anti-DDoS CDN', '技术支持':'Support','技术文档':'Documentation','文档中心':'Documentation','帮助文档':'Documentation','帮助中心':'Help Center','公告与新闻':'News & Announcements','新闻资讯':'News','官方公告':'Announcements', 'CPU':'CPU','内存':'Memory','硬盘':'Storage','带宽':'Bandwidth','线路':'Network','价格':'Price','防护DDOS':'DDoS Protection','月付':'Monthly','月':'Month','地区':'Region','可选配置':'Available Configurations', '搜索':'Search','全部':'All','上一页':'Previous','下一页':'Next','返回列表':'Back to List','查看详情':'View Details','更多':'More','暂无相关内容':'No content available.','未找到相关内容':'No results found.', '首页':'Home','联系客服':'Contact Support','系统盘':'System Disk','数据盘':'Data Disk','公开起售价':'Starting Price','CPU(核)':'CPU (Cores)','选择语言':'Select Language','关闭':'Close', }; let dictionary; let saveQueue=Promise.resolve(); const save=()=>{saveQueue=saveQueue.then(()=>writeJSON(dictFile,dictionary));return saveQueue;}; async function getDictionary() { if(!dictionary) dictionary={...await jsonFile(dictFile,{}),...overrides}; return dictionary; } export function textNodes($) { const values=[]; function visit(node) { if (node.type==='text') { if(hasHan(node.data)) values.push({node,value:node.data}); return; } if (node.type==='comment' || ['script','style','code','pre','svg','noscript'].includes(node.name) || node.attribs?.['data-no-translate']!==undefined || /(?:language-option|language-name|language-code|current-lang-label)/.test(node.attribs?.class||'')) return; for(const child of node.children||[]) visit(child); } visit($.root()[0]); return values; } export async function prepareTranslations(documents) { const dictionary=await getDictionary(); const keys=new Set(); for(const $ of documents) { for(const item of textNodes($)) keys.add(normalized(item.value)); $('*[title], *[alt], *[placeholder], *[aria-label], meta[name="description"], meta[name="keywords"]').each((_,el)=> { for(const attr of ['title','alt','placeholder','aria-label','content']) {const s=$(el).attr(attr);if(s&&hasHan(s))keys.add(normalized(s));} }); } const missing=[...keys].filter(s=>s && (!dictionary[s]||hasHan(dictionary[s]))); const batches=[];let batch=[];let length=0; for(const key of missing) {if(length+key.length>1800&&batch.length){batches.push(batch);batch=[];length=0;}batch.push(key);length+=key.length+12;} if(batch.length)batches.push(batch); console.log(`Translations: ${keys.size} Chinese strings, ${missing.length} new, ${batches.length} batches`); async function translateOne(value) { const source=/[\u3040-\u30ff]/.test(value)?'ja':/[\uac00-\ud7af]/.test(value)?'ko':'auto'; const url='https://translate.googleapis.com/translate_a/single?client=gtx&sl='+source+'&tl=en&dt=t&q='+encodeURIComponent(value); const r=await resource(url);if(r.status!==200)throw new Error(`Translation HTTP ${r.status}`); const json=JSON.parse(r.body.toString());return json[0].map(part=>part[0]||'').join(''); } let completed=0; await pool(batches,3,async values=> { const query=values.map((s,i)=>`ZX${String(i).padStart(4,'0')}: ${s}`).join('\n'); const translated=await translateOne(query); const matches=[...translated.matchAll(/ZX\s*(\d{4})\s*[::]?\s*([\s\S]*?)(?=ZX\s*\d{4}\s*[::]?|$)/gi)]; for(let i=0;iNumber(m[1])===i)?.[2]?.trim(); dictionary[values[i]]=found&&!hasHan(found)?found:await translateOne(values[i]); if(hasHan(dictionary[values[i]]))throw new Error(`Untranslated text: ${values[i].slice(0,80)}`); } await save(); console.log(`Translations: ${++completed}/${batches.length} batches saved`); }); await save(); } export async function localize($,language) { const dictionary=await getDictionary(); const convert=s=> { if(!hasHan(s))return s; if(language==='zh-cn')return toSimplified(s); if(language==='zh-hk')return toTraditional(toSimplified(s)); const key=normalized(s);const translated=dictionary[key]; if(!translated)throw new Error(`Missing English translation: ${key.slice(0,100)}`); return (s.match(/^\s*/)?.[0]||'')+translated+(s.match(/\s*$/)?.[0]||''); }; for(const item of textNodes($))item.node.data=convert(item.value); $('*[title], *[alt], *[placeholder], *[aria-label], meta[name="description"], meta[name="keywords"]').each((_,el)=>{ for(const attr of ['title','alt','placeholder','aria-label','content']){const s=$(el).attr(attr);if(s&&hasHan(s))$(el).attr(attr,convert(s));} }); return $; }