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);
+});