From 57ec04d9ecc51060225bb15867215c7475685f92 Mon Sep 17 00:00:00 2001
From: syuilo <Syuilotan@yahoo.co.jp>
Date: Fri, 28 Jan 2022 11:39:49 +0900
Subject: [PATCH] refactor(client): i18n.locale -> i18n.ts

---
 packages/client/src/account.ts                | 10 +--
 .../src/components/abuse-report-window.vue    | 10 +--
 .../client/src/components/autocomplete.vue    |  2 +-
 packages/client/src/components/captcha.vue    |  2 +-
 .../src/components/channel-follow-button.vue  |  6 +-
 .../client/src/components/channel-preview.vue |  6 +-
 packages/client/src/components/cw-button.vue  |  4 +-
 packages/client/src/components/dialog.vue     |  4 +-
 .../src/components/drive-select-dialog.vue    |  2 +-
 .../client/src/components/drive-window.vue    |  2 +-
 packages/client/src/components/drive.file.vue | 26 ++++----
 .../client/src/components/drive.folder.vue    | 24 +++----
 .../src/components/drive.nav-folder.vue       |  2 +-
 packages/client/src/components/drive.vue      | 50 +++++++--------
 .../client/src/components/emoji-picker.vue    | 10 +--
 .../client/src/components/follow-button.vue   | 12 ++--
 .../client/src/components/forgot-password.vue | 14 ++---
 packages/client/src/components/global/a.vue   | 10 +--
 .../client/src/components/global/header.vue   |  2 +-
 .../client/src/components/global/time.vue     |  6 +-
 .../client/src/components/note-detailed.vue   |  2 +-
 packages/client/src/components/note.vue       | 16 ++---
 packages/client/src/components/post-form.vue  | 58 ++++++++---------
 .../client/src/components/renote-button.vue   |  4 +-
 .../src/components/user-online-indicator.vue  |  8 +--
 packages/client/src/init.ts                   |  8 +--
 packages/client/src/menu.ts                   | 12 ++--
 packages/client/src/pages/_error_.vue         | 16 ++---
 packages/client/src/pages/about-misskey.vue   | 18 +++---
 packages/client/src/pages/about.vue           |  2 +-
 packages/client/src/pages/admin/emojis.vue    | 20 +++---
 packages/client/src/pages/admin/index.vue     | 62 +++++++++----------
 packages/client/src/pages/drive.vue           |  2 +-
 packages/client/src/pages/emojis.emoji.vue    |  2 +-
 packages/client/src/pages/emojis.vue          |  6 +-
 packages/client/src/pages/favorites.vue       |  2 +-
 packages/client/src/pages/featured.vue        |  2 +-
 packages/client/src/pages/federation.vue      |  2 +-
 packages/client/src/pages/follow-requests.vue |  2 +-
 packages/client/src/pages/mentions.vue        |  2 +-
 packages/client/src/pages/messages.vue        |  2 +-
 .../client/src/pages/my-antennas/create.vue   |  2 +-
 packages/client/src/pages/my-clips/index.vue  | 10 +--
 packages/client/src/pages/my-lists/index.vue  |  4 +-
 packages/client/src/pages/not-found.vue       |  2 +-
 packages/client/src/pages/notifications.vue   | 12 ++--
 packages/client/src/pages/preview.vue         |  2 +-
 packages/client/src/pages/reset-password.vue  |  6 +-
 packages/client/src/pages/settings/email.vue  |  4 +-
 .../src/pages/settings/import-export.vue      |  6 +-
 packages/client/src/pages/settings/index.vue  | 48 +++++++-------
 .../client/src/pages/settings/mute-block.vue  |  2 +-
 .../client/src/pages/settings/privacy.vue     |  2 +-
 .../client/src/pages/settings/profile.vue     | 50 +++++++--------
 .../src/pages/settings/theme.install.vue      | 14 ++---
 packages/client/src/pages/settings/theme.vue  |  2 +-
 packages/client/src/pages/signup-complete.vue |  6 +-
 packages/client/src/pages/theme-editor.vue    | 26 ++++----
 packages/client/src/pages/timeline.vue        | 20 +++---
 packages/client/src/scripts/get-note-menu.ts  | 60 +++++++++---------
 .../client/src/scripts/get-note-summary.ts    |  6 +-
 packages/client/src/scripts/get-user-menu.ts  | 36 +++++------
 packages/client/src/scripts/i18n.ts           | 10 +--
 packages/client/src/scripts/lookup-user.ts    |  4 +-
 packages/client/src/scripts/please-login.ts   |  2 +-
 packages/client/src/scripts/search.ts         |  4 +-
 packages/client/src/scripts/select-file.ts    | 14 ++---
 .../src/scripts/show-suspended-dialog.ts      |  4 +-
 .../client/src/scripts/use-leave-guard.ts     |  4 +-
 packages/client/src/ui/deck.vue               |  4 +-
 packages/client/src/ui/deck/deck-store.ts     |  4 +-
 packages/client/src/ui/universal.vue          |  4 +-
 packages/client/src/widgets/calendar.vue      | 14 ++---
 packages/client/src/widgets/timeline.vue      |  8 +--
 74 files changed, 424 insertions(+), 424 deletions(-)

