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