Skip to content

Commit

Permalink
feat(admin/src/utils): 添加 locales
Browse files Browse the repository at this point in the history
  • Loading branch information
caixw committed Jun 13, 2024
1 parent 9025bd8 commit 445a5a7
Show file tree
Hide file tree
Showing 17 changed files with 228 additions and 50 deletions.
2 changes: 1 addition & 1 deletion .github/workflows/vitest.yml
Original file line number Diff line number Diff line change
Expand Up @@ -24,7 +24,7 @@ jobs:
run: npm install

- name: Test
run: npm run test --coverage.enabled true
run: npm run test -w=admin --coverage.enabled true

- name: Report Coverage
# Set if: always() to also generate the report if tests are failing
Expand Down
2 changes: 2 additions & 0 deletions admin/src/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -6,7 +6,9 @@ export { create } from '@/app';

// utils
export type { Page, Problem, Return } from '@/utils/fetch';
export { build } from '@/utils/fetch';
export { sleep } from '@/utils/time';
export type { Locales } from '@/utils/locales';

// plugins
export { useAdmin } from '@/plugins';
Expand Down
File renamed without changes.
2 changes: 1 addition & 1 deletion admin/src/locales/locales.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2,4 +2,4 @@
//
// SPDX-License-Identifier: MIT

export const locales = ['en', 'zhHans'];
export const locales = ['en', 'cmn-Hans'];
17 changes: 17 additions & 0 deletions admin/src/locales/vuetify.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,17 @@
// SPDX-FileCopyrightText: 2024 caixw
//
// SPDX-License-Identifier: MIT

/**
* vuetify 与标准语言标签的充分一点关联
*/
export const maps: Map<string, string> = [
['cmn-Hans', 'zhHans'],
['zh-CN', 'zhHans'],
['zh', 'zhHans'],
['chi', 'zhHans'],
['zho', 'zhHans'],

['cmn-Hant', 'zhHant'],
['zh-TW', 'zhHant'],
];
23 changes: 14 additions & 9 deletions admin/src/plugins/admin/admin.ts
Original file line number Diff line number Diff line change
Expand Up @@ -6,13 +6,13 @@ import { Composer, createI18n } from 'vue-i18n';
import { useLocale } from 'vuetify';

import { locales } from '@/locales/locales';
import { Fetcher, Method, Return } from '@/utils/fetch';
import { build as buildFetch, Method, Return } from '@/utils/fetch';

import { Options, setLogo, setTitle } from './options';
import {Options, setLanguage, setLogo, setTitle} from './options';
import { MenuItem } from './page';