diff --git a/packages/client/src/account.ts b/packages/client/src/account.ts
index 5a935e1dc7..a04d0378c8 100644
--- a/packages/client/src/account.ts
+++ b/packages/client/src/account.ts
@@ -192,25 +192,25 @@ export async function openAccountMenu(opts: {
 	if (opts.withExtraOperation) {
 		popupMenu([...[{
 			type: 'link',
-			text: i18n.locale.profile,
+			text: i18n.ts.profile,
 			to: `/@${ $i.username }`,
 			avatar: $i,
 		}, null, ...(opts.includeCurrentAccount ? [createItem($i)] : []), ...accountItemPromises, {
 			icon: 'fas fa-plus',
-			text: i18n.locale.addAccount,
+			text: i18n.ts.addAccount,
 			action: () => {
 				popupMenu([{
-					text: i18n.locale.existingAccount,
+					text: i18n.ts.existingAccount,
 					action: () => { showSigninDialog(); },
 				}, {
-					text: i18n.locale.createAccount,
+					text: i18n.ts.createAccount,
 					action: () => { createAccount(); },
 				}], ev.currentTarget || ev.target);
 			},
 		}, {
 			type: 'link',
 			icon: 'fas fa-users',
-			text: i18n.locale.manageAccounts,
+			text: i18n.ts.manageAccounts,
 			to: `/settings/accounts`,
 		}]], ev.currentTarget || ev.target, {
 			align: 'left'
diff --git a/packages/client/src/components/abuse-report-window.vue b/packages/client/src/components/abuse-report-window.vue
index cd04f62bca..f2cb369802 100644
--- a/packages/client/src/components/abuse-report-window.vue
+++ b/packages/client/src/components/abuse-report-window.vue
@@ -2,7 +2,7 @@
 <XWindow ref="window" :initial-width="400" :initial-height="500" :can-resize="true" @closed="emit('closed')">
 	<template #header>
 		<i class="fas fa-exclamation-circle" style="margin-right: 0.5em;"></i>
-		<I18n :src="i18n.locale.reportAbuseOf" tag="span">
+		<I18n :src="i18n.ts.reportAbuseOf" tag="span">
 			<template #name>
 				<b><MkAcct :user="user"/></b>
 			</template>
@@ -11,12 +11,12 @@
 	<div class="dpvffvvy _monolithic_">
 		<div class="_section">
 			<MkTextarea v-model="comment">
-				<template #label>{{ i18n.locale.details }}</template>
-				<template #caption>{{ i18n.locale.fillAbuseReportDescription }}</template>
+				<template #label>{{ i18n.ts.details }}</template>
+				<template #caption>{{ i18n.ts.fillAbuseReportDescription }}</template>
 			</MkTextarea>
 		</div>
 		<div class="_section">
-			<MkButton primary full :disabled="comment.length === 0" @click="send">{{ i18n.locale.send }}</MkButton>
+			<MkButton primary full :disabled="comment.length === 0" @click="send">{{ i18n.ts.send }}</MkButton>
 		</div>
 	</div>
 </XWindow>
@@ -50,7 +50,7 @@ function send() {
 	}, undefined).then(res => {
 		os.alert({
 			type: 'success',
-			text: i18n.locale.abuseReported
+			text: i18n.ts.abuseReported
 		});
 		window.value?.close();
 		emit('closed');
diff --git a/packages/client/src/components/autocomplete.vue b/packages/client/src/components/autocomplete.vue
index 7ba83b7cb1..91a50ffa59 100644
--- a/packages/client/src/components/autocomplete.vue
+++ b/packages/client/src/components/autocomplete.vue
@@ -8,7 +8,7 @@
 			</span>
 			<span class="username">@{{ acct(user) }}</span>
 		</li>
-		<li tabindex="-1" class="choose" @click="chooseUser()" @keydown="onKeydown">{{ i18n.locale.selectUser }}</li>
+		<li tabindex="-1" class="choose" @click="chooseUser()" @keydown="onKeydown">{{ i18n.ts.selectUser }}</li>
 	</ol>
 	<ol v-else-if="hashtags.length > 0" ref="suggests" class="hashtags">
 		<li v-for="hashtag in hashtags" tabindex="-1" @click="complete(type, hashtag)" @keydown="onKeydown">
diff --git a/packages/client/src/components/captcha.vue b/packages/client/src/components/captcha.vue
index 307fc312bc..963ae25f8e 100644
--- a/packages/client/src/components/captcha.vue
+++ b/packages/client/src/components/captcha.vue
@@ -1,6 +1,6 @@
 <template>
 <div>
-	<span v-if="!available">{{ i18n.locale.waiting }}<MkEllipsis/></span>
+	<span v-if="!available">{{ i18n.ts.waiting }}<MkEllipsis/></span>
 	<div ref="captchaEl"></div>
 </div>
 </template>
diff --git a/packages/client/src/components/channel-follow-button.vue b/packages/client/src/components/channel-follow-button.vue
index 0ad5384cd5..7bbf5ae663 100644
--- a/packages/client/src/components/channel-follow-button.vue
+++ b/packages/client/src/components/channel-follow-button.vue
@@ -6,14 +6,14 @@
 >
 	<template v-if="!wait">
 		<template v-if="isFollowing">
-			<span v-if="full">{{ i18n.locale.unfollow }}</span><i class="fas fa-minus"></i>
+			<span v-if="full">{{ i18n.ts.unfollow }}</span><i class="fas fa-minus"></i>
 		</template>
 		<template v-else>
-			<span v-if="full">{{ i18n.locale.follow }}</span><i class="fas fa-plus"></i>
+			<span v-if="full">{{ i18n.ts.follow }}</span><i class="fas fa-plus"></i>
 		</template>
 	</template>
 	<template v-else>
-		<span v-if="full">{{ i18n.locale.processing }}</span><i class="fas fa-spinner fa-pulse fa-fw"></i>
+		<span v-if="full">{{ i18n.ts.processing }}</span><i class="fas fa-spinner fa-pulse fa-fw"></i>
 	</template>
 </button>
 </template>
diff --git a/packages/client/src/components/channel-preview.vue b/packages/client/src/components/channel-preview.vue
index 8d135a192f..dd3794a657 100644
--- a/packages/client/src/components/channel-preview.vue
+++ b/packages/client/src/components/channel-preview.vue
@@ -6,7 +6,7 @@
 		<div class="status">
 			<div>
 				<i class="fas fa-users fa-fw"></i>
-				<I18n :src="i18n.locale._channel.usersCount" tag="span" style="margin-left: 4px;">
+				<I18n :src="i18n.ts._channel.usersCount" tag="span" style="margin-left: 4px;">
 					<template #n>
 						<b>{{ channel.usersCount }}</b>
 					</template>
@@ -14,7 +14,7 @@
 			</div>
 			<div>
 				<i class="fas fa-pencil-alt fa-fw"></i>
-				<I18n :src="i18n.locale._channel.notesCount" tag="span" style="margin-left: 4px;">
+				<I18n :src="i18n.ts._channel.notesCount" tag="span" style="margin-left: 4px;">
 					<template #n>
 						<b>{{ channel.notesCount }}</b>
 					</template>
@@ -27,7 +27,7 @@
 	</article>
 	<footer>
 		<span v-if="channel.lastNotedAt">
-			{{ i18n.locale.updatedAt }}: <MkTime :time="channel.lastNotedAt"/>
+			{{ i18n.ts.updatedAt }}: <MkTime :time="channel.lastNotedAt"/>
 		</span>
 	</footer>
 </MkA>
diff --git a/packages/client/src/components/cw-button.vue b/packages/client/src/components/cw-button.vue
index ccfd11462a..e7c9aabe4e 100644
--- a/packages/client/src/components/cw-button.vue
+++ b/packages/client/src/components/cw-button.vue
@@ -1,6 +1,6 @@
 <template>
 <button class="nrvgflfu _button" @click="toggle">
-	<b>{{ modelValue ? i18n.locale._cw.hide : i18n.locale._cw.show }}</b>
+	<b>{{ modelValue ? i18n.ts._cw.hide : i18n.ts._cw.show }}</b>
 	<span v-if="!modelValue">{{ label }}</span>
 </button>
 </template>
@@ -25,7 +25,7 @@ const label = computed(() => {
 	return concat([
 		props.note.text ? [i18n.t('_cw.chars', { count: length(props.note.text) })] : [],
 		props.note.files && props.note.files.length !== 0 ? [i18n.t('_cw.files', { count: props.note.files.length }) ] : [],
-		props.note.poll != null ? [i18n.locale.poll] : []
+		props.note.poll != null ? [i18n.ts.poll] : []
 	] as string[][]).join(' / ');
 });
 
diff --git a/packages/client/src/components/dialog.vue b/packages/client/src/components/dialog.vue
index b6b649cde9..3e106a4f0c 100644
--- a/packages/client/src/components/dialog.vue
+++ b/packages/client/src/components/dialog.vue
@@ -28,8 +28,8 @@
 			</template>
 		</MkSelect>
 		<div v-if="(showOkButton || showCancelButton) && !actions" class="buttons">
-			<MkButton v-if="showOkButton" inline primary :autofocus="!input && !select" @click="ok">{{ (showCancelButton || input || select) ? i18n.locale.ok : i18n.locale.gotIt }}</MkButton>
-			<MkButton v-if="showCancelButton || input || select" inline @click="cancel">{{ i18n.locale.cancel }}</MkButton>
+			<MkButton v-if="showOkButton" inline primary :autofocus="!input && !select" @click="ok">{{ (showCancelButton || input || select) ? i18n.ts.ok : i18n.ts.gotIt }}</MkButton>
+			<MkButton v-if="showCancelButton || input || select" inline @click="cancel">{{ i18n.ts.cancel }}</MkButton>
 		</div>
 		<div v-if="actions" class="buttons">
 			<MkButton v-for="action in actions" :key="action.text" inline :primary="action.primary" @click="() => { action.callback(); close(); }">{{ action.text }}</MkButton>
diff --git a/packages/client/src/components/drive-select-dialog.vue b/packages/client/src/components/drive-select-dialog.vue
index 6d84511277..f6c59457d1 100644
--- a/packages/client/src/components/drive-select-dialog.vue
+++ b/packages/client/src/components/drive-select-dialog.vue
@@ -10,7 +10,7 @@
 	@closed="emit('closed')"
 >
 	<template #header>
-		{{ multiple ? ((type === 'file') ? i18n.locale.selectFiles : i18n.locale.selectFolders) : ((type === 'file') ? i18n.locale.selectFile : i18n.locale.selectFolder) }}
+		{{ multiple ? ((type === 'file') ? i18n.ts.selectFiles : i18n.ts.selectFolders) : ((type === 'file') ? i18n.ts.selectFile : i18n.ts.selectFolder) }}
 		<span v-if="selected.length > 0" style="margin-left: 8px; opacity: 0.5;">({{ number(selected.length) }})</span>
 	</template>
 	<XDrive :multiple="multiple" :select="type" @changeSelection="onChangeSelection" @selected="ok()"/>
diff --git a/packages/client/src/components/drive-window.vue b/packages/client/src/components/drive-window.vue
index 8b60bf7794..d08c5fb674 100644
--- a/packages/client/src/components/drive-window.vue
+++ b/packages/client/src/components/drive-window.vue
@@ -6,7 +6,7 @@
 	@closed="emit('closed')"
 >
 	<template #header>
-		{{ i18n.locale.drive }}
+		{{ i18n.ts.drive }}
 	</template>
 	<XDrive :initial-folder="initialFolder"/>
 </XWindow>
diff --git a/packages/client/src/components/drive.file.vue b/packages/client/src/components/drive.file.vue
index fd6a813838..669c0d7db1 100644
--- a/packages/client/src/components/drive.file.vue
+++ b/packages/client/src/components/drive.file.vue
@@ -10,15 +10,15 @@
 >
 	<div v-if="$i?.avatarId == file.id" class="label">
 		<img src="/client-assets/label.svg"/>
-		<p>{{ i18n.locale.avatar }}</p>
+		<p>{{ i18n.ts.avatar }}</p>
 	</div>
 	<div v-if="$i?.bannerId == file.id" class="label">
 		<img src="/client-assets/label.svg"/>
-		<p>{{ i18n.locale.banner }}</p>
+		<p>{{ i18n.ts.banner }}</p>
 	</div>
 	<div v-if="file.isSensitive" class="label red">
 		<img src="/client-assets/label-red.svg"/>
-		<p>{{ i18n.locale.nsfw }}</p>
+		<p>{{ i18n.ts.nsfw }}</p>
 	</div>
 
 	<MkDriveFileThumbnail class="thumbnail" :file="file" fit="contain"/>
@@ -61,30 +61,30 @@ const title = computed(() => `${props.file.name}\n${props.file.type} ${bytes(pro
 
 function getMenu() {
 	return [{
-		text: i18n.locale.rename,
+		text: i18n.ts.rename,
 		icon: 'fas fa-i-cursor',
 		action: rename
 	}, {
-		text: props.file.isSensitive ? i18n.locale.unmarkAsSensitive : i18n.locale.markAsSensitive,
+		text: props.file.isSensitive ? i18n.ts.unmarkAsSensitive : i18n.ts.markAsSensitive,
 		icon: props.file.isSensitive ? 'fas fa-eye' : 'fas fa-eye-slash',
 		action: toggleSensitive
 	}, {
-		text: i18n.locale.describeFile,
+		text: i18n.ts.describeFile,
 		icon: 'fas fa-i-cursor',
 		action: describe
 	}, null, {
-		text: i18n.locale.copyUrl,
+		text: i18n.ts.copyUrl,
 		icon: 'fas fa-link',
 		action: copyUrl
 	}, {
 		type: 'a',
 		href: props.file.url,
 		target: '_blank',
-		text: i18n.locale.download,
+		text: i18n.ts.download,
 		icon: 'fas fa-download',
 		download: props.file.name
 	}, null, {
-		text: i18n.locale.delete,
+		text: i18n.ts.delete,
 		icon: 'fas fa-trash-alt',
 		danger: true,
 		action: deleteFile
@@ -120,8 +120,8 @@ function onDragend() {
 
 function rename() {
 	os.inputText({
-		title: i18n.locale.renameFile,
-		placeholder: i18n.locale.inputNewFileName,
+		title: i18n.ts.renameFile,
+		placeholder: i18n.ts.inputNewFileName,
 		default: props.file.name,
 	}).then(({ canceled, result: name }) => {
 		if (canceled) return;
@@ -134,9 +134,9 @@ function rename() {
 
 function describe() {
 	os.popup(import('@/components/media-caption.vue'), {
-		title: i18n.locale.describeFile,
+		title: i18n.ts.describeFile,
 		input: {
-			placeholder: i18n.locale.inputNewDescription,
+			placeholder: i18n.ts.inputNewDescription,
 			default: props.file.comment !== null ? props.file.comment : '',
 		},
 		image: props.file
diff --git a/packages/client/src/components/drive.folder.vue b/packages/client/src/components/drive.folder.vue
index 20a6343cfe..57621bf097 100644
--- a/packages/client/src/components/drive.folder.vue
+++ b/packages/client/src/components/drive.folder.vue
@@ -20,7 +20,7 @@
 		{{ folder.name }}
 	</p>
 	<p v-if="defaultStore.state.uploadFolder == folder.id" class="upload">
-		{{ i18n.locale.uploadFolder }}
+		{{ i18n.ts.uploadFolder }}
 	</p>
 	<button v-if="selectMode" class="checkbox _button" :class="{ checked: isSelected }" @click.prevent.stop="checkboxClicked"></button>
 </div>
@@ -146,14 +146,14 @@ function onDrop(ev: DragEvent) {
 			switch (err) {
 				case 'detected-circular-definition':
 					os.alert({
-						title: i18n.locale.unableToProcess,
-						text: i18n.locale.circularReferenceFolder
+						title: i18n.ts.unableToProcess,
+						text: i18n.ts.circularReferenceFolder
 					});
 					break;
 				default:
 					os.alert({
 						type: 'error',
-						text: i18n.locale.somethingHappened
+						text: i18n.ts.somethingHappened
 					});
 			}
 		});
@@ -184,8 +184,8 @@ function go() {
 
 function rename() {
 	os.inputText({
-		title: i18n.locale.renameFolder,
-		placeholder: i18n.locale.inputNewFolderName,
+		title: i18n.ts.renameFolder,
+		placeholder: i18n.ts.inputNewFolderName,
 		default: props.folder.name
 	}).then(({ canceled, result: name }) => {
 		if (canceled) return;
@@ -208,14 +208,14 @@ function deleteFolder() {
 			case 'b0fc8a17-963c-405d-bfbc-859a487295e1':
 				os.alert({
 					type: 'error',
-					title: i18n.locale.unableToDelete,
-					text: i18n.locale.hasChildFilesOrFolders
+					title: i18n.ts.unableToDelete,
+					text: i18n.ts.hasChildFilesOrFolders
 				});
 				break;
 			default:
 				os.alert({
 					type: 'error',
-					text: i18n.locale.unableToDelete
+					text: i18n.ts.unableToDelete
 				});
 		}
 	});
@@ -227,7 +227,7 @@ function setAsUploadFolder() {
 
 function onContextmenu(ev: MouseEvent) {
 	os.contextMenu([{
-		text: i18n.locale.openInWindow,
+		text: i18n.ts.openInWindow,
 		icon: 'fas fa-window-restore',
 		action: () => {
 			os.popup(import('./drive-window.vue'), {
@@ -236,11 +236,11 @@ function onContextmenu(ev: MouseEvent) {
 			}, 'closed');
 		}
 	}, null, {
-		text: i18n.locale.rename,
+		text: i18n.ts.rename,
 		icon: 'fas fa-i-cursor',
 		action: rename,
 	}, null, {
-		text: i18n.locale.delete,
+		text: i18n.ts.delete,
 		icon: 'fas fa-trash-alt',
 		danger: true,
 		action: deleteFolder,
diff --git a/packages/client/src/components/drive.nav-folder.vue b/packages/client/src/components/drive.nav-folder.vue
index 7c35c5d3da..67223267c1 100644
--- a/packages/client/src/components/drive.nav-folder.vue
+++ b/packages/client/src/components/drive.nav-folder.vue
@@ -8,7 +8,7 @@
 	@drop.stop="onDrop"
 >
 	<i v-if="folder == null" class="fas fa-cloud"></i>
-	<span>{{ folder == null ? i18n.locale.drive : folder.name }}</span>
+	<span>{{ folder == null ? i18n.ts.drive : folder.name }}</span>
 </div>
 </template>
 
diff --git a/packages/client/src/components/drive.vue b/packages/client/src/components/drive.vue
index e27b0a5fbb..b706839540 100644
--- a/packages/client/src/components/drive.vue
+++ b/packages/client/src/components/drive.vue
@@ -54,7 +54,7 @@
 				/>
 				<!-- SEE: https://stackoverflow.com/questions/18744164/flex-box-align-last-row-to-grid -->
 				<div v-for="(n, i) in 16" :key="i" class="padding"></div>
-				<MkButton v-if="moreFolders" ref="moreFolders">{{ i18n.locale.loadMore }}</MkButton>
+				<MkButton v-if="moreFolders" ref="moreFolders">{{ i18n.ts.loadMore }}</MkButton>
 			</div>
 			<div v-show="files.length > 0" ref="filesContainer" class="files">
 				<XFile
@@ -71,12 +71,12 @@
 				/>
 				<!-- SEE: https://stackoverflow.com/questions/18744164/flex-box-align-last-row-to-grid -->
 				<div v-for="(n, i) in 16" :key="i" class="padding"></div>
-				<MkButton v-show="moreFiles" ref="loadMoreFiles" @click="fetchMoreFiles">{{ i18n.locale.loadMore }}</MkButton>
+				<MkButton v-show="moreFiles" ref="loadMoreFiles" @click="fetchMoreFiles">{{ i18n.ts.loadMore }}</MkButton>
 			</div>
 			<div v-if="files.length == 0 && folders.length == 0 && !fetching" class="empty">
 				<p v-if="draghover">{{ i18n.t('empty-draghover') }}</p>
-				<p v-if="!draghover && folder == null"><strong>{{ i18n.locale.emptyDrive }}</strong><br/>{{ i18n.t('empty-drive-description') }}</p>
-				<p v-if="!draghover && folder != null">{{ i18n.locale.emptyFolder }}</p>
+				<p v-if="!draghover && folder == null"><strong>{{ i18n.ts.emptyDrive }}</strong><br/>{{ i18n.t('empty-drive-description') }}</p>
+				<p v-if="!draghover && folder != null">{{ i18n.ts.emptyFolder }}</p>
 			</div>
 		</div>
 		<MkLoading v-if="fetching"/>
@@ -253,14 +253,14 @@ function onDrop(e: DragEvent): any {
 			switch (err) {
 				case 'detected-circular-definition':
 					os.alert({
-						title: i18n.locale.unableToProcess,
-						text: i18n.locale.circularReferenceFolder
+						title: i18n.ts.unableToProcess,
+						text: i18n.ts.circularReferenceFolder
 					});
 					break;
 				default:
 					os.alert({
 						type: 'error',
-						text: i18n.locale.somethingHappened
+						text: i18n.ts.somethingHappened
 					});
 			}
 		});
@@ -274,9 +274,9 @@ function selectLocalFile() {
 
 function urlUpload() {
 	os.inputText({
-		title: i18n.locale.uploadFromUrl,
+		title: i18n.ts.uploadFromUrl,
 		type: 'url',
-		placeholder: i18n.locale.uploadFromUrlDescription
+		placeholder: i18n.ts.uploadFromUrlDescription
 	}).then(({ canceled, result: url }) => {
 		if (canceled || !url) return;
 		os.api('drive/files/upload-from-url', {
@@ -285,16 +285,16 @@ function urlUpload() {
 		});
 
 		os.alert({
-			title: i18n.locale.uploadFromUrlRequested,
-			text: i18n.locale.uploadFromUrlMayTakeTime
+			title: i18n.ts.uploadFromUrlRequested,
+			text: i18n.ts.uploadFromUrlMayTakeTime
 		});
 	});
 }
 
 function createFolder() {
 	os.inputText({
-		title: i18n.locale.createFolder,
-		placeholder: i18n.locale.folderName
+		title: i18n.ts.createFolder,
+		placeholder: i18n.ts.folderName
 	}).then(({ canceled, result: name }) => {
 		if (canceled) return;
 		os.api('drive/folders/create', {
@@ -308,8 +308,8 @@ function createFolder() {
 
 function renameFolder(folderToRename: Misskey.entities.DriveFolder) {
 	os.inputText({
-		title: i18n.locale.renameFolder,
-		placeholder: i18n.locale.inputNewFolderName,
+		title: i18n.ts.renameFolder,
+		placeholder: i18n.ts.inputNewFolderName,
 		default: folderToRename.name
 	}).then(({ canceled, result: name }) => {
 		if (canceled) return;
@@ -334,14 +334,14 @@ function deleteFolder(folderToDelete: Misskey.entities.DriveFolder) {
 			case 'b0fc8a17-963c-405d-bfbc-859a487295e1':
 				os.alert({
 					type: 'error',
-					title: i18n.locale.unableToDelete,
-					text: i18n.locale.hasChildFilesOrFolders
+					title: i18n.ts.unableToDelete,
+					text: i18n.ts.hasChildFilesOrFolders
 				});
 				break;
 			default:
 				os.alert({
 					type: 'error',
-					text: i18n.locale.unableToDelete
+					text: i18n.ts.unableToDelete
 				});
 			}
 	});
@@ -562,29 +562,29 @@ function fetchMoreFiles() {
 
 function getMenu() {
 	return [{
-		text: i18n.locale.addFile,
+		text: i18n.ts.addFile,
 		type: 'label'
 	}, {
-		text: i18n.locale.upload,
+		text: i18n.ts.upload,
 		icon: 'fas fa-upload',
 		action: () => { selectLocalFile(); }
 	}, {
-		text: i18n.locale.fromUrl,
+		text: i18n.ts.fromUrl,
 		icon: 'fas fa-link',
 		action: () => { urlUpload(); }
 	}, null, {
-		text: folder.value ? folder.value.name : i18n.locale.drive,
+		text: folder.value ? folder.value.name : i18n.ts.drive,
 		type: 'label'
 	}, folder.value ? {
-		text: i18n.locale.renameFolder,
+		text: i18n.ts.renameFolder,
 		icon: 'fas fa-i-cursor',
 		action: () => { renameFolder(folder.value); }
 	} : undefined, folder.value ? {
-		text: i18n.locale.deleteFolder,
+		text: i18n.ts.deleteFolder,
 		icon: 'fas fa-trash-alt',
 		action: () => { deleteFolder(folder.value as Misskey.entities.DriveFolder); }
 	} : undefined, {
-		text: i18n.locale.createFolder,
+		text: i18n.ts.createFolder,
 		icon: 'fas fa-folder-plus',
 		action: () => { createFolder(); }
 	}];
diff --git a/packages/client/src/components/emoji-picker.vue b/packages/client/src/components/emoji-picker.vue
index 96670fa58c..f291510555 100644
--- a/packages/client/src/components/emoji-picker.vue
+++ b/packages/client/src/components/emoji-picker.vue
@@ -1,6 +1,6 @@
 <template>
 <div class="omfetrab" :class="['w' + width, 'h' + height, { big, asDrawer }]" :style="{ maxHeight: maxHeight ? maxHeight + 'px' : undefined }">
-	<input ref="search" v-model.trim="q" class="search" data-prevent-emoji-insert :class="{ filled: q != null && q != '' }" :placeholder="i18n.locale.search" @paste.stop="paste" @keyup.enter="done()">
+	<input ref="search" v-model.trim="q" class="search" data-prevent-emoji-insert :class="{ filled: q != null && q != '' }" :placeholder="i18n.ts.search" @paste.stop="paste" @keyup.enter="done()">
 	<div ref="emojis" class="emojis">
 		<section class="result">
 			<div v-if="searchResultCustom.length > 0">
@@ -43,7 +43,7 @@
 			</section>
 
 			<section>
-				<header class="_acrylic"><i class="far fa-clock fa-fw"></i> {{ i18n.locale.recentUsed }}</header>
+				<header class="_acrylic"><i class="far fa-clock fa-fw"></i> {{ i18n.ts.recentUsed }}</header>
 				<div>
 					<button v-for="emoji in recentlyUsedEmojis"
 						:key="emoji"
@@ -56,11 +56,11 @@
 			</section>
 		</div>
 		<div>
-			<header class="_acrylic">{{ i18n.locale.customEmojis }}</header>
-			<XSection v-for="category in customEmojiCategories" :key="'custom:' + category" :initial-shown="false" :emojis="customEmojis.filter(e => e.category === category).map(e => ':' + e.name + ':')" @chosen="chosen">{{ category || i18n.locale.other }}</XSection>
+			<header class="_acrylic">{{ i18n.ts.customEmojis }}</header>
+			<XSection v-for="category in customEmojiCategories" :key="'custom:' + category" :initial-shown="false" :emojis="customEmojis.filter(e => e.category === category).map(e => ':' + e.name + ':')" @chosen="chosen">{{ category || i18n.ts.other }}</XSection>
 		</div>
 		<div>
-			<header class="_acrylic">{{ i18n.locale.emoji }}</header>
+			<header class="_acrylic">{{ i18n.ts.emoji }}</header>
 			<XSection v-for="category in categories" :emojis="emojilist.filter(e => e.category === category).map(e => e.char)" @chosen="chosen">{{ category }}</XSection>
 		</div>
 	</div>
diff --git a/packages/client/src/components/follow-button.vue b/packages/client/src/components/follow-button.vue
index 345edb6441..93c9e891c1 100644
--- a/packages/client/src/components/follow-button.vue
+++ b/packages/client/src/components/follow-button.vue
@@ -6,23 +6,23 @@
 >
 	<template v-if="!wait">
 		<template v-if="hasPendingFollowRequestFromYou && user.isLocked">
-			<span v-if="full">{{ i18n.locale.followRequestPending }}</span><i class="fas fa-hourglass-half"></i>
+			<span v-if="full">{{ i18n.ts.followRequestPending }}</span><i class="fas fa-hourglass-half"></i>
 		</template>
 		<template v-else-if="hasPendingFollowRequestFromYou && !user.isLocked"> <!-- つまりリモートフォローの場合。 -->
-			<span v-if="full">{{ i18n.locale.processing }}</span><i class="fas fa-spinner fa-pulse"></i>
+			<span v-if="full">{{ i18n.ts.processing }}</span><i class="fas fa-spinner fa-pulse"></i>
 		</template>
 		<template v-else-if="isFollowing">
-			<span v-if="full">{{ i18n.locale.unfollow }}</span><i class="fas fa-minus"></i>
+			<span v-if="full">{{ i18n.ts.unfollow }}</span><i class="fas fa-minus"></i>
 		</template>
 		<template v-else-if="!isFollowing && user.isLocked">
-			<span v-if="full">{{ i18n.locale.followRequest }}</span><i class="fas fa-plus"></i>
+			<span v-if="full">{{ i18n.ts.followRequest }}</span><i class="fas fa-plus"></i>
 		</template>
 		<template v-else-if="!isFollowing && !user.isLocked">
-			<span v-if="full">{{ i18n.locale.follow }}</span><i class="fas fa-plus"></i>
+			<span v-if="full">{{ i18n.ts.follow }}</span><i class="fas fa-plus"></i>
 		</template>
 	</template>
 	<template v-else>
-		<span v-if="full">{{ i18n.locale.processing }}</span><i class="fas fa-spinner fa-pulse fa-fw"></i>
+		<span v-if="full">{{ i18n.ts.processing }}</span><i class="fas fa-spinner fa-pulse fa-fw"></i>
 	</template>
 </button>
 </template>
diff --git a/packages/client/src/components/forgot-password.vue b/packages/client/src/components/forgot-password.vue
index c74e1ac75e..46cbf6bd70 100644
--- a/packages/client/src/components/forgot-password.vue
+++ b/packages/client/src/components/forgot-password.vue
@@ -5,28 +5,28 @@
 	@close="dialog.close()"
 	@closed="emit('closed')"
 >
-	<template #header>{{ i18n.locale.forgotPassword }}</template>
+	<template #header>{{ i18n.ts.forgotPassword }}</template>
 
 	<form v-if="instance.enableEmail" class="bafeceda" @submit.prevent="onSubmit">
 		<div class="main _formRoot">
 			<MkInput v-model="username" class="_formBlock" type="text" pattern="^[a-zA-Z0-9_]+$" spellcheck="false" autofocus required>
-				<template #label>{{ i18n.locale.username }}</template>
+				<template #label>{{ i18n.ts.username }}</template>
 				<template #prefix>@</template>
 			</MkInput>
 
 			<MkInput v-model="email" class="_formBlock" type="email" spellcheck="false" required>
-				<template #label>{{ i18n.locale.emailAddress }}</template>
-				<template #caption>{{ i18n.locale._forgotPassword.enterEmail }}</template>
+				<template #label>{{ i18n.ts.emailAddress }}</template>
+				<template #caption>{{ i18n.ts._forgotPassword.enterEmail }}</template>
 			</MkInput>
 
-			<MkButton class="_formBlock" type="submit" :disabled="processing" primary style="margin: 0 auto;">{{ i18n.locale.send }}</MkButton>
+			<MkButton class="_formBlock" type="submit" :disabled="processing" primary style="margin: 0 auto;">{{ i18n.ts.send }}</MkButton>
 		</div>
 		<div class="sub">
-			<MkA to="/about" class="_link">{{ i18n.locale._forgotPassword.ifNoEmail }}</MkA>
+			<MkA to="/about" class="_link">{{ i18n.ts._forgotPassword.ifNoEmail }}</MkA>
 		</div>
 	</form>
 	<div v-else class="bafecedb">
-		{{ i18n.locale._forgotPassword.contactAdmin }}
+		{{ i18n.ts._forgotPassword.contactAdmin }}
 	</div>
 </XModalWindow>
 </template>
diff --git a/packages/client/src/components/global/a.vue b/packages/client/src/components/global/a.vue
index cf7385ca22..b1b6a0cdaf 100644
--- a/packages/client/src/components/global/a.vue
+++ b/packages/client/src/components/global/a.vue
@@ -43,31 +43,31 @@ function onContextmenu(ev) {
 		text: props.to,
 	}, {
 		icon: 'fas fa-window-maximize',
-		text: i18n.locale.openInWindow,
+		text: i18n.ts.openInWindow,
 		action: () => {
 			os.pageWindow(props.to);
 		}
 	}, sideViewHook ? {
 		icon: 'fas fa-columns',
-		text: i18n.locale.openInSideView,
+		text: i18n.ts.openInSideView,
 		action: () => {
 			sideViewHook(props.to);
 		}
 	} : undefined, {
 		icon: 'fas fa-expand-alt',
-		text: i18n.locale.showInPage,
+		text: i18n.ts.showInPage,
 		action: () => {
 			router.push(props.to);
 		}
 	}, null, {
 		icon: 'fas fa-external-link-alt',
-		text: i18n.locale.openInNewTab,
+		text: i18n.ts.openInNewTab,
 		action: () => {
 			window.open(props.to, '_blank');
 		}
 	}, {
 		icon: 'fas fa-link',
-		text: i18n.locale.copyLink,
+		text: i18n.ts.copyLink,
 		action: () => {
 			copyToClipboard(`${url}${props.to}`);
 		}
diff --git a/packages/client/src/components/global/header.vue b/packages/client/src/components/global/header.vue
index a241ece407..02598d95b3 100644
--- a/packages/client/src/components/global/header.vue
+++ b/packages/client/src/components/global/header.vue
@@ -104,7 +104,7 @@ export default defineComponent({
 			if (props.info.share) {
 				if (menu.length > 0) menu.push(null);
 				menu.push({
-					text: i18n.locale.share,
+					text: i18n.ts.share,
 					icon: 'fas fa-share-alt',
 					action: share
 				});
diff --git a/packages/client/src/components/global/time.vue b/packages/client/src/components/global/time.vue
index d2788264c5..19199fd408 100644
--- a/packages/client/src/components/global/time.vue
+++ b/packages/client/src/components/global/time.vue
@@ -31,9 +31,9 @@ const relative = $computed(() => {
 		ago >= 3600     ? i18n.t('_ago.hoursAgo',   { n: (~~(ago / 3600)).toString() }) :
 		ago >= 60       ? i18n.t('_ago.minutesAgo', { n: (~~(ago / 60)).toString() }) :
 		ago >= 10       ? i18n.t('_ago.secondsAgo', { n: (~~(ago % 60)).toString() }) :
-		ago >= -1       ? i18n.locale._ago.justNow :
-		ago <  -1       ? i18n.locale._ago.future :
-		i18n.locale._ago.unknown);
+		ago >= -1       ? i18n.ts._ago.justNow :
+		ago <  -1       ? i18n.ts._ago.future :
+		i18n.ts._ago.unknown);
 });
 
 function tick() {
diff --git a/packages/client/src/components/note-detailed.vue b/packages/client/src/components/note-detailed.vue
index a3b30f726e..5fc3a0f334 100644
--- a/packages/client/src/components/note-detailed.vue
+++ b/packages/client/src/components/note-detailed.vue
@@ -250,7 +250,7 @@ function menu(viaKeyboard = false): void {
 function showRenoteMenu(viaKeyboard = false): void {
 	if (!isMyRenote) return;
 	os.popupMenu([{
-		text: i18n.locale.unrenote,
+		text: i18n.ts.unrenote,
 		icon: 'fas fa-trash-alt',
 		danger: true,
 		action: () => {
diff --git a/packages/client/src/components/note.vue b/packages/client/src/components/note.vue
index fc89c2777b..6c596fb60d 100644
--- a/packages/client/src/components/note.vue
+++ b/packages/client/src/components/note.vue
@@ -10,13 +10,13 @@
 	:class="{ renote: isRenote }"
 >
 	<MkNoteSub v-if="appearNote.reply" :note="appearNote.reply" class="reply-to"/>
-	<div v-if="pinned" class="info"><i class="fas fa-thumbtack"></i> {{ i18n.locale.pinnedNote }}</div>
-	<div v-if="appearNote._prId_" class="info"><i class="fas fa-bullhorn"></i> {{ i18n.locale.promotion }}<button class="_textButton hide" @click="readPromo()">{{ i18n.locale.hideThisNote }} <i class="fas fa-times"></i></button></div>
-	<div v-if="appearNote._featuredId_" class="info"><i class="fas fa-bolt"></i> {{ i18n.locale.featured }}</div>
+	<div v-if="pinned" class="info"><i class="fas fa-thumbtack"></i> {{ i18n.ts.pinnedNote }}</div>
+	<div v-if="appearNote._prId_" class="info"><i class="fas fa-bullhorn"></i> {{ i18n.ts.promotion }}<button class="_textButton hide" @click="readPromo()">{{ i18n.ts.hideThisNote }} <i class="fas fa-times"></i></button></div>
+	<div v-if="appearNote._featuredId_" class="info"><i class="fas fa-bolt"></i> {{ i18n.ts.featured }}</div>
 	<div v-if="isRenote" class="renote">
 		<MkAvatar class="avatar" :user="note.user"/>
 		<i class="fas fa-retweet"></i>
-		<I18n :src="i18n.locale.renotedBy" tag="span">
+		<I18n :src="i18n.ts.renotedBy" tag="span">
 			<template #user>
 				<MkA v-user-preview="note.userId" class="name" :to="userPage(note.user)">
 					<MkUserName :user="note.user"/>
@@ -48,7 +48,7 @@
 				</p>
 				<div v-show="appearNote.cw == null || showContent" class="content" :class="{ collapsed }">
 					<div class="text">
-						<span v-if="appearNote.isHidden" style="opacity: 0.5">({{ i18n.locale.private }})</span>
+						<span v-if="appearNote.isHidden" style="opacity: 0.5">({{ i18n.ts.private }})</span>
 						<MkA v-if="appearNote.replyId" class="reply" :to="`/notes/${appearNote.replyId}`"><i class="fas fa-reply"></i></MkA>
 						<Mfm v-if="appearNote.text" :text="appearNote.text" :author="appearNote.user" :i="$i" :custom-emojis="appearNote.emojis"/>
 						<a v-if="appearNote.renote != null" class="rp">RN:</a>
@@ -67,7 +67,7 @@
 					<MkUrlPreview v-for="url in urls" :key="url" :url="url" :compact="true" :detail="false" class="url-preview"/>
 					<div v-if="appearNote.renote" class="renote"><XNoteSimple :note="appearNote.renote"/></div>
 					<button v-if="collapsed" class="fade _button" @click="collapsed = false">
-						<span>{{ i18n.locale.showMore }}</span>
+						<span>{{ i18n.ts.showMore }}</span>
 					</button>
 				</div>
 				<MkA v-if="appearNote.channel && !inChannel" class="channel" :to="`/channels/${appearNote.channel.id}`"><i class="fas fa-satellite-dish"></i> {{ appearNote.channel.name }}</MkA>
@@ -94,7 +94,7 @@
 	</article>
 </div>
 <div v-else class="muted" @click="muted = false">
-	<I18n :src="i18n.locale.userSaysSomething" tag="small">
+	<I18n :src="i18n.ts.userSaysSomething" tag="small">
 		<template #name>
 			<MkA v-user-preview="appearNote.userId" class="name" :to="userPage(appearNote.user)">
 				<MkUserName :user="appearNote.user"/>
@@ -238,7 +238,7 @@ function menu(viaKeyboard = false): void {
 function showRenoteMenu(viaKeyboard = false): void {
 	if (!isMyRenote) return;
 	os.popupMenu([{
-		text: i18n.locale.unrenote,
+		text: i18n.ts.unrenote,
 		icon: 'fas fa-trash-alt',
 		danger: true,
 		action: () => {
diff --git a/packages/client/src/components/post-form.vue b/packages/client/src/components/post-form.vue
index 2eda97e14d..a8882fc05f 100644
--- a/packages/client/src/components/post-form.vue
+++ b/packages/client/src/components/post-form.vue
@@ -8,28 +8,28 @@
 >
 	<header>
 		<button v-if="!fixed" class="cancel _button" @click="cancel"><i class="fas fa-times"></i></button>
-		<button v-click-anime v-tooltip="i18n.locale.switchAccount" class="account _button" @click="openAccountMenu">
+		<button v-click-anime v-tooltip="i18n.ts.switchAccount" class="account _button" @click="openAccountMenu">
 			<MkAvatar :user="postAccount ?? $i" class="avatar"/>
 		</button>
 		<div>
 			<span class="text-count" :class="{ over: textLength > maxTextLength }">{{ maxTextLength - textLength }}</span>
 			<span v-if="localOnly" class="local-only"><i class="fas fa-biohazard"></i></span>
-			<button ref="visibilityButton" v-tooltip="i18n.locale.visibility" class="_button visibility" :disabled="channel != null" @click="setVisibility">
+			<button ref="visibilityButton" v-tooltip="i18n.ts.visibility" class="_button visibility" :disabled="channel != null" @click="setVisibility">
 				<span v-if="visibility === 'public'"><i class="fas fa-globe"></i></span>
 				<span v-if="visibility === 'home'"><i class="fas fa-home"></i></span>
 				<span v-if="visibility === 'followers'"><i class="fas fa-unlock"></i></span>
 				<span v-if="visibility === 'specified'"><i class="fas fa-envelope"></i></span>
 			</button>
-			<button v-tooltip="i18n.locale.previewNoteText" class="_button preview" :class="{ active: showPreview }" @click="showPreview = !showPreview"><i class="fas fa-file-code"></i></button>
+			<button v-tooltip="i18n.ts.previewNoteText" class="_button preview" :class="{ active: showPreview }" @click="showPreview = !showPreview"><i class="fas fa-file-code"></i></button>
 			<button class="submit _buttonGradate" :disabled="!canPost" data-cy-open-post-form-submit @click="post">{{ submitText }}<i :class="reply ? 'fas fa-reply' : renote ? 'fas fa-quote-right' : 'fas fa-paper-plane'"></i></button>
 		</div>
 	</header>
 	<div class="form" :class="{ fixed }">
 		<XNoteSimple v-if="reply" class="preview" :note="reply"/>
 		<XNoteSimple v-if="renote" class="preview" :note="renote"/>
-		<div v-if="quoteId" class="with-quote"><i class="fas fa-quote-left"></i> {{ i18n.locale.quoteAttached }}<button @click="quoteId = null"><i class="fas fa-times"></i></button></div>
+		<div v-if="quoteId" class="with-quote"><i class="fas fa-quote-left"></i> {{ i18n.ts.quoteAttached }}<button @click="quoteId = null"><i class="fas fa-times"></i></button></div>
 		<div v-if="visibility === 'specified'" class="to-specified">
-			<span style="margin-right: 8px;">{{ i18n.locale.recipient }}</span>
+			<span style="margin-right: 8px;">{{ i18n.ts.recipient }}</span>
 			<div class="visibleUsers">
 				<span v-for="u in visibleUsers" :key="u.id">
 					<MkAcct :user="u"/>
@@ -38,21 +38,21 @@
 				<button class="_buttonPrimary" @click="addVisibleUser"><i class="fas fa-plus fa-fw"></i></button>
 			</div>
 		</div>
-		<MkInfo v-if="hasNotSpecifiedMentions" warn class="hasNotSpecifiedMentions">{{ i18n.locale.notSpecifiedMentionWarning }} - <button class="_textButton" @click="addMissingMention()">{{ i18n.locale.add }}</button></MkInfo>
-		<input v-show="useCw" ref="cwInputEl" v-model="cw" class="cw" :placeholder="i18n.locale.annotation" @keydown="onKeydown">
+		<MkInfo v-if="hasNotSpecifiedMentions" warn class="hasNotSpecifiedMentions">{{ i18n.ts.notSpecifiedMentionWarning }} - <button class="_textButton" @click="addMissingMention()">{{ i18n.ts.add }}</button></MkInfo>
+		<input v-show="useCw" ref="cwInputEl" v-model="cw" class="cw" :placeholder="i18n.ts.annotation" @keydown="onKeydown">
 		<textarea ref="textareaEl" v-model="text" class="text" :class="{ withCw: useCw }" :disabled="posting" :placeholder="placeholder" data-cy-post-form-text @keydown="onKeydown" @paste="onPaste" @compositionupdate="onCompositionUpdate" @compositionend="onCompositionEnd"/>
-		<input v-show="withHashtags" ref="hashtagsInputEl" v-model="hashtags" class="hashtags" :placeholder="i18n.locale.hashtags" list="hashtags">
+		<input v-show="withHashtags" ref="hashtagsInputEl" v-model="hashtags" class="hashtags" :placeholder="i18n.ts.hashtags" list="hashtags">
 		<XPostFormAttaches class="attaches" :files="files" @updated="updateFiles" @detach="detachFile" @changeSensitive="updateFileSensitive" @changeName="updateFileName"/>
 		<XPollEditor v-if="poll" v-model="poll" @destroyed="poll = null"/>
 		<XNotePreview v-if="showPreview" class="preview" :text="text"/>
 		<footer>
-			<button v-tooltip="i18n.locale.attachFile" class="_button" @click="chooseFileFrom"><i class="fas fa-photo-video"></i></button>
-			<button v-tooltip="i18n.locale.poll" class="_button" :class="{ active: poll }" @click="togglePoll"><i class="fas fa-poll-h"></i></button>
-			<button v-tooltip="i18n.locale.useCw" class="_button" :class="{ active: useCw }" @click="useCw = !useCw"><i class="fas fa-eye-slash"></i></button>
-			<button v-tooltip="i18n.locale.mention" class="_button" @click="insertMention"><i class="fas fa-at"></i></button>
-			<button v-tooltip="i18n.locale.hashtags" class="_button" :class="{ active: withHashtags }" @click="withHashtags = !withHashtags"><i class="fas fa-hashtag"></i></button>
-			<button v-tooltip="i18n.locale.emoji" class="_button" @click="insertEmoji"><i class="fas fa-laugh-squint"></i></button>
-			<button v-if="postFormActions.length > 0" v-tooltip="i18n.locale.plugin" class="_button" @click="showActions"><i class="fas fa-plug"></i></button>
+			<button v-tooltip="i18n.ts.attachFile" class="_button" @click="chooseFileFrom"><i class="fas fa-photo-video"></i></button>
+			<button v-tooltip="i18n.ts.poll" class="_button" :class="{ active: poll }" @click="togglePoll"><i class="fas fa-poll-h"></i></button>
+			<button v-tooltip="i18n.ts.useCw" class="_button" :class="{ active: useCw }" @click="useCw = !useCw"><i class="fas fa-eye-slash"></i></button>
+			<button v-tooltip="i18n.ts.mention" class="_button" @click="insertMention"><i class="fas fa-at"></i></button>
+			<button v-tooltip="i18n.ts.hashtags" class="_button" :class="{ active: withHashtags }" @click="withHashtags = !withHashtags"><i class="fas fa-hashtag"></i></button>
+			<button v-tooltip="i18n.ts.emoji" class="_button" @click="insertEmoji"><i class="fas fa-laugh-squint"></i></button>
+			<button v-if="postFormActions.length > 0" v-tooltip="i18n.ts.plugin" class="_button" @click="showActions"><i class="fas fa-plug"></i></button>
 		</footer>
 		<datalist id="hashtags">
 			<option v-for="hashtag in recentHashtags" :key="hashtag" :value="hashtag"/>
@@ -165,19 +165,19 @@ const draftKey = $computed((): string => {
 
 const placeholder = $computed((): string => {
 	if (props.renote) {
-		return i18n.locale._postForm.quotePlaceholder;
+		return i18n.ts._postForm.quotePlaceholder;
 	} else if (props.reply) {
-		return i18n.locale._postForm.replyPlaceholder;
+		return i18n.ts._postForm.replyPlaceholder;
 	} else if (props.channel) {
-		return i18n.locale._postForm.channelPlaceholder;
+		return i18n.ts._postForm.channelPlaceholder;
 	} else {
 		const xs = [
-			i18n.locale._postForm._placeholders.a,
-			i18n.locale._postForm._placeholders.b,
-			i18n.locale._postForm._placeholders.c,
-			i18n.locale._postForm._placeholders.d,
-			i18n.locale._postForm._placeholders.e,
-			i18n.locale._postForm._placeholders.f
+			i18n.ts._postForm._placeholders.a,
+			i18n.ts._postForm._placeholders.b,
+			i18n.ts._postForm._placeholders.c,
+			i18n.ts._postForm._placeholders.d,
+			i18n.ts._postForm._placeholders.e,
+			i18n.ts._postForm._placeholders.f
 		];
 		return xs[Math.floor(Math.random() * xs.length)];
 	}
@@ -185,10 +185,10 @@ const placeholder = $computed((): string => {
 
 const submitText = $computed((): string => {
 	return props.renote
-		? i18n.locale.quote
+		? i18n.ts.quote
 		: props.reply
-			? i18n.locale.reply
-			: i18n.locale.note;
+			? i18n.ts.reply
+			: i18n.ts.note;
 });
 
 const textLength = $computed((): number => {
@@ -342,7 +342,7 @@ function focus() {
 }
 
 function chooseFileFrom(ev) {
-	selectFiles(ev.currentTarget || ev.target, i18n.locale.attachFile).then(files_ => {
+	selectFiles(ev.currentTarget || ev.target, i18n.ts.attachFile).then(files_ => {
 		for (const file of files_) {
 			files.push(file);
 		}
@@ -447,7 +447,7 @@ async function onPaste(e: ClipboardEvent) {
 
 		os.confirm({
 			type: 'info',
-			text: i18n.locale.quoteQuestion,
+			text: i18n.ts.quoteQuestion,
 		}).then(({ canceled }) => {
 			if (canceled) {
 				insertTextAtCursor(textareaEl, paste);
diff --git a/packages/client/src/components/renote-button.vue b/packages/client/src/components/renote-button.vue
index 446686de10..c1c0d285e1 100644
--- a/packages/client/src/components/renote-button.vue
+++ b/packages/client/src/components/renote-button.vue
@@ -59,7 +59,7 @@ export default defineComponent({
 		const renote = (viaKeyboard = false) => {
 			pleaseLogin();
 			os.popupMenu([{
-				text: i18n.locale.renote,
+				text: i18n.ts.renote,
 				icon: 'fas fa-retweet',
 				action: () => {
 					os.api('notes/create', {
@@ -67,7 +67,7 @@ export default defineComponent({
 					});
 				}
 			}, {
-				text: i18n.locale.quote,
+				text: i18n.ts.quote,
 				icon: 'fas fa-quote-right',
 				action: () => {
 					os.post({
diff --git a/packages/client/src/components/user-online-indicator.vue b/packages/client/src/components/user-online-indicator.vue
index a87b0aeff5..a4f6f80383 100644
--- a/packages/client/src/components/user-online-indicator.vue
+++ b/packages/client/src/components/user-online-indicator.vue
@@ -13,10 +13,10 @@ const props = defineProps<{
 
 const text = $computed(() => {
 	switch (props.user.onlineStatus) {
-		case 'online': return i18n.locale.online;
-		case 'active': return i18n.locale.active;
-		case 'offline': return i18n.locale.offline;
-		case 'unknown': return i18n.locale.unknown;
+		case 'online': return i18n.ts.online;
+		case 'active': return i18n.ts.active;
+		case 'offline': return i18n.ts.offline;
+		case 'unknown': return i18n.ts.unknown;
 	}
 });
 </script>
diff --git a/packages/client/src/init.ts b/packages/client/src/init.ts
index af70aec70a..81e41febd1 100644
--- a/packages/client/src/init.ts
+++ b/packages/client/src/init.ts
@@ -185,7 +185,7 @@ app.config.globalProperties = {
 	$store: defaultStore,
 	$instance: instance,
 	$t: i18n.t,
-	$ts: i18n.locale,
+	$ts: i18n.ts,
 };
 
 app.use(router);
@@ -299,8 +299,8 @@ stream.on('_disconnected_', async () => {
 		reloadDialogShowing = true;
 		const { canceled } = await confirm({
 			type: 'warning',
-			title: i18n.locale.disconnectedFromServer,
-			text: i18n.locale.reloadConfirm,
+			title: i18n.ts.disconnectedFromServer,
+			text: i18n.ts.reloadConfirm,
 		});
 		reloadDialogShowing = false;
 		if (!canceled) {
@@ -324,7 +324,7 @@ if ($i) {
 	if ($i.isDeleted) {
 		alert({
 			type: 'warning',
-			text: i18n.locale.accountDeletionInProgress,
+			text: i18n.ts.accountDeletionInProgress,
 		});
 	}
 
diff --git a/packages/client/src/menu.ts b/packages/client/src/menu.ts
index 184779f21f..5f7a527095 100644
--- a/packages/client/src/menu.ts
+++ b/packages/client/src/menu.ts
@@ -73,7 +73,7 @@ export const menuDef = reactive({
 				})), null, {
 					type: 'link',
 					to: '/my/lists',
-					text: i18n.locale.manageLists,
+					text: i18n.ts.manageLists,
 					icon: 'fas fa-cog',
 				}];
 				items.value = _items;
@@ -104,7 +104,7 @@ export const menuDef = reactive({
 				})), null, {
 					type: 'link',
 					to: '/my/antennas',
-					text: i18n.locale.manageAntennas,
+					text: i18n.ts.manageAntennas,
 					icon: 'fas fa-cog',
 				}];
 				items.value = _items;
@@ -173,28 +173,28 @@ export const menuDef = reactive({
 		icon: 'fas fa-columns',
 		action: (ev) => {
 			os.popupMenu([{
-				text: i18n.locale.default,
+				text: i18n.ts.default,
 				active: ui === 'default' || ui === null,
 				action: () => {
 					localStorage.setItem('ui', 'default');
 					unisonReload();
 				}
 			}, {
-				text: i18n.locale.deck,
+				text: i18n.ts.deck,
 				active: ui === 'deck',
 				action: () => {
 					localStorage.setItem('ui', 'deck');
 					unisonReload();
 				}
 			}, {
-				text: i18n.locale.classic,
+				text: i18n.ts.classic,
 				active: ui === 'classic',
 				action: () => {
 					localStorage.setItem('ui', 'classic');
 					unisonReload();
 				}
 			}, /*{
-				text: i18n.locale.desktop + ' (β)',
+				text: i18n.ts.desktop + ' (β)',
 				active: ui === 'desktop',
 				action: () => {
 					localStorage.setItem('ui', 'desktop');
diff --git a/packages/client/src/pages/_error_.vue b/packages/client/src/pages/_error_.vue
index 7540995707..4cfe2e255c 100644
--- a/packages/client/src/pages/_error_.vue
+++ b/packages/client/src/pages/_error_.vue
@@ -3,15 +3,15 @@
 <transition :name="$store.state.animation ? 'zoom' : ''" appear>
 	<div v-show="loaded" class="mjndxjch">
 		<img src="https://xn--931a.moe/assets/error.jpg" class="_ghost"/>
-		<p><b><i class="fas fa-exclamation-triangle"></i> {{ i18n.locale.pageLoadError }}</b></p>
-		<p v-if="meta && (version === meta.version)">{{ i18n.locale.pageLoadErrorDescription }}</p>
-		<p v-else-if="serverIsDead">{{ i18n.locale.serverIsDead }}</p>
+		<p><b><i class="fas fa-exclamation-triangle"></i> {{ i18n.ts.pageLoadError }}</b></p>
+		<p v-if="meta && (version === meta.version)">{{ i18n.ts.pageLoadErrorDescription }}</p>
+		<p v-else-if="serverIsDead">{{ i18n.ts.serverIsDead }}</p>
 		<template v-else>
-			<p>{{ i18n.locale.newVersionOfClientAvailable }}</p>
-			<p>{{ i18n.locale.youShouldUpgradeClient }}</p>
-			<MkButton class="button primary" @click="reload">{{ i18n.locale.reload }}</MkButton>
+			<p>{{ i18n.ts.newVersionOfClientAvailable }}</p>
+			<p>{{ i18n.ts.youShouldUpgradeClient }}</p>
+			<MkButton class="button primary" @click="reload">{{ i18n.ts.reload }}</MkButton>
 		</template>
-		<p><MkA to="/docs/general/troubleshooting" class="_link">{{ i18n.locale.troubleshooting }}</MkA></p>
+		<p><MkA to="/docs/general/troubleshooting" class="_link">{{ i18n.ts.troubleshooting }}</MkA></p>
 		<p v-if="error" class="error">ERROR: {{ error }}</p>
 	</div>
 </transition>
@@ -54,7 +54,7 @@ function reload() {
 
 defineExpose({
 	[symbols.PAGE_INFO]: {
-		title: i18n.locale.error,
+		title: i18n.ts.error,
 		icon: 'fas fa-exclamation-triangle',
 	},
 });
diff --git a/packages/client/src/pages/about-misskey.vue b/packages/client/src/pages/about-misskey.vue
index f887e29cc0..0ffb6b9e1d 100644
--- a/packages/client/src/pages/about-misskey.vue
+++ b/packages/client/src/pages/about-misskey.vue
@@ -10,7 +10,7 @@
 				<span v-for="emoji in easterEggEmojis" :key="emoji.id" class="emoji" :data-physics-x="emoji.left" :data-physics-y="emoji.top" :class="{ _physics_circle_: !emoji.emoji.startsWith(':') }"><MkEmoji class="emoji" :emoji="emoji.emoji" :custom-emojis="$instance.emojis" :is-reaction="false" :normal="true" :no-style="true"/></span>
 			</div>
 			<div class="_formBlock" style="text-align: center;">
-				{{ i18n.locale._aboutMisskey.about }}<br><a href="https://misskey-hub.net/docs/misskey.html" target="_blank" class="_link">{{ i18n.locale.learnMore }}</a>
+				{{ i18n.ts._aboutMisskey.about }}<br><a href="https://misskey-hub.net/docs/misskey.html" target="_blank" class="_link">{{ i18n.ts.learnMore }}</a>
 			</div>
 			<div class="_formBlock" style="text-align: center;">
 				<MkButton primary rounded inline @click="iLoveMisskey">I <Mfm text="$[jelly ❤]"/> #Misskey</MkButton>
@@ -19,23 +19,23 @@
 				<div class="_formLinks">
 					<FormLink to="https://github.com/misskey-dev/misskey" external>
 						<template #icon><i class="fas fa-code"></i></template>
-						{{ i18n.locale._aboutMisskey.source }}
+						{{ i18n.ts._aboutMisskey.source }}
 						<template #suffix>GitHub</template>
 					</FormLink>
 					<FormLink to="https://crowdin.com/project/misskey" external>
 						<template #icon><i class="fas fa-language"></i></template>
-						{{ i18n.locale._aboutMisskey.translation }}
+						{{ i18n.ts._aboutMisskey.translation }}
 						<template #suffix>Crowdin</template>
 					</FormLink>
 					<FormLink to="https://www.patreon.com/syuilo" external>
 						<template #icon><i class="fas fa-hand-holding-medical"></i></template>
-						{{ i18n.locale._aboutMisskey.donate }}
+						{{ i18n.ts._aboutMisskey.donate }}
 						<template #suffix>Patreon</template>
 					</FormLink>
 				</div>
 			</FormSection>
 			<FormSection>
-				<template #label>{{ i18n.locale._aboutMisskey.contributors }}</template>
+				<template #label>{{ i18n.ts._aboutMisskey.contributors }}</template>
 				<div class="_formLinks">
 					<FormLink to="https://github.com/syuilo" external>@syuilo</FormLink>
 					<FormLink to="https://github.com/AyaMorisawa" external>@AyaMorisawa</FormLink>
@@ -47,12 +47,12 @@
 					<FormLink to="https://github.com/u1-liquid" external>@u1-liquid</FormLink>
 					<FormLink to="https://github.com/marihachi" external>@marihachi</FormLink>
 				</div>
-				<template #caption><MkLink url="https://github.com/misskey-dev/misskey/graphs/contributors">{{ i18n.locale._aboutMisskey.allContributors }}</MkLink></template>
+				<template #caption><MkLink url="https://github.com/misskey-dev/misskey/graphs/contributors">{{ i18n.ts._aboutMisskey.allContributors }}</MkLink></template>
 			</FormSection>
 			<FormSection>
-				<template #label><Mfm text="$[jelly ❤]"/> {{ i18n.locale._aboutMisskey.patrons }}</template>
+				<template #label><Mfm text="$[jelly ❤]"/> {{ i18n.ts._aboutMisskey.patrons }}</template>
 				<div v-for="patron in patrons" :key="patron">{{ patron }}</div>
-				<template #caption>{{ i18n.locale._aboutMisskey.morePatrons }}</template>
+				<template #caption>{{ i18n.ts._aboutMisskey.morePatrons }}</template>
 			</FormSection>
 		</div>
 	</MkSpacer>
@@ -194,7 +194,7 @@ onBeforeUnmount(() => {
 
 defineExpose({
 	[symbols.PAGE_INFO]: {
-		title: i18n.locale.aboutMisskey,
+		title: i18n.ts.aboutMisskey,
 		icon: null,
 		bg: 'var(--bg)',
 	},
diff --git a/packages/client/src/pages/about.vue b/packages/client/src/pages/about.vue
index a5984c548d..d5bab4baf8 100644
--- a/packages/client/src/pages/about.vue
+++ b/packages/client/src/pages/about.vue
@@ -90,7 +90,7 @@ const initStats = () => os.api('stats', {
 
 defineExpose({
 	[symbols.PAGE_INFO]: {
-		title: i18n.locale.instanceInfo,
+		title: i18n.ts.instanceInfo,
 		icon: 'fas fa-info-circle',
 		bg: 'var(--bg)',
 	},
diff --git a/packages/client/src/pages/admin/emojis.vue b/packages/client/src/pages/admin/emojis.vue
index 5b1dfe565a..f6fbf7dbd9 100644
--- a/packages/client/src/pages/admin/emojis.vue
+++ b/packages/client/src/pages/admin/emojis.vue
@@ -157,7 +157,7 @@ const remoteMenu = (emoji, ev: MouseEvent) => {
 		type: 'label',
 		text: ':' + emoji.name + ':',
 	}, {
-		text: i18n.locale.import,
+		text: i18n.ts.import,
 		icon: 'fas fa-plus',
 		action: () => { im(emoji) }
 	}], ev.currentTarget || ev.target);
@@ -166,14 +166,14 @@ const remoteMenu = (emoji, ev: MouseEvent) => {
 const menu = (ev: MouseEvent) => {
 	os.popupMenu([{
 		icon: 'fas fa-download',
-		text: i18n.locale.export,
+		text: i18n.ts.export,
 		action: async () => {
 			os.api('export-custom-emojis', {
 			})
 			.then(() => {
 				os.alert({
 					type: 'info',
-					text: i18n.locale.exportRequested,
+					text: i18n.ts.exportRequested,
 				});
 			}).catch((e) => {
 				os.alert({
@@ -184,7 +184,7 @@ const menu = (ev: MouseEvent) => {
 		}
 	}, {
 		icon: 'fas fa-upload',
-		text: i18n.locale.import,
+		text: i18n.ts.import,
 		action: async () => {
 			const file = await selectFile(ev.currentTarget || ev.target);
 			os.api('admin/emoji/import-zip', {
@@ -193,7 +193,7 @@ const menu = (ev: MouseEvent) => {
 			.then(() => {
 				os.alert({
 					type: 'info',
-					text: i18n.locale.importRequested,
+					text: i18n.ts.importRequested,
 				});
 			}).catch((e) => {
 				os.alert({
@@ -256,7 +256,7 @@ const setTagBulk = async () => {
 const delBulk = async () => {
 	const { canceled } = await os.confirm({
 		type: 'warning',
-		text: i18n.locale.deleteConfirm,
+		text: i18n.ts.deleteConfirm,
 	});
 	if (canceled) return;
 	await os.apiWithDialog('admin/emoji/delete-bulk', {
@@ -267,13 +267,13 @@ const delBulk = async () => {
 
 defineExpose({
 	[symbols.PAGE_INFO]: computed(() => ({
-		title: i18n.locale.customEmojis,
+		title: i18n.ts.customEmojis,
 		icon: 'fas fa-laugh',
 		bg: 'var(--bg)',
 		actions: [{
 			asFullButton: true,
 			icon: 'fas fa-plus',
-			text: i18n.locale.addEmoji,
+			text: i18n.ts.addEmoji,
 			handler: add,
 		}, {
 			icon: 'fas fa-ellipsis-h',
@@ -281,11 +281,11 @@ defineExpose({
 		}],
 		tabs: [{
 			active: tab.value === 'local',
-			title: i18n.locale.local,
+			title: i18n.ts.local,
 			onClick: () => { tab.value = 'local'; },
 		}, {
 			active: tab.value === 'remote',
-			title: i18n.locale.remote,
+			title: i18n.ts.remote,
 			onClick: () => { tab.value = 'remote'; },
 		},]
 	})),
diff --git a/packages/client/src/pages/admin/index.vue b/packages/client/src/pages/admin/index.vue
index 350e7defc6..6b1b5b86a9 100644
--- a/packages/client/src/pages/admin/index.vue
+++ b/packages/client/src/pages/admin/index.vue
@@ -55,7 +55,7 @@ export default defineComponent({
 
 	setup(props, context) {
 		const indexInfo = {
-			title: i18n.locale.controlPanel,
+			title: i18n.ts.controlPanel,
 			icon: 'fas fa-cog',
 			bg: 'var(--bg)',
 			hideHeader: true,
@@ -91,119 +91,119 @@ export default defineComponent({
 		});
 
 		const menuDef = computed(() => [{
-			title: i18n.locale.quickAction,
+			title: i18n.ts.quickAction,
 			items: [{
 				type: 'button',
 				icon: 'fas fa-search',
-				text: i18n.locale.lookup,
+				text: i18n.ts.lookup,
 				action: lookup,
 			}, ...(instance.disableRegistration ? [{
 				type: 'button',
 				icon: 'fas fa-user',
-				text: i18n.locale.invite,
+				text: i18n.ts.invite,
 				action: invite,
 			}] : [])],
 		}, {
-			title: i18n.locale.administration,
+			title: i18n.ts.administration,
 			items: [{
 				icon: 'fas fa-tachometer-alt',
-				text: i18n.locale.dashboard,
+				text: i18n.ts.dashboard,
 				to: '/admin/overview',
 				active: page.value === 'overview',
 			}, {
 				icon: 'fas fa-users',
-				text: i18n.locale.users,
+				text: i18n.ts.users,
 				to: '/admin/users',
 				active: page.value === 'users',
 			}, {
 				icon: 'fas fa-laugh',
-				text: i18n.locale.customEmojis,
+				text: i18n.ts.customEmojis,
 				to: '/admin/emojis',
 				active: page.value === 'emojis',
 			}, {
 				icon: 'fas fa-globe',
-				text: i18n.locale.federation,
+				text: i18n.ts.federation,
 				to: '/admin/federation',
 				active: page.value === 'federation',
 			}, {
 				icon: 'fas fa-clipboard-list',
-				text: i18n.locale.jobQueue,
+				text: i18n.ts.jobQueue,
 				to: '/admin/queue',
 				active: page.value === 'queue',
 			}, {
 				icon: 'fas fa-cloud',
-				text: i18n.locale.files,
+				text: i18n.ts.files,
 				to: '/admin/files',
 				active: page.value === 'files',
 			}, {
 				icon: 'fas fa-broadcast-tower',
-				text: i18n.locale.announcements,
+				text: i18n.ts.announcements,
 				to: '/admin/announcements',
 				active: page.value === 'announcements',
 			}, {
 				icon: 'fas fa-audio-description',
-				text: i18n.locale.ads,
+				text: i18n.ts.ads,
 				to: '/admin/ads',
 				active: page.value === 'ads',
 			}, {
 				icon: 'fas fa-exclamation-circle',
-				text: i18n.locale.abuseReports,
+				text: i18n.ts.abuseReports,
 				to: '/admin/abuses',
 				active: page.value === 'abuses',
 			}],
 		}, {
-			title: i18n.locale.settings,
+			title: i18n.ts.settings,
 			items: [{
 				icon: 'fas fa-cog',
-				text: i18n.locale.general,
+				text: i18n.ts.general,
 				to: '/admin/settings',
 				active: page.value === 'settings',
 			}, {
 				icon: 'fas fa-envelope',
-				text: i18n.locale.emailServer,
+				text: i18n.ts.emailServer,
 				to: '/admin/email-settings',
 				active: page.value === 'email-settings',
 			}, {
 				icon: 'fas fa-cloud',
-				text: i18n.locale.objectStorage,
+				text: i18n.ts.objectStorage,
 				to: '/admin/object-storage',
 				active: page.value === 'object-storage',
 			}, {
 				icon: 'fas fa-lock',
-				text: i18n.locale.security,
+				text: i18n.ts.security,
 				to: '/admin/security',
 				active: page.value === 'security',
 			}, {
 				icon: 'fas fa-globe',
-				text: i18n.locale.relays,
+				text: i18n.ts.relays,
 				to: '/admin/relays',
 				active: page.value === 'relays',
 			}, {
 				icon: 'fas fa-share-alt',
-				text: i18n.locale.integration,
+				text: i18n.ts.integration,
 				to: '/admin/integrations',
 				active: page.value === 'integrations',
 			}, {
 				icon: 'fas fa-ban',
-				text: i18n.locale.instanceBlocking,
+				text: i18n.ts.instanceBlocking,
 				to: '/admin/instance-block',
 				active: page.value === 'instance-block',
 			}, {
 				icon: 'fas fa-ghost',
-				text: i18n.locale.proxyAccount,
+				text: i18n.ts.proxyAccount,
 				to: '/admin/proxy-account',
 				active: page.value === 'proxy-account',
 			}, {
 				icon: 'fas fa-cogs',
-				text: i18n.locale.other,
+				text: i18n.ts.other,
 				to: '/admin/other-settings',
 				active: page.value === 'other-settings',
 			}],
 		}, {
-			title: i18n.locale.info,
+			title: i18n.ts.info,
 			items: [{
 				icon: 'fas fa-database',
-				text: i18n.locale.database,
+				text: i18n.ts.database,
 				to: '/admin/database',
 				active: page.value === 'database',
 			}],
@@ -275,25 +275,25 @@ export default defineComponent({
 
 		const lookup = (ev) => {
 			os.popupMenu([{
-				text: i18n.locale.user,
+				text: i18n.ts.user,
 				icon: 'fas fa-user',
 				action: () => {
 					lookupUser();
 				}
 			}, {
-				text: i18n.locale.note,
+				text: i18n.ts.note,
 				icon: 'fas fa-pencil-alt',
 				action: () => {
 					alert('TODO');
 				}
 			}, {
-				text: i18n.locale.file,
+				text: i18n.ts.file,
 				icon: 'fas fa-cloud',
 				action: () => {
 					alert('TODO');
 				}
 			}, {
-				text: i18n.locale.instance,
+				text: i18n.ts.instance,
 				icon: 'fas fa-globe',
 				action: () => {
 					alert('TODO');
@@ -305,7 +305,7 @@ export default defineComponent({
 			[symbols.PAGE_INFO]: INFO,
 			menuDef,
 			header: {
-				title: i18n.locale.controlPanel,
+				title: i18n.ts.controlPanel,
 			},
 			noMaintainerInformation,
 			noBotProtection,
diff --git a/packages/client/src/pages/drive.vue b/packages/client/src/pages/drive.vue
index 1e17bea0cc..68777bb083 100644
--- a/packages/client/src/pages/drive.vue
+++ b/packages/client/src/pages/drive.vue
@@ -15,7 +15,7 @@ let folder = $ref(null);
 
 defineExpose({
 	[symbols.PAGE_INFO]: computed(() => ({
-		title: folder ? folder.name : i18n.locale.drive,
+		title: folder ? folder.name : i18n.ts.drive,
 		icon: 'fas fa-cloud',
 		bg: 'var(--bg)',
 		hideHeader: true,
diff --git a/packages/client/src/pages/emojis.emoji.vue b/packages/client/src/pages/emojis.emoji.vue
index 83539ce7a3..9e4deb9ceb 100644
--- a/packages/client/src/pages/emojis.emoji.vue
+++ b/packages/client/src/pages/emojis.emoji.vue
@@ -23,7 +23,7 @@ function menu(ev) {
 		type: 'label',
 		text: ':' + props.emoji.name + ':',
 	}, {
-		text: i18n.locale.copy,
+		text: i18n.ts.copy,
 		icon: 'fas fa-copy',
 		action: () => {
 			copyToClipboard(`:${props.emoji.name}:`);
diff --git a/packages/client/src/pages/emojis.vue b/packages/client/src/pages/emojis.vue
index 6577f5abd9..69e3147750 100644
--- a/packages/client/src/pages/emojis.vue
+++ b/packages/client/src/pages/emojis.vue
@@ -16,14 +16,14 @@ const tab = ref('category');
 function menu(ev) {
 	os.popupMenu([{
 		icon: 'fas fa-download',
-		text: i18n.locale.export,
+		text: i18n.ts.export,
 		action: async () => {
 			os.api('export-custom-emojis', {
 			})
 			.then(() => {
 				os.alert({
 					type: 'info',
-					text: i18n.locale.exportRequested,
+					text: i18n.ts.exportRequested,
 				});
 			}).catch((e) => {
 				os.alert({
@@ -37,7 +37,7 @@ function menu(ev) {
 
 defineExpose({
 	[symbols.PAGE_INFO]: {
-		title: i18n.locale.customEmojis,
+		title: i18n.ts.customEmojis,
 		icon: 'fas fa-laugh',
 		bg: 'var(--bg)',
 		actions: [{
diff --git a/packages/client/src/pages/favorites.vue b/packages/client/src/pages/favorites.vue
index 8965b30d60..b4f6ff35bc 100644
--- a/packages/client/src/pages/favorites.vue
+++ b/packages/client/src/pages/favorites.vue
@@ -34,7 +34,7 @@ const pagingComponent = ref<InstanceType<typeof MkPagination>>();
 
 defineExpose({
 	[symbols.PAGE_INFO]: {
-		title: i18n.locale.favorites,
+		title: i18n.ts.favorites,
 		icon: 'fas fa-star',
 		bg: 'var(--bg)',
 	},
diff --git a/packages/client/src/pages/featured.vue b/packages/client/src/pages/featured.vue
index 725c70f0f7..14fe0cb740 100644
--- a/packages/client/src/pages/featured.vue
+++ b/packages/client/src/pages/featured.vue
@@ -17,7 +17,7 @@ const pagination = {
 
 defineExpose({
 	[symbols.PAGE_INFO]: {
-		title: i18n.locale.featured,
+		title: i18n.ts.featured,
 		icon: 'fas fa-fire-alt',
 		bg: 'var(--bg)',
 	},
diff --git a/packages/client/src/pages/federation.vue b/packages/client/src/pages/federation.vue
index 6a4a28b6b4..a4ae901f2a 100644
--- a/packages/client/src/pages/federation.vue
+++ b/packages/client/src/pages/federation.vue
@@ -135,7 +135,7 @@ function getStatus(instance) {
 
 defineExpose({
 	[symbols.PAGE_INFO]: {
-		title: i18n.locale.federation,
+		title: i18n.ts.federation,
 		icon: 'fas fa-globe',
 		bg: 'var(--bg)',
 	},
diff --git a/packages/client/src/pages/follow-requests.vue b/packages/client/src/pages/follow-requests.vue
index 764daa0d3e..6adc1a404b 100644
--- a/packages/client/src/pages/follow-requests.vue
+++ b/packages/client/src/pages/follow-requests.vue
@@ -60,7 +60,7 @@ function reject(user) {
 
 defineExpose({
 	[symbols.PAGE_INFO]: computed(() => ({
-		title: i18n.locale.followRequests,
+		title: i18n.ts.followRequests,
 		icon: 'fas fa-user-clock',
 		bg: 'var(--bg)',
 	})),
diff --git a/packages/client/src/pages/mentions.vue b/packages/client/src/pages/mentions.vue
index bda56fc729..9b57c956bf 100644
--- a/packages/client/src/pages/mentions.vue
+++ b/packages/client/src/pages/mentions.vue
@@ -16,7 +16,7 @@ const pagination = {
 
 defineExpose({
 	[symbols.PAGE_INFO]: {
-		title: i18n.locale.mentions,
+		title: i18n.ts.mentions,
 		icon: 'fas fa-at',
 		bg: 'var(--bg)',
 	},
diff --git a/packages/client/src/pages/messages.vue b/packages/client/src/pages/messages.vue
index 8efdc55586..09d51abf75 100644
--- a/packages/client/src/pages/messages.vue
+++ b/packages/client/src/pages/messages.vue
@@ -19,7 +19,7 @@ const pagination = {
 
 defineExpose({
 	[symbols.PAGE_INFO]: {
-		title: i18n.locale.directNotes,
+		title: i18n.ts.directNotes,
 		icon: 'fas fa-envelope',
 		bg: 'var(--bg)',
 	},
diff --git a/packages/client/src/pages/my-antennas/create.vue b/packages/client/src/pages/my-antennas/create.vue
index 427c9935c3..a08bece731 100644
--- a/packages/client/src/pages/my-antennas/create.vue
+++ b/packages/client/src/pages/my-antennas/create.vue
@@ -31,7 +31,7 @@ function onAntennaCreated() {
 
 defineExpose({
 	[symbols.PAGE_INFO]: {
-		title: i18n.locale.manageAntennas,
+		title: i18n.ts.manageAntennas,
 		icon: 'fas fa-satellite',
 		bg: 'var(--bg)',
 	},
diff --git a/packages/client/src/pages/my-clips/index.vue b/packages/client/src/pages/my-clips/index.vue
index 4b31e6c8ba..e287357a42 100644
--- a/packages/client/src/pages/my-clips/index.vue
+++ b/packages/client/src/pages/my-clips/index.vue
@@ -29,20 +29,20 @@ const pagination = {
 const pagingComponent = $ref<InstanceType<typeof MkPagination>>();
 
 async function create() {
-	const { canceled, result } = await os.form(i18n.locale.createNewClip, {
+	const { canceled, result } = await os.form(i18n.ts.createNewClip, {
 		name: {
 			type: 'string',
-			label: i18n.locale.name,
+			label: i18n.ts.name,
 		},
 		description: {
 			type: 'string',
 			required: false,
 			multiline: true,
-			label: i18n.locale.description,
+			label: i18n.ts.description,
 		},
 		isPublic: {
 			type: 'boolean',
-			label: i18n.locale.public,
+			label: i18n.ts.public,
 			default: false,
 		},
 	});
@@ -63,7 +63,7 @@ function onClipDeleted() {
 
 defineExpose({
 	[symbols.PAGE_INFO]: {
-		title: i18n.locale.clip,
+		title: i18n.ts.clip,
 		icon: 'fas fa-paperclip',
 		bg: 'var(--bg)',
 		action: {
diff --git a/packages/client/src/pages/my-lists/index.vue b/packages/client/src/pages/my-lists/index.vue
index e6fcba1b34..9ed9e2960e 100644
--- a/packages/client/src/pages/my-lists/index.vue
+++ b/packages/client/src/pages/my-lists/index.vue
@@ -31,7 +31,7 @@ const pagination = {
 
 async function create() {
 	const { canceled, result: name } = await os.inputText({
-		title: i18n.locale.enterListName,
+		title: i18n.ts.enterListName,
 	});
 	if (canceled) return;
 	await os.apiWithDialog('users/lists/create', { name: name });
@@ -40,7 +40,7 @@ async function create() {
 
 defineExpose({
 	[symbols.PAGE_INFO]: {
-		title: i18n.locale.manageLists,
+		title: i18n.ts.manageLists,
 		icon: 'fas fa-list-ul',
 		bg: 'var(--bg)',
 		action: {
diff --git a/packages/client/src/pages/not-found.vue b/packages/client/src/pages/not-found.vue
index 914fdb9297..cdeb54b88b 100644
--- a/packages/client/src/pages/not-found.vue
+++ b/packages/client/src/pages/not-found.vue
@@ -13,7 +13,7 @@ import { i18n } from '@/i18n';
 
 defineExpose({
 	[symbols.PAGE_INFO]: {
-		title: i18n.locale.notFound,
+		title: i18n.ts.notFound,
 		icon: 'fas fa-exclamation-triangle',
 		bg: 'var(--bg)',
 	},
diff --git a/packages/client/src/pages/notifications.vue b/packages/client/src/pages/notifications.vue
index 090e80f99a..96c5b3ca85 100644
--- a/packages/client/src/pages/notifications.vue
+++ b/packages/client/src/pages/notifications.vue
@@ -27,7 +27,7 @@ function setFilter(ev) {
 	}));
 	const items = includeTypes != null ? [{
 		icon: 'fas fa-times',
-		text: i18n.locale.clear,
+		text: i18n.ts.clear,
 		action: () => {
 			includeTypes = null;
 		}
@@ -37,16 +37,16 @@ function setFilter(ev) {
 
 defineExpose({
 	[symbols.PAGE_INFO]: computed(() => ({
-		title: i18n.locale.notifications,
+		title: i18n.ts.notifications,
 		icon: 'fas fa-bell',
 		bg: 'var(--bg)',
 		actions: [{
-			text: i18n.locale.filter,
+			text: i18n.ts.filter,
 			icon: 'fas fa-filter',
 			highlighted: includeTypes != null,
 			handler: setFilter,
 		}, {
-			text: i18n.locale.markAllAsRead,
+			text: i18n.ts.markAllAsRead,
 			icon: 'fas fa-check',
 			handler: () => {
 				os.apiWithDialog('notifications/mark-all-as-read');
@@ -54,11 +54,11 @@ defineExpose({
 		}],
 		tabs: [{
 			active: tab === 'all',
-			title: i18n.locale.all,
+			title: i18n.ts.all,
 			onClick: () => { tab = 'all'; },
 		}, {
 			active: tab === 'unread',
-			title: i18n.locale.unread,
+			title: i18n.ts.unread,
 			onClick: () => { tab = 'unread'; },
 		},]
 	})),
diff --git a/packages/client/src/pages/preview.vue b/packages/client/src/pages/preview.vue
index 8eb4549516..4accac4192 100644
--- a/packages/client/src/pages/preview.vue
+++ b/packages/client/src/pages/preview.vue
@@ -12,7 +12,7 @@ import { i18n } from '@/i18n';
 
 defineExpose({
 	[symbols.PAGE_INFO]: computed(() => ({
-		title: i18n.locale.preview,
+		title: i18n.ts.preview,
 		icon: 'fas fa-eye',
 		bg: 'var(--bg)',
 	})),
diff --git a/packages/client/src/pages/reset-password.vue b/packages/client/src/pages/reset-password.vue
index 8ef73858f6..7d008ae75c 100644
--- a/packages/client/src/pages/reset-password.vue
+++ b/packages/client/src/pages/reset-password.vue
@@ -3,10 +3,10 @@
 	<div class="_formRoot">
 		<FormInput v-model="password" type="password" class="_formBlock">
 			<template #prefix><i class="fas fa-lock"></i></template>
-			<template #label>{{ i18n.locale.newPassword }}</template>
+			<template #label>{{ i18n.ts.newPassword }}</template>
 		</FormInput>
 		
-		<FormButton primary class="_formBlock" @click="save">{{ i18n.locale.save }}</FormButton>
+		<FormButton primary class="_formBlock" @click="save">{{ i18n.ts.save }}</FormButton>
 	</div>
 </MkSpacer>
 </template>
@@ -43,7 +43,7 @@ onMounted(() => {
 
 defineExpose({
 	[symbols.PAGE_INFO]: {
-		title: i18n.locale.resetPassword,
+		title: i18n.ts.resetPassword,
 		icon: 'fas fa-lock',
 		bg: 'var(--bg)',
 	},
diff --git a/packages/client/src/pages/settings/email.vue b/packages/client/src/pages/settings/email.vue
index 54557f8773..4697fec9b7 100644
--- a/packages/client/src/pages/settings/email.vue
+++ b/packages/client/src/pages/settings/email.vue
@@ -62,7 +62,7 @@ export default defineComponent({
 		const emailAddress = ref($i.email);
 
 		const INFO = {
-			title: i18n.locale.email,
+			title: i18n.ts.email,
 			icon: 'fas fa-envelope',
 			bg: 'var(--bg)',
 		};
@@ -75,7 +75,7 @@ export default defineComponent({
 
 		const saveEmailAddress = () => {
 			os.inputText({
-				title: i18n.locale.password,
+				title: i18n.ts.password,
 				type: 'password'
 			}).then(({ canceled, result: password }) => {
 				if (canceled) return;
diff --git a/packages/client/src/pages/settings/import-export.vue b/packages/client/src/pages/settings/import-export.vue
index 21031c559e..7b554dcd88 100644
--- a/packages/client/src/pages/settings/import-export.vue
+++ b/packages/client/src/pages/settings/import-export.vue
@@ -60,7 +60,7 @@ export default defineComponent({
 
 	setup(props, context) {
 		const INFO = {
-			title: i18n.locale.importAndExport,
+			title: i18n.ts.importAndExport,
 			icon: 'fas fa-boxes',
 			bg: 'var(--bg)',
 		};
@@ -71,14 +71,14 @@ export default defineComponent({
 		const onExportSuccess = () => {
 			os.alert({
 				type: 'info',
-				text: i18n.locale.exportRequested,
+				text: i18n.ts.exportRequested,
 			});
 		};
 
 		const onImportSuccess = () => {
 			os.alert({
 				type: 'info',
-				text: i18n.locale.importRequested,
+				text: i18n.ts.importRequested,
 			});
 		};
 
diff --git a/packages/client/src/pages/settings/index.vue b/packages/client/src/pages/settings/index.vue
index 66c8b147bb..ac8414ddbc 100644
--- a/packages/client/src/pages/settings/index.vue
+++ b/packages/client/src/pages/settings/index.vue
@@ -49,7 +49,7 @@ export default defineComponent({
 
 	setup(props, context) {
 		const indexInfo = {
-			title: i18n.locale.settings,
+			title: i18n.ts.settings,
 			icon: 'fas fa-cog',
 			bg: 'var(--bg)',
 			hideHeader: true,
@@ -61,96 +61,96 @@ export default defineComponent({
 		const el = ref(null);
 		const childInfo = ref(null);
 		const menuDef = computed(() => [{
-			title: i18n.locale.basicSettings,
+			title: i18n.ts.basicSettings,
 			items: [{
 				icon: 'fas fa-user',
-				text: i18n.locale.profile,
+				text: i18n.ts.profile,
 				to: '/settings/profile',
 				active: page.value === 'profile',
 			}, {
 				icon: 'fas fa-lock-open',
-				text: i18n.locale.privacy,
+				text: i18n.ts.privacy,
 				to: '/settings/privacy',
 				active: page.value === 'privacy',
 			}, {
 				icon: 'fas fa-laugh',
-				text: i18n.locale.reaction,
+				text: i18n.ts.reaction,
 				to: '/settings/reaction',
 				active: page.value === 'reaction',
 			}, {
 				icon: 'fas fa-cloud',
-				text: i18n.locale.drive,
+				text: i18n.ts.drive,
 				to: '/settings/drive',
 				active: page.value === 'drive',
 			}, {
 				icon: 'fas fa-bell',
-				text: i18n.locale.notifications,
+				text: i18n.ts.notifications,
 				to: '/settings/notifications',
 				active: page.value === 'notifications',
 			}, {
 				icon: 'fas fa-envelope',
-				text: i18n.locale.email,
+				text: i18n.ts.email,
 				to: '/settings/email',
 				active: page.value === 'email',
 			}, {
 				icon: 'fas fa-share-alt',
-				text: i18n.locale.integration,
+				text: i18n.ts.integration,
 				to: '/settings/integration',
 				active: page.value === 'integration',
 			}, {
 				icon: 'fas fa-lock',
-				text: i18n.locale.security,
+				text: i18n.ts.security,
 				to: '/settings/security',
 				active: page.value === 'security',
 			}],
 		}, {
-			title: i18n.locale.clientSettings,
+			title: i18n.ts.clientSettings,
 			items: [{
 				icon: 'fas fa-cogs',
-				text: i18n.locale.general,
+				text: i18n.ts.general,
 				to: '/settings/general',
 				active: page.value === 'general',
 			}, {
 				icon: 'fas fa-palette',
-				text: i18n.locale.theme,
+				text: i18n.ts.theme,
 				to: '/settings/theme',
 				active: page.value === 'theme',
 			}, {
 				icon: 'fas fa-list-ul',
-				text: i18n.locale.menu,
+				text: i18n.ts.menu,
 				to: '/settings/menu',
 				active: page.value === 'menu',
 			}, {
 				icon: 'fas fa-music',
-				text: i18n.locale.sounds,
+				text: i18n.ts.sounds,
 				to: '/settings/sounds',
 				active: page.value === 'sounds',
 			}, {
 				icon: 'fas fa-plug',
-				text: i18n.locale.plugins,
+				text: i18n.ts.plugins,
 				to: '/settings/plugin',
 				active: page.value === 'plugin',
 			}],
 		}, {
-			title: i18n.locale.otherSettings,
+			title: i18n.ts.otherSettings,
 			items: [{
 				icon: 'fas fa-boxes',
-				text: i18n.locale.importAndExport,
+				text: i18n.ts.importAndExport,
 				to: '/settings/import-export',
 				active: page.value === 'import-export',
 			}, {
 				icon: 'fas fa-volume-mute',
-				text: i18n.locale.instanceMute,
+				text: i18n.ts.instanceMute,
 				to: '/settings/instance-mute',
 				active: page.value === 'instance-mute',
 			}, {
 				icon: 'fas fa-ban',
-				text: i18n.locale.muteAndBlock,
+				text: i18n.ts.muteAndBlock,
 				to: '/settings/mute-block',
 				active: page.value === 'mute-block',
 			}, {
 				icon: 'fas fa-comment-slash',
-				text: i18n.locale.wordMute,
+				text: i18n.ts.wordMute,
 				to: '/settings/word-mute',
 				active: page.value === 'word-mute',
 			}, {
@@ -160,7 +160,7 @@ export default defineComponent({
 				active: page.value === 'api',
 			}, {
 				icon: 'fas fa-ellipsis-h',
-				text: i18n.locale.other,
+				text: i18n.ts.other,
 				to: '/settings/other',
 				active: page.value === 'other',
 			}],
@@ -168,7 +168,7 @@ export default defineComponent({
 			items: [{
 				type: 'button',
 				icon: 'fas fa-trash',
-				text: i18n.locale.clearCache,
+				text: i18n.ts.clearCache,
 				action: () => {
 					localStorage.removeItem('locale');
 					localStorage.removeItem('theme');
@@ -177,7 +177,7 @@ export default defineComponent({
 			}, {
 				type: 'button',
 				icon: 'fas fa-sign-in-alt fa-flip-horizontal',
-				text: i18n.locale.logout,
+				text: i18n.ts.logout,
 				action: () => {
 					signout();
 				},
diff --git a/packages/client/src/pages/settings/mute-block.vue b/packages/client/src/pages/settings/mute-block.vue
index f4f9ebf8dd..28d11809e3 100644
--- a/packages/client/src/pages/settings/mute-block.vue
+++ b/packages/client/src/pages/settings/mute-block.vue
@@ -52,7 +52,7 @@ const blockingPagination = {
 
 defineExpose({
 	[symbols.PAGE_INFO]: {
-		title: i18n.locale.muteAndBlock,
+		title: i18n.ts.muteAndBlock,
 		icon: 'fas fa-ban',
 		bg: 'var(--bg)',
 	},
diff --git a/packages/client/src/pages/settings/privacy.vue b/packages/client/src/pages/settings/privacy.vue
index dd13ba4bd0..cfae7e9ca8 100644
--- a/packages/client/src/pages/settings/privacy.vue
+++ b/packages/client/src/pages/settings/privacy.vue
@@ -86,7 +86,7 @@ function save() {
 
 defineExpose({
 	[symbols.PAGE_INFO]: {
-		title: i18n.locale.privacy,
+		title: i18n.ts.privacy,
 		icon: 'fas fa-lock-open',
 		bg: 'var(--bg)',
 	},
diff --git a/packages/client/src/pages/settings/profile.vue b/packages/client/src/pages/settings/profile.vue
index f875146a2c..0786e7f4ae 100644
--- a/packages/client/src/pages/settings/profile.vue
+++ b/packages/client/src/pages/settings/profile.vue
@@ -3,45 +3,45 @@
 	<div class="llvierxe" :style="{ backgroundImage: $i.bannerUrl ? `url(${ $i.bannerUrl })` : null }">
 		<div class="avatar _acrylic">
 			<MkAvatar class="avatar" :user="$i" :disable-link="true" @click="changeAvatar"/>
-			<MkButton primary class="avatarEdit" @click="changeAvatar">{{ i18n.locale._profile.changeAvatar }}</MkButton>
+			<MkButton primary class="avatarEdit" @click="changeAvatar">{{ i18n.ts._profile.changeAvatar }}</MkButton>
 		</div>
-		<MkButton primary class="bannerEdit" @click="changeBanner">{{ i18n.locale._profile.changeBanner }}</MkButton>
+		<MkButton primary class="bannerEdit" @click="changeBanner">{{ i18n.ts._profile.changeBanner }}</MkButton>
 	</div>
 
 	<FormInput v-model="profile.name" :max="30" manual-save class="_formBlock">
-		<template #label>{{ i18n.locale._profile.name }}</template>
+		<template #label>{{ i18n.ts._profile.name }}</template>
 	</FormInput>
 
 	<FormTextarea v-model="profile.description" :max="500" tall manual-save class="_formBlock">
-		<template #label>{{ i18n.locale._profile.description }}</template>
-		<template #caption>{{ i18n.locale._profile.youCanIncludeHashtags }}</template>
+		<template #label>{{ i18n.ts._profile.description }}</template>
+		<template #caption>{{ i18n.ts._profile.youCanIncludeHashtags }}</template>
 	</FormTextarea>
 
 	<FormInput v-model="profile.location" manual-save class="_formBlock">
-		<template #label>{{ i18n.locale.location }}</template>
+		<template #label>{{ i18n.ts.location }}</template>
 		<template #prefix><i class="fas fa-map-marker-alt"></i></template>
 	</FormInput>
 
 	<FormInput v-model="profile.birthday" type="date" manual-save class="_formBlock">
-		<template #label>{{ i18n.locale.birthday }}</template>
+		<template #label>{{ i18n.ts.birthday }}</template>
 		<template #prefix><i class="fas fa-birthday-cake"></i></template>
 	</FormInput>
 
 	<FormSelect v-model="profile.lang" class="_formBlock">
-		<template #label>{{ i18n.locale.language }}</template>
+		<template #label>{{ i18n.ts.language }}</template>
 		<option v-for="x in langs" :key="x[0]" :value="x[0]">{{ x[1] }}</option>
 	</FormSelect>
 
 	<FormSlot>
-		<MkButton @click="editMetadata">{{ i18n.locale._profile.metadataEdit }}</MkButton>
-		<template #caption>{{ i18n.locale._profile.metadataDescription }}</template>
+		<MkButton @click="editMetadata">{{ i18n.ts._profile.metadataEdit }}</MkButton>
+		<template #caption>{{ i18n.ts._profile.metadataDescription }}</template>
 	</FormSlot>
 
-	<FormSwitch v-model="profile.isCat" class="_formBlock">{{ i18n.locale.flagAsCat }}<template #caption>{{ i18n.locale.flagAsCatDescription }}</template></FormSwitch>
+	<FormSwitch v-model="profile.isCat" class="_formBlock">{{ i18n.ts.flagAsCat }}<template #caption>{{ i18n.ts.flagAsCatDescription }}</template></FormSwitch>
 
-	<FormSwitch v-model="profile.isBot" class="_formBlock">{{ i18n.locale.flagAsBot }}<template #caption>{{ i18n.locale.flagAsBotDescription }}</template></FormSwitch>
+	<FormSwitch v-model="profile.isBot" class="_formBlock">{{ i18n.ts.flagAsBot }}<template #caption>{{ i18n.ts.flagAsBotDescription }}</template></FormSwitch>
 
-	<FormSwitch v-model="profile.alwaysMarkNsfw" class="_formBlock">{{ i18n.locale.alwaysMarkSensitive }}</FormSwitch>
+	<FormSwitch v-model="profile.alwaysMarkNsfw" class="_formBlock">{{ i18n.ts.alwaysMarkSensitive }}</FormSwitch>
 </div>
 </template>
 
@@ -102,7 +102,7 @@ function save() {
 }
 
 function changeAvatar(ev) {
-	selectFile(ev.currentTarget || ev.target, i18n.locale.avatar).then(async (file) => {
+	selectFile(ev.currentTarget || ev.target, i18n.ts.avatar).then(async (file) => {
 		const i = await os.apiWithDialog('i/update', {
 			avatarId: file.id,
 		});
@@ -112,7 +112,7 @@ function changeAvatar(ev) {
 }
 
 function changeBanner(ev) {
-	selectFile(ev.currentTarget || ev.target, i18n.locale.banner).then(async (file) => {
+	selectFile(ev.currentTarget || ev.target, i18n.ts.banner).then(async (file) => {
 		const i = await os.apiWithDialog('i/update', {
 			bannerId: file.id,
 		});
@@ -122,45 +122,45 @@ function changeBanner(ev) {
 }
 
 async function editMetadata() {
-	const { canceled, result } = await os.form(i18n.locale._profile.metadata, {
+	const { canceled, result } = await os.form(i18n.ts._profile.metadata, {
 		fieldName0: {
 			type: 'string',
-			label: i18n.locale._profile.metadataLabel + ' 1',
+			label: i18n.ts._profile.metadataLabel + ' 1',
 			default: additionalFields.fieldName0,
 		},
 		fieldValue0: {
 			type: 'string',
-			label: i18n.locale._profile.metadataContent + ' 1',
+			label: i18n.ts._profile.metadataContent + ' 1',
 			default: additionalFields.fieldValue0,
 		},
 		fieldName1: {
 			type: 'string',
-			label: i18n.locale._profile.metadataLabel + ' 2',
+			label: i18n.ts._profile.metadataLabel + ' 2',
 			default: additionalFields.fieldName1,
 		},
 		fieldValue1: {
 			type: 'string',
-			label: i18n.locale._profile.metadataContent + ' 2',
+			label: i18n.ts._profile.metadataContent + ' 2',
 			default: additionalFields.fieldValue1,
 		},
 		fieldName2: {
 			type: 'string',
-			label: i18n.locale._profile.metadataLabel + ' 3',
+			label: i18n.ts._profile.metadataLabel + ' 3',
 			default: additionalFields.fieldName2,
 		},
 		fieldValue2: {
 			type: 'string',
-			label: i18n.locale._profile.metadataContent + ' 3',
+			label: i18n.ts._profile.metadataContent + ' 3',
 			default: additionalFields.fieldValue2,
 		},
 		fieldName3: {
 			type: 'string',
-			label: i18n.locale._profile.metadataLabel + ' 4',
+			label: i18n.ts._profile.metadataLabel + ' 4',
 			default: additionalFields.fieldName3,
 		},
 		fieldValue3: {
 			type: 'string',
-			label: i18n.locale._profile.metadataContent + ' 4',
+			label: i18n.ts._profile.metadataContent + ' 4',
 			default: additionalFields.fieldValue3,
 		},
 	});
@@ -196,7 +196,7 @@ async function editMetadata() {
 
 defineExpose({
 	[symbols.PAGE_INFO]: {
-		title: i18n.locale.profile,
+		title: i18n.ts.profile,
 		icon: 'fas fa-user',
 		bg: 'var(--bg)',
 	},
diff --git a/packages/client/src/pages/settings/theme.install.vue b/packages/client/src/pages/settings/theme.install.vue
index e2a3f042b9..2d3514342e 100644
--- a/packages/client/src/pages/settings/theme.install.vue
+++ b/packages/client/src/pages/settings/theme.install.vue
@@ -1,12 +1,12 @@
 <template>
 <div class="_formRoot">
 	<FormTextarea v-model="installThemeCode" class="_formBlock">
-		<template #label>{{ i18n.locale._theme.code }}</template>
+		<template #label>{{ i18n.ts._theme.code }}</template>
 	</FormTextarea>
 
 	<div class="_formBlock" style="display: flex; gap: var(--margin); flex-wrap: wrap;">
-		<FormButton :disabled="installThemeCode == null" inline @click="() => preview(installThemeCode)"><i class="fas fa-eye"></i> {{ i18n.locale.preview }}</FormButton>
-		<FormButton :disabled="installThemeCode == null" primary inline @click="() => install(installThemeCode)"><i class="fas fa-check"></i> {{ i18n.locale.install }}</FormButton>
+		<FormButton :disabled="installThemeCode == null" inline @click="() => preview(installThemeCode)"><i class="fas fa-eye"></i> {{ i18n.ts.preview }}</FormButton>
+		<FormButton :disabled="installThemeCode == null" primary inline @click="() => install(installThemeCode)"><i class="fas fa-check"></i> {{ i18n.ts.install }}</FormButton>
 	</div>
 </div>
 </template>
@@ -32,21 +32,21 @@ function parseThemeCode(code: string) {
 	} catch (e) {
 		os.alert({
 			type: 'error',
-			text: i18n.locale._theme.invalid
+			text: i18n.ts._theme.invalid
 		});
 		return false;
 	}
 	if (!validateTheme(theme)) {
 		os.alert({
 			type: 'error',
-			text: i18n.locale._theme.invalid
+			text: i18n.ts._theme.invalid
 		});
 		return false;
 	}
 	if (getThemes().some(t => t.id === theme.id)) {
 		os.alert({
 			type: 'info',
-			text: i18n.locale._theme.alreadyInstalled
+			text: i18n.ts._theme.alreadyInstalled
 		});
 		return false;
 	}
@@ -71,7 +71,7 @@ async function install(code: string): Promise<void> {
 
 defineExpose({
 	[symbols.PAGE_INFO]: {
-		title: i18n.locale._theme.install,
+		title: i18n.ts._theme.install,
 		icon: 'fas fa-download',
 		bg: 'var(--bg)',
 	},
diff --git a/packages/client/src/pages/settings/theme.vue b/packages/client/src/pages/settings/theme.vue
index 658e36ec05..fefd72777a 100644
--- a/packages/client/src/pages/settings/theme.vue
+++ b/packages/client/src/pages/settings/theme.vue
@@ -116,7 +116,7 @@ export default defineComponent({
 
 	setup(props, { emit }) {
 		const INFO = {
-			title: i18n.locale.theme,
+			title: i18n.ts.theme,
 			icon: 'fas fa-palette',
 				bg: 'var(--bg)',
 		};
diff --git a/packages/client/src/pages/signup-complete.vue b/packages/client/src/pages/signup-complete.vue
index a10af1a4cc..344c9195f7 100644
--- a/packages/client/src/pages/signup-complete.vue
+++ b/packages/client/src/pages/signup-complete.vue
@@ -1,6 +1,6 @@
 <template>
 <div>
-	{{ i18n.locale.processing }}
+	{{ i18n.ts.processing }}
 </div>
 </template>
 
@@ -18,7 +18,7 @@ const props = defineProps<{
 onMounted(async () => {
 	await os.alert({
 		type: 'info',
-		text: i18n.t('clickToFinishEmailVerification', { ok: i18n.locale.gotIt }),
+		text: i18n.t('clickToFinishEmailVerification', { ok: i18n.ts.gotIt }),
 	});
 	const res = await os.apiWithDialog('signup-pending', {
 		code: props.code,
@@ -28,7 +28,7 @@ onMounted(async () => {
 
 defineExpose({
 	[symbols.PAGE_INFO]: {
-		title: i18n.locale.signup,
+		title: i18n.ts.signup,
 		icon: 'fas fa-user',
 	},
 });
diff --git a/packages/client/src/pages/theme-editor.vue b/packages/client/src/pages/theme-editor.vue
index 80b8c7806c..a53e23c1c5 100644
--- a/packages/client/src/pages/theme-editor.vue
+++ b/packages/client/src/pages/theme-editor.vue
@@ -2,7 +2,7 @@
 <MkSpacer :content-max="800" :margin-min="16" :margin-max="32">
 	<div class="cwepdizn _formRoot">
 		<FormFolder :default-open="true" class="_formBlock">
-			<template #label>{{ i18n.locale.backgroundColor }}</template>
+			<template #label>{{ i18n.ts.backgroundColor }}</template>
 			<div class="cwepdizn-colors">
 				<div class="row">
 					<button v-for="color in bgColors.filter(x => x.kind === 'light')" :key="color.color" class="color _button" :class="{ active: theme.props.bg === color.color }" @click="setBgColor(color)">
@@ -18,7 +18,7 @@
 		</FormFolder>
 
 		<FormFolder :default-open="true" class="_formBlock">
-			<template #label>{{ i18n.locale.accentColor }}</template>
+			<template #label>{{ i18n.ts.accentColor }}</template>
 			<div class="cwepdizn-colors">
 				<div class="row">
 					<button v-for="color in accentColors" :key="color" class="color rounded _button" :class="{ active: theme.props.accent === color }" @click="setAccentColor(color)">
@@ -29,7 +29,7 @@
 		</FormFolder>
 
 		<FormFolder :default-open="true" class="_formBlock">
-			<template #label>{{ i18n.locale.textColor }}</template>
+			<template #label>{{ i18n.ts.textColor }}</template>
 			<div class="cwepdizn-colors">
 				<div class="row">
 					<button v-for="color in fgColors" :key="color" class="color char _button" :class="{ active: (theme.props.fg === color.forLight) || (theme.props.fg === color.forDark) }" @click="setFgColor(color)">
@@ -41,22 +41,22 @@
 
 		<FormFolder :default-open="false" class="_formBlock">
 			<template #icon><i class="fas fa-code"></i></template>
-			<template #label>{{ i18n.locale.editCode }}</template>
+			<template #label>{{ i18n.ts.editCode }}</template>
 
 			<div class="_formRoot">
 				<FormTextarea v-model="themeCode" tall class="_formBlock">
-					<template #label>{{ i18n.locale._theme.code }}</template>
+					<template #label>{{ i18n.ts._theme.code }}</template>
 				</FormTextarea>
-				<FormButton primary class="_formBlock" @click="applyThemeCode">{{ i18n.locale.apply }}</FormButton>
+				<FormButton primary class="_formBlock" @click="applyThemeCode">{{ i18n.ts.apply }}</FormButton>
 			</div>
 		</FormFolder>
 
 		<FormFolder :default-open="false" class="_formBlock">
-			<template #label>{{ i18n.locale.addDescription }}</template>
+			<template #label>{{ i18n.ts.addDescription }}</template>
 
 			<div class="_formRoot">
 				<FormTextarea v-model="description">
-					<template #label>{{ i18n.locale._theme.description }}</template>
+					<template #label>{{ i18n.ts._theme.description }}</template>
 				</FormTextarea>
 			</div>
 		</FormFolder>
@@ -167,7 +167,7 @@ function applyThemeCode() {
 	} catch (err) {
 		os.alert({
 			type: 'error',
-			text: i18n.locale._theme.invalid,
+			text: i18n.ts._theme.invalid,
 		});
 		return;
 	}
@@ -177,7 +177,7 @@ function applyThemeCode() {
 
 async function saveAs() {
 	const { canceled, result: name } = await os.inputText({
-		title: i18n.locale.name,
+		title: i18n.ts.name,
 		allowEmpty: false,
 	});
 	if (canceled) return;
@@ -204,18 +204,18 @@ watch($$(theme), apply, { deep: true });
 
 defineExpose({
 	[symbols.PAGE_INFO]: {
-		title: i18n.locale.themeEditor,
+		title: i18n.ts.themeEditor,
 		icon: 'fas fa-palette',
 		bg: 'var(--bg)',
 		actions: [{
 			asFullButton: true,
 			icon: 'fas fa-eye',
-			text: i18n.locale.preview,
+			text: i18n.ts.preview,
 			handler: showPreview,
 		}, {
 			asFullButton: true,
 			icon: 'fas fa-check',
-			text: i18n.locale.saveAs,
+			text: i18n.ts.saveAs,
 			handler: saveAs,
 		}],
 	},
diff --git a/packages/client/src/pages/timeline.vue b/packages/client/src/pages/timeline.vue
index aabb953aec..a55fe1eb91 100644
--- a/packages/client/src/pages/timeline.vue
+++ b/packages/client/src/pages/timeline.vue
@@ -97,7 +97,7 @@ function saveSrc(): void {
 
 async function timetravel(): Promise<void> {
 	const { canceled, result: date } = await os.inputDate({
-		title: i18n.locale.date,
+		title: i18n.ts.date,
 	});
 	if (canceled) return;
 
@@ -110,47 +110,47 @@ function focus(): void {
 
 defineExpose({
 	[symbols.PAGE_INFO]: computed(() => ({
-		title: i18n.locale.timeline,
+		title: i18n.ts.timeline,
 		icon: src === 'local' ? 'fas fa-comments' : src === 'social' ? 'fas fa-share-alt' : src === 'global' ? 'fas fa-globe' : 'fas fa-home',
 		bg: 'var(--bg)',
 		actions: [{
 			icon: 'fas fa-list-ul',
-			text: i18n.locale.lists,
+			text: i18n.ts.lists,
 			handler: chooseList,
 		}, {
 			icon: 'fas fa-satellite',
-			text: i18n.locale.antennas,
+			text: i18n.ts.antennas,
 			handler: chooseAntenna,
 		}, {
 			icon: 'fas fa-satellite-dish',
-			text: i18n.locale.channel,
+			text: i18n.ts.channel,
 			handler: chooseChannel,
 		}, {
 			icon: 'fas fa-calendar-alt',
-			text: i18n.locale.jumpToSpecifiedDate,
+			text: i18n.ts.jumpToSpecifiedDate,
 			handler: timetravel,
 		}],
 		tabs: [{
 			active: src === 'home',
-			title: i18n.locale._timelines.home,
+			title: i18n.ts._timelines.home,
 			icon: 'fas fa-home',
 			iconOnly: true,
 			onClick: () => { src = 'home'; saveSrc(); },
 		}, ...(isLocalTimelineAvailable ? [{
 			active: src === 'local',
-			title: i18n.locale._timelines.local,
+			title: i18n.ts._timelines.local,
 			icon: 'fas fa-comments',
 			iconOnly: true,
 			onClick: () => { src = 'local'; saveSrc(); },
 		}, {
 			active: src === 'social',
-			title: i18n.locale._timelines.social,
+			title: i18n.ts._timelines.social,
 			icon: 'fas fa-share-alt',
 			iconOnly: true,
 			onClick: () => { src = 'social'; saveSrc(); },
 		}] : []), ...(isGlobalTimelineAvailable ? [{
 			active: src === 'global',
-			title: i18n.locale._timelines.global,
+			title: i18n.ts._timelines.global,
 			icon: 'fas fa-globe',
 			iconOnly: true,
 			onClick: () => { src = 'global'; saveSrc(); },
diff --git a/packages/client/src/scripts/get-note-menu.ts b/packages/client/src/scripts/get-note-menu.ts
index 3634f39632..b19656d3cc 100644
--- a/packages/client/src/scripts/get-note-menu.ts
+++ b/packages/client/src/scripts/get-note-menu.ts
@@ -27,7 +27,7 @@ export function getNoteMenu(props: {
 	function del(): void {
 		os.confirm({
 			type: 'warning',
-			text: i18n.locale.noteDeleteConfirm,
+			text: i18n.ts.noteDeleteConfirm,
 		}).then(({ canceled }) => {
 			if (canceled) return;
 
@@ -40,7 +40,7 @@ export function getNoteMenu(props: {
 	function delEdit(): void {
 		os.confirm({
 			type: 'warning',
-			text: i18n.locale.deleteAndEditConfirm,
+			text: i18n.ts.deleteAndEditConfirm,
 		}).then(({ canceled }) => {
 			if (canceled) return;
 
@@ -87,7 +87,7 @@ export function getNoteMenu(props: {
 			if (e.id === '72dab508-c64d-498f-8740-a8eec1ba385a') {
 				os.alert({
 					type: 'error',
-					text: i18n.locale.pinLimitExceeded
+					text: i18n.ts.pinLimitExceeded
 				});
 			}
 		});
@@ -97,22 +97,22 @@ export function getNoteMenu(props: {
 		const clips = await os.api('clips/list');
 		os.popupMenu([{
 			icon: 'fas fa-plus',
-			text: i18n.locale.createNew,
+			text: i18n.ts.createNew,
 			action: async () => {
-				const { canceled, result } = await os.form(i18n.locale.createNewClip, {
+				const { canceled, result } = await os.form(i18n.ts.createNewClip, {
 					name: {
 						type: 'string',
-						label: i18n.locale.name
+						label: i18n.ts.name
 					},
 					description: {
 						type: 'string',
 						required: false,
 						multiline: true,
-						label: i18n.locale.description
+						label: i18n.ts.description
 					},
 					isPublic: {
 						type: 'boolean',
-						label: i18n.locale.public,
+						label: i18n.ts.public,
 						default: false
 					}
 				});
@@ -133,7 +133,7 @@ export function getNoteMenu(props: {
 
 	async function promote(): Promise<void> {
 		const { canceled, result: days } = await os.inputNumber({
-			title: i18n.locale.numberOfDays,
+			title: i18n.ts.numberOfDays,
 		});
 
 		if (canceled) return;
@@ -171,69 +171,69 @@ export function getNoteMenu(props: {
 
 		menu = [{
 			icon: 'fas fa-copy',
-			text: i18n.locale.copyContent,
+			text: i18n.ts.copyContent,
 			action: copyContent
 		}, {
 			icon: 'fas fa-link',
-			text: i18n.locale.copyLink,
+			text: i18n.ts.copyLink,
 			action: copyLink
 		}, (appearNote.url || appearNote.uri) ? {
 			icon: 'fas fa-external-link-square-alt',
-			text: i18n.locale.showOnRemote,
+			text: i18n.ts.showOnRemote,
 			action: () => {
 				window.open(appearNote.url || appearNote.uri, '_blank');
 			}
 		} : undefined,
 		{
 			icon: 'fas fa-share-alt',
-			text: i18n.locale.share,
+			text: i18n.ts.share,
 			action: share
 		},
 		instance.translatorAvailable ? {
 			icon: 'fas fa-language',
-			text: i18n.locale.translate,
+			text: i18n.ts.translate,
 			action: translate
 		} : undefined,
 		null,
 		statePromise.then(state => state.isFavorited ? {
 			icon: 'fas fa-star',
-			text: i18n.locale.unfavorite,
+			text: i18n.ts.unfavorite,
 			action: () => toggleFavorite(false)
 		} : {
 			icon: 'fas fa-star',
-			text: i18n.locale.favorite,
+			text: i18n.ts.favorite,
 			action: () => toggleFavorite(true)
 		}),
 		{
 			icon: 'fas fa-paperclip',
-			text: i18n.locale.clip,
+			text: i18n.ts.clip,
 			action: () => clip()
 		},
 		(appearNote.userId != $i.id) ? statePromise.then(state => state.isWatching ? {
 			icon: 'fas fa-eye-slash',
-			text: i18n.locale.unwatch,
+			text: i18n.ts.unwatch,
 			action: () => toggleWatch(false)
 		} : {
 			icon: 'fas fa-eye',
-			text: i18n.locale.watch,
+			text: i18n.ts.watch,
 			action: () => toggleWatch(true)
 		}) : undefined,
 		statePromise.then(state => state.isMutedThread ? {
 			icon: 'fas fa-comment-slash',
-			text: i18n.locale.unmuteThread,
+			text: i18n.ts.unmuteThread,
 			action: () => toggleThreadMute(false)
 		} : {
 			icon: 'fas fa-comment-slash',
-			text: i18n.locale.muteThread,
+			text: i18n.ts.muteThread,
 			action: () => toggleThreadMute(true)
 		}),
 		appearNote.userId == $i.id ? ($i.pinnedNoteIds || []).includes(appearNote.id) ? {
 			icon: 'fas fa-thumbtack',
-			text: i18n.locale.unpin,
+			text: i18n.ts.unpin,
 			action: () => togglePin(false)
 		} : {
 			icon: 'fas fa-thumbtack',
-			text: i18n.locale.pin,
+			text: i18n.ts.pin,
 			action: () => togglePin(true)
 		} : undefined,
 		/*
@@ -241,7 +241,7 @@ export function getNoteMenu(props: {
 			null,
 			{
 				icon: 'fas fa-bullhorn',
-				text: i18n.locale.promote,
+				text: i18n.ts.promote,
 				action: promote
 			}]
 			: []
@@ -250,7 +250,7 @@ export function getNoteMenu(props: {
 			null,
 			{
 				icon: 'fas fa-exclamation-circle',
-				text: i18n.locale.reportAbuse,
+				text: i18n.ts.reportAbuse,
 				action: () => {
 					const u = appearNote.url || appearNote.uri || `${url}/notes/${appearNote.id}`;
 					os.popup(import('@/components/abuse-report-window.vue'), {
@@ -265,12 +265,12 @@ export function getNoteMenu(props: {
 			null,
 			appearNote.userId == $i.id ? {
 				icon: 'fas fa-edit',
-				text: i18n.locale.deleteAndEdit,
+				text: i18n.ts.deleteAndEdit,
 				action: delEdit
 			} : undefined,
 			{
 				icon: 'fas fa-trash-alt',
-				text: i18n.locale.delete,
+				text: i18n.ts.delete,
 				danger: true,
 				action: del
 			}]
@@ -280,15 +280,15 @@ export function getNoteMenu(props: {
 	} else {
 		menu = [{
 			icon: 'fas fa-copy',
-			text: i18n.locale.copyContent,
+			text: i18n.ts.copyContent,
 			action: copyContent
 		}, {
 			icon: 'fas fa-link',
-			text: i18n.locale.copyLink,
+			text: i18n.ts.copyLink,
 			action: copyLink
 		}, (appearNote.url || appearNote.uri) ? {
 			icon: 'fas fa-external-link-square-alt',
-			text: i18n.locale.showOnRemote,
+			text: i18n.ts.showOnRemote,
 			action: () => {
 				window.open(appearNote.url || appearNote.uri, '_blank');
 			}
diff --git a/packages/client/src/scripts/get-note-summary.ts b/packages/client/src/scripts/get-note-summary.ts
index bd394279cb..54b8d109d6 100644
--- a/packages/client/src/scripts/get-note-summary.ts
+++ b/packages/client/src/scripts/get-note-summary.ts
@@ -7,11 +7,11 @@ import { i18n } from '@/i18n';
  */
 export const getNoteSummary = (note: misskey.entities.Note): string => {
 	if (note.deletedAt) {
-		return `(${i18n.locale.deletedNote})`;
+		return `(${i18n.ts.deletedNote})`;
 	}
 
 	if (note.isHidden) {
-		return `(${i18n.locale.invisibleNote})`;
+		return `(${i18n.ts.invisibleNote})`;
 	}
 
 	let summary = '';
@@ -30,7 +30,7 @@ export const getNoteSummary = (note: misskey.entities.Note): string => {
 
 	// 投票が添付されているとき
 	if (note.poll) {
-		summary += ` (${i18n.locale.poll})`;
+		summary += ` (${i18n.ts.poll})`;
 	}
 
 	// 返信のとき
diff --git a/packages/client/src/scripts/get-user-menu.ts b/packages/client/src/scripts/get-user-menu.ts
index 7b910a0083..6d1f25a942 100644
--- a/packages/client/src/scripts/get-user-menu.ts
+++ b/packages/client/src/scripts/get-user-menu.ts
@@ -11,12 +11,12 @@ export function getUserMenu(user) {
 	const meId = $i ? $i.id : null;
 
 	async function pushList() {
-		const t = i18n.locale.selectList; // なぜか後で参照すると null になるので最初にメモリに確保しておく
+		const t = i18n.ts.selectList; // なぜか後で参照すると null になるので最初にメモリに確保しておく
 		const lists = await os.api('users/lists/list');
 		if (lists.length === 0) {
 			os.alert({
 				type: 'error',
-				text: i18n.locale.youHaveNoLists
+				text: i18n.ts.youHaveNoLists
 			});
 			return;
 		}
@@ -38,12 +38,12 @@ export function getUserMenu(user) {
 		if (groups.length === 0) {
 			os.alert({
 				type: 'error',
-				text: i18n.locale.youHaveNoGroups
+				text: i18n.ts.youHaveNoGroups
 			});
 			return;
 		}
 		const { canceled, result: groupId } = await os.select({
-			title: i18n.locale.group,
+			title: i18n.ts.group,
 			items: groups.map(group => ({
 				value: group.id, text: group.name
 			}))
@@ -64,7 +64,7 @@ export function getUserMenu(user) {
 	}
 
 	async function toggleBlock() {
-		if (!await getConfirmed(user.isBlocking ? i18n.locale.unblockConfirm : i18n.locale.blockConfirm)) return;
+		if (!await getConfirmed(user.isBlocking ? i18n.ts.unblockConfirm : i18n.ts.blockConfirm)) return;
 
 		os.apiWithDialog(user.isBlocking ? 'blocking/delete' : 'blocking/create', {
 			userId: user.id
@@ -119,70 +119,70 @@ export function getUserMenu(user) {
 
 	let menu = [{
 		icon: 'fas fa-at',
-		text: i18n.locale.copyUsername,
+		text: i18n.ts.copyUsername,
 		action: () => {
 			copyToClipboard(`@${user.username}@${user.host || host}`);
 		}
 	}, {
 		icon: 'fas fa-info-circle',
-		text: i18n.locale.info,
+		text: i18n.ts.info,
 		action: () => {
 			os.pageWindow(`/user-info/${user.id}`);
 		}
 	}, {
 		icon: 'fas fa-envelope',
-		text: i18n.locale.sendMessage,
+		text: i18n.ts.sendMessage,
 		action: () => {
 			os.post({ specified: user });
 		}
 	}, meId != user.id ? {
 		type: 'link',
 		icon: 'fas fa-comments',
-		text: i18n.locale.startMessaging,
+		text: i18n.ts.startMessaging,
 		to: '/my/messaging/' + Acct.toString(user),
 	} : undefined, null, {
 		icon: 'fas fa-list-ul',
-		text: i18n.locale.addToList,
+		text: i18n.ts.addToList,
 		action: pushList
 	}, meId != user.id ? {
 		icon: 'fas fa-users',
-		text: i18n.locale.inviteToGroup,
+		text: i18n.ts.inviteToGroup,
 		action: inviteGroup
 	} : undefined] as any;
 
 	if ($i && meId != user.id) {
 		menu = menu.concat([null, {
 			icon: user.isMuted ? 'fas fa-eye' : 'fas fa-eye-slash',
-			text: user.isMuted ? i18n.locale.unmute : i18n.locale.mute,
+			text: user.isMuted ? i18n.ts.unmute : i18n.ts.mute,
 			action: toggleMute
 		}, {
 			icon: 'fas fa-ban',
-			text: user.isBlocking ? i18n.locale.unblock : i18n.locale.block,
+			text: user.isBlocking ? i18n.ts.unblock : i18n.ts.block,
 			action: toggleBlock
 		}]);
 
 		if (user.isFollowed) {
 			menu = menu.concat([{
 				icon: 'fas fa-unlink',
-				text: i18n.locale.breakFollow,
+				text: i18n.ts.breakFollow,
 				action: invalidateFollow
 			}]);
 		}
 
 		menu = menu.concat([null, {
 			icon: 'fas fa-exclamation-circle',
-			text: i18n.locale.reportAbuse,
+			text: i18n.ts.reportAbuse,
 			action: reportAbuse
 		}]);
 
 		if (iAmModerator) {
 			menu = menu.concat([null, {
 				icon: 'fas fa-microphone-slash',
-				text: user.isSilenced ? i18n.locale.unsilence : i18n.locale.silence,
+				text: user.isSilenced ? i18n.ts.unsilence : i18n.ts.silence,
 				action: toggleSilence
 			}, {
 				icon: 'fas fa-snowflake',
-				text: user.isSuspended ? i18n.locale.unsuspend : i18n.locale.suspend,
+				text: user.isSuspended ? i18n.ts.unsuspend : i18n.ts.suspend,
 				action: toggleSuspend
 			}]);
 		}
@@ -191,7 +191,7 @@ export function getUserMenu(user) {
 	if ($i && meId === user.id) {
 		menu = menu.concat([null, {
 			icon: 'fas fa-pencil-alt',
-			text: i18n.locale.editProfile,
+			text: i18n.ts.editProfile,
 			action: () => {
 				router.push('/settings/profile');
 			}
diff --git a/packages/client/src/scripts/i18n.ts b/packages/client/src/scripts/i18n.ts
index 4fa398763a..3fe88e5514 100644
--- a/packages/client/src/scripts/i18n.ts
+++ b/packages/client/src/scripts/i18n.ts
@@ -1,8 +1,8 @@
 export class I18n<T extends Record<string, any>> {
-	public locale: T;
+	public ts: T;
 
 	constructor(locale: T) {
-		this.locale = locale;
+		this.ts = locale;
 
 		//#region BIND
 		this.t = this.t.bind(this);
@@ -11,9 +11,9 @@ export class I18n<T extends Record<string, any>> {
 
 	// string にしているのは、ドット区切りでのパス指定を許可するため
 	// なるべくこのメソッド使うよりもlocale直接参照の方がvueのキャッシュ効いてパフォーマンスが良いかも
-	public t(key: string, args?: Record<string, any>): string {
+	public t(key: string, args?: Record<string, string>): string {
 		try {
-			let str = key.split('.').reduce((o, i) => o[i], this.locale) as string;
+			let str = key.split('.').reduce((o, i) => o[i], this.ts) as unknown as string;
 
 			if (args) {
 				for (const [k, v] of Object.entries(args)) {
@@ -21,7 +21,7 @@ export class I18n<T extends Record<string, any>> {
 				}
 			}
 			return str;
-		} catch (e) {
+		} catch (err) {
 			console.warn(`missing localization '${key}'`);
 			return key;
 		}
diff --git a/packages/client/src/scripts/lookup-user.ts b/packages/client/src/scripts/lookup-user.ts
index 64874f86f6..8de5c84ce8 100644
--- a/packages/client/src/scripts/lookup-user.ts
+++ b/packages/client/src/scripts/lookup-user.ts
@@ -4,7 +4,7 @@ import * as os from '@/os';
 
 export async function lookupUser() {
 	const { canceled, result } = await os.inputText({
-		title: i18n.locale.usernameOrUserId,
+		title: i18n.ts.usernameOrUserId,
 	});
 	if (canceled) return;
 
@@ -19,7 +19,7 @@ export async function lookupUser() {
 		if (_notFound) {
 			os.alert({
 				type: 'error',
-				text: i18n.locale.noSuchUser
+				text: i18n.ts.noSuchUser
 			});
 		} else {
 			_notFound = true;
diff --git a/packages/client/src/scripts/please-login.ts b/packages/client/src/scripts/please-login.ts
index fe3919e4c7..aeaafa124b 100644
--- a/packages/client/src/scripts/please-login.ts
+++ b/packages/client/src/scripts/please-login.ts
@@ -6,7 +6,7 @@ export function pleaseLogin() {
 	if ($i) return;
 
 	alert({
-		title: i18n.locale.signinRequired,
+		title: i18n.ts.signinRequired,
 		text: null
 	});
 
diff --git a/packages/client/src/scripts/search.ts b/packages/client/src/scripts/search.ts
index a070b1121c..0aedee9c98 100644
--- a/packages/client/src/scripts/search.ts
+++ b/packages/client/src/scripts/search.ts
@@ -4,7 +4,7 @@ import { router } from '@/router';
 
 export async function search() {
 	const { canceled, result: query } = await os.inputText({
-		title: i18n.locale.search,
+		title: i18n.ts.search,
 	});
 	if (canceled || query == null || query === '') return;
 
@@ -46,7 +46,7 @@ export async function search() {
 			uri: q
 		});
 
-		os.promiseDialog(promise, null, null, i18n.locale.fetchingAsApObject);
+		os.promiseDialog(promise, null, null, i18n.ts.fetchingAsApObject);
 
 		const res = await promise;
 
diff --git a/packages/client/src/scripts/select-file.ts b/packages/client/src/scripts/select-file.ts
index 6bb3f8bf8a..56e0b564f3 100644
--- a/packages/client/src/scripts/select-file.ts
+++ b/packages/client/src/scripts/select-file.ts
@@ -41,9 +41,9 @@ function select(src: any, label: string | null, multiple: boolean): Promise<Driv
 
 		const chooseFileFromUrl = () => {
 			os.inputText({
-				title: i18n.locale.uploadFromUrl,
+				title: i18n.ts.uploadFromUrl,
 				type: 'url',
-				placeholder: i18n.locale.uploadFromUrlDescription
+				placeholder: i18n.ts.uploadFromUrlDescription
 			}).then(({ canceled, result: url }) => {
 				if (canceled) return;
 
@@ -64,8 +64,8 @@ function select(src: any, label: string | null, multiple: boolean): Promise<Driv
 				});
 
 				os.alert({
-					title: i18n.locale.uploadFromUrlRequested,
-					text: i18n.locale.uploadFromUrlMayTakeTime
+					title: i18n.ts.uploadFromUrlRequested,
+					text: i18n.ts.uploadFromUrlMayTakeTime
 				});
 			});
 		};
@@ -74,15 +74,15 @@ function select(src: any, label: string | null, multiple: boolean): Promise<Driv
 			text: label,
 			type: 'label'
 		} : undefined, {
-			text: i18n.locale.upload,
+			text: i18n.ts.upload,
 			icon: 'fas fa-upload',
 			action: chooseFileFromPc
 		}, {
-			text: i18n.locale.fromDrive,
+			text: i18n.ts.fromDrive,
 			icon: 'fas fa-cloud',
 			action: chooseFileFromDrive
 		}, {
-			text: i18n.locale.fromUrl,
+			text: i18n.ts.fromUrl,
 			icon: 'fas fa-link',
 			action: chooseFileFromUrl
 		}], src);
diff --git a/packages/client/src/scripts/show-suspended-dialog.ts b/packages/client/src/scripts/show-suspended-dialog.ts
index dcbb66933c..acfbc60e92 100644
--- a/packages/client/src/scripts/show-suspended-dialog.ts
+++ b/packages/client/src/scripts/show-suspended-dialog.ts
@@ -4,7 +4,7 @@ import { i18n } from '@/i18n';
 export function showSuspendedDialog() {
 	return os.alert({
 		type: 'error',
-		title: i18n.locale.yourAccountSuspendedTitle,
-		text: i18n.locale.yourAccountSuspendedDescription
+		title: i18n.ts.yourAccountSuspendedTitle,
+		text: i18n.ts.yourAccountSuspendedDescription
 	});
 }
diff --git a/packages/client/src/scripts/use-leave-guard.ts b/packages/client/src/scripts/use-leave-guard.ts
index 3984256251..33eea6b522 100644
--- a/packages/client/src/scripts/use-leave-guard.ts
+++ b/packages/client/src/scripts/use-leave-guard.ts
@@ -12,7 +12,7 @@ export function useLeaveGuard(enabled: Ref<boolean>) {
 
 			const { canceled } = await os.confirm({
 				type: 'warning',
-				text: i18n.locale.leaveConfirm,
+				text: i18n.ts.leaveConfirm,
 			});
 
 			return canceled;
@@ -23,7 +23,7 @@ export function useLeaveGuard(enabled: Ref<boolean>) {
 
 			const { canceled } = await os.confirm({
 				type: 'warning',
-				text: i18n.locale.leaveConfirm,
+				text: i18n.ts.leaveConfirm,
 			});
 
 			return !canceled;
diff --git a/packages/client/src/ui/deck.vue b/packages/client/src/ui/deck.vue
index 51a4853e9d..9accc34a88 100644
--- a/packages/client/src/ui/deck.vue
+++ b/packages/client/src/ui/deck.vue
@@ -104,7 +104,7 @@ export default defineComponent({
 			];
 
 			const { canceled, result: column } = await os.select({
-				title: i18n.locale._deck.addColumn,
+				title: i18n.ts._deck.addColumn,
 				items: columns.map(column => ({
 					value: column, text: i18n.t('_deck._columns.' + column)
 				}))
@@ -121,7 +121,7 @@ export default defineComponent({
 
 		const onContextmenu = (ev) => {
 			os.contextMenu([{
-				text: i18n.locale._deck.addColumn,
+				text: i18n.ts._deck.addColumn,
 				icon: null,
 				action: addColumn
 			}], ev);
diff --git a/packages/client/src/ui/deck/deck-store.ts b/packages/client/src/ui/deck/deck-store.ts
index 6b6b02f3f9..66db5e83ed 100644
--- a/packages/client/src/ui/deck/deck-store.ts
+++ b/packages/client/src/ui/deck/deck-store.ts
@@ -77,12 +77,12 @@ export const loadDeck = async () => {
 			deckStore.set('columns', [{
 				id: 'a',
 				type: 'main',
-				name: i18n.locale._deck._columns.main,
+				name: i18n.ts._deck._columns.main,
 				width: 350,
 			}, {
 				id: 'b',
 				type: 'notifications',
-				name: i18n.locale._deck._columns.notifications,
+				name: i18n.ts._deck._columns.notifications,
 				width: 330,
 			}]);
 			deckStore.set('layout', [['a'], ['b']]);
diff --git a/packages/client/src/ui/universal.vue b/packages/client/src/ui/universal.vue
index 16cc9a4f06..8fe9dcffaf 100644
--- a/packages/client/src/ui/universal.vue
+++ b/packages/client/src/ui/universal.vue
@@ -171,13 +171,13 @@ export default defineComponent({
 				text: path,
 			}, {
 				icon: 'fas fa-columns',
-				text: i18n.locale.openInSideView,
+				text: i18n.ts.openInSideView,
 				action: () => {
 					this.$refs.side.navigate(path);
 				}
 			}, {
 				icon: 'fas fa-window-maximize',
-				text: i18n.locale.openInWindow,
+				text: i18n.ts.openInWindow,
 				action: () => {
 					os.pageWindow(path);
 				}
diff --git a/packages/client/src/widgets/calendar.vue b/packages/client/src/widgets/calendar.vue
index b0e3edcb12..c6a69b3fb8 100644
--- a/packages/client/src/widgets/calendar.vue
+++ b/packages/client/src/widgets/calendar.vue
@@ -79,13 +79,13 @@ const tick = () => {
 	month.value = nm + 1;
 	day.value = nd;
 	weekDay.value = [
-		i18n.locale._weekday.sunday,
-		i18n.locale._weekday.monday,
-		i18n.locale._weekday.tuesday,
-		i18n.locale._weekday.wednesday,
-		i18n.locale._weekday.thursday,
-		i18n.locale._weekday.friday,
-		i18n.locale._weekday.saturday
+		i18n.ts._weekday.sunday,
+		i18n.ts._weekday.monday,
+		i18n.ts._weekday.tuesday,
+		i18n.ts._weekday.wednesday,
+		i18n.ts._weekday.thursday,
+		i18n.ts._weekday.friday,
+		i18n.ts._weekday.saturday
 	][now.getDay()];
 
 	const dayNumer   = now.getTime() - new Date(ny, nm, nd).getTime();
diff --git a/packages/client/src/widgets/timeline.vue b/packages/client/src/widgets/timeline.vue
index fa700cc8ee..0e4396c133 100644
--- a/packages/client/src/widgets/timeline.vue
+++ b/packages/client/src/widgets/timeline.vue
@@ -101,19 +101,19 @@ const choose = async (ev) => {
 		}
 	}));
 	os.popupMenu([{
-		text: i18n.locale._timelines.home,
+		text: i18n.ts._timelines.home,
 		icon: 'fas fa-home',
 		action: () => { setSrc('home') }
 	}, {
-		text: i18n.locale._timelines.local,
+		text: i18n.ts._timelines.local,
 		icon: 'fas fa-comments',
 		action: () => { setSrc('local') }
 	}, {
-		text: i18n.locale._timelines.social,
+		text: i18n.ts._timelines.social,
 		icon: 'fas fa-share-alt',
 		action: () => { setSrc('social') }
 	}, {
-		text: i18n.locale._timelines.global,
+		text: i18n.ts._timelines.global,
 		icon: 'fas fa-globe',
 		action: () => { setSrc('global') }
 	}, antennaItems.length > 0 ? null : undefined, ...antennaItems, listItems.length > 0 ? null : undefined, ...listItems], ev.currentTarget || ev.target).then(() => {