feat: update auto translation tool

This commit is contained in:
Zhang Minghan 2023-12-21 12:48:26 +08:00
parent 70cff90344
commit e3c537ddd9
7 changed files with 190 additions and 41 deletions

3
app/.gitignore vendored
View File

@ -23,3 +23,6 @@ dev-dist
*.njsproj *.njsproj
*.sln *.sln
*.sw? *.sw?
# Libre
db

View File

@ -80,14 +80,18 @@ function SettingsDialog() {
} }
> >
<SelectTrigger className={`select`}> <SelectTrigger className={`select`}>
<SelectValue placeholder={langsProps[i18n.language]} /> <SelectValue
placeholder={langsProps[i18n.language]}
/>
</SelectTrigger> </SelectTrigger>
<SelectContent> <SelectContent>
{Object.entries(langsProps).map(([key, value], idx) => ( {Object.entries(langsProps).map(
([key, value], idx) => (
<SelectItem key={idx} value={key}> <SelectItem key={idx} value={key}>
{value} {value}
</SelectItem> </SelectItem>
))} ),
)}
</SelectContent> </SelectContent>
</Select> </Select>
</div> </div>

View File

@ -1,9 +1,9 @@
import i18n from "i18next"; import i18n from "i18next";
import { initReactI18next } from "react-i18next"; import { initReactI18next } from "react-i18next";
import { getMemory, setMemory } from "@/utils/memory.ts"; import { getMemory, setMemory } from "@/utils/memory.ts";
import cn from '@/resources/i18n/cn.json'; import cn from "@/resources/i18n/cn.json";
import en from '@/resources/i18n/en.json'; import en from "@/resources/i18n/en.json";
import ru from '@/resources/i18n/ru.json'; import ru from "@/resources/i18n/ru.json";
// the translations // the translations
// (tip move them in a JSON file and import them, // (tip move them in a JSON file and import them,

View File

@ -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<string> {
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<string> {
from = getFormattedLanguage(from);
to = getFormattedLanguage(to);
return translate(content, from, to);
}

View File

@ -1,41 +1,13 @@
import { Plugin } from "vite"; import { Plugin, ResolvedConfig } from "vite";
import path from "path"; import { processTranslation } from "./translator";
import * as fs from "fs";
const defaultDevLang = "cn";
function readJSON(...paths: string[]): any {
return JSON.parse(fs.readFileSync(path.resolve(...paths)).toString());
}
export function createTranslationPlugin(): Plugin { export function createTranslationPlugin(): Plugin {
return { return {
name: "translate-plugin", name: "translate-plugin",
apply: "build", apply: "build",
configResolved(config) { async configResolved(config: ResolvedConfig) {
try { try {
const source = path.resolve(config.root, "src/resources/i18n"); await processTranslation(config);
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`);
});
} catch (e) { } catch (e) {
console.warn(`error during translation: ${e}`); console.warn(`error during translation: ${e}`);
} }

59
app/src/translator/io.ts Normal file
View File

@ -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<string, any>,
data: Record<string, any>,
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<string, any>, 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<string, any>,
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;
}

View File

@ -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<void> {
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.`,
);
}
}