mirror of
https://github.com/coaidev/coai.git
synced 2025-05-20 05:20:15 +09:00
feat: update auto translation tool
This commit is contained in:
parent
70cff90344
commit
e3c537ddd9
3
app/.gitignore
vendored
3
app/.gitignore
vendored
@ -23,3 +23,6 @@ dev-dist
|
|||||||
*.njsproj
|
*.njsproj
|
||||||
*.sln
|
*.sln
|
||||||
*.sw?
|
*.sw?
|
||||||
|
|
||||||
|
# Libre
|
||||||
|
db
|
||||||
|
@ -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>
|
||||||
|
@ -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,
|
||||||
|
51
app/src/translator/adapter.ts
Normal file
51
app/src/translator/adapter.ts
Normal 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);
|
||||||
|
}
|
@ -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
59
app/src/translator/io.ts
Normal 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;
|
||||||
|
}
|
60
app/src/translator/translator.ts
Normal file
60
app/src/translator/translator.ts
Normal 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.`,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
Loading…
Reference in New Issue
Block a user