diff --git a/i18n.mjs b/i18n.mjs new file mode 100644 index 0000000..e34ea37 --- /dev/null +++ b/i18n.mjs @@ -0,0 +1,121 @@ +/** + * 多国语言管理 + * + * node i18n.mjs init [默认语言代号] + * node i18n.mjs build [语言代号] + */ + +import fs from 'node:fs'; +import { readdir, readFile, writeFile } from 'node:fs/promises'; +import path from 'node:path'; +import common from './common.mjs'; + +class I18N { + + //构造函数,设置默认配置 + constructor(defaultLang, templateDir, langDir) { + this.defaultLang = typeof(defaultLang) != 'undefined' && defaultLang ? defaultLang : 'en'; + this.templateDir = typeof(templateDir) != 'undefined' && templateDir ? templateDir : './public/template/'; + this.langDir = typeof(langDir) != 'undefined' && langDir ? langDir : './i18n/'; + } + + //从模板文件中解析语言占位变量,并生成语言包文件 + async init(lang) { + const _self = this; + + try { + const files = await readdir(_self.templateDir); + let parseLangRes = null; + for (const file of files) { + parseLangRes = await _self.parseLangFromTemplate(_self.templateDir + file); + if (parseLangRes) { + console.log('Template file [%s] parse lang config done', file); + } + } + } catch (err) { + console.error('Read dir in function init failed', err); + return false; + } + + return true; + } + + //根据语言包文件以及模板文件,生成对应语言的html文件 + build(lang) { + const _self = this; + + + } + + //判断语言代码格式是否符合国际标准 + isIosLangCode(lang) { + return /^[a-z]{2}(?:\-[a-z]{2})$/i.test(lang); + } + + //更新语言包文件内容,合并新的数据到已有内容中 + async updateLangFile(langFile, langJson) { + const _self = this; + let updated = false; + + try { + let json = await readFile(langFile, { encoding: 'utf8'}); + if (json) { + let data = JSON.parse(json); + for (const key in langJson) { + data[key] = langJson[key]; + } + + updated = await writeFile(langFile, JSON.stringify(data, null, 4)); + }else { + updated = await writeFile(langFile, JSON.stringify(langJson, null, 4)); + } + } catch (err) { + console.error('updateLangFile failed', err); + } + + return updated; + } + + //解析单个模板文件,并生成语言包文件 + async parseLangFromTemplate(templateFilepath) { + const _self = this; + + let langJson = {}, + total = 0; + try { + const html_template = await readFile(templateFilepath, { encoding: 'utf8' }); + + const regHtmlLang = /[\s\S]*[\s\S]*/i; + let htmlLang = html_template.replace(regHtmlLang, "$1"); + if (htmlLang == html_template || _self.isIosLangCode(htmlLang) == false) { + htmlLang = _self.defaultLang; + }else { + htmlLang = htmlLang.toLowerCase(); + } + + const regLang = /\{([^\}\r\n:;]+)\}/ig; + const matches = html_template.matchAll(regLang); + for (const match of matches) { + langJson[match[1]] = match[1]; + total ++; + } + + //更新语言包文件 + if (total > 0) { + let langFile = _self.langDir + `${htmlLang}.json`; + const saved = await _self.updateLangFile(langFile, langJson); + if (!saved) { + return false; + } + } + } catch (err) { + console.error('parseLangFromTemplate failed', err); + return false; + } + + return langJson; + } + +} + +export default I18N; \ No newline at end of file diff --git a/i18n/en-us.json b/i18n/en-us.json new file mode 100644 index 0000000..243e4b4 --- /dev/null +++ b/i18n/en-us.json @@ -0,0 +1,33 @@ +{ + "HeroUnion - Open source web crawler union.": "HeroUnion - Open source web crawler union.", + "HeroUnion.website": "HeroUnion.website", + "HeroUnion - Open source web crawler union": "HeroUnion - Open source web crawler union", + "HeroUnion Stats": "HeroUnion Stats", + "It's running": "It's running", + "Tasks Stats": "Tasks Stats", + "Last": "Last", + "Waiting": "Waiting", + "Running": "Running", + "Total": "Total", + "Done": "Done", + "Failed": "Failed", + "Notify Stats": "Notify Stats", + "Bot Stats": "Bot Stats", + "Idle": "Idle", + "Busy": "Busy", + "Offline": "Offline", + "JSON Data": "JSON Data", + "Covenant of the Alliance": "Covenant of the Alliance", + "Please abide by the following conventions and stick to it for a better tomorrow for yourself and the whole society!": "Please abide by the following conventions and stick to it for a better tomorrow for yourself and the whole society!", + "Comply with local/national laws and regulations": "Comply with local/national laws and regulations", + "Data that requires login or VIP status to access will not be crawled": "Data that requires login or VIP status to access will not be crawled", + "Data that is explicitly prohibited from being collected by the target website will not be crawled": "Data that is explicitly prohibited from being collected by the target website will not be crawled", + "The commercial core data of the target website is not crawled": "The commercial core data of the target website is not crawled", + "Low concurrency, low frequency, does not affect the normal operation of the target website": "Low concurrency, low frequency, does not affect the normal operation of the target website", + "Bots": "Bots", + "HeroUnion App": "HeroUnion App", + "HeroUnion download": "HeroUnion download", + "HeroBot download": "HeroBot download", + "HeroUnion is only responsible for the scheduling of crawlers and tasks.": "HeroUnion is only responsible for the scheduling of crawlers and tasks.", + "The contracts supported by crawlers and the specific content of tasks have nothing to do with the alliance.": "The contracts supported by crawlers and the specific content of tasks have nothing to do with the alliance." +} \ No newline at end of file diff --git a/test/i18n.test.mjs b/test/i18n.test.mjs new file mode 100644 index 0000000..309dd8b --- /dev/null +++ b/test/i18n.test.mjs @@ -0,0 +1,16 @@ +/** + * i18n测试用例 + */ + +import test from 'node:test'; +import assert from 'node:assert'; +import common from '../common.mjs'; +import I18N from '../i18n.mjs'; + + +test('Init test', async (t) => { + const i18n = new I18N(); + const res = await i18n.init(); + + assert.ok(res); +});