export class Admin {
readonly #fetcher: Fetcher;
readonly #fetcher: Awaited<ReturnType<typeof buildFetch>>;
readonly #footer?: Array<MenuItem>;
readonly #menus: Array<MenuItem>;
readonly #titleSeparator: string;
Expand All @@ -24,10 +24,8 @@ export class Admin {

/**
* 构造函数
*
* @param o 用于初始化的参数
*/
constructor(o: Required<Options>, f: Fetcher) {
constructor(o: Required<Options>, f: Awaited<ReturnType<typeof buildFetch>>) {
this.#fetcher = f;
this.#footer = o.page.footer;
this.#menus = o.page.menus;
Expand Down Expand Up @@ -71,7 +69,7 @@ export class Admin {
get footer(): Array<MenuItem> { return this.#footer ?? []; }

set locale(v: string) {
this.#fetcher.locale = v; // 改变 fetch 中的 accept-language
this.#fetcher.locales.current = v; // 改变 fetch 中的 accept-language

document.documentElement.lang = v; // 改变 html.lang

Expand All @@ -80,11 +78,18 @@ export class Admin {
current.value = v;

this.#i18n.locale.value = v;// 改变当前框架内部的语言

(async()=>{await setLanguage(v);})();
}
get locale(): string { return this.#fetcher.locale; }
get locale(): string { return this.#fetcher.locales.current; }

/**
* 返回支持的语言
*/
get supportedLanguages() { return this.#fetcher.locales.supportedNames(); }

get i18n(): Composer { return this.#i18n; }

//--------------------- 以下是对 Fetcher 各个方法的转发 ----------------------------

async isLogin(): Promise<boolean> { return await this.#fetcher.isLogin(); }
Expand Down
10 changes: 10 additions & 0 deletions admin/src/plugins/admin/options.spec.ts
Original file line number Diff line number Diff line change
Expand Up @@ -24,6 +24,8 @@ test('buildOptions', async () => {
await localforage.clear();

let o = await buildOptions({
languages: ['zh'],
language: 'zh',
api: api,
page: page,
title: 'title',
Expand All @@ -33,6 +35,8 @@ test('buildOptions', async () => {

await localforage.clear();
expect(buildOptions({
languages: ['zh'],
language: 'zh',
api: api,
page: page,
title: '',
Expand All @@ -41,6 +45,8 @@ test('buildOptions', async () => {

await localforage.clear();
expect(buildOptions({
languages: ['zh'],
language: 'zh',
api: api,
page: page,
title: 'title',
Expand All @@ -49,12 +55,16 @@ test('buildOptions', async () => {

await localforage.clear();
await buildOptions({
languages: ['zh'],
language: 'zh',
api: api,
page: page,
title: 'title',
logo: 'logo'
});
o = await buildOptions({
languages: ['zh'],
language: 'zh',
api: api,
page: page,
title: 't1',
Expand Down
30 changes: 28 additions & 2 deletions admin/src/plugins/admin/options.ts
Original file line number Diff line number Diff line change
Expand Up @@ -9,11 +9,24 @@ import { MenuItem, Page, checkPage } from './page';

const siteTitleName = 'site_title';
const logoName = 'logo';
const languageName = 'language';

/**
* 管理后台的基本配置
*/
export interface Options {
/**
* 支持的语言
*/
languages: Array<string>

/**
* 默认的语言
*
* 该值会被保存在 localforage 之中,如果在 localforage 存在值,则此设置将不启作用。
*/
language: string

/**
* 网站的标题
*
Expand Down Expand Up @@ -78,6 +91,14 @@ export async function buildOptions(o: Options): Promise<Required<Options>> {
if (o.logo.length === 0) {
throw 'logo 不能为空';
}

const lang = await localforage.getItem<string>(languageName);
if (lang) {
o.language = lang;
}
if (o.language.length === 0) {
throw 'language 不能为空';
}

const opt: Required<Options> = Object.assign({}, presetOptions, o);

Expand All @@ -88,8 +109,9 @@ export async function buildOptions(o: Options): Promise<Required<Options>> {
checkAPI(opt.api);
checkPage(opt.page);

await localforage.setItem(logoName, o.logo);
await localforage.setItem(siteTitleName, o.title);
await setLogo(o.logo);
await setTitle(o.title);
await setLanguage(o.language);

return opt;
}
Expand All @@ -101,3 +123,7 @@ export async function setLogo(logo: string) {
export async function setTitle(title: string) {
await localforage.setItem(siteTitleName, title);
}

export async function setLanguage(lang: string) {
await localforage.setItem(languageName, lang);
}
5 changes: 4 additions & 1 deletion admin/src/plugins/admin/plugins.ts
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,7 @@
import { App, inject, InjectionKey } from 'vue';

import { build } from '@/utils/fetch';
import {Locales} from '@/utils/locales/locales.ts';

import { Admin } from './admin';
import { buildOptions, Options } from './options';
Expand All @@ -16,7 +17,9 @@ import { buildOptions, Options } from './options';
*/
export async function createAdmin(o: Options) {
const opt = await buildOptions(o);
const f = await build(opt.api.base, opt.api.login, opt.mimetype, navigator.languages[0]);
const l = new Locales(opt.languages, opt.language);
const f = await build(opt.api.base, opt.api.login, opt.mimetype, l);

return {
install(app: App) { // NOTE: install 不能是异步
app.provide(key, new Admin(opt, f));
Expand Down
3 changes: 3 additions & 0 deletions admin/src/utils/README.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
# utils

当前目录下提供了部分与框架和组件库无关的可复用功能。
10 changes: 7 additions & 3 deletions admin/src/utils/fetch/fetch.spec.ts
Original file line number Diff line number Diff line change
Expand Up @@ -5,19 +5,23 @@
import { describe, expect, test } from 'vitest';

import { build } from './fetch';
import {Locales} from '@/utils/locales/locales';

describe('fetch', () => {
const l = new Locales(['zh-Hans', 'zh-cn'], 'zh-cn');

test('build', async () => {
expect(async () => {
await build('http://localhost', '/login', 'not-exists', 'zh-CN');
await build('http://localhost', '/login', 'not-exists', l);
}).rejects.toThrowError('不支持的 contentType not-exists');

const f = await build('http://localhost', '/login', 'application/json', 'zh-CN');
const f = await build('http://localhost', '/login', 'application/json', l);
expect(f).not.toBeNull();
expect(f.locales).not.toBeNull();
});

test('buildURL', async () => {
const f = await build('http://localhost', '/login', 'application/json', 'zh-CN');
const f = await build('http://localhost', '/login', 'application/json', l);
expect(f.buildURL('/path')).toEqual('http://localhost/path');
expect(f.buildURL('path')).toEqual('http://localhost/path');
expect(() => { f.buildURL(''); }).toThrowError('参数 path 不能为空');
Expand Down
50 changes: 18 additions & 32 deletions admin/src/utils/fetch/fetch.ts
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@
// SPDX-License-Identifier: MIT

import { delToken, getToken, state, Token, TokenState, writeToken } from './token.ts';
import { Locales } from '@/utils/locales/locales';

export type Method = 'GET' | 'POST' | 'PUT' | 'PATCH' | 'DELETE';

Expand All @@ -19,50 +20,49 @@ interface Serializer {
}

/**
* 构建 Fetcher 对象
* @param baseURL API 的基地址,不能以 / 结尾
* @param contentType mimetype 的类型
* 返回一个对 fetch 进行二次包装的对象
*
* @param baseURL API 的基地址,不能以 / 结尾。
* @param contentType mimetype 的类型。
* @param loginPath 相对于 baseURL 的登录地址,该地址应该包含 POST、DELETE 和 PUT 三个请求,分别代表登录、退出和刷新令牌。
* @param locale 本地化的标签
* @param locales 管理本地化的对象,由该参数确定 accept-language 报头的内容。
*/
export async function build(baseURL: string, loginPath: string, contentType: string, locale: string): Promise<Fetcher> {
export async function build(baseURL: string, loginPath: string, contentType: string, locales: Locales): Promise<Fetcher> {
const t = await getToken();
return new Fetcher(baseURL, loginPath, contentType, locale, t);
return new Fetcher(baseURL, loginPath, contentType, locales, t);
}

/**
* 对 fetch 的二次包装
*/
export class Fetcher {
class Fetcher {
readonly #baseURL: string;
readonly #loginPath: string;
#locale: string;
readonly #locales: Locales;
#token: Token | null;

readonly #contentType: string;
readonly #serializer: Serializer;

constructor(baseURL: string, loginPath: string, contentType: string, locale: string, token: Token | null) {
/**
* 构造函数,参数参考 build
*/
constructor(baseURL: string, loginPath: string, contentType: string, locales: Locales, token: Token | null) {
const s = serializers.get(contentType);
if (!s) {
throw `不支持的 contentType ${contentType}`;
}

this.#baseURL = baseURL;
this.#loginPath = loginPath;
this.#locale = locale;
this.#locales = locales;
this.#token = token;

this.#contentType = contentType;
this.#serializer = s;
}

/**
* 改变请求时的 accept-language 报头值
* @param v 需要符合 accept-language 的要求
* 返回关联的 Locales 对象
*/
set locale(v: string) { this.#locale = v; }
get locale(): string { return this.#locale; }
get locales(): Locales { return this.#locales; }

/**
* 将 path 包装为一个 API 的 URL
Expand All @@ -82,9 +82,6 @@ export class Fetcher {

/**
* DELETE 请求
*
* @param path 相对于 baseURL 的地址
* @param withToken 是否带上令牌,可参考 request
*/
async delete(path: string, withToken = true): Promise<Return> {
return this.request(path, 'DELETE', undefined, withToken);
Expand All @@ -103,31 +100,20 @@ export class Fetcher {

/**
* PUT 请求
*
* @param path 相对于 baseURL 的地址
* @param body 上传的数据,若没有则为空
* @param withToken 是否带上令牌,可参考 request
*/
async put(path: string, body?: unknown, withToken = true): Promise<Return> {
return this.request(path, 'PUT', body, withToken);
}

/**
* PATCH 请求
*
* @param path 相对于 baseURL 的地址
* @param body 上传的数据,若没有则为空
* @param withToken 是否带上令牌,可参考 request
*/
async patch(path: string, body?: unknown, withToken = true): Promise<Return> {
return this.request(path, 'PATCH', body, withToken);
}

/**
* GET 请求
*
* @param path 相对于 baseURL 的路由地址;
* @param withToken 是否带上令牌,可参考 request;
*/
async get(path: string, withToken = true): Promise<Return> {
return this.request(path, 'GET', undefined, withToken);
Expand Down Expand Up @@ -216,7 +202,7 @@ export class Fetcher {
async withArgument(path: string, method: Method, token?: string, ct?: string, body?: BodyInit): Promise<Return> {
const h = new Headers({
'Accept': this.#contentType + '; charset=UTF-8',
'Accept-Language': this.#locale,
'Accept-Language': this.#locales.current,
});
if (token) {
h.set('Authorization', token);
Expand Down
2 changes: 1 addition & 1 deletion admin/src/utils/fetch/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,7 @@
//
// SPDX-License-Identifier: MIT

export { Fetcher, build } from './fetch';
export { build } from './fetch';
export type { Method, Page, Problem, Return } from './fetch';
export type { Token } from './token';

5 changes: 5 additions & 0 deletions admin/src/utils/locales/index.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
// SPDX-FileCopyrightText: 2024 caixw
//
// SPDX-License-Identifier: MIT

export { Locales } from './locales';
Loading

0 comments on commit 445a5a7

Please sign in to comment.