diff --git a/app/.gitignore b/app/.gitignore
index 6d6ae5a..0a87c48 100644
--- a/app/.gitignore
+++ b/app/.gitignore
@@ -23,3 +23,6 @@ dev-dist
*.njsproj
*.sln
*.sw?
+
+# Libre
+db
diff --git a/app/src/dialogs/SettingsDialog.tsx b/app/src/dialogs/SettingsDialog.tsx
index 019ba3c..acbde44 100644
--- a/app/src/dialogs/SettingsDialog.tsx
+++ b/app/src/dialogs/SettingsDialog.tsx
@@ -80,14 +80,18 @@ function SettingsDialog() {
}
>
-
+
- {Object.entries(langsProps).map(([key, value], idx) => (
-
- {value}
-
- ))}
+ {Object.entries(langsProps).map(
+ ([key, value], idx) => (
+
+ {value}
+
+ ),
+ )}
diff --git a/app/src/i18n.ts b/app/src/i18n.ts
index 697b830..2b5f0c0 100644
--- a/app/src/i18n.ts
+++ b/app/src/i18n.ts
@@ -1,9 +1,9 @@
import i18n from "i18next";
import { initReactI18next } from "react-i18next";
import { getMemory, setMemory } from "@/utils/memory.ts";
-import cn from '@/resources/i18n/cn.json';
-import en from '@/resources/i18n/en.json';
-import ru from '@/resources/i18n/ru.json';
+import cn from "@/resources/i18n/cn.json";
+import en from "@/resources/i18n/en.json";
+import ru from "@/resources/i18n/ru.json";
// the translations
// (tip move them in a JSON file and import them,
diff --git a/app/src/translator/adapter.ts b/app/src/translator/adapter.ts
new file mode 100644
index 0000000..b4096f6
--- /dev/null
+++ b/app/src/translator/adapter.ts
@@ -0,0 +1,51 @@
+// format language code to name/ISO 639-1 code map
+const languageTranslatorMap = {
+ cn: "zh-CN",
+ en: "en",
+ ru: "ru",
+ ja: "ja",
+ ko: "ko",
+ fr: "fr",
+ de: "de",
+ es: "es",
+ pt: "pt",
+ it: "it",
+};
+
+export function getFormattedLanguage(lang: string): string {
+ return languageTranslatorMap[lang.toLowerCase()] || lang;
+}
+
+const defaultMiddleLang = "en";
+
+type translationResponse = {
+ responseData: {
+ translatedText: string;
+ };
+};
+
+async function translate(
+ text: string,
+ from: string,
+ to: string,
+): Promise {
+ if (from === to || text.length === 0) return text;
+ const resp = await fetch(
+ `https://api.mymemory.translated.net/get?q=${encodeURIComponent(
+ text,
+ )}&langpair=${from}|${to}`,
+ );
+ const data: translationResponse = await resp.json();
+ return data.responseData.translatedText;
+}
+
+export function doTranslate(
+ content: string,
+ from: string,
+ to: string,
+): Promise {
+ from = getFormattedLanguage(from);
+ to = getFormattedLanguage(to);
+
+ return translate(content, from, to);
+}
diff --git a/app/src/translator/index.ts b/app/src/translator/index.ts
index e457a1d..28f1a22 100644
--- a/app/src/translator/index.ts
+++ b/app/src/translator/index.ts
@@ -1,41 +1,13 @@
-import { Plugin } from "vite";
-import path from "path";
-import * as fs from "fs";
-
-const defaultDevLang = "cn";
-
-function readJSON(...paths: string[]): any {
- return JSON.parse(fs.readFileSync(path.resolve(...paths)).toString());
-}
+import { Plugin, ResolvedConfig } from "vite";
+import { processTranslation } from "./translator";
export function createTranslationPlugin(): Plugin {
return {
name: "translate-plugin",
apply: "build",
- configResolved(config) {
+ async configResolved(config: ResolvedConfig) {
try {
- const source = path.resolve(config.root, "src/resources/i18n");
- const files = fs.readdirSync(source);
-
- const motherboard = `${defaultDevLang}.json`;
-
- console.log(files.includes(`${defaultDevLang}.json`))
- if (files.length === 0) {
- console.warn("no translation files found");
- return;
- } else if (!files.includes(motherboard)) {
- console.warn(`no default translation file found (${defaultDevLang}.json)`);
- return;
- }
-
- const data = readJSON(source, motherboard);
-
- files.forEach((file) => {
- if (file === motherboard) return;
- const lang = file.split(".")[0];
- const translation = readJSON(source, file);
- console.log(`translation file ${file} loaded`);
- });
+ await processTranslation(config);
} catch (e) {
console.warn(`error during translation: ${e}`);
}
diff --git a/app/src/translator/io.ts b/app/src/translator/io.ts
new file mode 100644
index 0000000..d133c85
--- /dev/null
+++ b/app/src/translator/io.ts
@@ -0,0 +1,59 @@
+import fs from "fs";
+import path from "path";
+
+export function readJSON(...paths: string[]): any {
+ return JSON.parse(fs.readFileSync(path.resolve(...paths)).toString());
+}
+
+export function writeJSON(data: any, ...paths: string[]): void {
+ fs.writeFileSync(path.resolve(...paths), JSON.stringify(data, null, 2));
+}
+
+export function getMigration(
+ mother: Record,
+ data: Record,
+ prefix: string,
+): string[] {
+ return Object.keys(mother)
+ .map((key): string[] => {
+ const template = mother[key],
+ translation = data[key];
+ const val = [prefix.length === 0 ? key : `${prefix}.${key}`];
+
+ switch (typeof template) {
+ case "string":
+ if (typeof translation !== "string") return val;
+ break;
+ case "object":
+ return getMigration(template, translation, val[0]);
+ default:
+ return typeof translation === typeof template ? [] : val;
+ }
+ })
+ .flat()
+ .filter((key) => key !== undefined && key.length > 0);
+}
+
+export function getTranslation(data: Record, path: string): any {
+ const keys = path.split(".");
+ let current = data;
+ for (const key of keys) {
+ if (current[key] === undefined) return undefined;
+ current = current[key];
+ }
+ return current;
+}
+
+export function setTranslation(
+ data: Record,
+ path: string,
+ value: any,
+): void {
+ const keys = path.split(".");
+ let current = data;
+ for (let i = 0; i < keys.length - 1; i++) {
+ if (current[keys[i]] === undefined) current[keys[i]] = {};
+ current = current[keys[i]];
+ }
+ current[keys[keys.length - 1]] = value;
+}
diff --git a/app/src/translator/translator.ts b/app/src/translator/translator.ts
new file mode 100644
index 0000000..1650c82
--- /dev/null
+++ b/app/src/translator/translator.ts
@@ -0,0 +1,60 @@
+import { ResolvedConfig } from "vite";
+import path from "path";
+import fs from "fs";
+import {
+ getMigration,
+ getTranslation,
+ readJSON,
+ setTranslation,
+ writeJSON,
+} from "./io";
+import { doTranslate } from "./adapter";
+
+export const defaultDevLang = "cn";
+
+export async function processTranslation(
+ config: ResolvedConfig,
+): Promise {
+ const source = path.resolve(config.root, "src/resources/i18n");
+ const files = fs.readdirSync(source);
+
+ const motherboard = `${defaultDevLang}.json`;
+
+ if (files.length === 0) {
+ console.warn("no translation files found");
+ return;
+ } else if (!files.includes(motherboard)) {
+ console.warn(`no default translation file found (${defaultDevLang}.json)`);
+ return;
+ }
+
+ const data = readJSON(source, motherboard);
+
+ const target = files.filter((file) => file !== motherboard);
+ for (const file of target) {
+ const lang = file.split(".")[0];
+ const translation = { ...readJSON(source, file) };
+
+ const migration = getMigration(data, translation, "");
+ for (const key of migration) {
+ const from = getTranslation(data, key);
+ const to =
+ typeof from === "string"
+ ? await doTranslate(from, defaultDevLang, lang)
+ : from;
+
+ console.log(
+ `[i18n] successfully translated: ${from} -> ${to} (lang: ${defaultDevLang} -> ${lang})`,
+ );
+ setTranslation(translation, key, to);
+ }
+
+ if (migration.length > 0) {
+ writeJSON(translation, source, file);
+ }
+
+ console.info(
+ `translation file ${file} loaded, ${migration.length} migration(s) found.`,
+ );
+ }
+}