diff options
| author | syuilo <Syuilotan@yahoo.co.jp> | 2024-01-20 09:53:29 +0900 |
|---|---|---|
| committer | syuilo <Syuilotan@yahoo.co.jp> | 2024-01-20 09:53:29 +0900 |
| commit | 91522381b65f45d6682bee0b4c816c7f72e5c28e (patch) | |
| tree | fe20b4b2a1353d723c82c9d5bc5b655604591a0e /packages/frontend/src/scripts | |
| parent | refactor: extract bubble-game engine as independent package (diff) | |
| parent | refactor: deprecate i18n.t (#13039) (diff) | |
| download | sharkey-91522381b65f45d6682bee0b4c816c7f72e5c28e.tar.gz sharkey-91522381b65f45d6682bee0b4c816c7f72e5c28e.tar.bz2 sharkey-91522381b65f45d6682bee0b4c816c7f72e5c28e.zip | |
Merge branch 'develop' of https://github.com/misskey-dev/misskey into develop
Diffstat (limited to 'packages/frontend/src/scripts')
| -rw-r--r-- | packages/frontend/src/scripts/get-drive-file-menu.ts | 2 | ||||
| -rw-r--r-- | packages/frontend/src/scripts/get-note-menu.ts | 4 | ||||
| -rw-r--r-- | packages/frontend/src/scripts/get-note-summary.ts | 2 | ||||
| -rw-r--r-- | packages/frontend/src/scripts/i18n.ts | 221 |
4 files changed, 204 insertions, 25 deletions
diff --git a/packages/frontend/src/scripts/get-drive-file-menu.ts b/packages/frontend/src/scripts/get-drive-file-menu.ts index 59c46c2cbc..91b1218527 100644 --- a/packages/frontend/src/scripts/get-drive-file-menu.ts +++ b/packages/frontend/src/scripts/get-drive-file-menu.ts @@ -66,7 +66,7 @@ function addApp() { async function deleteFile(file: Misskey.entities.DriveFile) { const { canceled } = await os.confirm({ type: 'warning', - text: i18n.t('driveFileDeleteConfirm', { name: file.name }), + text: i18n.tsx.driveFileDeleteConfirm({ name: file.name }), }); if (canceled) return; diff --git a/packages/frontend/src/scripts/get-note-menu.ts b/packages/frontend/src/scripts/get-note-menu.ts index 110be244cb..bfc3c4a8f1 100644 --- a/packages/frontend/src/scripts/get-note-menu.ts +++ b/packages/frontend/src/scripts/get-note-menu.ts @@ -47,7 +47,7 @@ export async function getNoteClipMenu(props: { if (err.id === '734806c4-542c-463a-9311-15c512803965') { const confirm = await os.confirm({ type: 'warning', - text: i18n.t('confirmToUnclipAlreadyClippedNote', { name: clip.name }), + text: i18n.tsx.confirmToUnclipAlreadyClippedNote({ name: clip.name }), }); if (!confirm.canceled) { os.apiWithDialog('clips/remove-note', { clipId: clip.id, noteId: appearNote.id }); @@ -231,7 +231,7 @@ export function getNoteMenu(props: { function share(): void { navigator.share({ - title: i18n.t('noteOf', { user: appearNote.user.name }), + title: i18n.tsx.noteOf({ user: appearNote.user.name }), text: appearNote.text, url: `${url}/notes/${appearNote.id}`, }); diff --git a/packages/frontend/src/scripts/get-note-summary.ts b/packages/frontend/src/scripts/get-note-summary.ts index 1fd9f04d46..2007e0ea97 100644 --- a/packages/frontend/src/scripts/get-note-summary.ts +++ b/packages/frontend/src/scripts/get-note-summary.ts @@ -30,7 +30,7 @@ export const getNoteSummary = (note: Misskey.entities.Note): string => { // ファイルが添付されているとき if ((note.files || []).length !== 0) { - summary += ` (${i18n.t('withNFiles', { n: note.files.length })})`; + summary += ` (${i18n.tsx.withNFiles({ n: note.files.length })})`; } // 投票が添付されているとき diff --git a/packages/frontend/src/scripts/i18n.ts b/packages/frontend/src/scripts/i18n.ts index 3366f3eac3..6aa1468e87 100644 --- a/packages/frontend/src/scripts/i18n.ts +++ b/packages/frontend/src/scripts/i18n.ts @@ -14,37 +14,39 @@ type FlattenKeys<T extends ILocale, TPrediction> = keyof { : never]: T[K]; }; -type ParametersOf<T extends ILocale, TKey extends FlattenKeys<T, ParameterizedString<string>>> = T extends ILocale - ? TKey extends `${infer K}.${infer C}` - // @ts-expect-error -- C は明らかに FlattenKeys<T[K], ParameterizedString<string>> になるが、型システムはここでは TKey がドット区切りであることのコンテキストを持たないので、型システムに合法にて示すことはできない。 - ? ParametersOf<T[K], C> - : TKey extends keyof T - ? T[TKey] extends ParameterizedString<infer P> - ? P - : never +type ParametersOf<T extends ILocale, TKey extends FlattenKeys<T, ParameterizedString>> = TKey extends `${infer K}.${infer C}` + // @ts-expect-error -- C は明らかに FlattenKeys<T[K], ParameterizedString> になるが、型システムはここでは TKey がドット区切りであることのコンテキストを持たないので、型システムに合法にて示すことはできない。 + ? ParametersOf<T[K], C> + : TKey extends keyof T + ? T[TKey] extends ParameterizedString<infer P> + ? P : never - : never; + : never; -type Ts<T extends ILocale> = { - readonly [K in keyof T as T[K] extends ParameterizedString<string> ? never : K]: T[K] extends ILocale ? Ts<T[K]> : string; +type Tsx<T extends ILocale> = { + readonly [K in keyof T as T[K] extends string ? never : K]: T[K] extends ParameterizedString<infer P> + ? (arg: { readonly [_ in P]: string | number }) => string + // @ts-expect-error -- 証明省略 + : Tsx<T[K]>; }; export class I18n<T extends ILocale> { - constructor(private locale: T) { + private tsxCache?: Tsx<T>; + + constructor(public locale: T) { //#region BIND this.t = this.t.bind(this); //#endregion } - public get ts(): Ts<T> { + public get ts(): T { if (_DEV_) { - class Handler<TTarget extends object> implements ProxyHandler<TTarget> { + class Handler<TTarget extends ILocale> implements ProxyHandler<TTarget> { get(target: TTarget, p: string | symbol): unknown { const value = target[p as keyof TTarget]; if (typeof value === 'object') { - // eslint-disable-next-line @typescript-eslint/no-non-null-assertion -- 実際には null がくることはないので。 - return new Proxy(value!, new Handler<TTarget[keyof TTarget] & object>()); + return new Proxy(value, new Handler<TTarget[keyof TTarget] & ILocale>()); } if (typeof value === 'string') { @@ -63,19 +65,148 @@ export class I18n<T extends ILocale> { } } - return new Proxy(this.locale, new Handler()) as Ts<T>; + return new Proxy(this.locale, new Handler()); } - return this.locale as Ts<T>; + return this.locale; + } + + public get tsx(): Tsx<T> { + if (_DEV_) { + if (this.tsxCache) { + return this.tsxCache; + } + + class Handler<TTarget extends ILocale> implements ProxyHandler<TTarget> { + get(target: TTarget, p: string | symbol): unknown { + const value = target[p as keyof TTarget]; + + if (typeof value === 'object') { + return new Proxy(value, new Handler<TTarget[keyof TTarget] & ILocale>()); + } + + if (typeof value === 'string') { + const quasis: string[] = []; + const expressions: string[] = []; + let cursor = 0; + + while (~cursor) { + const start = value.indexOf('{', cursor); + + if (!~start) { + quasis.push(value.slice(cursor)); + break; + } + + quasis.push(value.slice(cursor, start)); + + const end = value.indexOf('}', start); + + expressions.push(value.slice(start + 1, end)); + + cursor = end + 1; + } + + if (!expressions.length) { + console.error(`Unexpected locale key: ${String(p)}`); + + return () => value; + } + + return (arg) => { + let str = quasis[0]; + + for (let i = 0; i < expressions.length; i++) { + if (!Object.hasOwn(arg, expressions[i])) { + console.error(`Missing locale parameters: ${expressions[i]} at ${String(p)}`); + } + + str += arg[expressions[i]] + quasis[i + 1]; + } + + return str; + }; + } + + console.error(`Unexpected locale key: ${String(p)}`); + + return p; + } + } + + return this.tsxCache = new Proxy(this.locale, new Handler()) as unknown as Tsx<T>; + } + + // eslint-disable-next-line @typescript-eslint/no-unnecessary-condition + if (this.tsxCache) { + return this.tsxCache; + } + + function build(target: ILocale): Tsx<T> { + const result = {} as Tsx<T>; + + for (const k in target) { + if (!Object.hasOwn(target, k)) { + continue; + } + + const value = target[k as keyof typeof target]; + + if (typeof value === 'object') { + result[k] = build(value as ILocale); + } else if (typeof value === 'string') { + const quasis: string[] = []; + const expressions: string[] = []; + let cursor = 0; + + while (~cursor) { + const start = value.indexOf('{', cursor); + + if (!~start) { + quasis.push(value.slice(cursor)); + break; + } + + quasis.push(value.slice(cursor, start)); + + const end = value.indexOf('}', start); + + expressions.push(value.slice(start + 1, end)); + + cursor = end + 1; + } + + if (!expressions.length) { + continue; + } + + result[k] = (arg) => { + let str = quasis[0]; + + for (let i = 0; i < expressions.length; i++) { + str += arg[expressions[i]] + quasis[i + 1]; + } + + return str; + }; + } + } + return result; + } + + return this.tsxCache = build(this.locale); } /** - * @deprecated なるべくこのメソッド使うよりも locale 直接参照の方が vue のキャッシュ効いてパフォーマンスが良いかも + * @deprecated なるべくこのメソッド使うよりも ts 直接参照の方が vue のキャッシュ効いてパフォーマンスが良いかも */ public t<TKey extends FlattenKeys<T, string>>(key: TKey): string; - public t<TKey extends FlattenKeys<T, ParameterizedString<string>>>(key: TKey, args: { readonly [_ in ParametersOf<T, TKey>]: string | number }): string; + /** + * @deprecated なるべくこのメソッド使うよりも tsx 直接参照の方が vue のキャッシュ効いてパフォーマンスが良いかも + */ + public t<TKey extends FlattenKeys<T, ParameterizedString>>(key: TKey, args: { readonly [_ in ParametersOf<T, TKey>]: string | number }): string; public t(key: string, args?: { readonly [_: string]: string | number }) { - let str: string | ParameterizedString<string> | ILocale = this.locale; + let str: string | ParameterizedString | ILocale = this.locale; for (const k of key.split('.')) { str = str[k]; @@ -113,3 +244,51 @@ export class I18n<T extends ILocale> { return str; } } + +if (import.meta.vitest) { + const { describe, expect, it } = import.meta.vitest; + + describe('i18n', () => { + it('t', () => { + const i18n = new I18n({ + foo: 'foo', + bar: { + baz: 'baz', + qux: 'qux {0}' as unknown as ParameterizedString<'0'>, + quux: 'quux {0} {1}' as unknown as ParameterizedString<'0' | '1'>, + }, + }); + + expect(i18n.t('foo')).toBe('foo'); + expect(i18n.t('bar.baz')).toBe('baz'); + expect(i18n.tsx.bar.qux({ 0: 'hoge' })).toBe('qux hoge'); + expect(i18n.tsx.bar.quux({ 0: 'hoge', 1: 'fuga' })).toBe('quux hoge fuga'); + }); + it('ts', () => { + const i18n = new I18n({ + foo: 'foo', + bar: { + baz: 'baz', + qux: 'qux {0}' as unknown as ParameterizedString<'0'>, + quux: 'quux {0} {1}' as unknown as ParameterizedString<'0' | '1'>, + }, + }); + + expect(i18n.ts.foo).toBe('foo'); + expect(i18n.ts.bar.baz).toBe('baz'); + }); + it('tsx', () => { + const i18n = new I18n({ + foo: 'foo', + bar: { + baz: 'baz', + qux: 'qux {0}' as unknown as ParameterizedString<'0'>, + quux: 'quux {0} {1}' as unknown as ParameterizedString<'0' | '1'>, + }, + }); + + expect(i18n.tsx.bar.qux({ 0: 'hoge' })).toBe('qux hoge'); + expect(i18n.tsx.bar.quux({ 0: 'hoge', 1: 'fuga' })).toBe('quux hoge fuga'); + }); + }); +} |