Add i18n support

This commit is contained in:
2023-05-28 11:44:01 +08:00
parent 8f0ed03117
commit affe85f81c
11 changed files with 140 additions and 10 deletions

View File

@@ -1,7 +1,7 @@
{
"importMap": "./import_map.json",
"tasks": {
"server-dev": "deno run -A --unstable --watch=static/,routes/ server-dev.ts",
"server-dev": "deno run -A --unstable --watch=static/,routes/,translation/ server-dev.ts",
"test": "deno test --allow-read=./ --allow-net --allow-write=./ --allow-run=tasklist.exe --unstable",
"run": "deno run --allow-read=./ --allow-write=./ --allow-run=tasklist.exe --allow-env=DENO_DEPLOYMENT_ID --allow-net --unstable",
"compile": "deno compile --allow-read=./ --allow-write=./ --allow-run=tasklist.exe --allow-env=DENO_DEPLOYMENT_ID --allow-net --unstable",

9
deno.lock generated
View File

@@ -193,7 +193,10 @@
"https://deno.land/x/[email protected]/lib/core/zip-writer.js": "34809b421f5deb497ce8cabec730af858c12dde54a6205c78b5591460785dc1e",
"https://deno.land/x/[email protected]/lib/z-worker-inline.js": "df83d91413a2e79f76924f39f26f59e6efbe8f5487d3a91b7e92b6d64236927c",
"https://deno.land/x/[email protected]/lib/zip-fs.js": "a733360302f5fbec9cc01543cb9fcfe7bae3f35a50d0006626ce42fe8183b63f",
"https://esm.sh/*@preact/[email protected]": "40fc366f6816cf9eb5a0e25aa79fd531b851a70f678f8d6b348feb43624c09e5",
"https://esm.sh/*@preact/[email protected]": "f1591d7185a00b6f96fdf5f72a99bb7dde37c0e946c8854da71db6b99d430947",
"https://esm.sh/*[email protected]": "88ec8d8706b6a3f1e0fdad3862a2690dcd9b350d87bdc8e7bd0e27fbc0f7d29e",
"https://esm.sh/[email protected]/": "16d82ee0a75451f75b42d9a20db4da0ccae7ccc8cc09a41c73b4488aba010b94",
"https://esm.sh/[email protected]/Button": "bc60923d511c6e2e33a7064339b3e643a9c15e3ef232ab063ef570af2ef83dc8",
"https://esm.sh/[email protected]/Checkbox": "bf34f5cd8c6d015916d854d91aab2caf115463e97be9a461f8dd3370ea11a49c",
"https://esm.sh/[email protected]/Dialog": "b0ff8da9c770456748f7e065fecda2fc90f5364ea66cae75ff5f51d57f6a87eb",
@@ -294,6 +297,12 @@
"https://esm.sh/v124/@material/[email protected]/deno/ripple.mjs": "df299ee5ebdca82a60414bd942f39bd08eddb47071b7e8e1bcb2ae87f73f2c94",
"https://esm.sh/v124/@material/[email protected]/deno/util.js": "4df787664fc517d4545bb7d502f85de3b34cfa613e34edd6b4630ea21e673cf2",
"https://esm.sh/v124/@material/[email protected]/deno/index.js": "de08ffa76efb31c7fe817ea4a6da471cfe19060f49a21c6cbe6aa9533dc95aa1",
"https://esm.sh/v124/@preact/[email protected]/X-ZS8q/deno/signals-core.mjs": "c2787059fe616fcc0504a99a77930723c582d2630cae69db695a8765d8ed426e",
"https://esm.sh/v124/@preact/[email protected]/X-ZS8q/dist/signals-core.d.ts": "e39c59670049b772b2c7748907af01f4fb5d5706b88fa8266d9135925ca5baf7",
"https://esm.sh/v124/@preact/[email protected]/X-ZS8q/deno/signals.mjs": "9107036efc63362c0d88965534957e5c9e3cc4c6da6f10f7a7dcb3e8a29fb624",
"https://esm.sh/v124/@preact/[email protected]/X-ZS8q/dist/signals.d.ts": "2c766d8d5911bcbb1aac4ac1da8965e830c54bcb96da8feb4f4b3e5dcd9ee02c",
"https://esm.sh/v124/@types/[email protected]/index.d.ts": "cc67c7ba07d35570b854f24fd490a6a42e5caa6e17cb12dd360f25ba65b78754",
"https://esm.sh/v124/[email protected]/deno/accept-language-parser.mjs": "ed949ec1b8f1d41f9b98cee8e8f9af8ce1300e5dae29a61c16b66ab16c7e35ec",
"https://esm.sh/v124/[email protected]/deno/bind-decorator.mjs": "21a126bdebaca6e38120139d903c3485c4f57e15487646666e79c1b5c15d0e44",
"https://esm.sh/v124/[email protected]/index.d.ts": "4c68749a564a6facdf675416d75789ee5a557afda8960e0803cf6711fa569288",
"https://esm.sh/v124/[email protected]/Base/MaterialComponent.d.ts": "2ea7a1ffbff6c43619f3f85d69a9e9a3c62575f409fc93ad651f6906adba47d0",

View File

@@ -13,6 +13,7 @@
"twind": "https://esm.sh/[email protected]",
"twind/": "https://esm.sh/[email protected]/",
"$std/": "https://deno.land/[email protected]/",
"preact-material-components/": "https://esm.sh/[email protected]/"
"preact-material-components/": "https://esm.sh/[email protected]/",
"accept-language-parser/": "https://esm.sh/[email protected]/"
}
}

