summaryrefslogtreecommitdiff
path: root/packages/frontend/src/scripts
diff options
context:
space:
mode:
authorsyuilo <Syuilotan@yahoo.co.jp>2024-01-20 09:53:29 +0900
committersyuilo <Syuilotan@yahoo.co.jp>2024-01-20 09:53:29 +0900
commit91522381b65f45d6682bee0b4c816c7f72e5c28e (patch)
treefe20b4b2a1353d723c82c9d5bc5b655604591a0e /packages/frontend/src/scripts
parentrefactor: extract bubble-game engine as independent package (diff)
parentrefactor: deprecate i18n.t (#13039) (diff)
downloadsharkey-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.ts2
-rw-r--r--packages/frontend/src/scripts/get-note-menu.ts4
-rw-r--r--packages/frontend/src/scripts/get-note-summary.ts2
-rw-r--r--packages/frontend/src/scripts/i18n.ts221
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');
+ });
+ });
+}