mirror of
https://github.com/lifegpc/eh-downloader.git
synced 2026-06-06 05:38:44 +08:00
Add i18n support
This commit is contained in:
@@ -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
9
deno.lock
generated
@@ -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",
|
||||
|
||||
@@ -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]/"
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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>
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -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>
|
||||
);
|
||||
|
||||
12
server.ts
12
server.ts
@@ -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
16
server/i18n.ts
Normal 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
72
server/i18ns.ts
Normal 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}`);
|
||||
}
|
||||
}
|
||||
3
translation/en/common.jsonc
Normal file
3
translation/en/common.jsonc
Normal file
@@ -0,0 +1,3 @@
|
||||
{
|
||||
"title": "EH Downloader"
|
||||
}
|
||||
3
translation/zh-cn/common.jsonc
Normal file
3
translation/zh-cn/common.jsonc
Normal file
@@ -0,0 +1,3 @@
|
||||
{
|
||||
"title": "EH 下载器"
|
||||
}
|
||||
Reference in New Issue
Block a user