View File

@@ -7,11 +7,17 @@ import TopAppBar from "preact-material-components/TopAppBar";
import StyleSheet from "../components/StyleSheet.tsx";
import { GlobalCtx } from "../components/GlobalContext.tsx";
import Settings from "./Settings.tsx";
import t, { i18n_map, I18NMap } from "../server/i18n.ts";
export default class Container extends Component {
export type ContainerProps = {
i18n: I18NMap;
};
export default class Container extends Component<ContainerProps> {
static contextType = GlobalCtx;
declare context: ContextType<typeof GlobalCtx>;
render() {
i18n_map.value = this.props.i18n;
const [display, set_display] = useState(false);
const [show_settings, set_show_settings] = useState(false);
const close_all = () => {
@@ -21,6 +27,7 @@ export default class Container extends Component {
return (
<div>
<Head>
<title>{t("common.title")}</title>
<GlobalCtx.Provider value={this.context}>
<StyleSheet href="https://fonts.googleapis.com/icon?family=Material+Icons" />
<StyleSheet href="preact-material-components/style.css" />
@@ -32,7 +39,7 @@ export default class Container extends Component {
<TopAppBar.Section align-start>
<TopAppBar.Icon navigation>menu</TopAppBar.Icon>
<TopAppBar.Title>
EH Downloader
{t("common.title")}
</TopAppBar.Title>
</TopAppBar.Section>
<TopAppBar.Section align-end>

View File

@@ -9,6 +9,7 @@ import { ConfigType } from "../config.ts";
import SettingsCheckbox from "../components/SettingsCheckbox.tsx";
import SettingsContext from "../components/SettingsContext.tsx";
import SettingsText from "../components/SettingsText.tsx";
import { i18n_map } from "../server/i18n.ts";
export type SettingsProps = {
show: boolean;

View File

@@ -1,15 +1,23 @@
import { Head } from "$fresh/runtime.ts";
import { Handlers, PageProps } from "$fresh/server.ts";
import GlobalContext from "../components/GlobalContext.tsx";
import Container from "../islands/Container.tsx";
import { get_i18nmap, i18n_handle_request } from "../server/i18ns.ts";
export default function Index() {
export const handler: Handlers<string> = {
GET(req, ctx) {
const re = i18n_handle_request(req);
if (typeof re === "string") return ctx.render(re);
return re;
},
};
export default function Index({ data }: PageProps<string>) {
const i18n = get_i18nmap(data);
return (
<body>
<GlobalContext>
<Head>
<title>EH Downloader</title>
</Head>
<Container />
<Container i18n={i18n} />
</GlobalContext>
</body>
);

View File

@@ -1,9 +1,10 @@
import { start } from "$fresh/server.ts";
import { RenderFunction, start } from "$fresh/server.ts";
import { load_settings } from "./config.ts";
import manifest from "./fresh.gen.ts";
import { TaskManager } from "./task_manager.ts";
import twindPlugin from "$fresh/plugins/twind.ts";
import twindConfig from "./twind.config.ts";
import { load_translation } from "./server/i18ns.ts";
let task_manager: TaskManager | undefined = undefined;
let cfg_path: string | undefined = undefined;
@@ -18,12 +19,21 @@ export function get_cfg_path() {
return cfg_path;
}
const renderFn: RenderFunction = (ctx, render) => {
const u = new URL(ctx.url);
const lang = u.searchParams.get("lang");
if (lang) ctx.lang = lang;
render();
};
export async function startServer(path: string) {
cfg_path = path;
const cfg = await load_settings(path);
task_manager = new TaskManager(cfg);
await load_translation(task_manager.aborts);
return start(manifest, {
signal: task_manager.aborts,
plugins: [twindPlugin(twindConfig)],
render: renderFn,
});
}

16
server/i18n.ts Normal file
View File

@@ -0,0 +1,16 @@
import { signal } from "@preact/signals";
export type I18NMap = { [x: string]: string | I18NMap };
export const i18n_map = signal<I18NMap>({});
const NOT_FOUND = "__NOT_FOUND__";
export default function t(key: string) {
const keys = key.split(".");
let map: string | I18NMap = i18n_map.value;
for (const k of keys) {
if (typeof map === "string") return NOT_FOUND;
else map = map[k];
}
return typeof map === "string" ? map : NOT_FOUND;
}

72
server/i18ns.ts Normal file
View File

@@ -0,0 +1,72 @@
import { exists } from "std/fs/exists.ts";
import { parse } from "std/jsonc/mod.ts";
import { join } from "std/path/mod.ts";
import { I18NMap } from "./i18n.ts";
import { pick } from "accept-language-parser/";
const whole_maps = new Map<string, I18NMap>();
const LANGUAGES = ["zh-cn"];
type MODULE = "common";
const MODULES: MODULE[] = ["common"];
export async function load_translation(signal?: AbortSignal) {
const base = import.meta.resolve("../translation").slice(8);
const enmap: I18NMap = {};
for (const m of MODULES) {
const t = await Deno.readTextFile(join(base, "en", m + ".jsonc"), {
signal,
});
const v = <I18NMap> <unknown> parse(t);
enmap[m] = v;
}
whole_maps.set("en", enmap);
for (const l of LANGUAGES) {
const map: I18NMap = {};
for (const m of MODULES) {
const p = join(base, l, m + ".jsonc");
if (await exists(p)) {
const t = await Deno.readTextFile(join(base, l, m + ".jsonc"), {
signal,
});
const v = <I18NMap> <unknown> Object.assign(
structuredClone(enmap[m]),
parse(t),
);
map[m] = v;
} else map[m] = enmap[m];
}
whole_maps.set(l, map);
}
}
export function get_i18nmap(lang: string, ...modules: MODULE[]) {
const m = whole_maps.get(lang);
if (!m) throw Error(`Language ${lang} is not supported.`);
if (!modules.length) return m;
const r: I18NMap = {};
for (const n of modules) {
r[n] = m[n];
}
return r;
}
export function i18n_handle_request(req: Request) {
const u = new URL(req.url);
const lang = u.searchParams.get("lang");
if (lang && (lang === "en" || LANGUAGES.includes(lang))) {
return lang;
} else {
const a = req.headers.get("Accept-Language");
const l = (a
? pick(LANGUAGES, a) || pick(LANGUAGES, a, { loose: true })
: null) || "en";
const params = new URLSearchParams();
params.append("lang", l);
for (const p of u.searchParams.entries()) {
if (p[0] !== "lang") {
params.append(p[0], p[1]);
}
}
return Response.redirect(`${u.origin}${u.pathname}?${params}`);
}
}

View File

@@ -0,0 +1,3 @@
{
"title": "EH Downloader"
}

View File

@@ -0,0 +1,3 @@
{
"title": "EH 下载器"
}