Skip to content

Commit

Permalink
feat(pages): add i18n configuration (#11)
Browse files Browse the repository at this point in the history
  • Loading branch information
Cheerego7 authored Oct 27, 2024
1 parent 37e447b commit d98d778
Show file tree
Hide file tree
Showing 13 changed files with 225 additions and 32 deletions.
13 changes: 12 additions & 1 deletion .vscode/settings.json
Original file line number Diff line number Diff line change
Expand Up @@ -47,5 +47,16 @@
"scss",
"pcss",
"postcss"
]
],

// Enable i18n-ally
"i18n-ally.localesPaths": "desktop/renderer/src/locales",
"i18n-ally.pathMatcher": "{locale}.json",
"i18n-ally.displayLanguage": "zh-CN",
"i18n-ally.namespace": false,
"i18n-ally.keystyle": "nested",
"i18n-ally.sortKeys": true,
"i18n-ally.keepFulfilled": true,
"i18n-ally.enabledFrameworks": ["react"],
"i18n-ally.enabledParsers": ["json"],
}
4 changes: 3 additions & 1 deletion desktop/renderer/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -23,7 +23,9 @@
"react-dom": "^18.3.1",
"react-router-dom": "^6.27.0",
"value-enhancer": "overridden",
"use-value-enhancer": "overridden"
"use-value-enhancer": "overridden",
"val-i18n": "^0.1.11",
"val-i18n-react": "^0.1.5"
},
"devDependencies": {
"@types/js-yaml": "^4.0.9",
Expand Down
21 changes: 15 additions & 6 deletions desktop/renderer/src/App.tsx
Original file line number Diff line number Diff line change
@@ -1,8 +1,10 @@
import type { PropsWithChildren } from "react";
import React from "react";
import { useVal } from "use-value-enhancer";
import { I18nProvider } from "val-i18n-react";
import { AppContextProvider } from "./components/AppContextProvider";
import { ThemeProvider } from "./components/ThemeProvider";
import { useI18nLoader } from "./hooks";
import { type AppContext, Routes } from "./routes";

export interface StudioHomeProps {
Expand All @@ -14,15 +16,22 @@ export const StudioHome = ({
children,
}: PropsWithChildren<StudioHomeProps>) => {
const prefersColorScheme = useVal(appContext.settingStore.prefersColorScheme$);
const i18n = useI18nLoader(appContext.settingStore.localLanguage$);

if (!i18n) {
return null; // blank page
}

return (
<AppContextProvider context={appContext}>
<ThemeProvider
prefersColorScheme={prefersColorScheme}
>
<Routes />
{children}
</ThemeProvider>
<I18nProvider i18n={i18n}>
<ThemeProvider
prefersColorScheme={prefersColorScheme}
>
<Routes />
{children}
</ThemeProvider>
</I18nProvider>
</AppContextProvider>
);
};
14 changes: 6 additions & 8 deletions desktop/renderer/src/components/AppearancePicker/index.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -4,8 +4,9 @@ import type { OOMOLPrefersColorScheme } from "../ThemeProvider";
import { Radio } from "antd";

import React from "react";
import styles from "./AppearancePicker.module.scss";
import { useTranslate } from "val-i18n-react";

import styles from "./AppearancePicker.module.scss";
import autoSVG from "./icons/auto.svg";
import darkSVG from "./icons/dark.svg";
import lightSVG from "./icons/light.svg";
Expand All @@ -19,29 +20,26 @@ export const AppearancePicker: React.FC<AppearancePickerProps> = ({
defaultValue,
changeAppearance,
}) => {
// const t = useTranslate();
const t = useTranslate();
return (
<div className={styles.container}>
<Radio.Group defaultValue={defaultValue} onChange={changeAppearance}>
<Radio value="light">
<div className={styles.options}>
<img src={lightSVG} />
{/* <span>{t("settings.theme-light")}</span> */}
<span>light</span>
<span>{t("settings.theme-light")}</span>
</div>
</Radio>
<Radio value="dark">
<div className={styles.options}>
<img src={darkSVG} />
{/* <span>{t("settings.theme-dark")}</span> */}
<span>dark</span>
<span>{t("settings.theme-dark")}</span>
</div>
</Radio>
<Radio value="auto">
<div className={styles.options}>
<img src={autoSVG} />
{/* <span>{t("settings.theme-auto")}</span> */}
<span>auto</span>
<span>{t("settings.theme-auto")}</span>
</div>
</Radio>
</Radio.Group>
Expand Down
55 changes: 55 additions & 0 deletions desktop/renderer/src/hooks/i18n-loader.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,55 @@
import type { Locale, LocaleLang } from "val-i18n";
import type { ReadonlyVal } from "value-enhancer";

import { useEffect } from "react";
import { detectLang, I18n } from "val-i18n";
import { joinDisposers } from "~/misc/utils";
import { useAsyncMemo } from "./use-async-memo";

const i18nLoader = (lang?: string): Promise<I18n> => {
const localeModules = import.meta.glob<boolean, string, Locale>(
"~/locales/*.json",
{
import: "default",
},
);

const localeLoaders = Object.keys(localeModules).reduce(
(loaders, path) => {
if (localeModules[path]) {
const langMatch = path.match(/\/([^/]+)\.json$/);
if (langMatch) {
loaders[langMatch[1]] = localeModules[path];
}
}
return loaders;
},
{} as Record<LocaleLang, () => Promise<Locale>>,
);

const langs = Object.keys(localeLoaders);

return I18n.preload(
lang && localeLoaders[lang]
? lang
: detectLang(langs) || (localeLoaders.en ? "en" : langs[0]),
lang => localeLoaders[lang](),
);
};

export const useI18nLoader = (
localeLang$: ReadonlyVal<string | undefined>,
): I18n | undefined => {
const maybeI18n = useAsyncMemo(() => i18nLoader(localeLang$.value), []);
useEffect(
() =>
joinDisposers(
localeLang$.subscribe(lang => lang && void maybeI18n?.switchLang(lang)),
maybeI18n?.lang$.subscribe((lang) => {
document.documentElement.lang = lang;
}),
),
[maybeI18n, localeLang$],
);
return maybeI18n;
};
2 changes: 2 additions & 0 deletions desktop/renderer/src/hooks/index.ts
Original file line number Diff line number Diff line change
@@ -1,2 +1,4 @@
export * from "./i18n-loader";
export * from "./use-app-context";
export * from "./use-async-memo";
export * from "./use-isomorphic-layout-effect";
36 changes: 36 additions & 0 deletions desktop/renderer/src/hooks/use-async-memo.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,36 @@
import type { DependencyList } from "react";

import { useState } from "react";
import { isPromise } from "~/misc/utils";

import { useIsomorphicLayoutEffect } from "./use-isomorphic-layout-effect";

export const useAsyncMemo = <T>(
effect: (oldValue: T | undefined) => Promise<T> | T,
deps: DependencyList = [],
): T | undefined => {
const [value, setValue] = useState<T | undefined>();

useIsomorphicLayoutEffect(() => {
let isValid = true;

const maybePromise = effect(value);

if (isPromise(maybePromise)) {
void maybePromise.then((value) => {
if (isValid) {
setValue(value);
}
});
}
else {
setValue(maybePromise);
}

return () => {
isValid = false;
};
}, deps);

return value;
};
10 changes: 10 additions & 0 deletions desktop/renderer/src/locales/en.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,10 @@
{
"settings": {
"appearance": "Appearance",
"chinese": "Chinese",
"language": "Language",
"theme-light": "Light",
"theme-dark": "Dark",
"theme-auto": "Auto"
}
}
2 changes: 2 additions & 0 deletions desktop/renderer/src/locales/index.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,2 @@
export { default as en } from "./en.json";
export { default as zhCN } from "./zh-CN.json";
10 changes: 10 additions & 0 deletions desktop/renderer/src/locales/zh-CN.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,10 @@
{
"settings": {
"appearance": "外观",
"chinese": "中文",
"language": "语言",
"theme-light": "浅色",
"theme-dark": "深色",
"theme-auto": "自动"
}
}
22 changes: 22 additions & 0 deletions desktop/renderer/src/misc/utils.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,22 @@
export const isPromise = <T>(
value: T | PromiseLike<T>,
): value is PromiseLike<T> =>
value && typeof (value as PromiseLike<T>).then === "function";

export const noop = () => {};

const invoke = (fn?: (() => unknown) | null): void => {
try {
fn?.();
}
catch (e) {
console.error(e);
}
};

export const joinDisposers
= (
...disposers: ReadonlyArray<(() => void) | undefined | null>
): (() => void) =>
() =>
disposers.forEach(invoke);
40 changes: 24 additions & 16 deletions desktop/renderer/src/routes/Settings/index.tsx
Original file line number Diff line number Diff line change
@@ -1,55 +1,63 @@
import type { CheckboxChangeEvent } from "antd/es/checkbox";
import { Radio } from "antd";
import React from "react";

import { useVal } from "use-value-enhancer";
import { useLang, useTranslate } from "val-i18n-react";
import { AppearancePicker } from "~/components/AppearancePicker";
import type { OOMOLPrefersColorScheme } from "~/components/ThemeProvider";
import { useAppContext } from "~/hooks";

import styles from "./index.module.scss";

enum SelectLanguage {
Chinese = "zh-CN",
English = "en",
}

type Lang = "en" | "zh-CN";

export const Settings = () => {
// const t = useTranslate();
// const language = useLang();
const t = useTranslate();
const language = useLang();
const { settingStore } = useAppContext();

const prefersColorScheme = useVal(settingStore.prefersColorScheme$);

// const [selectLanguage, setSelectLanguage] = useState<Lang>(language as Lang);

const changeAppearance = (event: CheckboxChangeEvent) => {
const prefersColorScheme: OOMOLPrefersColorScheme = event.target.value;
settingStore.updatePrefersColorScheme(prefersColorScheme);
};

const changeLanguage = async (event: CheckboxChangeEvent) => {
const lang: Lang = event.target.value;
settingStore.updateLocalLanguage(lang);
};

return (
<div className={styles.container}>
{/* <p className={styles.label}>{t("setting.appearance")}</p> */}
<p className={styles.label}>Appearance</p>
<p className={styles.label}>{t("settings.appearance")}</p>
<AppearancePicker
defaultValue={prefersColorScheme}
changeAppearance={changeAppearance}
/>
{/* <div className={styles.languageSetting}> */}
{/* <span className={styles.label}>{t("setting.language")}</span> */}
{/* <Radio.Group
value={language}
<div className={styles.languageSetting}>
<span className={styles.label}>{t("settings.language")}</span>
<Radio.Group
defaultValue={
language === "zh-CN"
? SelectLanguage.Chinese
: SelectLanguage.English
}
onChange={openRestartModal}
rootClassName={styles.radioGroup}
onChange={changeLanguage}
>
<Radio value={SelectLanguage.Chinese}>
<span>{t("setting.chinese")}</span>
<span>{t("settings.chinese")}</span>
</Radio>
<Radio value={SelectLanguage.English}>
<span>English</span>
</Radio>
</Radio.Group> */}
{/* </div> */}
</Radio.Group>
</div>
</div>
);
};
28 changes: 28 additions & 0 deletions pnpm-lock.yaml

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

0 comments on commit d98d778

Please sign in to comment.