Skip to content

Commit

Permalink
Merge branch 'dev.admin/locale'
Browse files Browse the repository at this point in the history
添加了 core/locale 用于处理本地化相关的功能。
移除了原来的 solid-primitives/i18n。
  • Loading branch information
caixw committed Sep 23, 2024
2 parents fef27a5 + 334cafc commit cd70a8e
Show file tree
Hide file tree
Showing 54 changed files with 895 additions and 315 deletions.
12 changes: 10 additions & 2 deletions admin/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -43,6 +43,10 @@
"require": "./lib/components.cjs.js",
"import": "./lib/components.es.js"
},
"./messages/": {
"require": "./lib/messages/",
"import": "./lib/messages/"
},
"./tailwind.preset.ts": {
"require": "./tailwind.preset.ts",
"import": "./tailwind.preset.ts"
Expand All @@ -66,19 +70,23 @@
"./dev/components": {
"require": "./src/components/index.ts",
"import": "./src/components/index.ts"
},
"./dev/messages/": {
"require": "./src/messages/",
"import": "./src/messages/"
}
},
"scripts": {
"build": "vite build",
"test": "vitest --coverage.enabled true"
},
"dependencies": {
"@formatjs/intl-durationformat": "^0.2.4",
"@formatjs/intl-localematcher": "^0.5.4",
"@solid-primitives/i18n": "^2.1.1",
"@solidjs/router": "^0.14.5",
"intl-messageformat": "^10.5.14",
"localforage": "^1.10.0",
"material-symbols": "^0.23.0",
"pretty-bytes": "^6.1.1",
"quill": "^2.0.2",
"solid-js": "^1.8.22",
"xlsx": "https://cdn.sheetjs.com/xlsx-0.20.3/xlsx-0.20.3.tgz"
Expand Down
6 changes: 5 additions & 1 deletion admin/src/app/app.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -7,7 +7,7 @@ import { JSX, Show, createSignal } from 'solid-js';
import { render } from 'solid-js/web';

import { Drawer, Notify, SystemDialog } from '@/components';
import { API, initTheme } from '@/core';
import { API, Locale, initTheme } from '@/core';
import { buildContext } from './context';
import * as errors from './errors';
import { Options, build as buildOptions } from './options';
Expand All @@ -25,6 +25,10 @@ import { default as Toolbar } from './toolbar';
export async function create(elementID: string, o: Options) {
const opt = buildOptions(o);
const f = await API.build(opt.api.base, opt.api.login, opt.mimetype, opt.locales.fallback);
Locale.init(opt.locales.fallback, f);
for(const item of Object.entries(opt.locales.messages)) {
await Locale.addDict(item[0], ...item[1]);
}

render(() => {
initTheme(opt.theme.mode,opt.theme.scheme, opt.theme.contrast);
Expand Down
38 changes: 17 additions & 21 deletions admin/src/app/context/context.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -3,13 +3,12 @@
// SPDX-License-Identifier: MIT

import { useNavigate } from '@solidjs/router';
import { JSX, createContext, createSignal, useContext } from 'solid-js';
import localforage from 'localforage';
import { JSX, createContext, createResource, createSignal, useContext } from 'solid-js';

import { Options as buildOptions } from '@/app/options';
import { NotifyType } from '@/components/notify';
import { API, Account, Breakpoint, Breakpoints, Method, Problem, notify } from '@/core';
import { Locale, createI18n, names } from '@/locales';
import localforage from 'localforage';
import { API, Account, Breakpoint, Breakpoints, Locale, Method, Problem, notify } from '@/core';
import { createUser } from './user';

type Options = Required<buildOptions>;
Expand Down Expand Up @@ -43,10 +42,13 @@ export function useOptions(): Options {
}

export function buildContext(opt: Required<buildOptions>, f: API) {
const { getLocale, setLocale, t } = createI18n(opt.locales.fallback, opt.locales.messages);

const [user, { refetch }] = createUser(f, opt.api.info);

const [localeGetter, localeSetter] = createSignal<string>(navigator.language);
const [locale] = createResource(localeGetter, (info) => {
return Locale.build(info);
});

const [bp, setBP] = createSignal<Breakpoint>('xs');
const breakpoints = new Breakpoints();
breakpoints.onChange((val) => { setBP(val); });
Expand All @@ -63,13 +65,18 @@ export function buildContext(opt: Required<buildOptions>, f: API) {

// localStorage
await localforage.clear(async(err)=>{
await this.notify(t('_i.error.unknownError')!, err);
if (err) {
await this.notify(locale()!.t('_i.error.unknownError')!, err);
}
});

localStorage.clear();
sessionStorage.clear();

// TODO IndexedDB

const nav = useNavigate();
nav(opt.routes.public.home);
},

/**
Expand Down Expand Up @@ -197,26 +204,15 @@ export function buildContext(opt: Required<buildOptions>, f: API) {
* @param timeout 如果大于 0,超过此秒数时将自动关闭提法;
*/
async notify(title: string, body?: string, type: NotifyType = 'error', timeout = 5) {
if (opt.system.notification && await notify(title, body, opt.logo, getLocale(), timeout)) {
if (opt.system.notification && await notify(title, body, opt.logo, locale()!.locale.language, timeout)) {
return;
}
await window.notify(title, body, type, timeout);
},

// 以下为本地化相关功能
locale(): Locale { return locale()!; },

t,
get locale() { return getLocale(); },
set locale(v: Locale) {
document.documentElement.lang = v;
setLocale(v);
f.locale = v;
},

/**
* 返回支持的本地化列表
*/
get locales() { return names; }
switchLocale(id: string) { localeSetter(id); },
};

const Provider = (props: { children: JSX.Element }) => {
Expand Down
14 changes: 7 additions & 7 deletions admin/src/app/errors.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -12,9 +12,9 @@ export function NotFound() {
const opt = useOptions();
const nav = useNavigate();

return <XError header="404" title={ctx.t('_i.error.pageNotFound')}>
<Button palette='primary' onClick={() => { nav(opt.routes.private.home); }}>{ ctx.t('_i.error.backHome') }</Button>
<Button palette='primary' onClick={() => { nav(-1); }}>{ ctx.t('_i.error.backPrev') }</Button>
return <XError header="404" title={ctx.locale().t('_i.error.pageNotFound')}>
<Button palette='primary' onClick={() => { nav(opt.routes.private.home); }}>{ ctx.locale().t('_i.error.backHome') }</Button>
<Button palette='primary' onClick={() => { nav(-1); }}>{ ctx.locale().t('_i.error.backPrev') }</Button>
</XError>;
}

Expand All @@ -35,9 +35,9 @@ export function Unknown(err: any) {
props.title = err.toString();
}

return <XError header={props.header ?? ctx.t('_i.error.unknownError')} title={props.title} detail={props.detail}>
<Button palette='primary' onClick={() => { nav(opt.routes.private.home); }}>{ ctx.t('_i.error.backHome') }</Button>
<Button palette='primary' onClick={() => { nav(-1); }}>{ ctx.t('_i.error.backPrev') }</Button>
<Button palette='primary' onClick={() => window.location.reload()}>{ctx.t('_i.refresh')}</Button>
return <XError header={props.header ?? ctx.locale().t('_i.error.unknownError')} title={props.title} detail={props.detail}>
<Button palette='primary' onClick={() => { nav(opt.routes.private.home); }}>{ ctx.locale().t('_i.error.backHome') }</Button>
<Button palette='primary' onClick={() => { nav(-1); }}>{ ctx.locale().t('_i.error.backPrev') }</Button>
<Button palette='primary' onClick={() => window.location.reload()}>{ctx.locale().t('_i.refresh')}</Button>
</XError>;
}
2 changes: 1 addition & 1 deletion admin/src/app/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -8,4 +8,4 @@ export type { MenuItem, Options, Route, Routes } from './options';
export { useApp } from './context';
export type { AppContext } from './context';

export type { Messages as InternalMessages, KeyOfMessage, Locale, MessageKey, T } from '@/locales';
export type { Messages as InternalMessages } from '@/messages';
4 changes: 2 additions & 2 deletions admin/src/app/options/options.spec.ts
Original file line number Diff line number Diff line change
Expand Up @@ -18,8 +18,8 @@ test('build', async () => {

const locales: Locales = {
messages: {
'en': async () => { return (await import('@/locales/en')).default; },
'cmn-Hans': async () => { return (await import('@/locales/cmn-Hans')).default; },
'en': [async () => { return (await import('@/messages/en')).default; }],
'cmn-Hans': [async () => { return (await import('@/messages/cmn-Hans')).default; }],
},
locales: ['en', 'cmn-Hans'],
fallback: 'en'
Expand Down
22 changes: 11 additions & 11 deletions admin/src/app/options/options.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2,10 +2,8 @@
//
// SPDX-License-Identifier: MIT

import { BaseDict } from '@solid-primitives/i18n';

import { Contrast, Mimetype, Mode, Scheme } from '@/core';
import type { Locale, LocaleMessages } from '@/locales';
import { Contrast, DictLoader, Mimetype, Mode, Scheme } from '@/core';
import type { LocaleID } from '@/messages';
import { API, checkAPI } from './api';
import type { MenuItem, Routes } from './route';

Expand Down Expand Up @@ -80,21 +78,23 @@ interface System {

export interface Locales {
/**
* 本地化 ID 及对应内容的加载函数
* 指定本地化文本的加载方式
*
* 并不会自动加载内置的本地化对象,也需要在此指定。
*/
messages: LocaleMessages<BaseDict>;
messages: Record<string, Array<DictLoader>>;

/**
* 支持的语言
*
* 需要确保这些指定的语言可以通过 {@link Locales#loader} 正常加载内容。
*/
locales: Array<Locale>
locales: Array<LocaleID>;

/**
* 在找不到语言时的默认项
* 备用的本地化 ID
*
* 在所需的本地化 ID 无法找到时,会采用该值。
*/
fallback: Locale
fallback: LocaleID;
}

export interface Theme {
Expand Down
15 changes: 7 additions & 8 deletions admin/src/app/private.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -6,8 +6,7 @@ import { Navigate } from '@solidjs/router';
import { Accessor, createEffect, createMemo, ErrorBoundary, Match, ParentProps, Setter, Show, Switch } from 'solid-js';

import { Drawer, Item, List } from '@/components';
import { Breakpoint, Breakpoints } from '@/core';
import { T } from '@/locales';
import { Breakpoint, Breakpoints, Locale } from '@/core';
import { useApp, useOptions } from './context';
import * as errors from './errors';
import { MenuItem } from './options/route';
Expand Down Expand Up @@ -40,13 +39,13 @@ export function Private(props: Props) {
main={
<ErrorBoundary fallback={err=>errors.Unknown(err)}>{props.children}</ErrorBoundary>
}>
<List anchor>{buildItems(ctx.t, opt.menus)}</List>
<List anchor>{buildItems(ctx.locale(), opt.menus)}</List>
</Drawer>
</Match>
</Switch>;
}

function buildItems(t: T, menus: Array<MenuItem>) {
function buildItems(l: Locale, menus: Array<MenuItem>) {
const items: Array<Item> = [];
menus.forEach((mi) => {
switch (mi.type) {
Expand All @@ -56,8 +55,8 @@ function buildItems(t: T, menus: Array<MenuItem>) {
case 'group':
items.push({
type: 'group',
label: t(mi.label as any) ?? mi.label,
items: buildItems(t, mi.items)
label: l.t(mi.label),
items: buildItems(l, mi.items)
});
break;
case 'item':
Expand All @@ -67,13 +66,13 @@ function buildItems(t: T, menus: Array<MenuItem>) {
<Show when={mi.icon}>
<span class="c--icon">{mi.icon}</span>
</Show>
{t(mi.label as any) ?? mi.label}
{l.t(mi.label)}
</span>,
accesskey: mi.accesskey,
value: mi.path
};
if (mi.items) {
i.items = buildItems(t, mi.items);
i.items = buildItems(l, mi.items);
}

items.push(i);
Expand Down
30 changes: 15 additions & 15 deletions admin/src/app/settings.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -4,22 +4,22 @@

import { Choice, Divider, FieldAccessor, Options, RadioGroup } from '@/components';
import { changeContrast, changeMode, changeScheme, Contrast, genScheme, getContrast, getMode, getScheme, Mode } from '@/core/theme';
import { Locale } from '@/locales';
import { useApp } from './context';
import { useApp, useOptions } from './context';

const schemesSize = 15;

export default function() {
const ctx = useApp();
const opt = useOptions();

const modeFA = FieldAccessor<Mode>('mode', getMode('system'));
modeFA.onChange((m) => { changeMode(m); });

const contrastFA = FieldAccessor<Contrast>('contrast', getContrast('nopreference'));
contrastFA.onChange((m) => { changeContrast(m); });

const localeFA = FieldAccessor<Locale>('locale', ctx.locale, false);
localeFA.onChange((v) => { ctx.locale = v; });
const localeFA = FieldAccessor<string>('locale', ctx.locale().match(opt.locales.locales), false);
localeFA.onChange((v) => { ctx.switchLocale(v); });

const schemesOptions: Options<number> = [];
for (let i = 0; i < schemesSize; i++) {
Expand All @@ -39,38 +39,38 @@ export default function() {

return <div class="app-settings">
<RadioGroup vertical accessor={modeFA}
label={ <Label icon="settings_night_sight" title={ ctx.t('_i.theme.mode')! } desc={ ctx.t('_i.theme.modeDesc')! } /> }
label={ <Label icon="settings_night_sight" title={ ctx.locale().t('_i.theme.mode')! } desc={ ctx.locale().t('_i.theme.modeDesc')! } /> }
options={[
['system', <><span class="c--icon mr-2">brightness_6</span>{ctx.t('_i.theme.system')}</>],
['dark', <><span class="c--icon mr-2">dark_mode</span>{ctx.t('_i.theme.dark')}</>],
['light', <><span class="c--icon mr-2">light_mode</span>{ctx.t('_i.theme.light')}</>]
['system', <><span class="c--icon mr-2">brightness_6</span>{ctx.locale().t('_i.theme.system')}</>],
['dark', <><span class="c--icon mr-2">dark_mode</span>{ctx.locale().t('_i.theme.dark')}</>],
['light', <><span class="c--icon mr-2">light_mode</span>{ctx.locale().t('_i.theme.light')}</>]
]}
/>

<Divider />

<RadioGroup vertical accessor={contrastFA}
label={ <Label icon="contrast" title={ ctx.t('_i.theme.contrast')! } desc={ ctx.t('_i.theme.contrastDesc')! } /> }
label={ <Label icon="contrast" title={ ctx.locale().t('_i.theme.contrast')! } desc={ ctx.locale().t('_i.theme.contrastDesc')! } /> }
options={[
['more', <><span class="c--icon mr-2">exposure_plus_1</span>{ctx.t('_i.theme.more')}</>],
['nopreference', <><span class="c--icon mr-2">exposure_zero</span>{ctx.t('_i.theme.nopreference')}</>],
['less', <><span class="c--icon mr-2">exposure_neg_1</span>{ctx.t('_i.theme.less')}</>]
['more', <><span class="c--icon mr-2">exposure_plus_1</span>{ctx.locale().t('_i.theme.more')}</>],
['nopreference', <><span class="c--icon mr-2">exposure_zero</span>{ctx.locale().t('_i.theme.nopreference')}</>],
['less', <><span class="c--icon mr-2">exposure_neg_1</span>{ctx.locale().t('_i.theme.less')}</>]
]}
/>

<Divider />

<RadioGroup accessor={schemeFA} icon = {false} options={schemesOptions}
label={ <Label icon="palette" title={ ctx.t('_i.theme.color')! } desc={ ctx.t('_i.theme.colorDesc')! } /> }
label={ <Label icon="palette" title={ ctx.locale().t('_i.theme.color')! } desc={ ctx.locale().t('_i.theme.colorDesc')! } /> }
/>

<Divider />

<fieldset>
<legend>
<Label icon="translate" title={ ctx.t('_i.locale.locale')! } desc={ ctx.t('_i.locale.localeDesc')! } />
<Label icon="translate" title={ ctx.locale().t('_i.locale.locale')! } desc={ ctx.locale().t('_i.locale.localeDesc')! } />
</legend>
<Choice accessor={localeFA} options={ctx.locales} />
<Choice accessor={localeFA} options={ctx.locale().locales} />
</fieldset>
</div>;
}
Expand Down
6 changes: 3 additions & 3 deletions admin/src/app/toolbar.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -39,7 +39,7 @@ export default function Toolbar(props: Props) {
<div class="flex gap-2">
<Fullscreen />

<Button icon type="button" style='flat' title={ctx.t('_i.settings')} rounded
<Button icon type="button" style='flat' title={ctx.locale().t('_i.settings')} rounded
onClick={() =>props.settingsVisibleSetter(!props.settingsVisibleGetter())}>
settings
</Button>
Expand Down Expand Up @@ -72,7 +72,7 @@ function Username(): JSX.Element {
{
type: 'item',
value: 'logout',
label: ctx.t('_i.login.logout')
label: ctx.locale().t('_i.login.logout')
}
]}</Menu>
</Show>;
Expand All @@ -95,7 +95,7 @@ function Fullscreen(): JSX.Element {
});
};

return <Button icon type="button" style='flat' rounded onClick={toggleFullscreen} title={ctx.t('_i.fullscreen')}>
return <Button icon type="button" style='flat' rounded onClick={toggleFullscreen} title={ctx.locale().t('_i.fullscreen')}>
{fs() ? 'fullscreen_exit' : 'fullscreen'}
</Button>;
}
6 changes: 3 additions & 3 deletions admin/src/components/button/confirm.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -71,10 +71,10 @@ export default function(props: Props) {
pop.style.left = rect.left + 'px';
}}>{props.children}</Button>
<div popover="manual" ref={el=>pop=el} classList={{'c--confirm-button-panel':true, [`palette--${props.palette}`]:!!props.palette }}>
{props.prompt ?? ctx.t('_i.areYouSure')}
{props.prompt ?? ctx.locale().t('_i.areYouSure')}
<div class="actions">
<Button palette='secondary' onClick={() => pop.hidePopover()}>{props.cancel ?? ctx.t('_i.cancel')}</Button>
<Button palette='primary' autofocus onClick={confirm}>{props.ok ?? ctx.t('_i.ok')}</Button>
<Button palette='secondary' onClick={() => pop.hidePopover()}>{props.cancel ?? ctx.locale().t('_i.cancel')}</Button>
<Button palette='primary' autofocus onClick={confirm}>{props.ok ?? ctx.locale().t('_i.ok')}</Button>
</div>
</div>
</>;
Expand Down
Loading

0 comments on commit cd70a8e

Please sign in to comment.