diff options
| author | Acid Chicken (硫酸鶏) <root@acid-chicken.com> | 2024-01-20 08:11:59 +0900 |
|---|---|---|
| committer | GitHub <noreply@github.com> | 2024-01-20 08:11:59 +0900 |
| commit | 7881f06be060dd955499a6beff3ad1967ed628f6 (patch) | |
| tree | 7b91a1c5e0f1ef99d9f66e702c7d954a54958a56 /packages/frontend/src | |
| parent | feat: reversi (diff) | |
| download | misskey-7881f06be060dd955499a6beff3ad1967ed628f6.tar.gz misskey-7881f06be060dd955499a6beff3ad1967ed628f6.tar.bz2 misskey-7881f06be060dd955499a6beff3ad1967ed628f6.zip | |
refactor: deprecate i18n.t (#13039)
* refactor: deprecate i18n.t
* revert: deprecate i18n.t
This reverts commit 7dbf873a2f745040ee723df5db659acacff84e12.
* chore: reimpl
Diffstat (limited to 'packages/frontend/src')
80 files changed, 386 insertions, 189 deletions
diff --git a/packages/frontend/src/boot/main-boot.ts b/packages/frontend/src/boot/main-boot.ts index bdb145b39a..c99118f9b2 100644 --- a/packages/frontend/src/boot/main-boot.ts +++ b/packages/frontend/src/boot/main-boot.ts @@ -205,7 +205,7 @@ export async function mainBoot() { const lastUsedDate = parseInt(lastUsed, 10); // 二時間以上前なら if (Date.now() - lastUsedDate > 1000 * 60 * 60 * 2) { - toast(i18n.t('welcomeBackWithName', { + toast(i18n.tsx.welcomeBackWithName({ name: $i.name || $i.username, })); } diff --git a/packages/frontend/src/components/MkAnnouncementDialog.vue b/packages/frontend/src/components/MkAnnouncementDialog.vue index c649e69cd0..54cbbe18c2 100644 --- a/packages/frontend/src/components/MkAnnouncementDialog.vue +++ b/packages/frontend/src/components/MkAnnouncementDialog.vue @@ -44,7 +44,7 @@ async function ok() { const confirm = await os.confirm({ type: 'question', title: i18n.ts._announcement.readConfirmTitle, - text: i18n.t('_announcement.readConfirmText', { title: props.announcement.title }), + text: i18n.tsx._announcement.readConfirmText({ title: props.announcement.title }), }); if (confirm.canceled) return; } diff --git a/packages/frontend/src/components/MkCwButton.vue b/packages/frontend/src/components/MkCwButton.vue index 4a6d2dfba2..ca19a2122d 100644 --- a/packages/frontend/src/components/MkCwButton.vue +++ b/packages/frontend/src/components/MkCwButton.vue @@ -41,9 +41,9 @@ const emit = defineEmits<{ const label = computed(() => { return concat([ - props.text ? [i18n.t('_cw.chars', { count: props.text.length })] : [], + props.text ? [i18n.tsx._cw.chars({ count: props.text.length })] : [], props.renote ? [i18n.ts.quote] : [], - props.files.length !== 0 ? [i18n.t('_cw.files', { count: props.files.length })] : [], + props.files.length !== 0 ? [i18n.tsx._cw.files({ count: props.files.length })] : [], props.poll != null ? [i18n.ts.poll] : [], ] as string[][]).join(' / '); }); diff --git a/packages/frontend/src/components/MkDateSeparatedList.vue b/packages/frontend/src/components/MkDateSeparatedList.vue index 0a71b689fe..7b9a052868 100644 --- a/packages/frontend/src/components/MkDateSeparatedList.vue +++ b/packages/frontend/src/components/MkDateSeparatedList.vue @@ -46,7 +46,7 @@ export default defineComponent({ function getDateText(time: string) { const date = new Date(time).getDate(); const month = new Date(time).getMonth() + 1; - return i18n.t('monthAndDay', { + return i18n.tsx.monthAndDay({ month: month.toString(), day: date.toString(), }); diff --git a/packages/frontend/src/components/MkDialog.vue b/packages/frontend/src/components/MkDialog.vue index 3c1f83d335..3fc9f0e357 100644 --- a/packages/frontend/src/components/MkDialog.vue +++ b/packages/frontend/src/components/MkDialog.vue @@ -30,8 +30,8 @@ SPDX-License-Identifier: AGPL-3.0-only <MkInput v-if="input" v-model="inputValue" autofocus :type="input.type || 'text'" :placeholder="input.placeholder || undefined" :autocomplete="input.autocomplete" @keydown="onInputKeydown"> <template v-if="input.type === 'password'" #prefix><i class="ti ti-lock"></i></template> <template #caption> - <span v-if="okButtonDisabledReason === 'charactersExceeded'" v-text="i18n.t('_dialog.charactersExceeded', { current: (inputValue as string).length, max: input.maxLength ?? 'NaN' })"/> - <span v-else-if="okButtonDisabledReason === 'charactersBelow'" v-text="i18n.t('_dialog.charactersBelow', { current: (inputValue as string).length, min: input.minLength ?? 'NaN' })"/> + <span v-if="okButtonDisabledReason === 'charactersExceeded'" v-text="i18n.tsx._dialog.charactersExceeded({ current: (inputValue as string).length, max: input.maxLength ?? 'NaN' })"/> + <span v-else-if="okButtonDisabledReason === 'charactersBelow'" v-text="i18n.tsx._dialog.charactersBelow({ current: (inputValue as string).length, min: input.minLength ?? 'NaN' })"/> </template> </MkInput> <MkSelect v-if="select" v-model="selectedValue" autofocus> diff --git a/packages/frontend/src/components/MkDrive.vue b/packages/frontend/src/components/MkDrive.vue index dbf98cd622..560d5502d4 100644 --- a/packages/frontend/src/components/MkDrive.vue +++ b/packages/frontend/src/components/MkDrive.vue @@ -82,8 +82,8 @@ SPDX-License-Identifier: AGPL-3.0-only <MkButton v-show="moreFiles" ref="loadMoreFiles" @click="fetchMoreFiles">{{ i18n.ts.loadMore }}</MkButton> </div> <div v-if="files.length == 0 && folders.length == 0 && !fetching" :class="$style.empty"> - <div v-if="draghover">{{ i18n.t('empty-draghover') }}</div> - <div v-if="!draghover && folder == null"><strong>{{ i18n.ts.emptyDrive }}</strong><br/>{{ i18n.t('empty-drive-description') }}</div> + <div v-if="draghover">{{ i18n.ts['empty-draghover'] }}</div> + <div v-if="!draghover && folder == null"><strong>{{ i18n.ts.emptyDrive }}</strong><br/>{{ i18n.ts['empty-drive-description'] }}</div> <div v-if="!draghover && folder != null">{{ i18n.ts.emptyFolder }}</div> </div> </div> diff --git a/packages/frontend/src/components/MkFollowButton.vue b/packages/frontend/src/components/MkFollowButton.vue index 78c4fb3cd2..6bc96be1b9 100644 --- a/packages/frontend/src/components/MkFollowButton.vue +++ b/packages/frontend/src/components/MkFollowButton.vue @@ -84,7 +84,7 @@ async function onClick() { if (isFollowing.value) { const { canceled } = await os.confirm({ type: 'warning', - text: i18n.t('unfollowConfirm', { name: props.user.name || props.user.username }), + text: i18n.tsx.unfollowConfirm({ name: props.user.name || props.user.username }), }); if (canceled) return; diff --git a/packages/frontend/src/components/MkNote.vue b/packages/frontend/src/components/MkNote.vue index 9c4354ef5f..d7bb64661b 100644 --- a/packages/frontend/src/components/MkNote.vue +++ b/packages/frontend/src/components/MkNote.vue @@ -73,7 +73,7 @@ SPDX-License-Identifier: AGPL-3.0-only <div v-if="translating || translation" :class="$style.translation"> <MkLoading v-if="translating" mini/> <div v-else> - <b>{{ i18n.t('translatedFrom', { x: translation.sourceLang }) }}: </b> + <b>{{ i18n.tsx.translatedFrom({ x: translation.sourceLang }) }}: </b> <Mfm :text="translation.text" :author="appearNote.user" :nyaize="'respect'" :emojiUrls="appearNote.emojis"/> </div> </div> diff --git a/packages/frontend/src/components/MkNoteDetailed.vue b/packages/frontend/src/components/MkNoteDetailed.vue index e941827d74..1f5b283cfe 100644 --- a/packages/frontend/src/components/MkNoteDetailed.vue +++ b/packages/frontend/src/components/MkNoteDetailed.vue @@ -87,7 +87,7 @@ SPDX-License-Identifier: AGPL-3.0-only <div v-if="translating || translation" :class="$style.translation"> <MkLoading v-if="translating" mini/> <div v-else> - <b>{{ i18n.t('translatedFrom', { x: translation.sourceLang }) }}: </b> + <b>{{ i18n.tsx.translatedFrom({ x: translation.sourceLang }) }}: </b> <Mfm :text="translation.text" :author="appearNote.user" :nyaize="'respect'" :emojiUrls="appearNote.emojis"/> </div> </div> diff --git a/packages/frontend/src/components/MkNotification.vue b/packages/frontend/src/components/MkNotification.vue index ce8b054b39..92be20c6f6 100644 --- a/packages/frontend/src/components/MkNotification.vue +++ b/packages/frontend/src/components/MkNotification.vue @@ -56,8 +56,8 @@ SPDX-License-Identifier: AGPL-3.0-only <span v-else-if="notification.type === 'achievementEarned'">{{ i18n.ts._notification.achievementEarned }}</span> <span v-else-if="notification.type === 'test'">{{ i18n.ts._notification.testNotification }}</span> <MkA v-else-if="notification.user" v-user-preview="notification.user.id" :class="$style.headerName" :to="userPage(notification.user)"><MkUserName :user="notification.user"/></MkA> - <span v-else-if="notification.type === 'reaction:grouped'">{{ i18n.t('_notification.reactedBySomeUsers', { n: notification.reactions.length }) }}</span> - <span v-else-if="notification.type === 'renote:grouped'">{{ i18n.t('_notification.renotedBySomeUsers', { n: notification.users.length }) }}</span> + <span v-else-if="notification.type === 'reaction:grouped'">{{ i18n.tsx._notification.reactedBySomeUsers({ n: notification.reactions.length }) }}</span> + <span v-else-if="notification.type === 'renote:grouped'">{{ i18n.tsx._notification.renotedBySomeUsers({ n: notification.users.length }) }}</span> <span v-else>{{ notification.header }}</span> <MkTime v-if="withTime" :time="notification.createdAt" :class="$style.headerTime"/> </header> diff --git a/packages/frontend/src/components/MkNotificationSelectWindow.vue b/packages/frontend/src/components/MkNotificationSelectWindow.vue index 6725776f43..0e77d2a6aa 100644 --- a/packages/frontend/src/components/MkNotificationSelectWindow.vue +++ b/packages/frontend/src/components/MkNotificationSelectWindow.vue @@ -23,7 +23,7 @@ SPDX-License-Identifier: AGPL-3.0-only <MkButton inline @click="disableAll">{{ i18n.ts.disableAll }}</MkButton> <MkButton inline @click="enableAll">{{ i18n.ts.enableAll }}</MkButton> </div> - <MkSwitch v-for="ntype in notificationTypes" :key="ntype" v-model="typesMap[ntype].value">{{ i18n.t(`_notification._types.${ntype}`) }}</MkSwitch> + <MkSwitch v-for="ntype in notificationTypes" :key="ntype" v-model="typesMap[ntype].value">{{ i18n.ts._notification._types[ntype] }}</MkSwitch> </div> </MkSpacer> </MkModalWindow> diff --git a/packages/frontend/src/components/MkPoll.vue b/packages/frontend/src/components/MkPoll.vue index 4cac1fe9c3..7c58b58697 100644 --- a/packages/frontend/src/components/MkPoll.vue +++ b/packages/frontend/src/components/MkPoll.vue @@ -11,12 +11,12 @@ SPDX-License-Identifier: AGPL-3.0-only <span :class="$style.fg"> <template v-if="choice.isVoted"><i class="ti ti-check" style="margin-right: 4px; color: var(--accent);"></i></template> <Mfm :text="choice.text" :plain="true"/> - <span v-if="showResult" style="margin-left: 4px; opacity: 0.7;">({{ i18n.t('_poll.votesCount', { n: choice.votes }) }})</span> + <span v-if="showResult" style="margin-left: 4px; opacity: 0.7;">({{ i18n.tsx._poll.votesCount({ n: choice.votes }) }})</span> </span> </li> </ul> <p v-if="!readOnly" :class="$style.info"> - <span>{{ i18n.t('_poll.totalVotes', { n: total }) }}</span> + <span>{{ i18n.tsx._poll.totalVotes({ n: total }) }}</span> <span> · </span> <a v-if="!closed && !isVoted" style="color: inherit;" @click="showResult = !showResult">{{ showResult ? i18n.ts._poll.vote : i18n.ts._poll.showResult }}</a> <span v-if="isVoted">{{ i18n.ts._poll.voted }}</span> @@ -47,10 +47,11 @@ const remaining = ref(-1); const total = computed(() => sum(props.note.poll.choices.map(x => x.votes))); const closed = computed(() => remaining.value === 0); const isVoted = computed(() => !props.note.poll.multiple && props.note.poll.choices.some(c => c.isVoted)); -const timer = computed(() => i18n.t( - remaining.value >= 86400 ? '_poll.remainingDays' : - remaining.value >= 3600 ? '_poll.remainingHours' : - remaining.value >= 60 ? '_poll.remainingMinutes' : '_poll.remainingSeconds', { +const timer = computed(() => i18n.tsx._poll[ + remaining.value >= 86400 ? 'remainingDays' : + remaining.value >= 3600 ? 'remainingHours' : + remaining.value >= 60 ? 'remainingMinutes' : 'remainingSeconds' + ]({ s: Math.floor(remaining.value % 60), m: Math.floor(remaining.value / 60) % 60, h: Math.floor(remaining.value / 3600) % 24, @@ -81,7 +82,7 @@ const vote = async (id) => { const { canceled } = await os.confirm({ type: 'question', - text: i18n.t('voteConfirm', { choice: props.note.poll.choices[id].text }), + text: i18n.tsx.voteConfirm({ choice: props.note.poll.choices[id].text }), }); if (canceled) return; diff --git a/packages/frontend/src/components/MkPollEditor.vue b/packages/frontend/src/components/MkPollEditor.vue index 43e576d1ab..34f6c72b6e 100644 --- a/packages/frontend/src/components/MkPollEditor.vue +++ b/packages/frontend/src/components/MkPollEditor.vue @@ -10,7 +10,7 @@ SPDX-License-Identifier: AGPL-3.0-only </p> <ul> <li v-for="(choice, i) in choices" :key="i"> - <MkInput class="input" small :modelValue="choice" :placeholder="i18n.t('_poll.choiceN', { n: i + 1 })" @update:modelValue="onInput(i, $event)"> + <MkInput class="input" small :modelValue="choice" :placeholder="i18n.tsx._poll.choiceN({ n: i + 1 })" @update:modelValue="onInput(i, $event)"> </MkInput> <button class="_button" @click="remove(i)"> <i class="ti ti-x"></i> diff --git a/packages/frontend/src/components/MkSignupDialog.form.vue b/packages/frontend/src/components/MkSignupDialog.form.vue index 79e17c9aef..e27510fb34 100644 --- a/packages/frontend/src/components/MkSignupDialog.form.vue +++ b/packages/frontend/src/components/MkSignupDialog.form.vue @@ -263,7 +263,7 @@ async function onSubmit(): Promise<void> { os.alert({ type: 'success', title: i18n.ts._signup.almostThere, - text: i18n.t('_signup.emailSent', { email: email.value }), + text: i18n.tsx._signup.emailSent({ email: email.value }), }); emit('signupEmailPending'); } else { diff --git a/packages/frontend/src/components/MkSignupDialog.rules.vue b/packages/frontend/src/components/MkSignupDialog.rules.vue index 8cf7ce92ad..d42b496a34 100644 --- a/packages/frontend/src/components/MkSignupDialog.rules.vue +++ b/packages/frontend/src/components/MkSignupDialog.rules.vue @@ -105,7 +105,7 @@ async function updateAgreeServerRules(v: boolean) { const confirm = await os.confirm({ type: 'question', title: i18n.ts.doYouAgree, - text: i18n.t('iHaveReadXCarefullyAndAgree', { x: i18n.ts.serverRules }), + text: i18n.tsx.iHaveReadXCarefullyAndAgree({ x: i18n.ts.serverRules }), }); if (confirm.canceled) return; agreeServerRules.value = true; @@ -119,7 +119,7 @@ async function updateAgreeTosAndPrivacyPolicy(v: boolean) { const confirm = await os.confirm({ type: 'question', title: i18n.ts.doYouAgree, - text: i18n.t('iHaveReadXCarefullyAndAgree', { + text: i18n.tsx.iHaveReadXCarefullyAndAgree({ x: tosPrivacyPolicyLabel.value, }), }); @@ -135,7 +135,7 @@ async function updateAgreeNote(v: boolean) { const confirm = await os.confirm({ type: 'question', title: i18n.ts.doYouAgree, - text: i18n.t('iHaveReadXCarefullyAndAgree', { x: i18n.ts.basicNotesBeforeCreateAccount }), + text: i18n.tsx.iHaveReadXCarefullyAndAgree({ x: i18n.ts.basicNotesBeforeCreateAccount }), }); if (confirm.canceled) return; agreeNote.value = true; diff --git a/packages/frontend/src/components/MkSubNoteContent.vue b/packages/frontend/src/components/MkSubNoteContent.vue index 438140649e..13d0e6c2ca 100644 --- a/packages/frontend/src/components/MkSubNoteContent.vue +++ b/packages/frontend/src/components/MkSubNoteContent.vue @@ -13,7 +13,7 @@ SPDX-License-Identifier: AGPL-3.0-only <MkA v-if="note.renoteId" :class="$style.rp" :to="`/notes/${note.renoteId}`">RN: ...</MkA> </div> <details v-if="note.files.length > 0"> - <summary>({{ i18n.t('withNFiles', { n: note.files.length }) }})</summary> + <summary>({{ i18n.tsx.withNFiles({ n: note.files.length }) }})</summary> <MkMediaList :mediaList="note.files"/> </details> <details v-if="note.poll"> diff --git a/packages/frontend/src/components/MkTokenGenerateWindow.vue b/packages/frontend/src/components/MkTokenGenerateWindow.vue index a42767e1b6..28f2f29b7d 100644 --- a/packages/frontend/src/components/MkTokenGenerateWindow.vue +++ b/packages/frontend/src/components/MkTokenGenerateWindow.vue @@ -33,12 +33,12 @@ SPDX-License-Identifier: AGPL-3.0-only <MkButton inline @click="enableAll">{{ i18n.ts.enableAll }}</MkButton> </div> <div class="_gaps_s"> - <MkSwitch v-for="kind in Object.keys(permissionSwitches)" :key="kind" v-model="permissionSwitches[kind]">{{ i18n.t(`_permissions.${kind}`) }}</MkSwitch> + <MkSwitch v-for="kind in Object.keys(permissionSwitches)" :key="kind" v-model="permissionSwitches[kind]">{{ i18n.ts._permissions[kind] }}</MkSwitch> </div> <div v-if="iAmAdmin" :class="$style.adminPermissions"> <div :class="$style.adminPermissionsHeader"><b>{{ i18n.ts.adminPermission }}</b></div> <div class="_gaps_s"> - <MkSwitch v-for="kind in Object.keys(permissionSwitchesForAdmin)" :key="kind" v-model="permissionSwitchesForAdmin[kind]">{{ i18n.t(`_permissions.${kind}`) }}</MkSwitch> + <MkSwitch v-for="kind in Object.keys(permissionSwitchesForAdmin)" :key="kind" v-model="permissionSwitchesForAdmin[kind]">{{ i18n.ts._permissions[kind] }}</MkSwitch> </div> </div> </div> diff --git a/packages/frontend/src/components/MkTutorialDialog.vue b/packages/frontend/src/components/MkTutorialDialog.vue index 963e78a1ff..9c3b46e133 100644 --- a/packages/frontend/src/components/MkTutorialDialog.vue +++ b/packages/frontend/src/components/MkTutorialDialog.vue @@ -133,7 +133,7 @@ SPDX-License-Identifier: AGPL-3.0-only <a href="https://misskey-hub.net/docs/for-users/" target="_blank" class="_link">{{ i18n.ts.help }}</a> </template> </I18n> - <div>{{ i18n.t('_initialAccountSetting.haveFun', { name: instance.name ?? host }) }}</div> + <div>{{ i18n.tsx._initialAccountSetting.haveFun({ name: instance.name ?? host }) }}</div> <div class="_buttonsCenter" style="margin-top: 16px;"> <MkButton v-if="initialPage !== 4" rounded @click="page--"><i class="ti ti-arrow-left"></i> {{ i18n.ts.goBack }}</MkButton> <MkButton rounded primary gradate @click="close(false)">{{ i18n.ts.close }}</MkButton> diff --git a/packages/frontend/src/components/MkUserAnnouncementEditDialog.vue b/packages/frontend/src/components/MkUserAnnouncementEditDialog.vue index af094a8e8c..9e3a78fe22 100644 --- a/packages/frontend/src/components/MkUserAnnouncementEditDialog.vue +++ b/packages/frontend/src/components/MkUserAnnouncementEditDialog.vue @@ -118,7 +118,7 @@ async function done() { async function del() { const { canceled } = await os.confirm({ type: 'warning', - text: i18n.t('removeAreYouSure', { x: title.value }), + text: i18n.tsx.removeAreYouSure({ x: title.value }), }); if (canceled) return; diff --git a/packages/frontend/src/components/MkUserSetupDialog.Profile.vue b/packages/frontend/src/components/MkUserSetupDialog.Profile.vue index 37aa677b44..f082833838 100644 --- a/packages/frontend/src/components/MkUserSetupDialog.Profile.vue +++ b/packages/frontend/src/components/MkUserSetupDialog.Profile.vue @@ -68,7 +68,7 @@ function setAvatar(ev) { const { canceled } = await os.confirm({ type: 'question', - text: i18n.t('cropImageAsk'), + text: i18n.ts.cropImageAsk, okText: i18n.ts.cropYes, cancelText: i18n.ts.cropNo, }); diff --git a/packages/frontend/src/components/MkUserSetupDialog.vue b/packages/frontend/src/components/MkUserSetupDialog.vue index 05b55f77a7..4b0c540829 100644 --- a/packages/frontend/src/components/MkUserSetupDialog.vue +++ b/packages/frontend/src/components/MkUserSetupDialog.vue @@ -93,7 +93,7 @@ SPDX-License-Identifier: AGPL-3.0-only <div class="_gaps" style="text-align: center;"> <i class="ti ti-bell-ringing-2" style="display: block; margin: auto; font-size: 3em; color: var(--accent);"></i> <div style="font-size: 120%;">{{ i18n.ts.pushNotification }}</div> - <div style="padding: 0 16px;">{{ i18n.t('_initialAccountSetting.pushNotificationDescription', { name: instance.name ?? host }) }}</div> + <div style="padding: 0 16px;">{{ i18n.tsx._initialAccountSetting.pushNotificationDescription({ name: instance.name ?? host }) }}</div> <MkPushNotificationAllowButton primary showOnlyToRegister style="margin: 0 auto;"/> <div class="_buttonsCenter" style="margin-top: 16px;"> <MkButton rounded data-cy-user-setup-back @click="page--"><i class="ti ti-arrow-left"></i> {{ i18n.ts.goBack }}</MkButton> @@ -110,7 +110,7 @@ SPDX-License-Identifier: AGPL-3.0-only <div class="_gaps" style="text-align: center;"> <i class="ti ti-check" style="display: block; margin: auto; font-size: 3em; color: var(--accent);"></i> <div style="font-size: 120%;">{{ i18n.ts._initialAccountSetting.initialAccountSettingCompleted }}</div> - <div>{{ i18n.t('_initialAccountSetting.youCanContinueTutorial', { name: instance.name ?? host }) }}</div> + <div>{{ i18n.tsx._initialAccountSetting.youCanContinueTutorial({ name: instance.name ?? host }) }}</div> <div class="_buttonsCenter" style="margin-top: 16px;"> <MkButton rounded primary gradate data-cy-user-setup-continue @click="launchTutorial()">{{ i18n.ts._initialAccountSetting.startTutorial }} <i class="ti ti-arrow-right"></i></MkButton> </div> diff --git a/packages/frontend/src/components/MkWidgets.vue b/packages/frontend/src/components/MkWidgets.vue index ee4e29dd8f..b8b253de06 100644 --- a/packages/frontend/src/components/MkWidgets.vue +++ b/packages/frontend/src/components/MkWidgets.vue @@ -9,7 +9,7 @@ SPDX-License-Identifier: AGPL-3.0-only <header :class="$style.editHeader"> <MkSelect v-model="widgetAdderSelected" style="margin-bottom: var(--margin)" data-cy-widget-select> <template #label>{{ i18n.ts.selectWidget }}</template> - <option v-for="widget in widgetDefs" :key="widget" :value="widget">{{ i18n.t(`_widgets.${widget}`) }}</option> + <option v-for="widget in widgetDefs" :key="widget" :value="widget">{{ i18n.ts._widgets[widget] }}</option> </MkSelect> <MkButton inline primary data-cy-widget-add @click="addWidget"><i class="ti ti-plus"></i> {{ i18n.ts.add }}</MkButton> <MkButton inline @click="$emit('exit')">{{ i18n.ts.close }}</MkButton> @@ -109,7 +109,7 @@ function onContextmenu(widget: Widget, ev: MouseEvent) { os.contextMenu([{ type: 'label', - text: i18n.t(`_widgets.${widget.name}`), + text: i18n.ts._widgets[widget.name], }, { icon: 'ti ti-settings', text: i18n.ts.settings, diff --git a/packages/frontend/src/components/global/I18n.vue b/packages/frontend/src/components/global/I18n.vue new file mode 100644 index 0000000000..162aa2bcf8 --- /dev/null +++ b/packages/frontend/src/components/global/I18n.vue @@ -0,0 +1,46 @@ +<template> +<render/> +</template> + +<script setup lang="ts" generic="T extends string | ParameterizedString"> +import { computed, h } from 'vue'; +import type { ParameterizedString } from '../../../../../locales/index.js'; + +const props = withDefaults(defineProps<{ + src: T; + tag?: string; + // eslint-disable-next-line vue/require-default-prop + textTag?: string; +}>(), { + tag: 'span', +}); + +const slots = defineSlots<T extends ParameterizedString<infer R> ? { [K in R]: () => unknown } : NonNullable<unknown>>(); + +const parsed = computed(() => { + let str = props.src as string; + const value: (string | { arg: string; })[] = []; + for (;;) { + const nextBracketOpen = str.indexOf('{'); + const nextBracketClose = str.indexOf('}'); + + if (nextBracketOpen === -1) { + value.push(str); + break; + } else { + if (nextBracketOpen > 0) value.push(str.substring(0, nextBracketOpen)); + value.push({ + arg: str.substring(nextBracketOpen + 1, nextBracketClose), + }); + } + + str = str.substring(nextBracketClose + 1); + } + + return value; +}); + +const render = () => { + return h(props.tag, parsed.value.map(x => typeof x === 'string' ? (props.textTag ? h(props.textTag, x) : x) : slots[x.arg]())); +}; +</script> diff --git a/packages/frontend/src/components/global/MkTime.stories.impl.ts b/packages/frontend/src/components/global/MkTime.stories.impl.ts index 0eeefa4859..0e7f6a9bdf 100644 --- a/packages/frontend/src/components/global/MkTime.stories.impl.ts +++ b/packages/frontend/src/components/global/MkTime.stories.impl.ts @@ -123,7 +123,7 @@ export const DetailNow = { export const RelativeOneHourAgo = { ...Empty, async play({ canvasElement }) { - await expect(canvasElement).toHaveTextContent(i18n.t('_ago.hoursAgo', { n: 1 })); + await expect(canvasElement).toHaveTextContent(i18n.tsx._ago.hoursAgo({ n: 1 })); }, args: { ...Empty.args, @@ -162,7 +162,7 @@ export const DetailOneHourAgo = { export const RelativeOneDayAgo = { ...Empty, async play({ canvasElement }) { - await expect(canvasElement).toHaveTextContent(i18n.t('_ago.daysAgo', { n: 1 })); + await expect(canvasElement).toHaveTextContent(i18n.tsx._ago.daysAgo({ n: 1 })); }, args: { ...Empty.args, @@ -201,7 +201,7 @@ export const DetailOneDayAgo = { export const RelativeOneWeekAgo = { ...Empty, async play({ canvasElement }) { - await expect(canvasElement).toHaveTextContent(i18n.t('_ago.weeksAgo', { n: 1 })); + await expect(canvasElement).toHaveTextContent(i18n.tsx._ago.weeksAgo({ n: 1 })); }, args: { ...Empty.args, @@ -240,7 +240,7 @@ export const DetailOneWeekAgo = { export const RelativeOneMonthAgo = { ...Empty, async play({ canvasElement }) { - await expect(canvasElement).toHaveTextContent(i18n.t('_ago.monthsAgo', { n: 1 })); + await expect(canvasElement).toHaveTextContent(i18n.tsx._ago.monthsAgo({ n: 1 })); }, args: { ...Empty.args, @@ -279,7 +279,7 @@ export const DetailOneMonthAgo = { export const RelativeOneYearAgo = { ...Empty, async play({ canvasElement }) { - await expect(canvasElement).toHaveTextContent(i18n.t('_ago.yearsAgo', { n: 1 })); + await expect(canvasElement).toHaveTextContent(i18n.tsx._ago.yearsAgo({ n: 1 })); }, args: { ...Empty.args, diff --git a/packages/frontend/src/components/global/MkTime.vue b/packages/frontend/src/components/global/MkTime.vue index e11db9dc31..2b0bf246ad 100644 --- a/packages/frontend/src/components/global/MkTime.vue +++ b/packages/frontend/src/components/global/MkTime.vue @@ -55,21 +55,21 @@ const relative = computed<string>(() => { if (invalid) return i18n.ts._ago.invalid; return ( - ago.value >= 31536000 ? i18n.t('_ago.yearsAgo', { n: Math.round(ago.value / 31536000).toString() }) : - ago.value >= 2592000 ? i18n.t('_ago.monthsAgo', { n: Math.round(ago.value / 2592000).toString() }) : - ago.value >= 604800 ? i18n.t('_ago.weeksAgo', { n: Math.round(ago.value / 604800).toString() }) : - ago.value >= 86400 ? i18n.t('_ago.daysAgo', { n: Math.round(ago.value / 86400).toString() }) : - ago.value >= 3600 ? i18n.t('_ago.hoursAgo', { n: Math.round(ago.value / 3600).toString() }) : - ago.value >= 60 ? i18n.t('_ago.minutesAgo', { n: (~~(ago.value / 60)).toString() }) : - ago.value >= 10 ? i18n.t('_ago.secondsAgo', { n: (~~(ago.value % 60)).toString() }) : + ago.value >= 31536000 ? i18n.tsx._ago.yearsAgo({ n: Math.round(ago.value / 31536000).toString() }) : + ago.value >= 2592000 ? i18n.tsx._ago.monthsAgo({ n: Math.round(ago.value / 2592000).toString() }) : + ago.value >= 604800 ? i18n.tsx._ago.weeksAgo({ n: Math.round(ago.value / 604800).toString() }) : + ago.value >= 86400 ? i18n.tsx._ago.daysAgo({ n: Math.round(ago.value / 86400).toString() }) : + ago.value >= 3600 ? i18n.tsx._ago.hoursAgo({ n: Math.round(ago.value / 3600).toString() }) : + ago.value >= 60 ? i18n.tsx._ago.minutesAgo({ n: (~~(ago.value / 60)).toString() }) : + ago.value >= 10 ? i18n.tsx._ago.secondsAgo({ n: (~~(ago.value % 60)).toString() }) : ago.value >= -3 ? i18n.ts._ago.justNow : - ago.value < -31536000 ? i18n.t('_timeIn.years', { n: Math.round(-ago.value / 31536000).toString() }) : - ago.value < -2592000 ? i18n.t('_timeIn.months', { n: Math.round(-ago.value / 2592000).toString() }) : - ago.value < -604800 ? i18n.t('_timeIn.weeks', { n: Math.round(-ago.value / 604800).toString() }) : - ago.value < -86400 ? i18n.t('_timeIn.days', { n: Math.round(-ago.value / 86400).toString() }) : - ago.value < -3600 ? i18n.t('_timeIn.hours', { n: Math.round(-ago.value / 3600).toString() }) : - ago.value < -60 ? i18n.t('_timeIn.minutes', { n: (~~(-ago.value / 60)).toString() }) : - i18n.t('_timeIn.seconds', { n: (~~(-ago.value % 60)).toString() }) + ago.value < -31536000 ? i18n.tsx._timeIn.years({ n: Math.round(-ago.value / 31536000).toString() }) : + ago.value < -2592000 ? i18n.tsx._timeIn.months({ n: Math.round(-ago.value / 2592000).toString() }) : + ago.value < -604800 ? i18n.tsx._timeIn.weeks({ n: Math.round(-ago.value / 604800).toString() }) : + ago.value < -86400 ? i18n.tsx._timeIn.days({ n: Math.round(-ago.value / 86400).toString() }) : + ago.value < -3600 ? i18n.tsx._timeIn.hours({ n: Math.round(-ago.value / 3600).toString() }) : + ago.value < -60 ? i18n.tsx._timeIn.minutes({ n: (~~(-ago.value / 60)).toString() }) : + i18n.tsx._timeIn.seconds({ n: (~~(-ago.value % 60)).toString() }) ); }); diff --git a/packages/frontend/src/components/global/i18n.ts b/packages/frontend/src/components/global/i18n.ts deleted file mode 100644 index 2f4d7edabd..0000000000 --- a/packages/frontend/src/components/global/i18n.ts +++ /dev/null @@ -1,29 +0,0 @@ -/* - * SPDX-FileCopyrightText: syuilo and other misskey contributors - * SPDX-License-Identifier: AGPL-3.0-only - */ - -import { h } from 'vue'; - -export default function(props: { src: string; tag?: string; textTag?: string; }, { slots }) { - let str = props.src; - const parsed = [] as (string | { arg: string; })[]; - while (true) { - const nextBracketOpen = str.indexOf('{'); - const nextBracketClose = str.indexOf('}'); - - if (nextBracketOpen === -1) { - parsed.push(str); - break; - } else { - if (nextBracketOpen > 0) parsed.push(str.substring(0, nextBracketOpen)); - parsed.push({ - arg: str.substring(nextBracketOpen + 1, nextBracketClose), - }); - } - - str = str.substring(nextBracketClose + 1); - } - - return h(props.tag ?? 'span', parsed.map(x => typeof x === 'string' ? (props.textTag ? h(props.textTag, x) : x) : slots[x.arg]())); -} diff --git a/packages/frontend/src/components/index.ts b/packages/frontend/src/components/index.ts index a3e13c3a50..f3b476b15c 100644 --- a/packages/frontend/src/components/index.ts +++ b/packages/frontend/src/components/index.ts @@ -16,7 +16,7 @@ import MkUserName from './global/MkUserName.vue'; import MkEllipsis from './global/MkEllipsis.vue'; import MkTime from './global/MkTime.vue'; import MkUrl from './global/MkUrl.vue'; -import I18n from './global/i18n.js'; +import I18n from './global/I18n.vue'; import RouterView from './global/RouterView.vue'; import MkLoading from './global/MkLoading.vue'; import MkError from './global/MkError.vue'; diff --git a/packages/frontend/src/pages/about.vue b/packages/frontend/src/pages/about.vue index 4ba1b6da76..69cb6ef647 100644 --- a/packages/frontend/src/pages/about.vue +++ b/packages/frontend/src/pages/about.vue @@ -29,7 +29,7 @@ SPDX-License-Identifier: AGPL-3.0-only <template #key>Misskey</template> <template #value>{{ version }}</template> </MkKeyValue> - <div v-html="i18n.t('poweredByMisskeyDescription', { name: instance.name ?? host })"> + <div v-html="i18n.tsx.poweredByMisskeyDescription({ name: instance.name ?? host })"> </div> <FormLink to="/about-misskey">{{ i18n.ts.aboutMisskey }}</FormLink> </div> diff --git a/packages/frontend/src/pages/admin-file.vue b/packages/frontend/src/pages/admin-file.vue index 4a9c659a97..84f3d1f3f1 100644 --- a/packages/frontend/src/pages/admin-file.vue +++ b/packages/frontend/src/pages/admin-file.vue @@ -104,7 +104,7 @@ fetch(); async function del() { const { canceled } = await os.confirm({ type: 'warning', - text: i18n.t('removeAreYouSure', { x: file.value.name }), + text: i18n.tsx.removeAreYouSure({ x: file.value.name }), }); if (canceled) return; diff --git a/packages/frontend/src/pages/admin-user.vue b/packages/frontend/src/pages/admin-user.vue index 85417f0ecb..530bcca04a 100644 --- a/packages/frontend/src/pages/admin-user.vue +++ b/packages/frontend/src/pages/admin-user.vue @@ -182,9 +182,9 @@ SPDX-License-Identifier: AGPL-3.0-only </MkSelect> </div> <div class="charts"> - <div class="label">{{ i18n.t('recentNHours', { n: 90 }) }}</div> + <div class="label">{{ i18n.tsx.recentNHours({ n: 90 }) }}</div> <MkChart class="chart" :src="chartSrc" span="hour" :limit="90" :args="{ user, withoutAll: true }" :detailed="true"></MkChart> - <div class="label">{{ i18n.t('recentNDays', { n: 90 }) }}</div> + <div class="label">{{ i18n.tsx.recentNDays({ n: 90 }) }}</div> <MkChart class="chart" :src="chartSrc" span="day" :limit="90" :args="{ user, withoutAll: true }" :detailed="true"></MkChart> </div> </div> @@ -307,7 +307,7 @@ async function resetPassword() { }); os.alert({ type: 'success', - text: i18n.t('newPasswordIs', { password }), + text: i18n.tsx.newPasswordIs({ password }), }); } } @@ -390,7 +390,7 @@ async function deleteAccount() { if (confirm.canceled) return; const typed = await os.inputText({ - text: i18n.t('typeToConfirm', { x: user.value?.username }), + text: i18n.tsx.typeToConfirm({ x: user.value?.username }), }); if (typed.canceled) return; diff --git a/packages/frontend/src/pages/admin/ads.vue b/packages/frontend/src/pages/admin/ads.vue index eb9aef0e48..fe55fe3a02 100644 --- a/packages/frontend/src/pages/admin/ads.vue +++ b/packages/frontend/src/pages/admin/ads.vue @@ -160,7 +160,7 @@ function add() { function remove(ad) { os.confirm({ type: 'warning', - text: i18n.t('removeAreYouSure', { x: ad.url }), + text: i18n.tsx.removeAreYouSure({ x: ad.url }), }).then(({ canceled }) => { if (canceled) return; ads.value = ads.value.filter(x => x !== ad); diff --git a/packages/frontend/src/pages/admin/announcements.vue b/packages/frontend/src/pages/admin/announcements.vue index f941d512b3..44552fb88c 100644 --- a/packages/frontend/src/pages/admin/announcements.vue +++ b/packages/frontend/src/pages/admin/announcements.vue @@ -54,7 +54,7 @@ SPDX-License-Identifier: AGPL-3.0-only <MkSwitch v-model="announcement.needConfirmationToRead" :helpText="i18n.ts._announcement.needConfirmationToReadDescription"> {{ i18n.ts._announcement.needConfirmationToRead }} </MkSwitch> - <p v-if="announcement.reads">{{ i18n.t('nUsersRead', { n: announcement.reads }) }}</p> + <p v-if="announcement.reads">{{ i18n.tsx.nUsersRead({ n: announcement.reads }) }}</p> <div class="buttons _buttons"> <MkButton class="button" inline primary @click="save(announcement)"><i class="ti ti-device-floppy"></i> {{ i18n.ts.save }}</MkButton> <MkButton v-if="announcement.id != null" class="button" inline @click="archive(announcement)"><i class="ti ti-check"></i> {{ i18n.ts._announcement.end }} ({{ i18n.ts.archive }})</MkButton> @@ -109,7 +109,7 @@ function add() { function del(announcement) { os.confirm({ type: 'warning', - text: i18n.t('deleteAreYouSure', { x: announcement.title }), + text: i18n.tsx.deleteAreYouSure({ x: announcement.title }), }).then(({ canceled }) => { if (canceled) return; announcements.value = announcements.value.filter(x => x !== announcement); diff --git a/packages/frontend/src/pages/admin/branding.vue b/packages/frontend/src/pages/admin/branding.vue index 72b47949e7..dbbb3941d8 100644 --- a/packages/frontend/src/pages/admin/branding.vue +++ b/packages/frontend/src/pages/admin/branding.vue @@ -19,10 +19,10 @@ SPDX-License-Identifier: AGPL-3.0-only <template #prefix><i class="ti ti-link"></i></template> <template #label>{{ i18n.ts._serverSettings.iconUrl }} (App/192px)</template> <template #caption> - <div>{{ i18n.t('_serverSettings.appIconDescription', { host: instance.name ?? host }) }}</div> + <div>{{ i18n.tsx._serverSettings.appIconDescription({ host: instance.name ?? host }) }}</div> <div>({{ i18n.ts._serverSettings.appIconUsageExample }})</div> <div>{{ i18n.ts._serverSettings.appIconStyleRecommendation }}</div> - <div><strong>{{ i18n.t('_serverSettings.appIconResolutionMustBe', { resolution: '192x192px' }) }}</strong></div> + <div><strong>{{ i18n.tsx._serverSettings.appIconResolutionMustBe({ resolution: '192x192px' }) }}</strong></div> </template> </MkInput> @@ -30,10 +30,10 @@ SPDX-License-Identifier: AGPL-3.0-only <template #prefix><i class="ti ti-link"></i></template> <template #label>{{ i18n.ts._serverSettings.iconUrl }} (App/512px)</template> <template #caption> - <div>{{ i18n.t('_serverSettings.appIconDescription', { host: instance.name ?? host }) }}</div> + <div>{{ i18n.tsx._serverSettings.appIconDescription({ host: instance.name ?? host }) }}</div> <div>({{ i18n.ts._serverSettings.appIconUsageExample }})</div> <div>{{ i18n.ts._serverSettings.appIconStyleRecommendation }}</div> - <div><strong>{{ i18n.t('_serverSettings.appIconResolutionMustBe', { resolution: '512x512px' }) }}</strong></div> + <div><strong>{{ i18n.tsx._serverSettings.appIconResolutionMustBe({ resolution: '512x512px' }) }}</strong></div> </template> </MkInput> diff --git a/packages/frontend/src/pages/admin/relays.vue b/packages/frontend/src/pages/admin/relays.vue index 6811a8eba5..847c8bc1d4 100644 --- a/packages/frontend/src/pages/admin/relays.vue +++ b/packages/frontend/src/pages/admin/relays.vue @@ -14,7 +14,7 @@ SPDX-License-Identifier: AGPL-3.0-only <i v-if="relay.status === 'accepted'" class="ti ti-check" :class="$style.icon" style="color: var(--success);"></i> <i v-else-if="relay.status === 'rejected'" class="ti ti-ban" :class="$style.icon" style="color: var(--error);"></i> <i v-else class="ti ti-clock" :class="$style.icon"></i> - <span>{{ i18n.t(`_relayStatus.${relay.status}`) }}</span> + <span>{{ i18n.ts._relayStatus[relay.status] }}</span> </div> <MkButton class="button" inline danger @click="remove(relay.inbox)"><i class="ti ti-trash"></i> {{ i18n.ts.remove }}</MkButton> </div> diff --git a/packages/frontend/src/pages/admin/roles.role.vue b/packages/frontend/src/pages/admin/roles.role.vue index ff29f4ec1f..ad58255576 100644 --- a/packages/frontend/src/pages/admin/roles.role.vue +++ b/packages/frontend/src/pages/admin/roles.role.vue @@ -104,7 +104,7 @@ function edit() { async function del() { const { canceled } = await os.confirm({ type: 'warning', - text: i18n.t('deleteAreYouSure', { x: role.name }), + text: i18n.tsx.deleteAreYouSure({ x: role.name }), }); if (canceled) return; diff --git a/packages/frontend/src/pages/announcements.vue b/packages/frontend/src/pages/announcements.vue index c31c6d0903..e3c0ea574a 100644 --- a/packages/frontend/src/pages/announcements.vue +++ b/packages/frontend/src/pages/announcements.vue @@ -78,7 +78,7 @@ async function read(announcement) { const confirm = await os.confirm({ type: 'question', title: i18n.ts._announcement.readConfirmTitle, - text: i18n.t('_announcement.readConfirmText', { title: announcement.title }), + text: i18n.tsx._announcement.readConfirmText({ title: announcement.title }), }); if (confirm.canceled) return; } diff --git a/packages/frontend/src/pages/auth.form.vue b/packages/frontend/src/pages/auth.form.vue index 39a7924f94..50fd696af3 100644 --- a/packages/frontend/src/pages/auth.form.vue +++ b/packages/frontend/src/pages/auth.form.vue @@ -6,12 +6,12 @@ SPDX-License-Identifier: AGPL-3.0-only <template> <section> <div v-if="app.permission.length > 0"> - <p>{{ i18n.t('_auth.permission', { name }) }}</p> + <p>{{ i18n.tsx._auth.permission({ name }) }}</p> <ul> - <li v-for="p in app.permission" :key="p">{{ i18n.t(`_permissions.${p}`) }}</li> + <li v-for="p in app.permission" :key="p">{{ i18n.ts._permissions[p] }}</li> </ul> </div> - <div>{{ i18n.t('_auth.shareAccess', { name: `${name} (${app.id})` }) }}</div> + <div>{{ i18n.tsx._auth.shareAccess({ name: `${name} (${app.id})` }) }}</div> <div :class="$style.buttons"> <MkButton inline @click="cancel">{{ i18n.ts.cancel }}</MkButton> <MkButton inline primary @click="accept">{{ i18n.ts.accept }}</MkButton> diff --git a/packages/frontend/src/pages/auth.vue b/packages/frontend/src/pages/auth.vue index fd38e22ce8..9b409aa4f8 100644 --- a/packages/frontend/src/pages/auth.vue +++ b/packages/frontend/src/pages/auth.vue @@ -25,7 +25,7 @@ SPDX-License-Identifier: AGPL-3.0-only <h1>{{ i18n.ts._auth.denied }}</h1> </div> <div v-if="state == 'accepted' && session"> - <h1>{{ session.app.isAuthorized ? i18n.t('already-authorized') : i18n.ts.allowed }}</h1> + <h1>{{ session.app.isAuthorized ? i18n.ts['already-authorized'] : i18n.ts.allowed }}</h1> <p v-if="session.app.callbackUrl"> {{ i18n.ts._auth.callback }} <MkEllipsis/> diff --git a/packages/frontend/src/pages/avatar-decorations.vue b/packages/frontend/src/pages/avatar-decorations.vue index 376679fd17..f71a7685bd 100644 --- a/packages/frontend/src/pages/avatar-decorations.vue +++ b/packages/frontend/src/pages/avatar-decorations.vue @@ -60,7 +60,7 @@ function add() { function del(avatarDecoration) { os.confirm({ type: 'warning', - text: i18n.t('deleteAreYouSure', { x: avatarDecoration.name }), + text: i18n.tsx.deleteAreYouSure({ x: avatarDecoration.name }), }).then(({ canceled }) => { if (canceled) return; avatarDecorations.value = avatarDecorations.value.filter(x => x !== avatarDecoration); diff --git a/packages/frontend/src/pages/channel-editor.vue b/packages/frontend/src/pages/channel-editor.vue index 99b93444db..bbe5dc0a4e 100644 --- a/packages/frontend/src/pages/channel-editor.vue +++ b/packages/frontend/src/pages/channel-editor.vue @@ -174,7 +174,7 @@ function save() { async function archive() { const { canceled } = await os.confirm({ type: 'warning', - title: i18n.t('channelArchiveConfirmTitle', { name: name.value }), + title: i18n.tsx.channelArchiveConfirmTitle({ name: name.value }), text: i18n.ts.channelArchiveConfirmDescription, }); diff --git a/packages/frontend/src/pages/clip.vue b/packages/frontend/src/pages/clip.vue index e55e99a6fa..8640561583 100644 --- a/packages/frontend/src/pages/clip.vue +++ b/packages/frontend/src/pages/clip.vue @@ -145,7 +145,7 @@ const headerActions = computed(() => clip.value && isOwned.value ? [{ handler: async (): Promise<void> => { const { canceled } = await os.confirm({ type: 'warning', - text: i18n.t('deleteAreYouSure', { x: clip.value.name }), + text: i18n.tsx.deleteAreYouSure({ x: clip.value.name }), }); if (canceled) return; diff --git a/packages/frontend/src/pages/drive.file.info.vue b/packages/frontend/src/pages/drive.file.info.vue index 64c3ad70ba..3e45f5e88c 100644 --- a/packages/frontend/src/pages/drive.file.info.vue +++ b/packages/frontend/src/pages/drive.file.info.vue @@ -180,7 +180,7 @@ async function deleteFile() { const { canceled } = await os.confirm({ type: 'warning', - text: i18n.t('driveFileDeleteConfirm', { name: file.value.name }), + text: i18n.tsx.driveFileDeleteConfirm({ name: file.value.name }), }); if (canceled) return; diff --git a/packages/frontend/src/pages/drop-and-fusion.vue b/packages/frontend/src/pages/drop-and-fusion.vue index beb2e714e0..b995521dfb 100644 --- a/packages/frontend/src/pages/drop-and-fusion.vue +++ b/packages/frontend/src/pages/drop-and-fusion.vue @@ -45,7 +45,7 @@ SPDX-License-Identifier: AGPL-3.0-only <div :class="$style.frame"> <div :class="$style.frameInner"> <div class="_gaps_s" style="padding: 16px;"> - <div><b>{{ i18n.t('lastNDays', { n: 7 }) }} {{ i18n.ts.ranking }}</b> ({{ gameMode }})</div> + <div><b>{{ i18n.tsx.lastNDays({ n: 7 }) }} {{ i18n.ts.ranking }}</b> ({{ gameMode }})</div> <div v-if="ranking" class="_gaps_s"> <div v-for="r in ranking" :key="r.id" :class="$style.rankingRecord"> <MkAvatar :link="true" style="width: 24px; height: 24px; margin-right: 4px;" :user="r.user"/> diff --git a/packages/frontend/src/pages/emoji-edit-dialog.vue b/packages/frontend/src/pages/emoji-edit-dialog.vue index 1ef150bc2d..9dcc7bc035 100644 --- a/packages/frontend/src/pages/emoji-edit-dialog.vue +++ b/packages/frontend/src/pages/emoji-edit-dialog.vue @@ -185,7 +185,7 @@ async function done() { async function del() { const { canceled } = await os.confirm({ type: 'warning', - text: i18n.t('removeAreYouSure', { x: name.value }), + text: i18n.tsx.removeAreYouSure({ x: name.value }), }); if (canceled) return; diff --git a/packages/frontend/src/pages/flash/flash-edit.vue b/packages/frontend/src/pages/flash/flash-edit.vue index ba350f1c0a..8f60b83a6c 100644 --- a/packages/frontend/src/pages/flash/flash-edit.vue +++ b/packages/frontend/src/pages/flash/flash-edit.vue @@ -438,7 +438,7 @@ function show() { async function del() { const { canceled } = await os.confirm({ type: 'warning', - text: i18n.t('deleteAreYouSure', { x: flash.value.title }), + text: i18n.tsx.deleteAreYouSure({ x: flash.value.title }), }); if (canceled) return; diff --git a/packages/frontend/src/pages/follow.vue b/packages/frontend/src/pages/follow.vue index eefef828bd..44364bb0f2 100644 --- a/packages/frontend/src/pages/follow.vue +++ b/packages/frontend/src/pages/follow.vue @@ -20,7 +20,7 @@ import { mainRouter } from '@/global/router/main.js'; async function follow(user): Promise<void> { const { canceled } = await os.confirm({ type: 'question', - text: i18n.t('followConfirm', { name: user.name || user.username }), + text: i18n.tsx.followConfirm({ name: user.name || user.username }), }); if (canceled) { diff --git a/packages/frontend/src/pages/instance-info.vue b/packages/frontend/src/pages/instance-info.vue index 4211dc0d87..9519a8a3f4 100644 --- a/packages/frontend/src/pages/instance-info.vue +++ b/packages/frontend/src/pages/instance-info.vue @@ -95,9 +95,9 @@ SPDX-License-Identifier: AGPL-3.0-only </MkSelect> </div> <div class="charts"> - <div class="label">{{ i18n.t('recentNHours', { n: 90 }) }}</div> + <div class="label">{{ i18n.tsx.recentNHours({ n: 90 }) }}</div> <MkChart class="chart" :src="chartSrc" span="hour" :limit="90" :args="{ host: host }" :detailed="true"></MkChart> - <div class="label">{{ i18n.t('recentNDays', { n: 90 }) }}</div> + <div class="label">{{ i18n.tsx.recentNDays({ n: 90 }) }}</div> <MkChart class="chart" :src="chartSrc" span="day" :limit="90" :args="{ host: host }" :detailed="true"></MkChart> </div> </div> diff --git a/packages/frontend/src/pages/invite.vue b/packages/frontend/src/pages/invite.vue index 61030741fa..d8613a67d3 100644 --- a/packages/frontend/src/pages/invite.vue +++ b/packages/frontend/src/pages/invite.vue @@ -19,9 +19,9 @@ SPDX-License-Identifier: AGPL-3.0-only </MKSpacer> <MkSpacer v-else :contentMax="800"> <div class="_gaps_m" style="text-align: center;"> - <div v-if="resetCycle && inviteLimit">{{ i18n.t('inviteLimitResetCycle', { time: resetCycle, limit: inviteLimit }) }}</div> + <div v-if="resetCycle && inviteLimit">{{ i18n.tsx.inviteLimitResetCycle({ time: resetCycle, limit: inviteLimit }) }}</div> <MkButton inline primary rounded :disabled="currentInviteLimit !== null && currentInviteLimit <= 0" @click="create"><i class="ti ti-user-plus"></i> {{ i18n.ts.createInviteCode }}</MkButton> - <div v-if="currentInviteLimit !== null">{{ i18n.t('createLimitRemaining', { limit: currentInviteLimit }) }}</div> + <div v-if="currentInviteLimit !== null">{{ i18n.tsx.createLimitRemaining({ limit: currentInviteLimit }) }}</div> <MkPagination ref="pagingComponent" :pagination="pagination"> <template #default="{ items }"> diff --git a/packages/frontend/src/pages/miauth.vue b/packages/frontend/src/pages/miauth.vue index 539cb462ad..27ae1bdbc8 100644 --- a/packages/frontend/src/pages/miauth.vue +++ b/packages/frontend/src/pages/miauth.vue @@ -20,13 +20,13 @@ SPDX-License-Identifier: AGPL-3.0-only </div> <div v-else> <div v-if="_permissions.length > 0"> - <p v-if="name">{{ i18n.t('_auth.permission', { name }) }}</p> + <p v-if="name">{{ i18n.tsx._auth.permission({ name }) }}</p> <p v-else>{{ i18n.ts._auth.permissionAsk }}</p> <ul> - <li v-for="p in _permissions" :key="p">{{ i18n.t(`_permissions.${p}`) }}</li> + <li v-for="p in _permissions" :key="p">{{ i18n.ts._permissions[p] }}</li> </ul> </div> - <div v-if="name">{{ i18n.t('_auth.shareAccess', { name }) }}</div> + <div v-if="name">{{ i18n.tsx._auth.shareAccess({ name }) }}</div> <div v-else>{{ i18n.ts._auth.shareAccessAsk }}</div> <div :class="$style.buttons"> <MkButton inline @click="deny">{{ i18n.ts.cancel }}</MkButton> diff --git a/packages/frontend/src/pages/my-antennas/editor.vue b/packages/frontend/src/pages/my-antennas/editor.vue index 45acbb2158..a4c6ca6f52 100644 --- a/packages/frontend/src/pages/my-antennas/editor.vue +++ b/packages/frontend/src/pages/my-antennas/editor.vue @@ -116,7 +116,7 @@ async function saveAntenna() { async function deleteAntenna() { const { canceled } = await os.confirm({ type: 'warning', - text: i18n.t('removeAreYouSure', { x: props.antenna.name }), + text: i18n.tsx.removeAreYouSure({ x: props.antenna.name }), }); if (canceled) return; diff --git a/packages/frontend/src/pages/my-lists/index.vue b/packages/frontend/src/pages/my-lists/index.vue index 14e2315843..295112b0ba 100644 --- a/packages/frontend/src/pages/my-lists/index.vue +++ b/packages/frontend/src/pages/my-lists/index.vue @@ -19,7 +19,7 @@ SPDX-License-Identifier: AGPL-3.0-only <div v-if="items.length > 0" class="_gaps"> <MkA v-for="list in items" :key="list.id" class="_panel" :class="$style.list" :to="`/my/lists/${ list.id }`"> - <div style="margin-bottom: 4px;">{{ list.name }} <span :class="$style.nUsers">({{ i18n.t('nUsers', { n: `${list.userIds.length}/${$i.policies['userEachUserListsLimit']}` }) }})</span></div> + <div style="margin-bottom: 4px;">{{ list.name }} <span :class="$style.nUsers">({{ i18n.tsx.nUsers({ n: `${list.userIds.length}/${$i.policies['userEachUserListsLimit']}` }) }})</span></div> <MkAvatars :userIds="list.userIds" :limit="10"/> </MkA> </div> diff --git a/packages/frontend/src/pages/my-lists/list.vue b/packages/frontend/src/pages/my-lists/list.vue index 85775a2fdd..7207e956db 100644 --- a/packages/frontend/src/pages/my-lists/list.vue +++ b/packages/frontend/src/pages/my-lists/list.vue @@ -25,7 +25,7 @@ SPDX-License-Identifier: AGPL-3.0-only <MkFolder defaultOpen> <template #label>{{ i18n.ts.members }}</template> - <template #caption>{{ i18n.t('nUsers', { n: `${list.userIds.length}/${$i.policies['userEachUserListsLimit']}` }) }}</template> + <template #caption>{{ i18n.tsx.nUsers({ n: `${list.userIds.length}/${$i.policies['userEachUserListsLimit']}` }) }}</template> <div class="_gaps_s"> <MkButton rounded primary style="margin: 0 auto;" @click="addUser()">{{ i18n.ts.addUser }}</MkButton> @@ -155,7 +155,7 @@ async function deleteList() { if (!list.value) return; const { canceled } = await os.confirm({ type: 'warning', - text: i18n.t('removeAreYouSure', { x: list.value.name }), + text: i18n.tsx.removeAreYouSure({ x: list.value.name }), }); if (canceled) return; diff --git a/packages/frontend/src/pages/note.vue b/packages/frontend/src/pages/note.vue index 4c0e9bbb98..9b72f9b2ac 100644 --- a/packages/frontend/src/pages/note.vue +++ b/packages/frontend/src/pages/note.vue @@ -147,7 +147,7 @@ definePageMetadata(computed(() => note.value ? { avatar: note.value.user, path: `/notes/${note.value.id}`, share: { - title: i18n.t('noteOf', { user: note.value.user.name }), + title: i18n.tsx.noteOf({ user: note.value.user.name }), text: note.value.text, }, } : null)); diff --git a/packages/frontend/src/pages/notifications.vue b/packages/frontend/src/pages/notifications.vue index 8913a89adb..e6098c90b3 100644 --- a/packages/frontend/src/pages/notifications.vue +++ b/packages/frontend/src/pages/notifications.vue @@ -51,7 +51,7 @@ const directNotesPagination = { function setFilter(ev) { const typeItems = notificationTypes.map(t => ({ - text: i18n.t(`_notification._types.${t}`), + text: i18n.ts._notification._types[t], active: includeTypes.value && includeTypes.value.includes(t), action: () => { includeTypes.value = [t]; diff --git a/packages/frontend/src/pages/oauth.vue b/packages/frontend/src/pages/oauth.vue index 878fa6be4e..38b9dd60c4 100644 --- a/packages/frontend/src/pages/oauth.vue +++ b/packages/frontend/src/pages/oauth.vue @@ -9,13 +9,13 @@ SPDX-License-Identifier: AGPL-3.0-only <MkSpacer :contentMax="800"> <div v-if="$i"> <div v-if="permissions.length > 0"> - <p v-if="name">{{ i18n.t('_auth.permission', { name }) }}</p> + <p v-if="name">{{ i18n.tsx._auth.permission({ name }) }}</p> <p v-else>{{ i18n.ts._auth.permissionAsk }}</p> <ul> - <li v-for="p in permissions" :key="p">{{ i18n.t(`_permissions.${p}`) }}</li> + <li v-for="p in permissions" :key="p">{{ i18n.ts._permissions[p] }}</li> </ul> </div> - <div v-if="name">{{ i18n.t('_auth.shareAccess', { name }) }}</div> + <div v-if="name">{{ i18n.tsx._auth.shareAccess({ name }) }}</div> <div v-else>{{ i18n.ts._auth.shareAccessAsk }}</div> <form :class="$style.buttons" action="/oauth/decision" accept-charset="utf-8" method="post"> <input name="login_token" type="hidden" :value="$i.token"/> diff --git a/packages/frontend/src/pages/page-editor/page-editor.vue b/packages/frontend/src/pages/page-editor/page-editor.vue index 6db72dccba..bd85b97d59 100644 --- a/packages/frontend/src/pages/page-editor/page-editor.vue +++ b/packages/frontend/src/pages/page-editor/page-editor.vue @@ -175,7 +175,7 @@ function save() { function del() { os.confirm({ type: 'warning', - text: i18n.t('removeAreYouSure', { x: title.value.trim() }), + text: i18n.tsx.removeAreYouSure({ x: title.value.trim() }), }).then(({ canceled }) => { if (canceled) return; misskeyApi('pages/delete', { diff --git a/packages/frontend/src/pages/reversi/game.board.vue b/packages/frontend/src/pages/reversi/game.board.vue index 18fd74427c..582967ad2b 100644 --- a/packages/frontend/src/pages/reversi/game.board.vue +++ b/packages/frontend/src/pages/reversi/game.board.vue @@ -10,17 +10,17 @@ SPDX-License-Identifier: AGPL-3.0-only <div style="overflow: clip; line-height: 28px;"> <div v-if="!iAmPlayer && !game.isEnded && turnUser" class="turn"> - <Mfm :key="'turn:' + turnUser.id" :text="i18n.t('_reversi.turnOf', { name: turnUser.name ?? turnUser.username })" :plain="true" :customEmojis="turnUser.emojis"/> + <Mfm :key="'turn:' + turnUser.id" :text="i18n.tsx._reversi.turnOf({ name: turnUser.name ?? turnUser.username })" :plain="true" :customEmojis="turnUser.emojis"/> <MkEllipsis/> </div> <div v-if="(logPos !== logs.length) && turnUser" class="turn"> - <Mfm :key="'past-turn-of:' + turnUser.id" :text="i18n.t('_reversi.pastTurnOf', { name: turnUser.name ?? turnUser.username })" :plain="true" :customEmojis="turnUser.emojis"/> + <Mfm :key="'past-turn-of:' + turnUser.id" :text="i18n.tsx._reversi.pastTurnOf({ name: turnUser.name ?? turnUser.username })" :plain="true" :customEmojis="turnUser.emojis"/> </div> <div v-if="iAmPlayer && !game.isEnded && !isMyTurn" class="turn1">{{ i18n.ts._reversi.opponentTurn }}<MkEllipsis/></div> <div v-if="iAmPlayer && !game.isEnded && isMyTurn" class="turn2" style="animation: tada 1s linear infinite both;">{{ i18n.ts._reversi.myTurn }}</div> <div v-if="game.isEnded && logPos == logs.length" class="result"> <template v-if="game.winner"> - <Mfm :key="'won'" :text="i18n.t('_reversi.won', { name: game.winner.name ?? game.winner.username })" :plain="true" :customEmojis="game.winner.emojis"/> + <Mfm :key="'won'" :text="i18n.tsx._reversi.won({ name: game.winner.name ?? game.winner.username })" :plain="true" :customEmojis="game.winner.emojis"/> <span v-if="game.surrendered != null"> ({{ i18n.ts._reversi.surrendered }})</span> </template> <template v-else>{{ i18n.ts._reversi.drawn }}</template> @@ -62,7 +62,7 @@ SPDX-License-Identifier: AGPL-3.0-only </div> </div> - <div class="status"><b>{{ i18n.t('_reversi.turnCount', { count: logPos }) }}</b> {{ i18n.ts._reversi.black }}:{{ engine.blackCount }} {{ i18n.ts._reversi.white }}:{{ engine.whiteCount }} {{ i18n.ts._reversi.total }}:{{ engine.blackCount + engine.whiteCount }}</div> + <div class="status"><b>{{ i18n.tsx._reversi.turnCount({ count: logPos }) }}</b> {{ i18n.ts._reversi.black }}:{{ engine.blackCount }} {{ i18n.ts._reversi.white }}:{{ engine.whiteCount }} {{ i18n.ts._reversi.total }}:{{ engine.blackCount + engine.whiteCount }}</div> <div v-if="!game.isEnded && iAmPlayer" class="_buttonsCenter"> <MkButton danger @click="surrender">{{ i18n.ts._reversi.surrender }}</MkButton> diff --git a/packages/frontend/src/pages/settings/2fa.vue b/packages/frontend/src/pages/settings/2fa.vue index 35331738fd..dae1f03ccb 100644 --- a/packages/frontend/src/pages/settings/2fa.vue +++ b/packages/frontend/src/pages/settings/2fa.vue @@ -141,7 +141,7 @@ async function unregisterKey(key) { const confirm = await os.confirm({ type: 'question', title: i18n.ts._2fa.removeKey, - text: i18n.t('_2fa.removeKeyConfirm', { name: key.name }), + text: i18n.tsx._2fa.removeKeyConfirm({ name: key.name }), }); if (confirm.canceled) return; diff --git a/packages/frontend/src/pages/settings/apps.vue b/packages/frontend/src/pages/settings/apps.vue index 4a778d4b38..525b4e7519 100644 --- a/packages/frontend/src/pages/settings/apps.vue +++ b/packages/frontend/src/pages/settings/apps.vue @@ -30,7 +30,7 @@ SPDX-License-Identifier: AGPL-3.0-only <details> <summary>{{ i18n.ts.details }}</summary> <ul> - <li v-for="p in token.permission" :key="p">{{ i18n.t(`_permissions.${p}`) }}</li> + <li v-for="p in token.permission" :key="p">{{ i18n.ts._permissions[p] }}</li> </ul> </details> <div> diff --git a/packages/frontend/src/pages/settings/avatar-decoration.vue b/packages/frontend/src/pages/settings/avatar-decoration.vue index 70565cc990..ecfa1ca257 100644 --- a/packages/frontend/src/pages/settings/avatar-decoration.vue +++ b/packages/frontend/src/pages/settings/avatar-decoration.vue @@ -6,7 +6,7 @@ SPDX-License-Identifier: AGPL-3.0-only <template> <div> <div v-if="!loading" class="_gaps"> - <MkInfo>{{ i18n.t('_profile.avatarDecorationMax', { max: $i.policies.avatarDecorationLimit }) }} ({{ i18n.t('remainingN', { n: $i.policies.avatarDecorationLimit - $i.avatarDecorations.length }) }})</MkInfo> + <MkInfo>{{ i18n.tsx._profile.avatarDecorationMax({ max: $i.policies.avatarDecorationLimit }) }} ({{ i18n.tsx.remainingN({ n: $i.policies.avatarDecorationLimit - $i.avatarDecorations.length }) }})</MkInfo> <MkAvatar :class="$style.avatar" :user="$i" forceShowDecoration/> diff --git a/packages/frontend/src/pages/settings/general.vue b/packages/frontend/src/pages/settings/general.vue index e52a5ee04f..bbc910a32a 100644 --- a/packages/frontend/src/pages/settings/general.vue +++ b/packages/frontend/src/pages/settings/general.vue @@ -77,9 +77,9 @@ SPDX-License-Identifier: AGPL-3.0-only <MkRadios v-model="mediaListWithOneImageAppearance"> <template #label>{{ i18n.ts.mediaListWithOneImageAppearance }}</template> <option value="expand">{{ i18n.ts.default }}</option> - <option value="16_9">{{ i18n.t('limitTo', { x: '16:9' }) }}</option> - <option value="1_1">{{ i18n.t('limitTo', { x: '1:1' }) }}</option> - <option value="2_3">{{ i18n.t('limitTo', { x: '2:3' }) }}</option> + <option value="16_9">{{ i18n.tsx.limitTo({ x: '16:9' }) }}</option> + <option value="1_1">{{ i18n.tsx.limitTo({ x: '1:1' }) }}</option> + <option value="2_3">{{ i18n.tsx.limitTo({ x: '2:3' }) }}</option> </MkRadios> </div> </FormSection> diff --git a/packages/frontend/src/pages/settings/migration.vue b/packages/frontend/src/pages/settings/migration.vue index 2699f0ad63..6e5de0a333 100644 --- a/packages/frontend/src/pages/settings/migration.vue +++ b/packages/frontend/src/pages/settings/migration.vue @@ -21,7 +21,7 @@ SPDX-License-Identifier: AGPL-3.0-only <div class="_gaps"> <MkInput v-for="(_, i) in accountAliases" v-model="accountAliases[i]"> <template #prefix><i class="ti ti-plane-arrival"></i></template> - <template #label>{{ i18n.t('_accountMigration.moveFromLabel', { n: i + 1 }) }}</template> + <template #label>{{ i18n.tsx._accountMigration.moveFromLabel({ n: i + 1 }) }}</template> </MkInput> </div> </div> @@ -97,7 +97,7 @@ async function move(): Promise<void> { const account = moveToAccount.value; const confirm = await os.confirm({ type: 'warning', - text: i18n.t('_accountMigration.migrationConfirm', { account }), + text: i18n.tsx._accountMigration.migrationConfirm({ account }), }); if (confirm.canceled) return; await os.apiWithDialog('i/move', { diff --git a/packages/frontend/src/pages/settings/mute-block.word-mute.vue b/packages/frontend/src/pages/settings/mute-block.word-mute.vue index 7328967c51..a70adaf359 100644 --- a/packages/frontend/src/pages/settings/mute-block.word-mute.vue +++ b/packages/frontend/src/pages/settings/mute-block.word-mute.vue @@ -64,7 +64,7 @@ async function save() { os.alert({ type: 'error', title: i18n.ts.regexpError, - text: i18n.t('regexpErrorDescription', { tab: 'word mute', line: i + 1 }) + '\n' + err.toString(), + text: i18n.tsx.regexpErrorDescription({ tab: 'word mute', line: i + 1 }) + '\n' + err.toString(), }); // re-throw error so these invalid settings are not saved throw err; diff --git a/packages/frontend/src/pages/settings/notifications.vue b/packages/frontend/src/pages/settings/notifications.vue index 766f33ff65..b1b1e4fb03 100644 --- a/packages/frontend/src/pages/settings/notifications.vue +++ b/packages/frontend/src/pages/settings/notifications.vue @@ -9,7 +9,7 @@ SPDX-License-Identifier: AGPL-3.0-only <template #label>{{ i18n.ts.notificationRecieveConfig }}</template> <div class="_gaps_s"> <MkFolder v-for="type in notificationTypes.filter(x => !nonConfigurableNotificationTypes.includes(x))" :key="type"> - <template #label>{{ i18n.t('_notification._types.' + type) }}</template> + <template #label>{{ i18n.ts._notification._types[type] }}</template> <template #suffix> {{ $i.notificationRecieveConfig[type]?.type === 'never' ? i18n.ts.none : diff --git a/packages/frontend/src/pages/settings/profile.vue b/packages/frontend/src/pages/settings/profile.vue index e2d98cbf29..3e707041eb 100644 --- a/packages/frontend/src/pages/settings/profile.vue +++ b/packages/frontend/src/pages/settings/profile.vue @@ -206,7 +206,7 @@ function changeAvatar(ev) { const { canceled } = await os.confirm({ type: 'question', - text: i18n.t('cropImageAsk'), + text: i18n.ts.cropImageAsk, okText: i18n.ts.cropYes, cancelText: i18n.ts.cropNo, }); @@ -232,7 +232,7 @@ function changeBanner(ev) { const { canceled } = await os.confirm({ type: 'question', - text: i18n.t('cropImageAsk'), + text: i18n.ts.cropImageAsk, okText: i18n.ts.cropYes, cancelText: i18n.ts.cropNo, }); diff --git a/packages/frontend/src/pages/settings/sounds.vue b/packages/frontend/src/pages/settings/sounds.vue index 9fbcce2286..a116a0407f 100644 --- a/packages/frontend/src/pages/settings/sounds.vue +++ b/packages/frontend/src/pages/settings/sounds.vue @@ -19,7 +19,7 @@ SPDX-License-Identifier: AGPL-3.0-only <template #label>{{ i18n.ts.sounds }}</template> <div class="_gaps_s"> <MkFolder v-for="type in operationTypes" :key="type"> - <template #label>{{ i18n.t('_sfx.' + type) }}</template> + <template #label>{{ i18n.ts._sfx[type] }}</template> <template #suffix>{{ getSoundTypeName(sounds[type].type) }}</template> <XSound :type="sounds[type].type" :volume="sounds[type].volume" :fileId="sounds[type].fileId" :fileUrl="sounds[type].fileUrl" @update="(res) => updated(type, res)"/> @@ -33,9 +33,9 @@ SPDX-License-Identifier: AGPL-3.0-only <script lang="ts" setup> import { Ref, computed, ref } from 'vue'; +import XSound from './sounds.sound.vue'; import type { SoundType, OperationType } from '@/scripts/sound.js'; import type { SoundStore } from '@/store.js'; -import XSound from './sounds.sound.vue'; import MkRange from '@/components/MkRange.vue'; import MkButton from '@/components/MkButton.vue'; import FormSection from '@/components/form/section.vue'; diff --git a/packages/frontend/src/pages/settings/theme.install.vue b/packages/frontend/src/pages/settings/theme.install.vue index 45970c88e6..2946a43398 100644 --- a/packages/frontend/src/pages/settings/theme.install.vue +++ b/packages/frontend/src/pages/settings/theme.install.vue @@ -33,7 +33,7 @@ async function install(code: string): Promise<void> { await installTheme(code); os.alert({ type: 'success', - text: i18n.t('_theme.installed', { name: theme.name }), + text: i18n.tsx._theme.installed({ name: theme.name }), }); } catch (err) { switch (err.message.toLowerCase()) { diff --git a/packages/frontend/src/pages/settings/webhook.edit.vue b/packages/frontend/src/pages/settings/webhook.edit.vue index a122c4c819..d079d0f92b 100644 --- a/packages/frontend/src/pages/settings/webhook.edit.vue +++ b/packages/frontend/src/pages/settings/webhook.edit.vue @@ -99,7 +99,7 @@ async function save(): Promise<void> { async function del(): Promise<void> { const { canceled } = await os.confirm({ type: 'warning', - text: i18n.t('deleteAreYouSure', { x: webhook.name }), + text: i18n.tsx.deleteAreYouSure({ x: webhook.name }), }); if (canceled) return; diff --git a/packages/frontend/src/pages/signup-complete.vue b/packages/frontend/src/pages/signup-complete.vue index 3f007b7afc..4e99ef5ae7 100644 --- a/packages/frontend/src/pages/signup-complete.vue +++ b/packages/frontend/src/pages/signup-complete.vue @@ -12,7 +12,7 @@ SPDX-License-Identifier: AGPL-3.0-only <i class="ti ti-user-check"></i> </div> <div class="_gaps_m" style="padding: 32px;"> - <div>{{ i18n.t('clickToFinishEmailVerification', { ok: i18n.ts.gotIt }) }}</div> + <div>{{ i18n.tsx.clickToFinishEmailVerification({ ok: i18n.ts.gotIt }) }}</div> <div> <MkButton gradate large rounded type="submit" :disabled="submitting" data-cy-admin-ok style="margin: 0 auto;"> {{ submitting ? i18n.ts.processing : i18n.ts.gotIt }}<MkEllipsis v-if="submitting"/> diff --git a/packages/frontend/src/pages/theme-editor.vue b/packages/frontend/src/pages/theme-editor.vue index e14bd6d89b..738b015a99 100644 --- a/packages/frontend/src/pages/theme-editor.vue +++ b/packages/frontend/src/pages/theme-editor.vue @@ -208,7 +208,7 @@ async function saveAs() { changed.value = false; os.alert({ type: 'success', - text: i18n.t('_theme.installed', { name: theme.value.name }), + text: i18n.tsx._theme.installed({ name: theme.value.name }), }); } diff --git a/packages/frontend/src/pages/user/home.vue b/packages/frontend/src/pages/user/home.vue index ed9722b7ed..e8687b148b 100644 --- a/packages/frontend/src/pages/user/home.vue +++ b/packages/frontend/src/pages/user/home.vue @@ -87,7 +87,7 @@ SPDX-License-Identifier: AGPL-3.0-only </dl> <dl v-if="user.birthday" class="field"> <dt class="name"><i class="ti ti-cake ti-fw"></i> {{ i18n.ts.birthday }}</dt> - <dd class="value">{{ user.birthday.replace('-', '/').replace('-', '/') }} ({{ i18n.t('yearsOld', { age }) }})</dd> + <dd class="value">{{ user.birthday.replace('-', '/').replace('-', '/') }} ({{ i18n.tsx.yearsOld({ age }) }})</dd> </dl> <dl class="field"> <dt class="name"><i class="ti ti-calendar ti-fw"></i> {{ i18n.ts.registeredDate }}</dt> 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'); + }); + }); +} diff --git a/packages/frontend/src/ui/deck.vue b/packages/frontend/src/ui/deck.vue index 304ebbf0b2..f58756b284 100644 --- a/packages/frontend/src/ui/deck.vue +++ b/packages/frontend/src/ui/deck.vue @@ -189,7 +189,7 @@ const addColumn = async (ev) => { const { canceled, result: column } = await os.select({ title: i18n.ts._deck.addColumn, items: columns.map(column => ({ - value: column, text: i18n.t('_deck._columns.' + column), + value: column, text: i18n.ts._deck._columns[column], })), }); if (canceled) return; @@ -197,7 +197,7 @@ const addColumn = async (ev) => { addColumnToStore({ type: column, id: uuid(), - name: i18n.t('_deck._columns.' + column), + name: i18n.ts._deck._columns[column], width: 330, }); }; @@ -256,7 +256,7 @@ function changeProfile(ev: MouseEvent) { async function deleteProfile() { const { canceled } = await os.confirm({ type: 'warning', - text: i18n.t('deleteAreYouSure', { x: deckStore.state.profile }), + text: i18n.tsx.deleteAreYouSure({ x: deckStore.state.profile }), }); if (canceled) return; diff --git a/packages/frontend/src/widgets/WidgetCalendar.vue b/packages/frontend/src/widgets/WidgetCalendar.vue index c78e291a2e..5bd16584d2 100644 --- a/packages/frontend/src/widgets/WidgetCalendar.vue +++ b/packages/frontend/src/widgets/WidgetCalendar.vue @@ -7,11 +7,11 @@ SPDX-License-Identifier: AGPL-3.0-only <div :class="[$style.root, { _panel: !widgetProps.transparent }]" data-cy-mkw-calendar> <div :class="[$style.calendar, { [$style.isHoliday]: isHoliday }]"> <p :class="$style.monthAndYear"> - <span :class="$style.year">{{ i18n.t('yearX', { year }) }}</span> - <span :class="$style.month">{{ i18n.t('monthX', { month }) }}</span> + <span :class="$style.year">{{ i18n.tsx.yearX({ year }) }}</span> + <span :class="$style.month">{{ i18n.tsx.monthX({ month }) }}</span> </p> - <p v-if="month === 1 && day === 1" class="day">🎉{{ i18n.t('dayX', { day }) }}<span style="display: inline-block; transform: scaleX(-1);">🎉</span></p> - <p v-else :class="$style.day">{{ i18n.t('dayX', { day }) }}</p> + <p v-if="month === 1 && day === 1" class="day">🎉{{ i18n.tsx.dayX({ day }) }}<span style="display: inline-block; transform: scaleX(-1);">🎉</span></p> + <p v-else :class="$style.day">{{ i18n.tsx.dayX({ day }) }}</p> <p :class="$style.weekDay">{{ weekDay }}</p> </div> <div :class="$style.info"> diff --git a/packages/frontend/src/widgets/WidgetSlideshow.vue b/packages/frontend/src/widgets/WidgetSlideshow.vue index 94bf6d7eec..f2fc24ddf7 100644 --- a/packages/frontend/src/widgets/WidgetSlideshow.vue +++ b/packages/frontend/src/widgets/WidgetSlideshow.vue @@ -9,7 +9,7 @@ SPDX-License-Identifier: AGPL-3.0-only <p v-if="widgetProps.folderId == null"> {{ i18n.ts.folder }} </p> - <p v-if="widgetProps.folderId != null && images.length === 0 && !fetching">{{ i18n.t('no-image') }}</p> + <p v-if="widgetProps.folderId != null && images.length === 0 && !fetching">{{ i18n.ts['no-image'] }}</p> <div ref="slideA" class="slide a"></div> <div ref="slideB" class="slide b"></div> </div> diff --git a/packages/frontend/src/widgets/WidgetTimeline.vue b/packages/frontend/src/widgets/WidgetTimeline.vue index 8f46bc0206..f1e4897634 100644 --- a/packages/frontend/src/widgets/WidgetTimeline.vue +++ b/packages/frontend/src/widgets/WidgetTimeline.vue @@ -15,7 +15,7 @@ SPDX-License-Identifier: AGPL-3.0-only </template> <template #header> <button class="_button" @click="choose"> - <span>{{ widgetProps.src === 'list' ? widgetProps.list.name : widgetProps.src === 'antenna' ? widgetProps.antenna.name : i18n.t('_timelines.' + widgetProps.src) }}</span> + <span>{{ widgetProps.src === 'list' ? widgetProps.list.name : widgetProps.src === 'antenna' ? widgetProps.antenna.name : i18n.ts._timelines[widgetProps.src] }}</span> <i :class="menuOpened ? 'ti ti-chevron-up' : 'ti ti-chevron-down'" style="margin-left: 8px;"></i> </button> </template> diff --git a/packages/frontend/src/widgets/WidgetTrends.vue b/packages/frontend/src/widgets/WidgetTrends.vue index 35925a9088..65d9235565 100644 --- a/packages/frontend/src/widgets/WidgetTrends.vue +++ b/packages/frontend/src/widgets/WidgetTrends.vue @@ -14,7 +14,7 @@ SPDX-License-Identifier: AGPL-3.0-only <div v-for="stat in stats" :key="stat.tag"> <div class="tag"> <MkA class="a" :to="`/tags/${ encodeURIComponent(stat.tag) }`" :title="stat.tag">#{{ stat.tag }}</MkA> - <p>{{ i18n.t('nUsersMentioned', { n: stat.usersCount }) }}</p> + <p>{{ i18n.tsx.nUsersMentioned({ n: stat.usersCount }) }}</p> </div> <MkMiniChart class="chart" :src="stat.chart"/> </div> |