From 4ee859b69104168dd490ca286a46ef2bc831940d Mon Sep 17 00:00:00 2001
From: syuilo <Syuilotan@yahoo.co.jp>
Date: Fri, 21 Jan 2022 21:47:36 +0900
Subject: [PATCH 01/46] =?UTF-8?q?fix(client):=20=E3=82=BF=E3=82=A4?=
 =?UTF-8?q?=E3=83=A0=E3=83=A9=E3=82=A4=E3=83=B3=E3=81=AEkeep-alive?=
 =?UTF-8?q?=E3=81=8C=E5=8A=B9=E3=81=8B=E3=81=AA=E3=81=8F=E3=81=AA=E3=81=A3?=
 =?UTF-8?q?=E3=81=A6=E3=81=84=E3=82=8B=E3=81=AE=E3=82=92=E4=BF=AE=E6=AD=A3?=
MIME-Version: 1.0
Content-Type: text/plain; charset=UTF-8
Content-Transfer-Encoding: 8bit

---
 packages/client/src/pages/timeline.vue      | 6 ++++++
 packages/client/src/ui/classic.vue          | 2 +-
 packages/client/src/ui/deck/main-column.vue | 2 +-
 packages/client/src/ui/universal.vue        | 2 +-
 packages/client/src/ui/zen.vue              | 2 +-
 5 files changed, 10 insertions(+), 4 deletions(-)

diff --git a/packages/client/src/pages/timeline.vue b/packages/client/src/pages/timeline.vue
index ecd1ae6257..aabb953aec 100644
--- a/packages/client/src/pages/timeline.vue
+++ b/packages/client/src/pages/timeline.vue
@@ -17,6 +17,12 @@
 </MkSpacer>
 </template>
 
+<script lang="ts">
+export default {
+	name: 'MkTimelinePage',
+}
+</script>
+
 <script lang="ts" setup>
 import { defineAsyncComponent, computed, watch } from 'vue';
 import XTimeline from '@/components/timeline.vue';
diff --git a/packages/client/src/ui/classic.vue b/packages/client/src/ui/classic.vue
index 1603ea6399..cf2ad9cbf3 100644
--- a/packages/client/src/ui/classic.vue
+++ b/packages/client/src/ui/classic.vue
@@ -16,7 +16,7 @@
 					<template #header><MkHeader v-if="pageInfo && !pageInfo.hideHeader" :info="pageInfo"/></template>
 					<router-view v-slot="{ Component }">
 						<transition :name="$store.state.animation ? 'page' : ''" mode="out-in" @enter="onTransition">
-							<keep-alive :include="['timeline']">
+							<keep-alive :include="['MkTimelinePage']">
 								<component :is="Component" :ref="changePage"/>
 							</keep-alive>
 						</transition>
diff --git a/packages/client/src/ui/deck/main-column.vue b/packages/client/src/ui/deck/main-column.vue
index b77d6f35cc..cb045e9a46 100644
--- a/packages/client/src/ui/deck/main-column.vue
+++ b/packages/client/src/ui/deck/main-column.vue
@@ -11,7 +11,7 @@
 		<template #header><MkHeader v-if="pageInfo && !pageInfo.hideHeader" :info="pageInfo"/></template>
 		<router-view v-slot="{ Component }">
 			<transition>
-				<keep-alive :include="['timeline']">
+				<keep-alive :include="['MkTimelinePage']">
 					<component :is="Component" :ref="changePage" @contextmenu.stop="onContextmenu"/>
 				</keep-alive>
 			</transition>
diff --git a/packages/client/src/ui/universal.vue b/packages/client/src/ui/universal.vue
index 9fc2177ee0..4b356f1511 100644
--- a/packages/client/src/ui/universal.vue
+++ b/packages/client/src/ui/universal.vue
@@ -9,7 +9,7 @@
 					<template #header><MkHeader v-if="pageInfo && !pageInfo.hideHeader" :info="pageInfo"/></template>
 					<router-view v-slot="{ Component }">
 						<transition :name="$store.state.animation ? 'page' : ''" mode="out-in" @enter="onTransition">
-							<keep-alive :include="['timeline']">
+							<keep-alive :include="['MkTimelinePage']">
 								<component :is="Component" :ref="changePage"/>
 							</keep-alive>
 						</transition>
diff --git a/packages/client/src/ui/zen.vue b/packages/client/src/ui/zen.vue
index 7c72232cfd..a7234f729b 100644
--- a/packages/client/src/ui/zen.vue
+++ b/packages/client/src/ui/zen.vue
@@ -8,7 +8,7 @@
 			<div class="content">
 				<router-view v-slot="{ Component }">
 					<transition :name="$store.state.animation ? 'page' : ''" mode="out-in" @enter="onTransition">
-						<keep-alive :include="['timeline']">
+						<keep-alive :include="['MkTimelinePage']">
 							<component :is="Component" :ref="changePage"/>
 						</keep-alive>
 					</transition>

From e2cefb7f476bfa46844a9ee027af523e9e01f21d Mon Sep 17 00:00:00 2001
From: xianon <xianon@hotmail.co.jp>
Date: Sat, 22 Jan 2022 02:13:29 +0900
Subject: [PATCH 02/46] =?UTF-8?q?NodeInfo=20=E3=81=AB=E3=83=A6=E3=83=BC?=
 =?UTF-8?q?=E3=82=B6=E3=83=BC=E6=95=B0=E3=81=A8=E6=8A=95=E7=A8=BF=E6=95=B0?=
 =?UTF-8?q?=E3=81=AE=E6=83=85=E5=A0=B1=E3=82=92=E8=BF=BD=E5=8A=A0=E3=81=99?=
 =?UTF-8?q?=E3=82=8B=20(#8126)?=
MIME-Version: 1.0
Content-Type: text/plain; charset=UTF-8
Content-Transfer-Encoding: 8bit

---
 packages/backend/src/server/nodeinfo.ts | 32 ++++++++++++-------------
 1 file changed, 16 insertions(+), 16 deletions(-)

diff --git a/packages/backend/src/server/nodeinfo.ts b/packages/backend/src/server/nodeinfo.ts
index 097c6c664d..44f32bf882 100644
--- a/packages/backend/src/server/nodeinfo.ts
+++ b/packages/backend/src/server/nodeinfo.ts
@@ -1,9 +1,8 @@
 import * as Router from '@koa/router';
 import config from '@/config/index';
 import { fetchMeta } from '@/misc/fetch-meta';
-import { Users } from '@/models/index';
-// import User from '../models/user';
-// import Note from '../models/note';
+import { Users, Notes } from '@/models/index';
+import { Not, IsNull, MoreThan } from 'typeorm';
 
 const router = new Router();
 
@@ -19,20 +18,21 @@ export const links = [/* (awaiting release) {
 }];
 
 const nodeinfo2 = async () => {
+	const now = Date.now();
 	const [
 		meta,
-		// total,
-		// activeHalfyear,
-		// activeMonth,
-		// localPosts,
-		// localComments
+		total,
+		activeHalfyear,
+		activeMonth,
+		localPosts,
+		localComments,
 	] = await Promise.all([
 		fetchMeta(true),
-		// User.count({ host: null }),
-		// User.count({ host: null, updatedAt: { $gt: new Date(Date.now() - 15552000000) } }),
-		// User.count({ host: null, updatedAt: { $gt: new Date(Date.now() - 2592000000) } }),
-		// Note.count({ '_user.host': null, replyId: null }),
-		// Note.count({ '_user.host': null, replyId: { $ne: null } })
+		Users.count({ where: { host: null } }),
+		Users.count({ where: { host: null, updatedAt: MoreThan(new Date(now - 15552000000)) } }),
+		Users.count({ where: { host: null, updatedAt: MoreThan(new Date(now - 2592000000)) } }),
+		Notes.count({ where: { userHost: null, replyId: null } }),
+		Notes.count({ where: { userHost: null, replyId: Not(IsNull()) } }),
 	]);
 
 	const proxyAccount = meta.proxyAccountId ? await Users.pack(meta.proxyAccountId).catch(() => null) : null;
@@ -50,9 +50,9 @@ const nodeinfo2 = async () => {
 		},
 		openRegistrations: !meta.disableRegistration,
 		usage: {
-			users: {}, // { total, activeHalfyear, activeMonth },
-			// localPosts,
-			// localComments
+			users: { total, activeHalfyear, activeMonth },
+			localPosts,
+			localComments,
 		},
 		metadata: {
 			nodeName: meta.name,

From d8905a9588f3d1c2d7112993c1b842a31f4d9a74 Mon Sep 17 00:00:00 2001
From: MeiMei <30769358+mei23@users.noreply.github.com>
Date: Sun, 23 Jan 2022 01:52:35 +0900
Subject: [PATCH 03/46] Unifying Misskey-specific IRIs in JSON-LD `@context`
 Resolve #8116  (#8178)

* Unifying Misskey-specific IRIs in JSON-LD `@context` Resolve #8116

* CHANGELOG
---
 CHANGELOG.md                                              | 1 +
 packages/backend/src/remote/activitypub/renderer/index.ts | 2 +-
 2 files changed, 2 insertions(+), 1 deletion(-)

diff --git a/CHANGELOG.md b/CHANGELOG.md
index 5228be120b..d01358b278 100644
--- a/CHANGELOG.md
+++ b/CHANGELOG.md
@@ -22,6 +22,7 @@
 - カスタム絵文字一括編集機能
 - カスタム絵文字一括インポート
 - 投稿フォームで一時的に投稿するアカウントを切り替えられるように
+- Unifying Misskey-specific IRIs in JSON-LD `@context`
 
 ### Bugfixes
 
diff --git a/packages/backend/src/remote/activitypub/renderer/index.ts b/packages/backend/src/remote/activitypub/renderer/index.ts
index 911118e4c9..cffc9bfe04 100644
--- a/packages/backend/src/remote/activitypub/renderer/index.ts
+++ b/packages/backend/src/remote/activitypub/renderer/index.ts
@@ -32,7 +32,7 @@ export const renderActivity = (x: any): IActivity | null => {
 				PropertyValue: 'schema:PropertyValue',
 				value: 'schema:value',
 				// Misskey
-				misskey: `${config.url}/ns#`,
+				misskey: 'https://misskey-hub.net/ns#',
 				'_misskey_content': 'misskey:_misskey_content',
 				'_misskey_quote': 'misskey:_misskey_quote',
 				'_misskey_reaction': 'misskey:_misskey_reaction',

From 7aecf15f9473ceafc625c07f69241a37bd61ecbf Mon Sep 17 00:00:00 2001
From: MeiMei <30769358+mei23@users.noreply.github.com>
Date: Sun, 23 Jan 2022 22:52:35 +0900
Subject: [PATCH 04/46] =?UTF-8?q?refactor,=20enhance:=20=E3=83=89=E3=83=A9?=
 =?UTF-8?q?=E3=82=A4=E3=83=96=E5=BC=95=E6=95=B0=E3=81=AE=E3=82=AA=E3=83=96?=
 =?UTF-8?q?=E3=82=B8=E3=82=A7=E3=82=AF=E3=83=88=E5=8C=96,=20=E8=BF=BD?=
 =?UTF-8?q?=E5=8A=A0=E6=99=82=E3=81=AEcomment=E6=8C=87=E5=AE=9A=20(#8180)?=
MIME-Version: 1.0
Content-Type: text/plain; charset=UTF-8
Content-Transfer-Encoding: 8bit

* refactor: ドライブの引数をオブジェクト化する Resolve #8177

* Resolve #8181

* fix

* archivePath
---
 .../queue/processors/db/export-blocking.ts    |  2 +-
 .../processors/db/export-custom-emojis.ts     |  2 +-
 .../queue/processors/db/export-following.ts   |  2 +-
 .../src/queue/processors/db/export-mute.ts    |  2 +-
 .../src/queue/processors/db/export-notes.ts   |  2 +-
 .../queue/processors/db/export-user-lists.ts  |  2 +-
 .../processors/db/import-custom-emojis.ts     |  2 +-
 .../src/remote/activitypub/models/image.ts    | 12 +++-
 .../server/api/endpoints/admin/emoji/copy.ts  |  4 +-
 .../api/endpoints/drive/files/create.ts       |  8 ++-
 .../endpoints/drive/files/upload-from-url.ts  |  4 +-
 .../backend/src/services/drive/add-file.ts    | 58 +++++++++++--------
 .../src/services/drive/upload-from-url.ts     | 29 +++++++---
 13 files changed, 82 insertions(+), 47 deletions(-)

diff --git a/packages/backend/src/queue/processors/db/export-blocking.ts b/packages/backend/src/queue/processors/db/export-blocking.ts
index af5c7eba15..01edaaeb63 100644
--- a/packages/backend/src/queue/processors/db/export-blocking.ts
+++ b/packages/backend/src/queue/processors/db/export-blocking.ts
@@ -86,7 +86,7 @@ export async function exportBlocking(job: Bull.Job<DbUserJobData>, done: any): P
 	logger.succ(`Exported to: ${path}`);
 
 	const fileName = 'blocking-' + dateFormat(new Date(), 'yyyy-mm-dd-HH-MM-ss') + '.csv';
-	const driveFile = await addFile(user, path, fileName, null, null, true);
+	const driveFile = await addFile({ user, path, name: fileName, force: true });
 
 	logger.succ(`Exported to: ${driveFile.id}`);
 	cleanup();
diff --git a/packages/backend/src/queue/processors/db/export-custom-emojis.ts b/packages/backend/src/queue/processors/db/export-custom-emojis.ts
index 0c06b12c9a..240a542fec 100644
--- a/packages/backend/src/queue/processors/db/export-custom-emojis.ts
+++ b/packages/backend/src/queue/processors/db/export-custom-emojis.ts
@@ -111,7 +111,7 @@ export async function exportCustomEmojis(job: Bull.Job, done: () => void): Promi
 		logger.succ(`Exported to: ${archivePath}`);
 
 		const fileName = 'custom-emojis-' + dateFormat(new Date(), 'yyyy-mm-dd-HH-MM-ss') + '.zip';
-		const driveFile = await addFile(user, archivePath, fileName, null, null, true);
+		const driveFile = await addFile({ user, path: archivePath, name: fileName, force: true });
 
 		logger.succ(`Exported to: ${driveFile.id}`);
 		cleanup();
diff --git a/packages/backend/src/queue/processors/db/export-following.ts b/packages/backend/src/queue/processors/db/export-following.ts
index 0c088dc371..06572acec1 100644
--- a/packages/backend/src/queue/processors/db/export-following.ts
+++ b/packages/backend/src/queue/processors/db/export-following.ts
@@ -87,7 +87,7 @@ export async function exportFollowing(job: Bull.Job<DbUserJobData>, done: () =>
 	logger.succ(`Exported to: ${path}`);
 
 	const fileName = 'following-' + dateFormat(new Date(), 'yyyy-mm-dd-HH-MM-ss') + '.csv';
-	const driveFile = await addFile(user, path, fileName, null, null, true);
+	const driveFile = await addFile({ user, path, name: fileName, force: true });
 
 	logger.succ(`Exported to: ${driveFile.id}`);
 	cleanup();
diff --git a/packages/backend/src/queue/processors/db/export-mute.ts b/packages/backend/src/queue/processors/db/export-mute.ts
index f5928b875d..4a856f8ef9 100644
--- a/packages/backend/src/queue/processors/db/export-mute.ts
+++ b/packages/backend/src/queue/processors/db/export-mute.ts
@@ -86,7 +86,7 @@ export async function exportMute(job: Bull.Job<DbUserJobData>, done: any): Promi
 	logger.succ(`Exported to: ${path}`);
 
 	const fileName = 'mute-' + dateFormat(new Date(), 'yyyy-mm-dd-HH-MM-ss') + '.csv';
-	const driveFile = await addFile(user, path, fileName, null, null, true);
+	const driveFile = await addFile({ user, path, name: fileName, force: true });
 
 	logger.succ(`Exported to: ${driveFile.id}`);
 	cleanup();
diff --git a/packages/backend/src/queue/processors/db/export-notes.ts b/packages/backend/src/queue/processors/db/export-notes.ts
index df7675dec7..305abf44cf 100644
--- a/packages/backend/src/queue/processors/db/export-notes.ts
+++ b/packages/backend/src/queue/processors/db/export-notes.ts
@@ -95,7 +95,7 @@ export async function exportNotes(job: Bull.Job<DbUserJobData>, done: any): Prom
 	logger.succ(`Exported to: ${path}`);
 
 	const fileName = 'notes-' + dateFormat(new Date(), 'yyyy-mm-dd-HH-MM-ss') + '.json';
-	const driveFile = await addFile(user, path, fileName, null, null, true);
+	const driveFile = await addFile({ user, path, name: fileName, force: true });
 
 	logger.succ(`Exported to: ${driveFile.id}`);
 	cleanup();
diff --git a/packages/backend/src/queue/processors/db/export-user-lists.ts b/packages/backend/src/queue/processors/db/export-user-lists.ts
index b9b6cb0de6..f907cf9526 100644
--- a/packages/backend/src/queue/processors/db/export-user-lists.ts
+++ b/packages/backend/src/queue/processors/db/export-user-lists.ts
@@ -63,7 +63,7 @@ export async function exportUserLists(job: Bull.Job<DbUserJobData>, done: any):
 	logger.succ(`Exported to: ${path}`);
 
 	const fileName = 'user-lists-' + dateFormat(new Date(), 'yyyy-mm-dd-HH-MM-ss') + '.csv';
-	const driveFile = await addFile(user, path, fileName, null, null, true);
+	const driveFile = await addFile({ user, path, name: fileName, force: true });
 
 	logger.succ(`Exported to: ${driveFile.id}`);
 	cleanup();
diff --git a/packages/backend/src/queue/processors/db/import-custom-emojis.ts b/packages/backend/src/queue/processors/db/import-custom-emojis.ts
index d2b0eb269a..04e93671ed 100644
--- a/packages/backend/src/queue/processors/db/import-custom-emojis.ts
+++ b/packages/backend/src/queue/processors/db/import-custom-emojis.ts
@@ -59,7 +59,7 @@ export async function importCustomEmojis(job: Bull.Job<DbUserImportJobData>, don
 			await Emojis.delete({
 				name: emojiInfo.name,
 			});
-			const driveFile = await addFile(null, emojiPath, record.fileName, null, null, true);
+			const driveFile = await addFile({ user: null, path: emojiPath, name: record.fileName, force: true });
 			const emoji = await Emojis.insert({
 				id: genId(),
 				updatedAt: new Date(),
diff --git a/packages/backend/src/remote/activitypub/models/image.ts b/packages/backend/src/remote/activitypub/models/image.ts
index 902eb36a17..6f60b7827d 100644
--- a/packages/backend/src/remote/activitypub/models/image.ts
+++ b/packages/backend/src/remote/activitypub/models/image.ts
@@ -1,4 +1,4 @@
-import uploadFromUrl from '@/services/drive/upload-from-url';
+import { uploadFromUrl } from '@/services/drive/upload-from-url';
 import { IRemoteUser } from '@/models/entities/user';
 import Resolver from '../resolver';
 import { fetchMeta } from '@/misc/fetch-meta';
@@ -28,9 +28,15 @@ export async function createImage(actor: IRemoteUser, value: any): Promise<Drive
 	logger.info(`Creating the Image: ${image.url}`);
 
 	const instance = await fetchMeta();
-	const cache = instance.cacheRemoteFiles;
 
-	let file = await uploadFromUrl(image.url, actor, null, image.url, image.sensitive, false, !cache, truncate(image.name, DB_MAX_IMAGE_COMMENT_LENGTH));
+	let file = await uploadFromUrl({
+		url: image.url,
+		user: actor,
+		uri: image.url,
+		sensitive: image.sensitive,
+		isLink: !instance.cacheRemoteFiles,
+		comment: truncate(image.name, DB_MAX_IMAGE_COMMENT_LENGTH)
+	});
 
 	if (file.isLink) {
 		// URLが異なっている場合、同じ画像が以前に異なるURLで登録されていたということなので、
diff --git a/packages/backend/src/server/api/endpoints/admin/emoji/copy.ts b/packages/backend/src/server/api/endpoints/admin/emoji/copy.ts
index 4f53739793..17cbf208aa 100644
--- a/packages/backend/src/server/api/endpoints/admin/emoji/copy.ts
+++ b/packages/backend/src/server/api/endpoints/admin/emoji/copy.ts
@@ -6,7 +6,7 @@ import { getConnection } from 'typeorm';
 import { ApiError } from '../../../error';
 import { DriveFile } from '@/models/entities/drive-file';
 import { ID } from '@/misc/cafy-id';
-import uploadFromUrl from '@/services/drive/upload-from-url';
+import { uploadFromUrl } from '@/services/drive/upload-from-url';
 import { publishBroadcastStream } from '@/services/stream';
 
 export const meta = {
@@ -54,7 +54,7 @@ export default define(meta, async (ps, me) => {
 
 	try {
 		// Create file
-		driveFile = await uploadFromUrl(emoji.originalUrl, null, null, null, false, true);
+		driveFile = await uploadFromUrl({ url: emoji.originalUrl, user: null, force: true });
 	} catch (e) {
 		throw new ApiError();
 	}
diff --git a/packages/backend/src/server/api/endpoints/drive/files/create.ts b/packages/backend/src/server/api/endpoints/drive/files/create.ts
index 3799181540..dd65ab0611 100644
--- a/packages/backend/src/server/api/endpoints/drive/files/create.ts
+++ b/packages/backend/src/server/api/endpoints/drive/files/create.ts
@@ -6,6 +6,7 @@ import define from '../../../define';
 import { apiLogger } from '../../../logger';
 import { ApiError } from '../../../error';
 import { DriveFiles } from '@/models/index';
+import { DB_MAX_IMAGE_COMMENT_LENGTH } from '@/misc/hard-limits';
 
 export const meta = {
 	tags: ['drive'],
@@ -32,6 +33,11 @@ export const meta = {
 			default: null,
 		},
 
+		comment: {
+			validator: $.optional.nullable.str.max(DB_MAX_IMAGE_COMMENT_LENGTH),
+			default: null,
+		},
+
 		isSensitive: {
 			validator: $.optional.either($.bool, $.str),
 			default: false,
@@ -79,7 +85,7 @@ export default define(meta, async (ps, user, _, file, cleanup) => {
 
 	try {
 		// Create file
-		const driveFile = await addFile(user, file.path, name, null, ps.folderId, ps.force, false, null, null, ps.isSensitive);
+		const driveFile = await addFile({ user, path: file.path, name, comment: ps.comment, folderId: ps.folderId, force: ps.force, sensitive: ps.isSensitive });
 		return await DriveFiles.pack(driveFile, { self: true });
 	} catch (e) {
 		apiLogger.error(e);
diff --git a/packages/backend/src/server/api/endpoints/drive/files/upload-from-url.ts b/packages/backend/src/server/api/endpoints/drive/files/upload-from-url.ts
index 6ab1ca137d..40da1a4fb4 100644
--- a/packages/backend/src/server/api/endpoints/drive/files/upload-from-url.ts
+++ b/packages/backend/src/server/api/endpoints/drive/files/upload-from-url.ts
@@ -1,7 +1,7 @@
 import $ from 'cafy';
 import { ID } from '@/misc/cafy-id';
 import ms from 'ms';
-import uploadFromUrl from '@/services/drive/upload-from-url';
+import { uploadFromUrl } from '@/services/drive/upload-from-url';
 import define from '../../../define';
 import { DriveFiles } from '@/models/index';
 import { publishMainStream } from '@/services/stream';
@@ -54,7 +54,7 @@ export const meta = {
 
 // eslint-disable-next-line import/no-default-export
 export default define(meta, async (ps, user) => {
-	uploadFromUrl(ps.url, user, ps.folderId, null, ps.isSensitive, ps.force, false, ps.comment).then(file => {
+	uploadFromUrl({ url: ps.url, user, folderId: ps.folderId, sensitive: ps.isSensitive, force: ps.force, comment: ps.comment }).then(file => {
 		DriveFiles.pack(file, { self: true }).then(packedFile => {
 			publishMainStream(user.id, 'urlUploadFinished', {
 				marker: ps.marker,
diff --git a/packages/backend/src/services/drive/add-file.ts b/packages/backend/src/services/drive/add-file.ts
index 9de4465eb9..a89e068f45 100644
--- a/packages/backend/src/services/drive/add-file.ts
+++ b/packages/backend/src/services/drive/add-file.ts
@@ -297,33 +297,45 @@ async function deleteOldFile(user: IRemoteUser) {
 	}
 }
 
+type AddFileArgs = {
+	/** User who wish to add file */
+	user: { id: User['id']; host: User['host'] } | null;
+	/** File path */
+	path: string;
+	/** Name */
+	name?: string | null;
+	/** Comment */
+	comment?: string | null;
+	/** Folder ID */
+	folderId?: any;
+	/** If set to true, forcibly upload the file even if there is a file with the same hash. */
+	force?: boolean;
+	/** Do not save file to local */
+	isLink?: boolean;
+	/** URL of source (URLからアップロードされた場合(ローカル/リモート)の元URL) */
+	url?: string | null;
+	/** URL of source (リモートインスタンスのURLからアップロードされた場合の元URL) */
+	uri?: string | null;
+	/** Mark file as sensitive */
+	sensitive?: boolean | null;
+};
+
 /**
  * Add file to drive
  *
- * @param user User who wish to add file
- * @param path File path
- * @param name Name
- * @param comment Comment
- * @param folderId Folder ID
- * @param force If set to true, forcibly upload the file even if there is a file with the same hash.
- * @param isLink Do not save file to local
- * @param url URL of source (URLからアップロードされた場合(ローカル/リモート)の元URL)
- * @param uri URL of source (リモートインスタンスのURLからアップロードされた場合の元URL)
- * @param sensitive Mark file as sensitive
- * @return Created drive file
  */
-export async function addFile(
-	user: { id: User['id']; host: User['host'] } | null,
-	path: string,
-	name: string | null = null,
-	comment: string | null = null,
-	folderId: any = null,
-	force: boolean = false,
-	isLink: boolean = false,
-	url: string | null = null,
-	uri: string | null = null,
-	sensitive: boolean | null = null
-): Promise<DriveFile> {
+export async function addFile({
+	user,
+	path,
+	name = null,
+	comment = null,
+	folderId = null,
+	force = false,
+	isLink = false,
+	url = null,
+	uri = null,
+	sensitive = null
+}: AddFileArgs): Promise<DriveFile> {
 	const info = await getFileInfo(path);
 	logger.info(`${JSON.stringify(info)}`);
 
diff --git a/packages/backend/src/services/drive/upload-from-url.ts b/packages/backend/src/services/drive/upload-from-url.ts
index a723c3e9ac..7c5fa5ce3f 100644
--- a/packages/backend/src/services/drive/upload-from-url.ts
+++ b/packages/backend/src/services/drive/upload-from-url.ts
@@ -10,16 +10,27 @@ import { DriveFiles } from '@/models/index';
 
 const logger = driveLogger.createSubLogger('downloader');
 
-export default async (
-	url: string,
-	user: { id: User['id']; host: User['host'] } | null,
-	folderId: DriveFolder['id'] | null = null,
-	uri: string | null = null,
+type Args = {
+	url: string;
+	user: { id: User['id']; host: User['host'] } | null;
+	folderId?: DriveFolder['id'] | null;
+	uri?: string | null;
+	sensitive?: boolean;
+	force?: boolean;
+	isLink?: boolean;
+	comment?: string | null;
+};
+
+export async function uploadFromUrl({
+	url,
+	user,
+	folderId = null,
+	uri = null,
 	sensitive = false,
 	force = false,
-	link = false,
+	isLink = false,
 	comment = null
-): Promise<DriveFile> => {
+}: Args): Promise<DriveFile> {
 	let name = new URL(url).pathname.split('/').pop() || null;
 	if (name == null || !DriveFiles.validateFileName(name)) {
 		name = null;
@@ -41,7 +52,7 @@ export default async (
 	let error;
 
 	try {
-		driveFile = await addFile(user, path, name, comment, folderId, force, link, url, uri, sensitive);
+		driveFile = await addFile({ user, path, name, comment, folderId, force, isLink, url, uri, sensitive });
 		logger.succ(`Got: ${driveFile.id}`);
 	} catch (e) {
 		error = e;
@@ -59,4 +70,4 @@ export default async (
 	} else {
 		return driveFile!;
 	}
-};
+}

From fa554f168454b85777cb553c9fc051dcfe188fd8 Mon Sep 17 00:00:00 2001
From: MeiMei <30769358+mei23@users.noreply.github.com>
Date: Mon, 24 Jan 2022 02:18:27 +0900
Subject: [PATCH 05/46] =?UTF-8?q?fix:=20=E3=82=A2=E3=83=83=E3=83=97?=
 =?UTF-8?q?=E3=83=AD=E3=83=BC=E3=83=89=E3=82=A8=E3=83=A9=E3=83=BC=E6=99=82?=
 =?UTF-8?q?=E3=81=AE=E5=87=A6=E7=90=86=E3=82=92=E4=BF=AE=E6=AD=A3=20(#8182?=
 =?UTF-8?q?)?=
MIME-Version: 1.0
Content-Type: text/plain; charset=UTF-8
Content-Transfer-Encoding: 8bit

* アップロードのエラー応答で詰むのを修正

* CHANGELOG
---
 CHANGELOG.md              | 1 +
 packages/client/src/os.ts | 2 +-
 2 files changed, 2 insertions(+), 1 deletion(-)

diff --git a/CHANGELOG.md b/CHANGELOG.md
index d01358b278..d7cd03541d 100644
--- a/CHANGELOG.md
+++ b/CHANGELOG.md
@@ -25,6 +25,7 @@
 - Unifying Misskey-specific IRIs in JSON-LD `@context`
 
 ### Bugfixes
+- アップロードエラー時の処理を修正
 
 ## 12.101.1 (2021/12/29)
 
diff --git a/packages/client/src/os.ts b/packages/client/src/os.ts
index 378523e1bc..c16ea717ad 100644
--- a/packages/client/src/os.ts
+++ b/packages/client/src/os.ts
@@ -570,7 +570,7 @@ export function upload(file: File, folder?: any, name?: string): Promise<Misskey
 			const xhr = new XMLHttpRequest();
 			xhr.open('POST', apiUrl + '/drive/files/create', true);
 			xhr.onload = (ev) => {
-				if (ev.target == null || ev.target.response == null) {
+				if (xhr.status !== 200 || ev.target == null || ev.target.response == null) {
 					// TODO: 消すのではなくて再送できるようにしたい
 					uploads.value = uploads.value.filter(x => x.id != id);
 

From 562c4d99e6ec1812f9efab25121cfe233805f731 Mon Sep 17 00:00:00 2001
From: Hyunseung Jeon <dogdriip@gmail.com>
Date: Tue, 25 Jan 2022 18:01:29 +0900
Subject: [PATCH 06/46] fix: change keypress to keydown (#8192)

---
 packages/client/src/pages/messaging/messaging-room.form.vue | 4 ++--
 1 file changed, 2 insertions(+), 2 deletions(-)

diff --git a/packages/client/src/pages/messaging/messaging-room.form.vue b/packages/client/src/pages/messaging/messaging-room.form.vue
index f2a90fbfba..0fc7c8a5df 100644
--- a/packages/client/src/pages/messaging/messaging-room.form.vue
+++ b/packages/client/src/pages/messaging/messaging-room.form.vue
@@ -7,7 +7,7 @@
 		ref="text"
 		v-model="text"
 		:placeholder="$ts.inputMessageHere"
-		@keypress="onKeypress"
+		@keydown="onKeydown"
 		@compositionupdate="onCompositionUpdate"
 		@paste="onPaste"
 	></textarea>
@@ -141,7 +141,7 @@ export default defineComponent({
 			//#endregion
 		},
 
-		onKeypress(e) {
+		onKeydown(e) {
 			this.typing();
 			if ((e.which == 10 || e.which == 13) && (e.ctrlKey || e.metaKey) && this.canSend) {
 				this.send();

From 3f610edc2a8c79a959b148d293a42a5981450425 Mon Sep 17 00:00:00 2001
From: =?UTF-8?q?=E8=80=81=E5=85=84?= <lao__xong@outlook.com>
Date: Tue, 25 Jan 2022 22:16:09 +0800
Subject: [PATCH 07/46] Update docker-compose.yml (#8163)

Fix sometime es may cannot start
refer:https://m.html.cn/site/111215825993025.html
---
 docker-compose.yml | 1 +
 1 file changed, 1 insertion(+)

diff --git a/docker-compose.yml b/docker-compose.yml
index 717b756c7d..e1d51668a7 100644
--- a/docker-compose.yml
+++ b/docker-compose.yml
@@ -40,6 +40,7 @@ services:
 #    image: docker.elastic.co/elasticsearch/elasticsearch-oss:6.4.2
 #    environment:
 #      - "ES_JAVA_OPTS=-Xms512m -Xmx512m"
+#      - "TAKE_FILE_OWNERSHIP=111"
 #    networks:
 #      - internal_network
 #    volumes:

From e51f59e1b74ec5f7fa642b618f09c9bc4f6f1b9a Mon Sep 17 00:00:00 2001
From: Johann150 <johann.galle@protonmail.com>
Date: Tue, 25 Jan 2022 15:18:21 +0100
Subject: [PATCH 08/46] disable animations on more transitions (#8112)

---
 packages/client/src/components/form/suspense.vue       | 2 +-
 packages/client/src/components/notification-toast.vue  | 2 +-
 packages/client/src/components/toast.vue               | 2 +-
 packages/client/src/components/ui/container.vue        | 2 +-
 packages/client/src/components/ui/folder.vue           | 2 +-
 packages/client/src/components/ui/pagination.vue       | 2 +-
 packages/client/src/components/ui/tooltip.vue          | 2 +-
 packages/client/src/components/url-preview-popup.vue   | 2 +-
 packages/client/src/components/url-preview.vue         | 2 +-
 packages/client/src/components/user-preview.vue        | 2 +-
 packages/client/src/pages/gallery/post.vue             | 2 +-
 packages/client/src/pages/messaging/messaging-room.vue | 2 +-
 packages/client/src/pages/my-groups/group.vue          | 4 ++--
 packages/client/src/pages/my-lists/list.vue            | 4 ++--
 packages/client/src/pages/note.vue                     | 2 +-
 packages/client/src/pages/page.vue                     | 2 +-
 packages/client/src/pages/user/index.vue               | 2 +-
 packages/client/src/ui/classic.vue                     | 4 ++--
 packages/client/src/ui/deck.vue                        | 4 ++--
 packages/client/src/ui/universal.vue                   | 8 ++++----
 packages/client/src/ui/visitor/b.vue                   | 4 ++--
 packages/client/src/widgets/federation.vue             | 2 +-
 packages/client/src/widgets/trends.vue                 | 2 +-
 23 files changed, 31 insertions(+), 31 deletions(-)

diff --git a/packages/client/src/components/form/suspense.vue b/packages/client/src/components/form/suspense.vue
index 4d5debe604..2ad55dacae 100644
--- a/packages/client/src/components/form/suspense.vue
+++ b/packages/client/src/components/form/suspense.vue
@@ -1,5 +1,5 @@
 <template>
-<transition name="fade" mode="out-in">
+<transition :name="$store.state.animation ? 'fade' : ''" mode="out-in">
 	<div v-if="pending">
 		<MkLoading/>
 	</div>
diff --git a/packages/client/src/components/notification-toast.vue b/packages/client/src/components/notification-toast.vue
index fbd8467a6e..b2ab1029ad 100644
--- a/packages/client/src/components/notification-toast.vue
+++ b/packages/client/src/components/notification-toast.vue
@@ -1,6 +1,6 @@
 <template>
 <div class="mk-notification-toast" :style="{ zIndex }">
-	<transition name="notification-toast" appear @after-leave="$emit('closed')">
+	<transition :name="$store.state.animation ? 'notification-toast' : ''" appear @after-leave="$emit('closed')">
 		<XNotification v-if="showing" :notification="notification" class="notification _acrylic"/>
 	</transition>
 </div>
diff --git a/packages/client/src/components/toast.vue b/packages/client/src/components/toast.vue
index 031aa45633..c114379716 100644
--- a/packages/client/src/components/toast.vue
+++ b/packages/client/src/components/toast.vue
@@ -1,6 +1,6 @@
 <template>
 <div class="mk-toast">
-	<transition name="toast" appear @after-leave="emit('closed')">
+	<transition :name="$store.state.animation ? 'toast' : ''" appear @after-leave="emit('closed')">
 		<div v-if="showing" class="body _acrylic" :style="{ zIndex }">
 			<div class="message">
 				{{ message }}
diff --git a/packages/client/src/components/ui/container.vue b/packages/client/src/components/ui/container.vue
index fcd9f32290..7c595d8116 100644
--- a/packages/client/src/components/ui/container.vue
+++ b/packages/client/src/components/ui/container.vue
@@ -10,7 +10,7 @@
 			</button>
 		</div>
 	</header>
-	<transition name="container-toggle"
+	<transition :name="$store.state.animation ? 'container-toggle' : ''"
 		@enter="enter"
 		@after-enter="afterEnter"
 		@leave="leave"
diff --git a/packages/client/src/components/ui/folder.vue b/packages/client/src/components/ui/folder.vue
index 9795b1d81a..fe1602b2bb 100644
--- a/packages/client/src/components/ui/folder.vue
+++ b/packages/client/src/components/ui/folder.vue
@@ -8,7 +8,7 @@
 			<template v-else><i class="fas fa-angle-down"></i></template>
 		</button>
 	</header>
-	<transition name="folder-toggle"
+	<transition :name="$store.state.animation ? 'folder-toggle' : ''"
 		@enter="enter"
 		@after-enter="afterEnter"
 		@leave="leave"
diff --git a/packages/client/src/components/ui/pagination.vue b/packages/client/src/components/ui/pagination.vue
index 9c18fc5ce5..13f3215671 100644
--- a/packages/client/src/components/ui/pagination.vue
+++ b/packages/client/src/components/ui/pagination.vue
@@ -1,5 +1,5 @@
 <template>
-<transition name="fade" mode="out-in">
+<transition :name="$store.state.animation ? 'fade' : ''" mode="out-in">
 	<MkLoading v-if="fetching"/>
 
 	<MkError v-else-if="error" @retry="init()"/>
diff --git a/packages/client/src/components/ui/tooltip.vue b/packages/client/src/components/ui/tooltip.vue
index 2e48ab623e..394b068352 100644
--- a/packages/client/src/components/ui/tooltip.vue
+++ b/packages/client/src/components/ui/tooltip.vue
@@ -1,5 +1,5 @@
 <template>
-<transition name="tooltip" appear @after-leave="$emit('closed')">
+<transition :name="$store.state.animation ? 'tooltip' : ''" appear @after-leave="$emit('closed')">
 	<div v-show="showing" ref="el" class="buebdbiu _acrylic _shadow" :style="{ zIndex, maxWidth: maxWidth + 'px' }">
 		<slot>{{ text }}</slot>
 	</div>
diff --git a/packages/client/src/components/url-preview-popup.vue b/packages/client/src/components/url-preview-popup.vue
index c345bafcf9..5f3717ab91 100644
--- a/packages/client/src/components/url-preview-popup.vue
+++ b/packages/client/src/components/url-preview-popup.vue
@@ -1,6 +1,6 @@
 <template>
 <div class="fgmtyycl" :style="{ zIndex, top: top + 'px', left: left + 'px' }">
-	<transition name="zoom" @after-leave="$emit('closed')">
+	<transition :name="$store.state.animation ? 'zoom' : ''" @after-leave="$emit('closed')">
 		<MkUrlPreview v-if="showing" class="_popup _shadow" :url="url"/>
 	</transition>
 </div>
diff --git a/packages/client/src/components/url-preview.vue b/packages/client/src/components/url-preview.vue
index bf3b358797..6c57957617 100644
--- a/packages/client/src/components/url-preview.vue
+++ b/packages/client/src/components/url-preview.vue
@@ -7,7 +7,7 @@
 	<iframe ref="tweet" scrolling="no" frameborder="no" :style="{ position: 'relative', width: '100%', height: `${tweetHeight}px` }" :src="`https://platform.twitter.com/embed/index.html?embedId=${embedId}&amp;hideCard=false&amp;hideThread=false&amp;lang=en&amp;theme=${$store.state.darkMode ? 'dark' : 'light'}&amp;id=${tweetId}`"></iframe>
 </div>
 <div v-else v-size="{ max: [400, 350] }" class="mk-url-preview">
-	<transition name="zoom" mode="out-in">
+	<transition :name="$store.state.animation ? 'zoom' : ''" mode="out-in">
 		<component :is="self ? 'MkA' : 'a'" v-if="!fetching" :class="{ compact }" :[attr]="self ? url.substr(local.length) : url" rel="nofollow noopener" :target="target" :title="url">
 			<div v-if="thumbnail" class="thumbnail" :style="`background-image: url('${thumbnail}')`">
 				<button v-if="!playerEnabled && player.url" class="_button" :title="$ts.enablePlayer" @click.prevent="playerEnabled = true"><i class="fas fa-play-circle"></i></button>
diff --git a/packages/client/src/components/user-preview.vue b/packages/client/src/components/user-preview.vue
index f85a32fbe7..51c5330564 100644
--- a/packages/client/src/components/user-preview.vue
+++ b/packages/client/src/components/user-preview.vue
@@ -1,5 +1,5 @@
 <template>
-<transition name="popup" appear @after-leave="$emit('closed')">
+<transition :name="$store.state.animation ? 'popup' : ''" appear @after-leave="$emit('closed')">
 	<div v-if="showing" class="fxxzrfni _popup _shadow" :style="{ zIndex, top: top + 'px', left: left + 'px' }" @mouseover="() => { $emit('mouseover'); }" @mouseleave="() => { $emit('mouseleave'); }">
 		<div v-if="fetched" class="info">
 			<div class="banner" :style="user.bannerUrl ? `background-image: url(${user.bannerUrl})` : ''"></div>
diff --git a/packages/client/src/pages/gallery/post.vue b/packages/client/src/pages/gallery/post.vue
index fff2b6a74e..1755c23286 100644
--- a/packages/client/src/pages/gallery/post.vue
+++ b/packages/client/src/pages/gallery/post.vue
@@ -1,6 +1,6 @@
 <template>
 <div class="_root">
-	<transition name="fade" mode="out-in">
+	<transition :name="$store.state.animation ? 'fade' : ''" mode="out-in">
 		<div v-if="post" class="rkxwuolj">
 			<div class="files">
 				<div v-for="file in post.files" :key="file.id" class="file">
diff --git a/packages/client/src/pages/messaging/messaging-room.vue b/packages/client/src/pages/messaging/messaging-room.vue
index 9a34551ddd..a715dad6de 100644
--- a/packages/client/src/pages/messaging/messaging-room.vue
+++ b/packages/client/src/pages/messaging/messaging-room.vue
@@ -24,7 +24,7 @@
 				</I18n>
 				<MkEllipsis/>
 			</div>
-			<transition name="fade">
+			<transition :name="$store.state.animation ? 'fade' : ''">
 				<div v-show="showIndicator" class="new-message">
 					<button class="_buttonPrimary" @click="onIndicatorClick"><i class="fas fa-arrow-circle-down"></i>{{ $ts.newMessageExists }}</button>
 				</div>
diff --git a/packages/client/src/pages/my-groups/group.vue b/packages/client/src/pages/my-groups/group.vue
index c307f037a6..92c0483af9 100644
--- a/packages/client/src/pages/my-groups/group.vue
+++ b/packages/client/src/pages/my-groups/group.vue
@@ -1,6 +1,6 @@
 <template>
 <div class="mk-group-page">
-	<transition name="zoom" mode="out-in">
+	<transition :name="$store.state.animation ? 'zoom' : ''" mode="out-in">
 		<div v-if="group" class="_section">
 			<div class="_content" style="display: flex; gap: var(--margin); flex-wrap: wrap;">
 				<MkButton inline @click="invite()">{{ $ts.invite }}</MkButton>
@@ -11,7 +11,7 @@
 		</div>
 	</transition>
 
-	<transition name="zoom" mode="out-in">
+	<transition :name="$store.state.animation ? 'zoom' : ''" mode="out-in">
 		<div v-if="group" class="_section members _gap">
 			<div class="_title">{{ $ts.members }}</div>
 			<div class="_content">
diff --git a/packages/client/src/pages/my-lists/list.vue b/packages/client/src/pages/my-lists/list.vue
index a25522f933..bc24f58431 100644
--- a/packages/client/src/pages/my-lists/list.vue
+++ b/packages/client/src/pages/my-lists/list.vue
@@ -1,7 +1,7 @@
 <template>
 <MkSpacer :content-max="700">
 	<div class="mk-list-page">
-		<transition name="zoom" mode="out-in">
+		<transition :name="$store.state.animation ? 'zoom' : ''" mode="out-in">
 			<div v-if="list" class="_section">
 				<div class="_content">
 					<MkButton inline @click="addUser()">{{ $ts.addUser }}</MkButton>
@@ -11,7 +11,7 @@
 			</div>
 		</transition>
 
-		<transition name="zoom" mode="out-in">
+		<transition :name="$store.state.animation ? 'zoom' : ''" mode="out-in">
 			<div v-if="list" class="_section members _gap">
 				<div class="_title">{{ $ts.members }}</div>
 				<div class="_content">
diff --git a/packages/client/src/pages/note.vue b/packages/client/src/pages/note.vue
index 72ac85ee90..efeea345dc 100644
--- a/packages/client/src/pages/note.vue
+++ b/packages/client/src/pages/note.vue
@@ -1,7 +1,7 @@
 <template>
 <MkSpacer :content-max="800">
 	<div class="fcuexfpr">
-		<transition name="fade" mode="out-in">
+		<transition :name="$store.state.animation ? 'fade' : ''" mode="out-in">
 			<div v-if="note" class="note">
 				<div v-if="showNext" class="_gap">
 					<XNotes class="_content" :pagination="next" :no-gap="true"/>
diff --git a/packages/client/src/pages/page.vue b/packages/client/src/pages/page.vue
index 429d1ddea2..b2c039a269 100644
--- a/packages/client/src/pages/page.vue
+++ b/packages/client/src/pages/page.vue
@@ -1,6 +1,6 @@
 <template>
 <MkSpacer :content-max="700">
-	<transition name="fade" mode="out-in">
+	<transition :name="$store.state.animation ? 'fade' : ''" mode="out-in">
 		<div v-if="page" :key="page.id" v-size="{ max: [450] }" class="xcukqgmh">
 			<div class="_block main">
 				<!--
diff --git a/packages/client/src/pages/user/index.vue b/packages/client/src/pages/user/index.vue
index 0b96368587..e767abf1ac 100644
--- a/packages/client/src/pages/user/index.vue
+++ b/packages/client/src/pages/user/index.vue
@@ -1,6 +1,6 @@
 <template>
 <div>
-<transition name="fade" mode="out-in">
+<transition :name="$store.state.animation ? 'fade' : ''" mode="out-in">
 	<div v-if="user && narrow === false" class="ftskorzw wide">
 		<MkRemoteCaution v-if="user.host != null" :href="user.url"/>
 
diff --git a/packages/client/src/ui/classic.vue b/packages/client/src/ui/classic.vue
index cf2ad9cbf3..c61cbc433e 100644
--- a/packages/client/src/ui/classic.vue
+++ b/packages/client/src/ui/classic.vue
@@ -30,7 +30,7 @@
 		</div>
 	</div>
 
-	<transition name="tray-back">
+	<transition :name="$store.state.animation ? 'tray-back' : ''">
 		<div v-if="widgetsShowing"
 			class="tray-back _modalBg"
 			@click="widgetsShowing = false"
@@ -38,7 +38,7 @@
 		></div>
 	</transition>
 
-	<transition name="tray">
+	<transition :name="$store.state.animation ? 'tray' : ''">
 		<XWidgets v-if="widgetsShowing" class="tray"/>
 	</transition>
 
diff --git a/packages/client/src/ui/deck.vue b/packages/client/src/ui/deck.vue
index 73dc83180f..51a4853e9d 100644
--- a/packages/client/src/ui/deck.vue
+++ b/packages/client/src/ui/deck.vue
@@ -29,7 +29,7 @@
 		<button class="button post _button" @click="post()"><i class="fas fa-pencil-alt"></i></button>
 	</div>
 
-	<transition name="menu-back">
+	<transition :name="$store.state.animation ? 'menu-back' : ''">
 		<div v-if="drawerMenuShowing"
 			class="menu-back _modalBg"
 			@click="drawerMenuShowing = false"
@@ -37,7 +37,7 @@
 		></div>
 	</transition>
 
-	<transition name="menu">
+	<transition :name="$store.state.animation ? 'menu' : ''">
 		<XDrawerMenu v-if="drawerMenuShowing" class="menu"/>
 	</transition>
 
diff --git a/packages/client/src/ui/universal.vue b/packages/client/src/ui/universal.vue
index 4b356f1511..16cc9a4f06 100644
--- a/packages/client/src/ui/universal.vue
+++ b/packages/client/src/ui/universal.vue
@@ -36,7 +36,7 @@
 		<button class="button post _button" @click="post()"><i class="fas fa-pencil-alt"></i></button>
 	</div>
 
-	<transition name="menuDrawer-back">
+	<transition :name="$store.state.animation ? 'menuDrawer-back' : ''">
 		<div v-if="drawerMenuShowing"
 			class="menuDrawer-back _modalBg"
 			@click="drawerMenuShowing = false"
@@ -44,11 +44,11 @@
 		></div>
 	</transition>
 
-	<transition name="menuDrawer">
+	<transition :name="$store.state.animation ? 'menuDrawer' : ''">
 		<XDrawerMenu v-if="drawerMenuShowing" class="menuDrawer"/>
 	</transition>
 
-	<transition name="widgetsDrawer-back">
+	<transition :name="$store.state.animation ? 'widgetsDrawer-back' : ''">
 		<div v-if="widgetsShowing"
 			class="widgetsDrawer-back _modalBg"
 			@click="widgetsShowing = false"
@@ -56,7 +56,7 @@
 		></div>
 	</transition>
 
-	<transition name="widgetsDrawer">
+	<transition :name="$store.state.animation ? 'widgetsDrawer' : ''">
 		<XWidgets v-if="widgetsShowing" class="widgetsDrawer"/>
 	</transition>
 
diff --git a/packages/client/src/ui/visitor/b.vue b/packages/client/src/ui/visitor/b.vue
index f6ad13d9a0..c9c0a1f72e 100644
--- a/packages/client/src/ui/visitor/b.vue
+++ b/packages/client/src/ui/visitor/b.vue
@@ -25,7 +25,7 @@
 		</div>
 	</div>
 
-	<transition name="tray-back">
+	<transition :name="$store.state.animation ? 'tray-back' : ''">
 		<div v-if="showMenu"
 			class="menu-back _modalBg"
 			@click="showMenu = false"
@@ -33,7 +33,7 @@
 		></div>
 	</transition>
 
-	<transition name="tray">
+	<transition :name="$store.state.animation ? 'tray' : ''">
 		<div v-if="showMenu" class="menu">
 			<MkA to="/" class="link" active-class="active"><i class="fas fa-home icon"></i>{{ $ts.home }}</MkA>
 			<MkA to="/explore" class="link" active-class="active"><i class="fas fa-hashtag icon"></i>{{ $ts.explore }}</MkA>
diff --git a/packages/client/src/widgets/federation.vue b/packages/client/src/widgets/federation.vue
index ed7350188e..4c43117e48 100644
--- a/packages/client/src/widgets/federation.vue
+++ b/packages/client/src/widgets/federation.vue
@@ -4,7 +4,7 @@
 
 	<div class="wbrkwalb">
 		<MkLoading v-if="fetching"/>
-		<transition-group v-else tag="div" name="chart" class="instances">
+		<transition-group v-else tag="div" :name="$store.state.animation ? 'chart' : ''" class="instances">
 			<div v-for="(instance, i) in instances" :key="instance.id" class="instance">
 				<img v-if="instance.iconUrl" :src="instance.iconUrl" alt=""/>
 				<div class="body">
diff --git a/packages/client/src/widgets/trends.vue b/packages/client/src/widgets/trends.vue
index 5768a8d5d1..eb5eb4049f 100644
--- a/packages/client/src/widgets/trends.vue
+++ b/packages/client/src/widgets/trends.vue
@@ -4,7 +4,7 @@
 
 	<div class="wbrkwala">
 		<MkLoading v-if="fetching"/>
-		<transition-group v-else tag="div" name="chart" class="tags">
+		<transition-group v-else tag="div" :name="$store.state.animation ? 'chart' : ''" class="tags">
 			<div v-for="stat in stats" :key="stat.tag">
 				<div class="tag">
 					<MkA class="a" :to="`/tags/${ encodeURIComponent(stat.tag) }`" :title="stat.tag">#{{ stat.tag }}</MkA>

From c3f3f118c3a34fe77af318e7781c6bdc53d2f06a Mon Sep 17 00:00:00 2001
From: syuilo <Syuilotan@yahoo.co.jp>
Date: Wed, 26 Jan 2022 00:51:01 +0900
Subject: [PATCH 09/46] :art:

---
 packages/client/src/pages/user/index.vue | 948 +++++++++--------------
 1 file changed, 357 insertions(+), 591 deletions(-)

diff --git a/packages/client/src/pages/user/index.vue b/packages/client/src/pages/user/index.vue
index e767abf1ac..599e24d81c 100644
--- a/packages/client/src/pages/user/index.vue
+++ b/packages/client/src/pages/user/index.vue
@@ -1,196 +1,125 @@
 <template>
 <div>
-<transition :name="$store.state.animation ? 'fade' : ''" mode="out-in">
-	<div v-if="user && narrow === false" class="ftskorzw wide">
-		<MkRemoteCaution v-if="user.host != null" :href="user.url"/>
+	<transition name="fade" mode="out-in">
+		<MkSpacer v-if="user" :content-max="narrow ? 800 : 1100">
+			<div v-size="{ max: [500] }" class="ftskorzw" :class="{ wide: !narrow }">
+				<div class="main">
+					<!-- TODO -->
+					<!-- <div class="punished" v-if="user.isSuspended"><i class="fas fa-exclamation-triangle" style="margin-right: 8px;"></i> {{ $ts.userSuspended }}</div> -->
+					<!-- <div class="punished" v-if="user.isSilenced"><i class="fas fa-exclamation-triangle" style="margin-right: 8px;"></i> {{ $ts.userSilenced }}</div> -->
 
-		<div class="banner-container" :style="style">
-			<div ref="banner" class="banner" :style="style"></div>
-		</div>
-		<div class="contents">
-			<div class="side _forceContainerFull_">
-				<MkAvatar class="avatar" :user="user" :disable-preview="true" :show-indicator="true"/>
-				<div class="name">
-					<MkUserName :user="user" :nowrap="false" class="name"/>
-					<MkAcct :user="user" :detail="true" class="acct"/>
-				</div>
-				<div v-if="$i && $i.id != user.id && user.isFollowed" class="followed"><span>{{ $ts.followsYou }}</span></div>
-				<div class="status">
-					<MkA :to="userPage(user)" :class="{ active: page === 'index' }">
-						<b>{{ number(user.notesCount) }}</b>
-						<span>{{ $ts.notes }}</span>
-					</MkA>
-					<MkA :to="userPage(user, 'following')" :class="{ active: page === 'following' }">
-						<b>{{ number(user.followingCount) }}</b>
-						<span>{{ $ts.following }}</span>
-					</MkA>
-					<MkA :to="userPage(user, 'followers')" :class="{ active: page === 'followers' }">
-						<b>{{ number(user.followersCount) }}</b>
-						<span>{{ $ts.followers }}</span>
-					</MkA>
-				</div>
-				<div class="description">
-					<Mfm v-if="user.description" :text="user.description" :is-note="false" :author="user" :i="$i" :custom-emojis="user.emojis"/>
-					<p v-else class="empty">{{ $ts.noAccountDescription }}</p>
-				</div>
-				<div class="fields system">
-					<dl v-if="user.location" class="field">
-						<dt class="name"><i class="fas fa-map-marker fa-fw"></i> {{ $ts.location }}</dt>
-						<dd class="value">{{ user.location }}</dd>
-					</dl>
-					<dl v-if="user.birthday" class="field">
-						<dt class="name"><i class="fas fa-birthday-cake fa-fw"></i> {{ $ts.birthday }}</dt>
-						<dd class="value">{{ user.birthday.replace('-', '/').replace('-', '/') }} ({{ $t('yearsOld', { age }) }})</dd>
-					</dl>
-					<dl class="field">
-						<dt class="name"><i class="fas fa-calendar-alt fa-fw"></i> {{ $ts.registeredDate }}</dt>
-						<dd class="value">{{ new Date(user.createdAt).toLocaleString() }} (<MkTime :time="user.createdAt"/>)</dd>
-					</dl>
-				</div>
-				<div v-if="user.fields.length > 0" class="fields">
-					<dl v-for="(field, i) in user.fields" :key="i" class="field">
-						<dt class="name">
-							<Mfm :text="field.name" :plain="true" :custom-emojis="user.emojis" :colored="false"/>
-						</dt>
-						<dd class="value">
-							<Mfm :text="field.value" :author="user" :i="$i" :custom-emojis="user.emojis" :colored="false"/>
-						</dd>
-					</dl>
-				</div>
-				<XActivity :key="user.id" :user="user" class="_gap"/>
-				<XPhotos :key="user.id" :user="user" class="_gap"/>
-			</div>
-			<div class="main">
-				<div class="actions">
-					<button class="menu _button" @click="menu"><i class="fas fa-ellipsis-h"></i></button>
-					<MkFollowButton v-if="!$i || $i.id != user.id" :user="user" :inline="true" :transparent="false" :full="true" large class="koudoku"/>
-				</div>
-				<template v-if="page === 'index'">
-					<div v-if="user.pinnedNotes.length > 0" class="_gap">
-						<XNote v-for="note in user.pinnedNotes" :key="note.id" class="note _gap" :note="note" :pinned="true" @update:note="pinnedNoteUpdated(note, $event)"/>
-					</div>
-					<div class="_gap">
-						<XUserTimeline :user="user"/>
-					</div>
-				</template>
-				<XFollowList v-else-if="page === 'following'" type="following" :user="user" class="_gap"/>
-				<XFollowList v-else-if="page === 'followers'" type="followers" :user="user" class="_gap"/>
-				<XClips v-else-if="page === 'clips'" :user="user" class="_gap"/>
-				<XPages v-else-if="page === 'pages'" :user="user" class="_gap"/>
-			</div>
-		</div>
-	</div>
-	<MkSpacer v-else-if="user && narrow === true" :content-max="800">
-		<div v-size="{ max: [500] }" class="ftskorzw narrow">
-			<!-- TODO -->
-			<!-- <div class="punished" v-if="user.isSuspended"><i class="fas fa-exclamation-triangle" style="margin-right: 8px;"></i> {{ $ts.userSuspended }}</div> -->
-			<!-- <div class="punished" v-if="user.isSilenced"><i class="fas fa-exclamation-triangle" style="margin-right: 8px;"></i> {{ $ts.userSilenced }}</div> -->
+					<div class="profile">
+						<MkRemoteCaution v-if="user.host != null" :href="user.url" class="warn"/>
 
-			<div class="profile">
-				<MkRemoteCaution v-if="user.host != null" :href="user.url" class="warn"/>
-
-				<div :key="user.id" class="_block main">
-					<div class="banner-container" :style="style">
-						<div ref="banner" class="banner" :style="style"></div>
-						<div class="fade"></div>
-						<div class="title">
-							<MkUserName class="name" :user="user" :nowrap="true"/>
-							<div class="bottom">
-								<span class="username"><MkAcct :user="user" :detail="true" /></span>
-								<span v-if="user.isAdmin" :title="$ts.isAdmin" style="color: var(--badge);"><i class="fas fa-bookmark"></i></span>
-								<span v-if="!user.isAdmin && user.isModerator" :title="$ts.isModerator" style="color: var(--badge);"><i class="far fa-bookmark"></i></span>
-								<span v-if="user.isLocked" :title="$ts.isLocked"><i class="fas fa-lock"></i></span>
-								<span v-if="user.isBot" :title="$ts.isBot"><i class="fas fa-robot"></i></span>
+						<div :key="user.id" class="_block main">
+							<div class="banner-container" :style="style">
+								<div ref="banner" class="banner" :style="style"></div>
+								<div class="fade"></div>
+								<div class="title">
+									<MkUserName class="name" :user="user" :nowrap="true"/>
+									<div class="bottom">
+										<span class="username"><MkAcct :user="user" :detail="true" /></span>
+										<span v-if="user.isAdmin" :title="$ts.isAdmin" style="color: var(--badge);"><i class="fas fa-bookmark"></i></span>
+										<span v-if="!user.isAdmin && user.isModerator" :title="$ts.isModerator" style="color: var(--badge);"><i class="far fa-bookmark"></i></span>
+										<span v-if="user.isLocked" :title="$ts.isLocked"><i class="fas fa-lock"></i></span>
+										<span v-if="user.isBot" :title="$ts.isBot"><i class="fas fa-robot"></i></span>
+									</div>
+								</div>
+								<span v-if="$i && $i.id != user.id && user.isFollowed" class="followed">{{ $ts.followsYou }}</span>
+								<div v-if="$i" class="actions">
+									<button class="menu _button" @click="menu"><i class="fas fa-ellipsis-h"></i></button>
+									<MkFollowButton v-if="$i.id != user.id" :user="user" :inline="true" :transparent="false" :full="true" class="koudoku"/>
+								</div>
+							</div>
+							<MkAvatar class="avatar" :user="user" :disable-preview="true" :show-indicator="true"/>
+							<div class="title">
+								<MkUserName :user="user" :nowrap="false" class="name"/>
+								<div class="bottom">
+									<span class="username"><MkAcct :user="user" :detail="true" /></span>
+									<span v-if="user.isAdmin" :title="$ts.isAdmin" style="color: var(--badge);"><i class="fas fa-bookmark"></i></span>
+									<span v-if="!user.isAdmin && user.isModerator" :title="$ts.isModerator" style="color: var(--badge);"><i class="far fa-bookmark"></i></span>
+									<span v-if="user.isLocked" :title="$ts.isLocked"><i class="fas fa-lock"></i></span>
+									<span v-if="user.isBot" :title="$ts.isBot"><i class="fas fa-robot"></i></span>
+								</div>
+							</div>
+							<div class="description">
+								<Mfm v-if="user.description" :text="user.description" :is-note="false" :author="user" :i="$i" :custom-emojis="user.emojis"/>
+								<p v-else class="empty">{{ $ts.noAccountDescription }}</p>
+							</div>
+							<div class="fields system">
+								<dl v-if="user.location" class="field">
+									<dt class="name"><i class="fas fa-map-marker fa-fw"></i> {{ $ts.location }}</dt>
+									<dd class="value">{{ user.location }}</dd>
+								</dl>
+								<dl v-if="user.birthday" class="field">
+									<dt class="name"><i class="fas fa-birthday-cake fa-fw"></i> {{ $ts.birthday }}</dt>
+									<dd class="value">{{ user.birthday.replace('-', '/').replace('-', '/') }} ({{ $t('yearsOld', { age }) }})</dd>
+								</dl>
+								<dl class="field">
+									<dt class="name"><i class="fas fa-calendar-alt fa-fw"></i> {{ $ts.registeredDate }}</dt>
+									<dd class="value">{{ new Date(user.createdAt).toLocaleString() }} (<MkTime :time="user.createdAt"/>)</dd>
+								</dl>
+							</div>
+							<div v-if="user.fields.length > 0" class="fields">
+								<dl v-for="(field, i) in user.fields" :key="i" class="field">
+									<dt class="name">
+										<Mfm :text="field.name" :plain="true" :custom-emojis="user.emojis" :colored="false"/>
+									</dt>
+									<dd class="value">
+										<Mfm :text="field.value" :author="user" :i="$i" :custom-emojis="user.emojis" :colored="false"/>
+									</dd>
+								</dl>
+							</div>
+							<div class="status">
+								<MkA v-click-anime :to="userPage(user)" :class="{ active: page === 'index' }">
+									<b>{{ number(user.notesCount) }}</b>
+									<span>{{ $ts.notes }}</span>
+								</MkA>
+								<MkA v-click-anime :to="userPage(user, 'following')" :class="{ active: page === 'following' }">
+									<b>{{ number(user.followingCount) }}</b>
+									<span>{{ $ts.following }}</span>
+								</MkA>
+								<MkA v-click-anime :to="userPage(user, 'followers')" :class="{ active: page === 'followers' }">
+									<b>{{ number(user.followersCount) }}</b>
+									<span>{{ $ts.followers }}</span>
+								</MkA>
 							</div>
 						</div>
-						<span v-if="$i && $i.id != user.id && user.isFollowed" class="followed">{{ $ts.followsYou }}</span>
-						<div v-if="$i" class="actions">
-							<button class="menu _button" @click="menu"><i class="fas fa-ellipsis-h"></i></button>
-							<MkFollowButton v-if="$i.id != user.id" :user="user" :inline="true" :transparent="false" :full="true" class="koudoku"/>
-						</div>
 					</div>
-					<MkAvatar class="avatar" :user="user" :disable-preview="true" :show-indicator="true"/>
-					<div class="title">
-						<MkUserName :user="user" :nowrap="false" class="name"/>
-						<div class="bottom">
-							<span class="username"><MkAcct :user="user" :detail="true" /></span>
-							<span v-if="user.isAdmin" :title="$ts.isAdmin" style="color: var(--badge);"><i class="fas fa-bookmark"></i></span>
-							<span v-if="!user.isAdmin && user.isModerator" :title="$ts.isModerator" style="color: var(--badge);"><i class="far fa-bookmark"></i></span>
-							<span v-if="user.isLocked" :title="$ts.isLocked"><i class="fas fa-lock"></i></span>
-							<span v-if="user.isBot" :title="$ts.isBot"><i class="fas fa-robot"></i></span>
-						</div>
-					</div>
-					<div class="description">
-						<Mfm v-if="user.description" :text="user.description" :is-note="false" :author="user" :i="$i" :custom-emojis="user.emojis"/>
-						<p v-else class="empty">{{ $ts.noAccountDescription }}</p>
-					</div>
-					<div class="fields system">
-						<dl v-if="user.location" class="field">
-							<dt class="name"><i class="fas fa-map-marker fa-fw"></i> {{ $ts.location }}</dt>
-							<dd class="value">{{ user.location }}</dd>
-						</dl>
-						<dl v-if="user.birthday" class="field">
-							<dt class="name"><i class="fas fa-birthday-cake fa-fw"></i> {{ $ts.birthday }}</dt>
-							<dd class="value">{{ user.birthday.replace('-', '/').replace('-', '/') }} ({{ $t('yearsOld', { age }) }})</dd>
-						</dl>
-						<dl class="field">
-							<dt class="name"><i class="fas fa-calendar-alt fa-fw"></i> {{ $ts.registeredDate }}</dt>
-							<dd class="value">{{ new Date(user.createdAt).toLocaleString() }} (<MkTime :time="user.createdAt"/>)</dd>
-						</dl>
-					</div>
-					<div v-if="user.fields.length > 0" class="fields">
-						<dl v-for="(field, i) in user.fields" :key="i" class="field">
-							<dt class="name">
-								<Mfm :text="field.name" :plain="true" :custom-emojis="user.emojis" :colored="false"/>
-							</dt>
-							<dd class="value">
-								<Mfm :text="field.value" :author="user" :i="$i" :custom-emojis="user.emojis" :colored="false"/>
-							</dd>
-						</dl>
-					</div>
-					<div class="status">
-						<MkA v-click-anime :to="userPage(user)" :class="{ active: page === 'index' }">
-							<b>{{ number(user.notesCount) }}</b>
-							<span>{{ $ts.notes }}</span>
-						</MkA>
-						<MkA v-click-anime :to="userPage(user, 'following')" :class="{ active: page === 'following' }">
-							<b>{{ number(user.followingCount) }}</b>
-							<span>{{ $ts.following }}</span>
-						</MkA>
-						<MkA v-click-anime :to="userPage(user, 'followers')" :class="{ active: page === 'followers' }">
-							<b>{{ number(user.followersCount) }}</b>
-							<span>{{ $ts.followers }}</span>
-						</MkA>
+
+					<div class="contents">
+						<template v-if="page === 'index'">
+							<div>
+								<div v-if="user.pinnedNotes.length > 0" class="_gap">
+									<XNote v-for="note in user.pinnedNotes" :key="note.id" class="note _block" :note="note" :pinned="true"/>
+								</div>
+								<MkInfo v-else-if="$i && $i.id === user.id">{{ $ts.userPagePinTip }}</MkInfo>
+								<template v-if="narrow">
+									<XPhotos :key="user.id" :user="user"/>
+									<XActivity :key="user.id" :user="user" style="margin-top: var(--margin);"/>
+								</template>
+							</div>
+							<div>
+								<XUserTimeline :user="user"/>
+							</div>
+						</template>
+						<XFollowList v-else-if="page === 'following'" type="following" :user="user" class="_content _gap"/>
+						<XFollowList v-else-if="page === 'followers'" type="followers" :user="user" class="_content _gap"/>
+						<XReactions v-else-if="page === 'reactions'" :user="user" class="_gap"/>
+						<XClips v-else-if="page === 'clips'" :user="user" class="_gap"/>
+						<XPages v-else-if="page === 'pages'" :user="user" class="_gap"/>
+						<XGallery v-else-if="page === 'gallery'" :user="user" class="_gap"/>
 					</div>
 				</div>
+				<div v-if="!narrow" class="sub">
+					<XPhotos :key="user.id" :user="user"/>
+					<XActivity :key="user.id" :user="user" style="margin-top: var(--margin);"/>
+				</div>
 			</div>
-
-			<div class="contents">
-				<template v-if="page === 'index'">
-					<div>
-						<div v-if="user.pinnedNotes.length > 0" class="_gap">
-							<XNote v-for="note in user.pinnedNotes" :key="note.id" class="note _block" :note="note" :pinned="true" @update:note="pinnedNoteUpdated(note, $event)"/>
-						</div>
-						<MkInfo v-else-if="$i && $i.id === user.id">{{ $ts.userPagePinTip }}</MkInfo>
-						<XPhotos :key="user.id" :user="user"/>
-						<XActivity :key="user.id" :user="user" style="margin-top: var(--margin);"/>
-					</div>
-					<div>
-						<XUserTimeline :user="user"/>
-					</div>
-				</template>
-				<XFollowList v-else-if="page === 'following'" type="following" :user="user" class="_content _gap"/>
-				<XFollowList v-else-if="page === 'followers'" type="followers" :user="user" class="_content _gap"/>
-				<XReactions v-else-if="page === 'reactions'" :user="user" class="_gap"/>
-				<XClips v-else-if="page === 'clips'" :user="user" class="_gap"/>
-				<XPages v-else-if="page === 'pages'" :user="user" class="_gap"/>
-				<XGallery v-else-if="page === 'gallery'" :user="user" class="_gap"/>
-			</div>
-		</div>
-	</MkSpacer>
-	<MkError v-else-if="error" @retry="fetch()"/>
-	<MkLoading v-else/>
-</transition>
+		</MkSpacer>
+		<MkError v-else-if="error" @retry="fetch()"/>
+		<MkLoading v-else/>
+	</transition>
 </div>
 </template>
 
@@ -314,7 +243,7 @@ export default defineComponent({
 
 	mounted() {
 		window.requestAnimationFrame(this.parallaxLoop);
-		this.narrow = true//this.$el.clientWidth < 1000;
+		this.narrow = this.$el.clientWidth < 1000;
 	},
 
 	beforeUnmount() {
@@ -356,11 +285,6 @@ export default defineComponent({
 			banner.style.backgroundPosition = `center calc(50% - ${pos}px)`;
 		},
 
-		pinnedNoteUpdated(oldValue, newValue) {
-			const i = this.user.pinnedNotes.findIndex(n => n === oldValue);
-			this.user.pinnedNotes[i] = newValue;
-		},
-
 		number,
 
 		userPage
@@ -378,448 +302,290 @@ export default defineComponent({
 	opacity: 0;
 }
 
-.ftskorzw.wide {
+.ftskorzw {
 
-	> .banner-container {
-		position: relative;
-		height: 300px;
-		overflow: hidden;
-		background-size: cover;
-		background-position: center;
+	> .main {
 
-		> .banner {
-			height: 100%;
-			background-color: #4c5e6d;
-			background-size: cover;
-			background-position: center;
-			box-shadow: 0 0 128px rgba(0, 0, 0, 0.5) inset;
-			will-change: background-position;
-		}
-	}
-
-	> .contents {
-		display: flex;
-		padding: 16px;
-
-		> .side {
-			width: 360px;
-
-			> .avatar {
-				display: block;
-				width: 180px;
-				height: 180px;
-				margin: -130px auto 0 auto;
-			}
-
-			> .name {
-				padding: 16px 0px 20px 0;
-				text-align: center;
-
-				> .name {
-					display: block;
-					font-size: 1.75em;
-					font-weight: bold;
-				}
-			}
-
-			> .followed {
-				text-align: center;
-
-				> span {
-					display: inline-block;
-					font-size: 80%;
-					padding: 8px 12px;
-					margin-bottom: 20px;
-					border: solid 0.5px var(--divider);
-					border-radius: 999px;
-				}
-			}
-
-			> .status {
-				display: flex;
-				padding: 20px 16px;
-				border-top: solid 0.5px var(--divider);
-				font-size: 90%;
-
-				> a {
-					flex: 1;
-					text-align: center;
-
-					&.active {
-						color: var(--accent);
-					}
-
-					&:hover {
-						text-decoration: none;
-					}
-
-					> b {
-						display: block;
-						line-height: 16px;
-					}
-
-					> span {
-						font-size: 75%;
-					}
-				}
-			}
-
-			> .description {
-				padding: 20px 16px;
-				border-top: solid 0.5px var(--divider);
-				font-size: 90%;
-			}
-
-			> .fields {
-				padding: 20px 16px;
-				border-top: solid 0.5px var(--divider);
-				font-size: 90%;
-
-				> .field {
-					display: flex;
-					padding: 0;
-					margin: 0;
-					align-items: center;
-
-					&:not(:last-child) {
-						margin-bottom: 8px;
-					}
-
-					> .name {
-						width: 30%;
-						overflow: hidden;
-						white-space: nowrap;
-						text-overflow: ellipsis;
-						font-weight: bold;
-					}
-
-					> .value {
-						width: 70%;
-						overflow: hidden;
-						white-space: nowrap;
-						text-overflow: ellipsis;
-						margin: 0;
-					}
-				}
-			}
+		> .punished {
+			font-size: 0.8em;
+			padding: 16px;
 		}
 
-		> .main {
-			flex: 1;
-			margin-left: var(--margin);
-			min-width: 0;
+		> .profile {
 
-			> .nav {
-				display: flex;
-				align-items: center;
-				margin-top: var(--margin);
-				//font-size: 120%;
-				font-weight: bold;
-
-				> .link {
-					display: inline-block;
-					padding: 15px 24px 12px 24px;
-					text-align: center;
-					border-bottom: solid 3px transparent;
-
-					&:hover {
-						text-decoration: none;
-					}
-
-					&.active {
-						color: var(--accent);
-						border-bottom-color: var(--accent);
-					}
-
-					&:not(.active):hover {
-						color: var(--fgHighlighted);
-					}
-
-					> .icon {
-						margin-right: 6px;
-					}
-				}
-
-				> .actions {
-					display: flex;
-					align-items: center;
-					margin-left: auto;
-
-					> .menu {
-						padding: 12px 16px;
-					}
-				}
-			}
-		}
-	}
-}
-
-.ftskorzw.narrow {
-	box-sizing: border-box;
-	overflow: clip;
-	background: var(--bg);
-
-	> .punished {
-		font-size: 0.8em;
-		padding: 16px;
-	}
-
-	> .profile {
-
-		> .main {
-			position: relative;
-			overflow: hidden;
-
-			> .banner-container {
+			> .main {
 				position: relative;
-				height: 250px;
 				overflow: hidden;
-				background-size: cover;
-				background-position: center;
 
-				> .banner {
-					height: 100%;
-					background-color: #4c5e6d;
+				> .banner-container {
+					position: relative;
+					height: 250px;
+					overflow: hidden;
 					background-size: cover;
 					background-position: center;
-					box-shadow: 0 0 128px rgba(0, 0, 0, 0.5) inset;
-					will-change: background-position;
-				}
 
-				> .fade {
-					position: absolute;
-					bottom: 0;
-					left: 0;
-					width: 100%;
-					height: 78px;
-					background: linear-gradient(transparent, rgba(#000, 0.7));
-				}
+					> .banner {
+						height: 100%;
+						background-color: #4c5e6d;
+						background-size: cover;
+						background-position: center;
+						box-shadow: 0 0 128px rgba(0, 0, 0, 0.5) inset;
+						will-change: background-position;
+					}
 
-				> .followed {
-					position: absolute;
-					top: 12px;
-					left: 12px;
-					padding: 4px 8px;
-					color: #fff;
-					background: rgba(0, 0, 0, 0.7);
-					font-size: 0.7em;
-					border-radius: 6px;
-				}
+					> .fade {
+						position: absolute;
+						bottom: 0;
+						left: 0;
+						width: 100%;
+						height: 78px;
+						background: linear-gradient(transparent, rgba(#000, 0.7));
+					}
 
-				> .actions {
-					position: absolute;
-					top: 12px;
-					right: 12px;
-					-webkit-backdrop-filter: var(--blur, blur(8px));
-					backdrop-filter: var(--blur, blur(8px));
-					background: rgba(0, 0, 0, 0.2);
-					padding: 8px;
-					border-radius: 24px;
-
-					> .menu {
-						vertical-align: bottom;
-						height: 31px;
-						width: 31px;
+					> .followed {
+						position: absolute;
+						top: 12px;
+						left: 12px;
+						padding: 4px 8px;
 						color: #fff;
-						text-shadow: 0 0 8px #000;
-						font-size: 16px;
+						background: rgba(0, 0, 0, 0.7);
+						font-size: 0.7em;
+						border-radius: 6px;
 					}
 
-					> .koudoku {
-						margin-left: 4px;
-						vertical-align: bottom;
-					}
-				}
+					> .actions {
+						position: absolute;
+						top: 12px;
+						right: 12px;
+						-webkit-backdrop-filter: var(--blur, blur(8px));
+						backdrop-filter: var(--blur, blur(8px));
+						background: rgba(0, 0, 0, 0.2);
+						padding: 8px;
+						border-radius: 24px;
 
-				> .title {
-					position: absolute;
-					bottom: 0;
-					left: 0;
-					width: 100%;
-					padding: 0 0 8px 154px;
-					box-sizing: border-box;
-					color: #fff;
+						> .menu {
+							vertical-align: bottom;
+							height: 31px;
+							width: 31px;
+							color: #fff;
+							text-shadow: 0 0 8px #000;
+							font-size: 16px;
+						}
 
-					> .name {
-						display: block;
-						margin: 0;
-						line-height: 32px;
-						font-weight: bold;
-						font-size: 1.8em;
-						text-shadow: 0 0 8px #000;
+						> .koudoku {
+							margin-left: 4px;
+							vertical-align: bottom;
+						}
 					}
 
-					> .bottom {
-						> * {
-							display: inline-block;
-							margin-right: 16px;
-							line-height: 20px;
-							opacity: 0.8;
+					> .title {
+						position: absolute;
+						bottom: 0;
+						left: 0;
+						width: 100%;
+						padding: 0 0 8px 154px;
+						box-sizing: border-box;
+						color: #fff;
 
-							&.username {
-								font-weight: bold;
+						> .name {
+							display: block;
+							margin: 0;
+							line-height: 32px;
+							font-weight: bold;
+							font-size: 1.8em;
+							text-shadow: 0 0 8px #000;
+						}
+
+						> .bottom {
+							> * {
+								display: inline-block;
+								margin-right: 16px;
+								line-height: 20px;
+								opacity: 0.8;
+
+								&.username {
+									font-weight: bold;
+								}
 							}
 						}
 					}
 				}
-			}
-
-			> .title {
-				display: none;
-				text-align: center;
-				padding: 50px 8px 16px 8px;
-				font-weight: bold;
-				border-bottom: solid 0.5px var(--divider);
-
-				> .bottom {
-					> * {
-						display: inline-block;
-						margin-right: 8px;
-						opacity: 0.8;
-					}
-				}
-			}
-
-			> .avatar {
-				display: block;
-				position: absolute;
-				top: 170px;
-				left: 16px;
-				z-index: 2;
-				width: 120px;
-				height: 120px;
-				box-shadow: 1px 1px 3px rgba(#000, 0.2);
-			}
-
-			> .description {
-				padding: 24px 24px 24px 154px;
-				font-size: 0.95em;
-
-				> .empty {
-					margin: 0;
-					opacity: 0.5;
-				}
-			}
-
-			> .fields {
-				padding: 24px;
-				font-size: 0.9em;
-				border-top: solid 0.5px var(--divider);
-
-				> .field {
-					display: flex;
-					padding: 0;
-					margin: 0;
-					align-items: center;
-
-					&:not(:last-child) {
-						margin-bottom: 8px;
-					}
-
-					> .name {
-						width: 30%;
-						overflow: hidden;
-						white-space: nowrap;
-						text-overflow: ellipsis;
-						font-weight: bold;
-						text-align: center;
-					}
-
-					> .value {
-						width: 70%;
-						overflow: hidden;
-						white-space: nowrap;
-						text-overflow: ellipsis;
-						margin: 0;
-					}
-				}
-
-				&.system > .field > .name {
-				}
-			}
-
-			> .status {
-				display: flex;
-				padding: 24px;
-				border-top: solid 0.5px var(--divider);
-
-				> a {
-					flex: 1;
-					text-align: center;
-
-					&.active {
-						color: var(--accent);
-					}
-
-					&:hover {
-						text-decoration: none;
-					}
-
-					> b {
-						display: block;
-						line-height: 16px;
-					}
-
-					> span {
-						font-size: 70%;
-					}
-				}
-			}
-		}
-	}
-
-	> .contents {
-		> .content {
-			margin-bottom: var(--margin);
-		}
-	}
-
-	&.max-width_500px {
-		> .profile > .main {
-			> .banner-container {
-				height: 140px;
-
-				> .fade {
-					display: none;
-				}
 
 				> .title {
 					display: none;
+					text-align: center;
+					padding: 50px 8px 16px 8px;
+					font-weight: bold;
+					border-bottom: solid 0.5px var(--divider);
+
+					> .bottom {
+						> * {
+							display: inline-block;
+							margin-right: 8px;
+							opacity: 0.8;
+						}
+					}
 				}
-			}
 
-			> .title {
-				display: block;
-			}
+				> .avatar {
+					display: block;
+					position: absolute;
+					top: 170px;
+					left: 16px;
+					z-index: 2;
+					width: 120px;
+					height: 120px;
+					box-shadow: 1px 1px 3px rgba(#000, 0.2);
+				}
 
-			> .avatar {
-				top: 90px;
-				left: 0;
-				right: 0;
-				width: 92px;
-				height: 92px;
-				margin: auto;
-			}
+				> .description {
+					padding: 24px 24px 24px 154px;
+					font-size: 0.95em;
 
-			> .description {
-				padding: 16px;
-				text-align: center;
-			}
+					> .empty {
+						margin: 0;
+						opacity: 0.5;
+					}
+				}
 
-			> .fields {
-				padding: 16px;
-			}
+				> .fields {
+					padding: 24px;
+					font-size: 0.9em;
+					border-top: solid 0.5px var(--divider);
 
-			> .status {
-				padding: 16px;
+					> .field {
+						display: flex;
+						padding: 0;
+						margin: 0;
+						align-items: center;
+
+						&:not(:last-child) {
+							margin-bottom: 8px;
+						}
+
+						> .name {
+							width: 30%;
+							overflow: hidden;
+							white-space: nowrap;
+							text-overflow: ellipsis;
+							font-weight: bold;
+							text-align: center;
+						}
+
+						> .value {
+							width: 70%;
+							overflow: hidden;
+							white-space: nowrap;
+							text-overflow: ellipsis;
+							margin: 0;
+						}
+					}
+
+					&.system > .field > .name {
+					}
+				}
+
+				> .status {
+					display: flex;
+					padding: 24px;
+					border-top: solid 0.5px var(--divider);
+
+					> a {
+						flex: 1;
+						text-align: center;
+
+						&.active {
+							color: var(--accent);
+						}
+
+						&:hover {
+							text-decoration: none;
+						}
+
+						> b {
+							display: block;
+							line-height: 16px;
+						}
+
+						> span {
+							font-size: 70%;
+						}
+					}
+				}
 			}
 		}
 
 		> .contents {
-			> .nav {
-				font-size: 80%;
+			> .content {
+				margin-bottom: var(--margin);
 			}
 		}
 	}
+
+	&.max-width_500px {
+		> .main {
+			> .profile > .main {
+				> .banner-container {
+					height: 140px;
+
+					> .fade {
+						display: none;
+					}
+
+					> .title {
+						display: none;
+					}
+				}
+
+				> .title {
+					display: block;
+				}
+
+				> .avatar {
+					top: 90px;
+					left: 0;
+					right: 0;
+					width: 92px;
+					height: 92px;
+					margin: auto;
+				}
+
+				> .description {
+					padding: 16px;
+					text-align: center;
+				}
+
+				> .fields {
+					padding: 16px;
+				}
+
+				> .status {
+					padding: 16px;
+				}
+			}
+
+			> .contents {
+				> .nav {
+					font-size: 80%;
+				}
+			}
+		}
+	}
+
+	&.wide {
+		display: flex;
+		width: 100%;
+
+		> .main {
+			width: 100%;
+			min-width: 0;
+		}
+
+		> .sub {
+			max-width: 350px;
+			min-width: 350px;
+			margin-left: var(--margin);
+		}
+	}
 }
 </style>

From 91c56ceb6eaa8d865c1dc209f0630145195e327a Mon Sep 17 00:00:00 2001
From: syuilo <Syuilotan@yahoo.co.jp>
Date: Wed, 26 Jan 2022 00:51:11 +0900
Subject: [PATCH 10/46] Update CHANGELOG.md

---
 CHANGELOG.md | 1 +
 1 file changed, 1 insertion(+)

diff --git a/CHANGELOG.md b/CHANGELOG.md
index d7cd03541d..835c504637 100644
--- a/CHANGELOG.md
+++ b/CHANGELOG.md
@@ -23,6 +23,7 @@
 - カスタム絵文字一括インポート
 - 投稿フォームで一時的に投稿するアカウントを切り替えられるように
 - Unifying Misskey-specific IRIs in JSON-LD `@context`
+- セキュリティの向上
 
 ### Bugfixes
 - アップロードエラー時の処理を修正

From 37a4e5f4fce8004864720a3ed87eaff03d5c0685 Mon Sep 17 00:00:00 2001
From: syuilo <Syuilotan@yahoo.co.jp>
Date: Wed, 26 Jan 2022 00:51:26 +0900
Subject: [PATCH 11/46] refactor(backend): use insert instead of save

---
 packages/backend/src/server/api/endpoints/app/create.ts       | 4 ++--
 packages/backend/src/server/api/endpoints/channels/create.ts  | 4 ++--
 packages/backend/src/server/api/endpoints/i/2fa/key-done.ts   | 2 +-
 .../backend/src/server/api/endpoints/i/2fa/register-key.ts    | 2 +-
 packages/backend/src/services/following/requests/create.ts    | 4 ++--
 .../backend/src/services/register-or-fetch-instance-doc.ts    | 4 ++--
 6 files changed, 10 insertions(+), 10 deletions(-)

diff --git a/packages/backend/src/server/api/endpoints/app/create.ts b/packages/backend/src/server/api/endpoints/app/create.ts
index 838cbf606e..fbe6690f1d 100644
--- a/packages/backend/src/server/api/endpoints/app/create.ts
+++ b/packages/backend/src/server/api/endpoints/app/create.ts
@@ -46,7 +46,7 @@ export default define(meta, async (ps, user) => {
 	const permission = unique(ps.permission.map(v => v.replace(/^(.+)(\/|-)(read|write)$/, '$3:$1')));
 
 	// Create account
-	const app = await Apps.save({
+	const app = await Apps.insert({
 		id: genId(),
 		createdAt: new Date(),
 		userId: user ? user.id : null,
@@ -55,7 +55,7 @@ export default define(meta, async (ps, user) => {
 		permission,
 		callbackUrl: ps.callbackUrl,
 		secret: secret,
-	});
+	}).then(x => Apps.findOneOrFail(x.identifiers[0]));
 
 	return await Apps.pack(app, null, {
 		detail: true,
diff --git a/packages/backend/src/server/api/endpoints/channels/create.ts b/packages/backend/src/server/api/endpoints/channels/create.ts
index 48be728d99..68cdf1143e 100644
--- a/packages/backend/src/server/api/endpoints/channels/create.ts
+++ b/packages/backend/src/server/api/endpoints/channels/create.ts
@@ -56,14 +56,14 @@ export default define(meta, async (ps, user) => {
 		}
 	}
 
-	const channel = await Channels.save({
+	const channel = await Channels.insert({
 		id: genId(),
 		createdAt: new Date(),
 		userId: user.id,
 		name: ps.name,
 		description: ps.description || null,
 		bannerId: banner ? banner.id : null,
-	} as Channel);
+	} as Channel).then(x => Channels.findOneOrFail(x.identifiers[0]));
 
 	return await Channels.pack(channel, user);
 });
diff --git a/packages/backend/src/server/api/endpoints/i/2fa/key-done.ts b/packages/backend/src/server/api/endpoints/i/2fa/key-done.ts
index 271632d362..26e9a60886 100644
--- a/packages/backend/src/server/api/endpoints/i/2fa/key-done.ts
+++ b/packages/backend/src/server/api/endpoints/i/2fa/key-done.ts
@@ -130,7 +130,7 @@ export default define(meta, async (ps, user) => {
 
 	const credentialIdString = credentialId.toString('hex');
 
-	await UserSecurityKeys.save({
+	await UserSecurityKeys.insert({
 		userId: user.id,
 		id: credentialIdString,
 		lastUsed: new Date(),
diff --git a/packages/backend/src/server/api/endpoints/i/2fa/register-key.ts b/packages/backend/src/server/api/endpoints/i/2fa/register-key.ts
index b6b0fd50b4..057e54c69b 100644
--- a/packages/backend/src/server/api/endpoints/i/2fa/register-key.ts
+++ b/packages/backend/src/server/api/endpoints/i/2fa/register-key.ts
@@ -45,7 +45,7 @@ export default define(meta, async (ps, user) => {
 
 	const challengeId = genId();
 
-	await AttestationChallenges.save({
+	await AttestationChallenges.insert({
 		userId: user.id,
 		id: challengeId,
 		challenge: hash(Buffer.from(challenge, 'utf-8')).toString('hex'),
diff --git a/packages/backend/src/services/following/requests/create.ts b/packages/backend/src/services/following/requests/create.ts
index cc1abcce19..e45023015d 100644
--- a/packages/backend/src/services/following/requests/create.ts
+++ b/packages/backend/src/services/following/requests/create.ts
@@ -25,7 +25,7 @@ export default async function(follower: { id: User['id']; host: User['host']; ur
 	if (blocking != null) throw new Error('blocking');
 	if (blocked != null) throw new Error('blocked');
 
-	const followRequest = await FollowRequests.save({
+	const followRequest = await FollowRequests.insert({
 		id: genId(),
 		createdAt: new Date(),
 		followerId: follower.id,
@@ -39,7 +39,7 @@ export default async function(follower: { id: User['id']; host: User['host']; ur
 		followeeHost: followee.host,
 		followeeInbox: Users.isRemoteUser(followee) ? followee.inbox : undefined,
 		followeeSharedInbox: Users.isRemoteUser(followee) ? followee.sharedInbox : undefined,
-	});
+	}).then(x => FollowRequests.findOneOrFail(x.identifiers[0]));
 
 	// Publish receiveRequest event
 	if (Users.isLocalUser(followee)) {
diff --git a/packages/backend/src/services/register-or-fetch-instance-doc.ts b/packages/backend/src/services/register-or-fetch-instance-doc.ts
index a548ab0497..18b42ed15b 100644
--- a/packages/backend/src/services/register-or-fetch-instance-doc.ts
+++ b/packages/backend/src/services/register-or-fetch-instance-doc.ts
@@ -16,12 +16,12 @@ export async function registerOrFetchInstanceDoc(host: string): Promise<Instance
 	const index = await Instances.findOne({ host });
 
 	if (index == null) {
-		const i = await Instances.save({
+		const i = await Instances.insert({
 			id: genId(),
 			host,
 			caughtAt: new Date(),
 			lastCommunicatedAt: new Date(),
-		});
+		}).then(x => Instances.findOneOrFail(x.identifiers[0]));
 
 		federationChart.update(true);
 

From 65a19f0c7594dc0af73b42230b806066b848a66d Mon Sep 17 00:00:00 2001
From: syuilo <Syuilotan@yahoo.co.jp>
Date: Wed, 26 Jan 2022 00:52:34 +0900
Subject: [PATCH 12/46] Update CONTRIBUTING.md

---
 CONTRIBUTING.md | 3 +++
 1 file changed, 3 insertions(+)

diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md
index c7a4773bc7..8f10184289 100644
--- a/CONTRIBUTING.md
+++ b/CONTRIBUTING.md
@@ -95,6 +95,9 @@ Some of the existing components are implemented in the Options API, but it is an
 
 Just execute `yarn` to fix it.
 
+### INSERTするときにはsaveではなくinsertを使用する
+#6441
+
 ### placeholder
 SQLをクエリビルダで組み立てる際、使用するプレースホルダは重複してはならない
 例えば

From 4e1974c6e6c8d36e6dd8b12c74e219bcf4cfbf7a Mon Sep 17 00:00:00 2001
From: Derek <skeh@is.nota.live>
Date: Tue, 25 Jan 2022 11:26:12 -0700
Subject: [PATCH 13/46] enhance: Improve poll-editor UI + composition port
 (#8186)

* Poll editor UI changes

Use a horizontal layout when possible, wrap to vertical when constrained

* Port poll-editor to composition API

* Fix poll-editor `get` time calcs

* fix

Co-authored-by: syuilo <Syuilotan@yahoo.co.jp>
---
 .../client/src/components/poll-editor.vue     | 224 ++++++++----------
 packages/client/src/components/post-form.vue  |  17 +-
 2 files changed, 101 insertions(+), 140 deletions(-)

diff --git a/packages/client/src/components/poll-editor.vue b/packages/client/src/components/poll-editor.vue
index fad0cf1593..d8f898ee13 100644
--- a/packages/client/src/components/poll-editor.vue
+++ b/packages/client/src/components/poll-editor.vue
@@ -3,7 +3,7 @@
 	<p v-if="choices.length < 2" class="caution">
 		<i class="fas fa-exclamation-triangle"></i>{{ $ts._poll.noOnlyOneChoice }}
 	</p>
-	<ul ref="choices">
+	<ul>
 		<li v-for="(choice, i) in choices" :key="i">
 			<MkInput class="input" :model-value="choice" :placeholder="$t('_poll.choiceN', { n: i + 1 })" @update:modelValue="onInput(i, $event)">
 			</MkInput>
@@ -14,8 +14,8 @@
 	</ul>
 	<MkButton v-if="choices.length < 10" class="add" @click="add">{{ $ts.add }}</MkButton>
 	<MkButton v-else class="add" disabled>{{ $ts._poll.noMore }}</MkButton>
+	<MkSwitch v-model="multiple">{{ $ts._poll.canMultipleVote }}</MkSwitch>
 	<section>
-		<MkSwitch v-model="multiple">{{ $ts._poll.canMultipleVote }}</MkSwitch>
 		<div>
 			<MkSelect v-model="expiration">
 				<template #label>{{ $ts._poll.expiration }}</template>
@@ -31,7 +31,7 @@
 					<template #label>{{ $ts._poll.deadlineTime }}</template>
 				</MkInput>
 			</section>
-			<section v-if="expiration === 'after'">
+			<section v-else-if="expiration === 'after'">
 				<MkInput v-model="after" type="number" class="input">
 					<template #label>{{ $ts._poll.duration }}</template>
 				</MkInput>
@@ -47,8 +47,8 @@
 </div>
 </template>
 
-<script lang="ts">
-import { defineComponent } from 'vue';
+<script lang="ts" setup>
+import { ref, watch } from 'vue';
 import { addTime } from '@/scripts/time';
 import { formatDateTimeString } from '@/scripts/format-time-string';
 import MkInput from './form/input.vue';
@@ -56,125 +56,85 @@ import MkSelect from './form/select.vue';
 import MkSwitch from './form/switch.vue';
 import MkButton from './ui/button.vue';
 
-export default defineComponent({
-	components: {
-		MkInput,
-		MkSelect,
-		MkSwitch,
-		MkButton,
-	},
+const props = defineProps<{
+	modelValue: {
+		expiresAt: string;
+		expiredAfter: number;
+		choices: string[];
+		multiple: boolean;
+	};
+}>();
+const emit = defineEmits<{
+	(ev: 'update:modelValue', v: {
+		expiresAt: string;
+		expiredAfter: number;
+		choices: string[];
+		multiple: boolean;
+	}): void;
+}>();
 
-	props: {
-		poll: {
-			type: Object,
-			required: true
+const choices = ref(props.modelValue.choices);
+const multiple = ref(props.modelValue.multiple);
+const expiration = ref('infinite');
+const atDate = ref(formatDateTimeString(addTime(new Date(), 1, 'day'), 'yyyy-MM-dd'));
+const atTime = ref('00:00');
+const after = ref(0);
+const unit = ref('second');
+
+if (props.modelValue.expiresAt) {
+	expiration.value = 'at';
+	atDate.value = atTime.value = props.modelValue.expiresAt;
+} else if (typeof props.modelValue.expiredAfter === 'number') {
+	expiration.value = 'after';
+	after.value = props.modelValue.expiredAfter / 1000;
+} else {
+	expiration.value = 'infinite';
+}
+
+function onInput(i, value) {
+	choices.value[i] = value;
+}
+
+function add() {
+	choices.value.push('');
+	// TODO
+	// nextTick(() => {
+	//   (this.$refs.choices as any).childNodes[this.choices.length - 1].childNodes[0].focus();
+	// });
+}
+
+function remove(i) {
+	choices.value = choices.value.filter((_, _i) => _i != i);
+}
+
+function get() {
+	const calcAt = () => {
+		return new Date(`${atDate.value} ${atTime.value}`).getTime();
+	};
+
+	const calcAfter = () => {
+		let base = parseInt(after.value);
+		switch (unit.value) {
+			case 'day': base *= 24;
+			case 'hour': base *= 60;
+			case 'minute': base *= 60;
+			case 'second': return base *= 1000;
+			default: return null;
 		}
-	},
+	};
 
-	emits: ['updated'],
+	return {
+		choices: choices.value,
+		multiple: multiple.value,
+		...(
+			expiration.value === 'at' ? { expiresAt: calcAt() } :
+			expiration.value === 'after' ? { expiredAfter: calcAfter() } : {}
+		)
+	};
+}
 
-	data() {
-		return {
-			choices: this.poll.choices,
-			multiple: this.poll.multiple,
-			expiration: 'infinite',
-			atDate: formatDateTimeString(addTime(new Date(), 1, 'day'), 'yyyy-MM-dd'),
-			atTime: '00:00',
-			after: 0,
-			unit: 'second',
-		};
-	},
-
-	watch: {
-		choices: {
-			handler() {
-				this.$emit('updated', this.get());
-			},
-			deep: true
-		},
-		multiple: {
-			handler() {
-				this.$emit('updated', this.get());
-			},
-		},
-		expiration: {
-			handler() {
-				this.$emit('updated', this.get());
-			},
-		},
-		atDate: {
-			handler() {
-				this.$emit('updated', this.get());
-			},
-		},
-		after: {
-			handler() {
-				this.$emit('updated', this.get());
-			},
-		},
-		unit: {
-			handler() {
-				this.$emit('updated', this.get());
-			},
-		},
-	},
-
-	created() {
-		const poll = this.poll;
-		if (poll.expiresAt) {
-			this.expiration = 'at';
-			this.atDate = this.atTime = poll.expiresAt;
-		} else if (typeof poll.expiredAfter === 'number') {
-			this.expiration = 'after';
-			this.after = poll.expiredAfter / 1000;
-		} else {
-			this.expiration = 'infinite';
-		}
-	},
-
-	methods: {
-		onInput(i, e) {
-			this.choices[i] = e;
-		},
-
-		add() {
-			this.choices.push('');
-			this.$nextTick(() => {
-				// TODO
-				//(this.$refs.choices as any).childNodes[this.choices.length - 1].childNodes[0].focus();
-			});
-		},
-
-		remove(i) {
-			this.choices = this.choices.filter((_, _i) => _i != i);
-		},
-
-		get() {
-			const at = () => {
-				return new Date(`${this.atDate} ${this.atTime}`).getTime();
-			};
-
-			const after = () => {
-				let base = parseInt(this.after);
-				switch (this.unit) {
-					case 'day': base *= 24;
-					case 'hour': base *= 60;
-					case 'minute': base *= 60;
-					case 'second': return base *= 1000;
-					default: return null;
-				}
-			};
-
-			return {
-				choices: this.choices,
-				multiple: this.multiple,
-				...(
-					this.expiration === 'at' ? { expiresAt: at() } :
-					this.expiration === 'after' ? { expiredAfter: after() } : {}
-				)
-			};
-		},
-	}
+watch([choices, multiple, expiration, atDate, atTime, after, unit], () => emit('update:modelValue', get()), {
+	deep: true,
 });
 </script>
 
@@ -216,7 +176,7 @@ export default defineComponent({
 	}
 
 	> .add {
-		margin: 8px 0 0 0;
+		margin: 8px 0;
 		z-index: 1;
 	}
 
@@ -225,21 +185,27 @@ export default defineComponent({
 
 		> div {
 			margin: 0 8px;
+			display: flex;
+			flex-direction: row;
+			flex-wrap: wrap;
+			gap: 12px;
 
 			&:last-child {
 				flex: 1 0 auto;
 
-				> section {
-					align-items: center;
-					display: flex;
-					margin: -32px 0 0;
+				> div {
+					flex-grow: 1;
+				}
 
-					> &:first-child {
-						margin-right: 16px;
-					}
+				> section {
+					// MAGIC: Prevent div above from growing unless wrapped to its own line
+					flex-grow: 9999;
+					align-items: end;
+					display: flex;
+					gap: 4px;
 
 					> .input {
-						flex: 1 0 auto;
+						flex: 1 1 auto;
 					}
 				}
 			}
diff --git a/packages/client/src/components/post-form.vue b/packages/client/src/components/post-form.vue
index 0dcec26932..ed78c5a3fb 100644
--- a/packages/client/src/components/post-form.vue
+++ b/packages/client/src/components/post-form.vue
@@ -43,7 +43,7 @@
 		<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">
 		<XPostFormAttaches class="attaches" :files="files" @updated="updateFiles" @detach="detachFile" @changeSensitive="updateFileSensitive" @changeName="updateFileName"/>
-		<XPollEditor v-if="poll" :poll="poll" @destroyed="poll = null" @updated="onPollUpdate"/>
+		<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>
@@ -111,9 +111,9 @@ const props = withDefaults(defineProps<{
 });
 
 const emit = defineEmits<{
-	(e: 'posted'): void;
-	(e: 'cancel'): void;
-	(e: 'esc'): void;
+	(ev: 'posted'): void;
+	(ev: 'cancel'): void;
+	(ev: 'esc'): void;
 }>();
 
 const textareaEl = $ref<HTMLTextAreaElement | null>(null);
@@ -127,8 +127,8 @@ let files = $ref(props.initialFiles ?? []);
 let poll = $ref<{
 	choices: string[];
 	multiple: boolean;
-	expiresAt: string;
-	expiredAfter: string;
+	expiresAt: string | null;
+	expiredAfter: string | null;
 } | null>(null);
 let useCw = $ref(false);
 let showPreview = $ref(false);
@@ -371,11 +371,6 @@ function upload(file: File, name?: string) {
 	});
 }
 
-function onPollUpdate(poll) {
-	poll = poll;
-	saveDraft();
-}
-
 function setVisibility() {
 	if (props.channel) {
 		// TODO: information dialog

From 0b4a076f661d56cb11ebc66588f174216f8e7c7d Mon Sep 17 00:00:00 2001
From: syuilo <Syuilotan@yahoo.co.jp>
Date: Wed, 26 Jan 2022 03:26:34 +0900
Subject: [PATCH 14/46] refactor

---
 packages/client/.eslintrc.js               | 1 +
 packages/client/src/components/captcha.vue | 2 +-
 2 files changed, 2 insertions(+), 1 deletion(-)

diff --git a/packages/client/.eslintrc.js b/packages/client/.eslintrc.js
index e0113019ac..d414f86ed3 100644
--- a/packages/client/.eslintrc.js
+++ b/packages/client/.eslintrc.js
@@ -47,6 +47,7 @@ module.exports = {
 		"vue/no-unused-components": "warn",
 		"vue/valid-v-for": "warn",
 		"vue/return-in-computed-property": "warn",
+		"vue/no-setup-props-destructure": "warn",
 		"vue/max-attributes-per-line": "off",
 		"vue/html-self-closing": "off",
 		"vue/singleline-html-element-content-newline": "off",
diff --git a/packages/client/src/components/captcha.vue b/packages/client/src/components/captcha.vue
index 770804cf44..307fc312bc 100644
--- a/packages/client/src/components/captcha.vue
+++ b/packages/client/src/components/captcha.vue
@@ -38,7 +38,7 @@ const props = defineProps<{
 }>();
 
 const emit = defineEmits<{
-	(e: 'update:modelValue', v: string | null): void;
+	(ev: 'update:modelValue', v: string | null): void;
 }>();
 
 const available = ref(false);

From 110ae539b82f1dc19c625e8089a0e69f1f4fed1b Mon Sep 17 00:00:00 2001
From: syuilo <Syuilotan@yahoo.co.jp>
Date: Wed, 26 Jan 2022 03:27:19 +0900
Subject: [PATCH 15/46] Update CONTRIBUTING.md

---
 CONTRIBUTING.md | 5 +++--
 1 file changed, 3 insertions(+), 2 deletions(-)

diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md
index 8f10184289..27f5598a66 100644
--- a/CONTRIBUTING.md
+++ b/CONTRIBUTING.md
@@ -87,8 +87,9 @@ Configuration files are located in [`/.github/workflows`](/.github/workflows).
 
 ## Vue
 Misskey uses Vue(v3) as its front-end framework.
-**When creating a new component, please use the Composition API (with [setup sugar](https://v3.vuejs.org/api/sfc-script-setup.html) and [ref sugar](https://github.com/vuejs/rfcs/discussions/369)) instead of the Options API.**
-Some of the existing components are implemented in the Options API, but it is an old implementation. Refactors that migrate those components to the Composition API are also welcome.
+- Use TypeScript.
+- **When creating a new component, please use the Composition API (with [setup sugar](https://v3.vuejs.org/api/sfc-script-setup.html) and [ref sugar](https://github.com/vuejs/rfcs/discussions/369)) instead of the Options API.**
+	- Some of the existing components are implemented in the Options API, but it is an old implementation. Refactors that migrate those components to the Composition API are also welcome.
 
 ## Notes
 ### How to resolve conflictions occurred at yarn.lock?

From ad2a3eed9be108df1e294f323935241502c07410 Mon Sep 17 00:00:00 2001
From: syuilo <Syuilotan@yahoo.co.jp>
Date: Wed, 26 Jan 2022 03:31:10 +0900
Subject: [PATCH 16/46] :art:

---
 packages/client/src/components/poll-editor.vue | 2 +-
 1 file changed, 1 insertion(+), 1 deletion(-)

diff --git a/packages/client/src/components/poll-editor.vue b/packages/client/src/components/poll-editor.vue
index d8f898ee13..6f3f23a2d3 100644
--- a/packages/client/src/components/poll-editor.vue
+++ b/packages/client/src/components/poll-editor.vue
@@ -140,7 +140,7 @@ watch([choices, multiple, expiration, atDate, atTime, after, unit], () => emit('
 
 <style lang="scss" scoped>
 .zmdxowus {
-	padding: 8px;
+	padding: 8px 16px;
 
 	> .caution {
 		margin: 0 0 8px 0;

From 5a33789cd118d7a1574036878615e30d8b8afc16 Mon Sep 17 00:00:00 2001
From: syuilo <Syuilotan@yahoo.co.jp>
Date: Wed, 26 Jan 2022 03:37:43 +0900
Subject: [PATCH 17/46] Update extensions.json

---
 .vscode/extensions.json | 1 -
 1 file changed, 1 deletion(-)

diff --git a/.vscode/extensions.json b/.vscode/extensions.json
index 04544f46b9..9adb0d0697 100644
--- a/.vscode/extensions.json
+++ b/.vscode/extensions.json
@@ -4,6 +4,5 @@
 		"eg2.vscode-npm-script",
 		"dbaeumer.vscode-eslint",
 		"johnsoncodehk.volar",
-		"sysoev.language-stylus"
 	]
 }

From 25cac0089fe3ba1fff39d109c4c59f611533a572 Mon Sep 17 00:00:00 2001
From: Kainoa Kanter <44733677+ThatOneCalculator@users.noreply.github.com>
Date: Wed, 26 Jan 2022 00:11:41 -0800
Subject: [PATCH 18/46] Fix pop-out bug (#8170)

---
 packages/client/src/scripts/popout.ts | 4 ++--
 1 file changed, 2 insertions(+), 2 deletions(-)

diff --git a/packages/client/src/scripts/popout.ts b/packages/client/src/scripts/popout.ts
index 51b8d72868..b8286a2a76 100644
--- a/packages/client/src/scripts/popout.ts
+++ b/packages/client/src/scripts/popout.ts
@@ -1,8 +1,8 @@
 import * as config from '@/config';
 
 export function popout(path: string, w?: HTMLElement) {
-	let url = path.startsWith('http://') || path.startsWith('https://') ? path : config.url + path;
-	url += '?zen'; // TODO: ちゃんとURLパースしてクエリ付ける
+	let url = path.startsWith('http://') || path.startsWith('https://') ? path : config.url + "/" + path;
+	url += '?zen';
 	if (w) {
 		const position = w.getBoundingClientRect();
 		const width = parseInt(getComputedStyle(w, '').width, 10);

From 449709f6cbbf3acc34c051ac77306eb0b38143e8 Mon Sep 17 00:00:00 2001
From: syuilo <Syuilotan@yahoo.co.jp>
Date: Wed, 26 Jan 2022 18:42:14 +0900
Subject: [PATCH 19/46] refactor: fix type

---
 packages/backend/src/remote/resolve-user.ts | 2 +-
 1 file changed, 1 insertion(+), 1 deletion(-)

diff --git a/packages/backend/src/remote/resolve-user.ts b/packages/backend/src/remote/resolve-user.ts
index df6226cc50..747735ecaa 100644
--- a/packages/backend/src/remote/resolve-user.ts
+++ b/packages/backend/src/remote/resolve-user.ts
@@ -37,7 +37,7 @@ export async function resolveUser(username: string, host: string | null, option?
 		});
 	}
 
-	const user = await Users.findOne({ usernameLower, host }, option) as IRemoteUser;
+	const user = await Users.findOne({ usernameLower, host }, option) as IRemoteUser | null;
 
 	const acctLower = `${usernameLower}@${host}`;
 

From 928c51ba7ba1212f4e5d6859a62dfcfafea19562 Mon Sep 17 00:00:00 2001
From: syuilo <Syuilotan@yahoo.co.jp>
Date: Wed, 26 Jan 2022 18:48:44 +0900
Subject: [PATCH 20/46] refactor(backend): fix type

---
 packages/backend/src/server/api/authenticate.ts | 2 +-
 1 file changed, 1 insertion(+), 1 deletion(-)

diff --git a/packages/backend/src/server/api/authenticate.ts b/packages/backend/src/server/api/authenticate.ts
index d33e9e3753..9e2f3eb743 100644
--- a/packages/backend/src/server/api/authenticate.ts
+++ b/packages/backend/src/server/api/authenticate.ts
@@ -10,7 +10,7 @@ export class AuthenticationError extends Error {
 	}
 }
 
-export default async (token: string): Promise<[User | null | undefined, App | null | undefined]> => {
+export default async (token: string | null): Promise<[User | null | undefined, AccessToken | null | undefined]> => {
 	if (token == null) {
 		return [null, null];
 	}

From 69e08abaef6c789ad67c21da0ae35600d8c175d0 Mon Sep 17 00:00:00 2001
From: syuilo <Syuilotan@yahoo.co.jp>
Date: Wed, 26 Jan 2022 18:52:40 +0900
Subject: [PATCH 21/46] refactor(backend): fix type

---
 packages/backend/src/server/activitypub.ts | 4 ++--
 1 file changed, 2 insertions(+), 2 deletions(-)

diff --git a/packages/backend/src/server/activitypub.ts b/packages/backend/src/server/activitypub.ts
index da6a00e58e..bbbc231b8c 100644
--- a/packages/backend/src/server/activitypub.ts
+++ b/packages/backend/src/server/activitypub.ts
@@ -67,7 +67,7 @@ router.get('/notes/:note', async (ctx, next) => {
 
 	const note = await Notes.findOne({
 		id: ctx.params.note,
-		visibility: In(['public', 'home']),
+		visibility: In(['public' as const, 'home' as const]),
 		localOnly: false,
 	});
 
@@ -96,7 +96,7 @@ router.get('/notes/:note/activity', async ctx => {
 	const note = await Notes.findOne({
 		id: ctx.params.note,
 		userHost: null,
-		visibility: In(['public', 'home']),
+		visibility: In(['public' as const, 'home' as const]),
 		localOnly: false,
 	});
 

From f6a6766f7d8f9eadc4fd24263eaa4e44e4805f03 Mon Sep 17 00:00:00 2001
From: syuilo <Syuilotan@yahoo.co.jp>
Date: Thu, 27 Jan 2022 00:05:39 +0900
Subject: [PATCH 22/46] New Crowdin updates (#8096)

* New translations ja-JP.yml (Esperanto)

* New translations ja-JP.yml (Esperanto)

* New translations ja-JP.yml (Esperanto)

* New translations ja-JP.yml (Esperanto)

* New translations ja-JP.yml (Esperanto)

* New translations ja-JP.yml (Esperanto)

* New translations ja-JP.yml (Esperanto)

* New translations ja-JP.yml (Esperanto)

* New translations ja-JP.yml (Esperanto)

* New translations ja-JP.yml (Esperanto)

* New translations ja-JP.yml (French)

* New translations ja-JP.yml (French)

* New translations ja-JP.yml (French)

* New translations ja-JP.yml (Italian)

* New translations ja-JP.yml (Italian)

* New translations ja-JP.yml (Italian)

* New translations ja-JP.yml (Italian)

* New translations ja-JP.yml (Italian)

* New translations ja-JP.yml (Italian)

* New translations ja-JP.yml (Italian)

* New translations ja-JP.yml (Italian)

* New translations ja-JP.yml (Italian)

* New translations ja-JP.yml (Italian)

* New translations ja-JP.yml (Italian)

* New translations ja-JP.yml (Italian)

* New translations ja-JP.yml (Italian)

* New translations ja-JP.yml (Italian)

* New translations ja-JP.yml (Italian)

* New translations ja-JP.yml (Italian)

* New translations ja-JP.yml (Italian)

* New translations ja-JP.yml (Italian)

* New translations ja-JP.yml (Italian)

* New translations ja-JP.yml (Italian)

* New translations ja-JP.yml (Italian)

* New translations ja-JP.yml (Italian)

* New translations ja-JP.yml (Italian)

* New translations ja-JP.yml (Russian)

* New translations ja-JP.yml (Russian)

* New translations ja-JP.yml (Russian)

* New translations ja-JP.yml (Russian)

* New translations ja-JP.yml (Russian)

* New translations ja-JP.yml (Russian)

* New translations ja-JP.yml (Russian)

* New translations ja-JP.yml (Russian)

* New translations ja-JP.yml (Russian)

* New translations ja-JP.yml (Esperanto)

* New translations ja-JP.yml (Esperanto)

* New translations ja-JP.yml (Esperanto)

* New translations ja-JP.yml (Esperanto)

* New translations ja-JP.yml (French)

* New translations ja-JP.yml (Chinese Simplified)

* New translations ja-JP.yml (Japanese, Kansai)

* New translations ja-JP.yml (Esperanto)

* New translations ja-JP.yml (Indonesian)

* New translations ja-JP.yml (English)

* New translations ja-JP.yml (Chinese Traditional)

* New translations ja-JP.yml (Ukrainian)

* New translations ja-JP.yml (Spanish)

* New translations ja-JP.yml (Russian)

* New translations ja-JP.yml (Polish)

* New translations ja-JP.yml (Dutch)

* New translations ja-JP.yml (Korean)

* New translations ja-JP.yml (Italian)

* New translations ja-JP.yml (German)

* New translations ja-JP.yml (Czech)

* New translations ja-JP.yml (Arabic)

* New translations ja-JP.yml (Esperanto)

* New translations ja-JP.yml (Esperanto)

* New translations ja-JP.yml (Esperanto)

* New translations ja-JP.yml (Czech)

* New translations ja-JP.yml (Czech)

* New translations ja-JP.yml (French)

* New translations ja-JP.yml (French)

* New translations ja-JP.yml (French)

* New translations ja-JP.yml (Chinese Simplified)

* New translations ja-JP.yml (Japanese, Kansai)

* New translations ja-JP.yml (Esperanto)

* New translations ja-JP.yml (Indonesian)

* New translations ja-JP.yml (English)

* New translations ja-JP.yml (Chinese Traditional)

* New translations ja-JP.yml (Ukrainian)

* New translations ja-JP.yml (Spanish)

* New translations ja-JP.yml (Russian)

* New translations ja-JP.yml (Polish)

* New translations ja-JP.yml (Dutch)

* New translations ja-JP.yml (Korean)

* New translations ja-JP.yml (Italian)

* New translations ja-JP.yml (German)

* New translations ja-JP.yml (Czech)

* New translations ja-JP.yml (Arabic)

* New translations ja-JP.yml (Esperanto)

* New translations ja-JP.yml (Esperanto)

* New translations ja-JP.yml (Esperanto)

* New translations ja-JP.yml (Esperanto)

* New translations ja-JP.yml (Esperanto)

* New translations ja-JP.yml (Esperanto)

* New translations ja-JP.yml (Esperanto)

* New translations ja-JP.yml (Esperanto)

* New translations ja-JP.yml (Esperanto)

* New translations ja-JP.yml (Russian)

* New translations ja-JP.yml (Russian)

* New translations ja-JP.yml (Esperanto)

* New translations ja-JP.yml (Spanish)

* New translations ja-JP.yml (Spanish)

* New translations ja-JP.yml (Spanish)

* New translations ja-JP.yml (Spanish)

* New translations ja-JP.yml (Spanish)

* New translations ja-JP.yml (Spanish)

* New translations ja-JP.yml (Spanish)

* New translations ja-JP.yml (German)

* New translations ja-JP.yml (English)

* New translations ja-JP.yml (English)

* New translations ja-JP.yml (Italian)

* New translations ja-JP.yml (French)

* New translations ja-JP.yml (German)

* New translations ja-JP.yml (English)

* New translations ja-JP.yml (Chinese Simplified)

* New translations ja-JP.yml (Esperanto)

* New translations ja-JP.yml (Esperanto)

* New translations ja-JP.yml (Esperanto)

* New translations ja-JP.yml (Esperanto)

* New translations ja-JP.yml (Esperanto)

* New translations ja-JP.yml (Esperanto)

* New translations ja-JP.yml (Esperanto)

* New translations ja-JP.yml (Esperanto)

* New translations ja-JP.yml (Esperanto)

* New translations ja-JP.yml (Esperanto)

* New translations ja-JP.yml (Esperanto)

* New translations ja-JP.yml (Esperanto)

* New translations ja-JP.yml (Esperanto)

* New translations ja-JP.yml (Esperanto)

* New translations ja-JP.yml (Esperanto)

* New translations ja-JP.yml (Esperanto)

* New translations ja-JP.yml (Bengali)

* New translations ja-JP.yml (Esperanto)

* New translations ja-JP.yml (Esperanto)

* New translations ja-JP.yml (Esperanto)

* New translations ja-JP.yml (Esperanto)

* New translations ja-JP.yml (Esperanto)

* New translations ja-JP.yml (Esperanto)

* New translations ja-JP.yml (Esperanto)

* New translations ja-JP.yml (Esperanto)

* New translations ja-JP.yml (Arabic)

* New translations ja-JP.yml (Arabic)
---
 locales/ar-SA.yml |  88 ++++------------------
 locales/bn-BD.yml |   1 +
 locales/cs-CZ.yml |  57 ++++++++++++---
 locales/de-DE.yml | 104 +-------------------------
 locales/en-US.yml | 105 ++------------------------
 locales/eo-UY.yml | 134 ++++++++++++++-------------------
 locales/es-ES.yml | 139 +++++++++++------------------------
 locales/fr-FR.yml | 125 ++++++-------------------------
 locales/id-ID.yml | 100 -------------------------
 locales/it-IT.yml | 176 ++++++++++++++++++++++++++------------------
 locales/ja-KS.yml |  87 ----------------------
 locales/ko-KR.yml | 100 -------------------------
 locales/nl-NL.yml |   6 --
 locales/pl-PL.yml |  94 ------------------------
 locales/ru-RU.yml | 183 +++++++++++++---------------------------------
 locales/uk-UA.yml |  98 -------------------------
 locales/zh-CN.yml | 101 +------------------------
 locales/zh-TW.yml |  95 ------------------------
 18 files changed, 345 insertions(+), 1448 deletions(-)
 create mode 100644 locales/bn-BD.yml

diff --git a/locales/ar-SA.yml b/locales/ar-SA.yml
index 8942f9a553..5a053cdee9 100644
--- a/locales/ar-SA.yml
+++ b/locales/ar-SA.yml
@@ -237,7 +237,6 @@ uploadFromUrlDescription: "رابط الملف المراد رفعه"
 uploadFromUrlRequested: "الرفع مطلوب"
 uploadFromUrlMayTakeTime: "سيستغرق بعض الوقت لاتمام الرفع "
 explore: "استكشاف"
-games: "ألعاب ميسكي"
 messageRead: "مقروءة"
 noMoreHistory: "لا يوجد المزيد من التاريخ"
 startMessaging: "ابدأ محادثة"
@@ -515,7 +514,6 @@ yourAccountSuspendedDescription: "عُلق الحساب بسبب انتهاك ش
 menu: "القائمة"
 divider: "فاصل"
 addItem: "إضافة عنصر"
-rooms: "الغرفة"
 relays: "المُرَحلات"
 addRelay: "إضافة مُرحّل"
 addedRelays: "المرحلات المضافة"
@@ -690,6 +688,7 @@ notRecommended: "غير مستحسن"
 botProtection: "الحماية من الحسابات الآلية"
 instanceBlocking: "المثيلات المحجوبة"
 selectAccount: "اختر حسابًا"
+switchAccount: "تغيير الحساب"
 enabled: "مفعّل"
 disabled: "معطّل"
 quickAction: "الإجراءات السّريعة"
@@ -736,6 +735,7 @@ keepCw: "أبقِ على تحذيرات المحتوى"
 lastCommunication: "آخر تواصل"
 resolved: "عولج"
 unresolved: "لم يعالج"
+breakFollow: "إلغاء الاشتراك"
 itsOn: "مفعّل"
 itsOff: "معطّل"
 emailRequiredForSignup: "عنوان البريد الإلكتروني إلزامي للتسجيل"
@@ -751,6 +751,8 @@ unmuteThread: "ارفع الكتم عن النقاش"
 deleteAccountConfirm: "سيحذف حسابك نهائيًا، أتريد المتابعة؟"
 incorrectPassword: "كلمة السر خاطئة."
 hide: "إخفاء"
+leaveGroup: "مغادرة الفريق"
+welcomeBackWithName: "مرحبًا بك مجددًا {name}"
 _emailUnavailable:
   used: "هذا البريد الإلكتروني مستخدم"
   format: "صيغة البريد الإلكتروني غير صالحة"
@@ -758,6 +760,7 @@ _emailUnavailable:
   smtp: "خادم البريد الإلكتروتي لا يستجيب"
 _ffVisibility:
   public: "علني"
+  followers: "مرئية لمتابِعيك فقط"
   private: "خاص"
 _signup:
   almostThere: "كدت تنتهي"
@@ -842,34 +845,6 @@ _mfm:
   rainbow: "قوس قزح"
   rainbowDescription: "اجعل المحتوى يظهر بألوان الطيف"
   rotate: "تدوير"
-_reversi:
-  gameSettings: "إعدادات اللعبة"
-  chooseBoard: "اختر اللوح"
-  blackOrWhite: "أسود/أبيض"
-  blackIs: "{name} سيلعب بالأسود"
-  rules: "القوانين"
-  botSettings: "خيارات الحسابات الآلية"
-  thisGameIsStartedSoon: "ستبدأ اللعبة خلال بضع ثوانٍ"
-  waitingForOther: "ينتظر دور الخصم"
-  waitingForMe: "ينتظر دورك"
-  waitingBoth: "استعد"
-  ready: "جاهز"
-  cancelReady: "ألغ الجهوزية"
-  opponentTurn: "دور الخصم"
-  myTurn: "دورك"
-  turnOf: "دور {name}"
-  pastTurnOf: "دور {name}"
-  surrender: "استسلم"
-  drawn: "تعادل"
-  won: "فاز {name}"
-  black: "أسود"
-  white: "أبيض"
-  total: "المجموع"
-  turnCount: "الدور {count}"
-  myGames: "جولاتي"
-  allGames: "كل الجولات"
-  ended: "انتهت"
-  playing: "يُلعب الآن"
 _instanceTicker:
   remote: "أظهر للمستخدمين البِعاد"
 _serverDisconnectedBehavior:
@@ -886,6 +861,8 @@ _channel:
   usersCount: "{n} منتسب"
   notesCount: "{n} ملاحظة"
 _menuDisplay:
+  sideFull: "جانبي"
+  top: "الأعلى"
   hide: "إخفاء"
 _wordMute:
   muteWords: "الكلمات المحظورة"
@@ -1152,50 +1129,6 @@ _timelines:
   local: "المحلي"
   social: "الاجتماعي"
   global: "الشامل"
-_rooms:
-  roomOf: "غرفة {user}"
-  translate: "أنقل"
-  rotate: "تدوير"
-  exit: "رجوع"
-  remove: "أزل"
-  clear: "أزل الكل"
-  clearConfirm: "أتريد إزالة كل الأثاث من الغرفة؟"
-  leaveConfirm: "لديك تغييرات غير محفوظة. أتريد المتابعة دون حفظها؟"
-  chooseImage: "اختر صورة"
-  roomType: "نوع الغرفة"
-  carpetColor: "لون السّجاد"
-  _roomType:
-    default: "افتراضي"
-    washitsu: "الأسلوب الياباني"
-  _furnitures:
-    milk: "علبة حليب"
-    bed: "سرير"
-    low-table: "طاولة قصيرة"
-    desk: "مكتب"
-    chair: "كرسي"
-    chair2: "كرسي 2"
-    fan: "مروحة"
-    pc: "حاسوب"
-    plant: "نبات زينة"
-    plant2: "نبات زينة 2"
-    eraser: "ممحاة"
-    pencil: "قلم رصاص"
-    pudding: "بودينغ"
-    book: "كتاب"
-    book2: "كتاب 2"
-    piano: "بيانو"
-    server: "خادم"
-    moon: "قمر"
-    monitor: "شاشة التحكم"
-    keyboard: "لوحة مفاتيح"
-    wall-clock: "ساعة حائط"
-    photoframe: "إطار صورة"
-    cube: "مكعب"
-    tv: "تلفاز"
-    pinguin: "بطريق"
-    sofa: "أريكة"
-    bin: "سلة مهملات"
-    banknote: "أوراق نقدية"
 _pages:
   newPage: "أنشئ صفحة جديدة"
   editPage: "عدّل الصفحة"
@@ -1204,16 +1137,21 @@ _pages:
   updated: "نجح تعديل الصفحة"
   deleted: "نجح حذف الصفحة"
   pageSetting: "إعدادات الصفحة"
+  viewSource: "اظهر المصدر"
   viewPage: "اعرض صفحاتك"
   like: "أعجبني"
   unlike: "أزل الإعجاب"
   my: "صفحاتي"
+  featured: "الأكثر شعبية"
   contents: "المحتوى"
+  title: "العنوان"
+  summary: "ملخص الصفحة"
   alignCenter: "توسيط العناصر"
   hideTitleWhenPinned: "اخف عنوان الصفحة عند تدبيسها في ملف الشخصي"
   font: "الخط"
   fontSerif: "Serif"
   fontSansSerif: "Sans Serif"
+  chooseBlock: "إضافة كتلة"
   selectType: "اختر النوع"
   enterVariableName: "أدخل اسم المتغيّر"
   variableNameIsAlreadyUsed: "هذا الاسم محجوز"
@@ -1222,6 +1160,8 @@ _pages:
   specialBlocks: "خاص"
   blocks:
     text: "نص"
+    textarea: "حقل نصي"
+    section: "قسم"
     image: "الصور"
     button: "زرّ"
     _if:
diff --git a/locales/bn-BD.yml b/locales/bn-BD.yml
new file mode 100644
index 0000000000..ed97d539c0
--- /dev/null
+++ b/locales/bn-BD.yml
@@ -0,0 +1 @@
+---
diff --git a/locales/cs-CZ.yml b/locales/cs-CZ.yml
index 90e8dbc9c1..dcbbfcab82 100644
--- a/locales/cs-CZ.yml
+++ b/locales/cs-CZ.yml
@@ -207,7 +207,6 @@ uploadFromUrl: "Nahrát z URL adresy"
 uploadFromUrlDescription: "URL adresa souboru, který chcete nahrát"
 uploadFromUrlMayTakeTime: "Může trvat nějakou dobu, dokud nebude dokončeno nahrávání."
 explore: "Objevovat"
-games: "Misskey hry"
 messageRead: "Přečtené"
 noMoreHistory: "To je vše"
 startMessaging: "Zahájit chat"
@@ -272,6 +271,8 @@ monthX: "{month}"
 yearX: "{year}"
 pages: "Stránky"
 integration: "Integrace"
+connectService: "Připojit"
+disconnectService: "Odpojit"
 enableLocalTimeline: "Povolit lokální čas"
 enableGlobalTimeline: "Povolit globální čas"
 registration: "Registrace"
@@ -280,8 +281,10 @@ invite: "Pozvat"
 inMb: "V megabajtech"
 iconUrl: "Favicon URL"
 bannerUrl: "Baner URL"
+backgroundImageUrl: "Adresa URL obrázku pozadí"
 basicInfo: "Základní informace"
 hcaptcha: "hCaptcha"
+enableHcaptcha: "Aktivovat hCaptchu"
 hcaptchaSecretKey: "Tajný Klíč (Secret Key)"
 recaptcha: "reCAPTCHA"
 enableRecaptcha: "Zapnout ReCAPTCHu"
@@ -293,6 +296,7 @@ antennaSource: "Zdroj Antény"
 enableServiceworker: "Povolit ServiceWorker"
 caseSensitive: "Rozlišuje malá a velká písmena"
 connectedTo: "Následující účty jsou připojeny"
+popularTags: "Populární tagy"
 userList: "Seznamy"
 about: "Informace"
 aboutMisskey: "O Misskey"
@@ -336,6 +340,9 @@ next: "Další"
 retype: "Zadejte znovu"
 noteOf: "{user} poznámky"
 inviteToGroup: "Pozvat do skupiny"
+newMessageExists: "Máte novou zprávu"
+onlyOneFileCanBeAttached: "Ke zprávě můžete přiložit jenom jeden soubor"
+signinRequired: "Přihlašte se, prosím"
 invitations: "Pozvat"
 checking: "Ověřuji"
 available: "K dispozici"
@@ -363,10 +370,13 @@ signinHistory: "Historie přihlášení"
 category: "Kategorie"
 tags: "Štítky"
 createAccount: "Vytvořit účet"
+existingAccount: "Existující účet"
+regenerate: "Obnovit"
 fontSize: "Velikost písma"
 openImageInNewTab: "Otevřít obrázek v novém panelu"
 dashboard: "Přehled"
 local: "Lokální"
+remote: "Vzdálené"
 total: "Celkem"
 weekOverWeekChanges: "Týdně"
 dayOverDayChanges: "Denně"
@@ -376,6 +386,9 @@ accountSettings: "Nastavení účtu"
 promotion: "Propagace"
 promote: "Propagovat"
 numberOfDays: "Počet dní"
+deleteAll: "Smazat vše"
+showFixedPostForm: "Zobrazit formulář pro nové příspěvky nad časovou osou"
+masterVolume: "Celková hlasitost"
 chooseEmoji: "Vybrat emotikon"
 unableToProcess: "Operace nebyla dokončena."
 recentUsed: "Naposledy použité"
@@ -385,25 +398,57 @@ installedApps: "Autorizované aplikace"
 nothing: "Nic nebylo nalezeno"
 lastUsedDate: "Poslední použití"
 state: "Stav"
+sort: "Seřadit"
 ascendingOrder: "Vzestupně"
 descendingOrder: "Sestupně"
 scratchpad: "Zápisník"
 output: "Výstup"
 script: "Skript"
+updateRemoteUser: "Aktualizovat informace o vzdáleném účtu"
 deleteAllFiles: "Smazat všechny soubory"
 deleteAllFilesConfirm: "Jste si jistí že chcete smazat všechny soubory?"
 userSuspended: "Tomuto uživateli byl pozastaven účet."
+menu: "Menu"
 addItem: "Přidat položku"
-rooms: "Místnost"
 inboxUrl: "Inbox URL"
 deletedNote: "Odstraněné příspěvky"
 invisibleNote: "Skryté příspěvky"
+description: "Popis"
+author: "Autor"
+manage: "Administrace"
+small: "Malé"
+generateAccessToken: "Vygenerovat přístupový token"
+permission: "Oprávnění"
+enableAll: "Povolit vše"
+disableAll: "Vypnout vše"
+notificationType: "Typy oznámení"
+edit: "Upravit"
+emailServer: "Mailový server"
+enableEmail: "Zapnout email dystribuci"
+email: "Email"
+emailAddress: "Emailová adresa"
+smtpConfig: "Konfigurace SMTP serveru"
 smtpHost: "Hostitel"
+smtpPort: "Port"
 smtpUser: "Uživatelské jméno"
 smtpPass: "Heslo"
+smtpSecureInfo: "Toto vypněte pokud používáte STARTTLS"
+makeActive: "Aktivovat"
+display: "Zobrazit"
+copy: "Kopírovat"
+logs: "Logy"
+database: "Databáze"
+create: "Vytvořit"
+notificationSetting: "Nastavení oznámení"
+useGlobalSetting: "Použít globální nastavení"
+other: "Ostatní"
+fileIdOrUrl: "ID nebo URL souboru"
+behavior: "Chování"
+sample: "Ukázka"
 clearCache: "Vyprázdnit mezipaměť"
 info: "Informace"
 user: "Uživatelé"
+administration: "Administrace"
 _email:
   _follow:
     title: "Máte nového následovníka"
@@ -412,9 +457,8 @@ _mfm:
   quote: "Citovat"
   emoji: "Vlastní emoji"
   search: "Vyhledávání"
-_reversi:
-  total: "Celkem"
 _theme:
+  description: "Popis"
   keys:
     mention: "Zmínění"
     renote: "Přeposlat"
@@ -442,11 +486,6 @@ _exportOrImport:
   userLists: "Seznamy"
 _timelines:
   home: "Domů"
-_rooms:
-  _roomType:
-    default: "Výchozí"
-  _furnitures:
-    monitor: "Monitorovat"
 _pages:
   blocks:
     image: "Obrázky"
diff --git a/locales/de-DE.yml b/locales/de-DE.yml
index 240a67778f..2f327a905c 100644
--- a/locales/de-DE.yml
+++ b/locales/de-DE.yml
@@ -242,7 +242,6 @@ uploadFromUrlDescription: "URL der hochzuladenden Datei"
 uploadFromUrlRequested: "Upload angefordert"
 uploadFromUrlMayTakeTime: "Es kann eine Weile dauern, bis das Hochladen abgeschlossen ist."
 explore: "Erkunden"
-games: "Misskey-Spiele"
 messageRead: "Gelesen"
 noMoreHistory: "Kein weiterer Verlauf vorhanden"
 startMessaging: "Neuen Chat erstellen"
@@ -537,7 +536,6 @@ yourAccountSuspendedDescription: "Dieses Benutzerkonto wurde gesperrt, da es geg
 menu: "Menü"
 divider: "Trenner"
 addItem: "Element hinzufügen"
-rooms: "Raum"
 relays: "Relays"
 addRelay: "Relay hinzufügen"
 inboxUrl: "inbox-URL"
@@ -621,8 +619,11 @@ reportAbuse: "Melden"
 reportAbuseOf: "{name} melden"
 fillAbuseReportDescription: "Bitte gib zusätzliche Informationen zu dieser Meldung an. Falls es sich um eine spezielle Notiz handelt, bitte gib dessen URL an."
 abuseReported: "Die Meldung wurde versendet. Vielen Dank."
+reporter: "Melder"
 reporteeOrigin: "Herkunft des Gemeldeten"
 reporterOrigin: "Herkunft des Meldenden"
+forwardReport: "Meldung an fremde Instanz weiterleiten"
+forwardReportIsAnonymous: "Anstatt deines Benutzerkontos wird bei der fremden Instanz ein anonymes Systemkonto als Melder angezeigt."
 send: "Senden"
 abuseMarkAsResolved: "Meldung als gelöst markieren"
 openInNewTab: "In neuem Tab öffnen"
@@ -670,7 +671,6 @@ emailVerified: "Email-Adresse bestätigt"
 noteFavoritesCount: "Anzahl an als Favorit markierter Notizen"
 pageLikesCount: "Anzahl an als \"Gefällt mir\" markierter Seiten"
 pageLikedCount: "Anzahl erhaltener \"Gefällt mir\" auf Seiten"
-reversiCount: "Anzahl an Reversi-Runden"
 contact: "Kontakt"
 useSystemFont: "Standardschriftart des Systems verwenden"
 clips: "Clips"
@@ -746,6 +746,7 @@ notRecommended: "Nicht empfohlen"
 botProtection: "Bot-Schutz"
 instanceBlocking: "Blockierte Instanzen"
 selectAccount: "Benutzerkonto auswählen"
+switchAccount: "Konto wechseln"
 enabled: "Aktiviert"
 disabled: "Deaktiviert"
 quickAction: "Schnellaktionen"
@@ -944,39 +945,6 @@ _mfm:
   sparkleDescription: "Verleiht Inhalt einen glitzernden Partikeleffekt."
   rotate: "Drehen"
   rotateDescription: "Dreht den Inhalt um einen angegebenen Winkel"
-_reversi:
-  reversi: "Reversi"
-  gameSettings: "Spieleinstellungen"
-  chooseBoard: "Spielbrett auswählen"
-  blackOrWhite: "Schwarz/Weiß"
-  blackIs: "{name} spielt Schwarz"
-  rules: "Regeln"
-  botSettings: "Optionen des Computergegners"
-  thisGameIsStartedSoon: "Dieses Spiel beginnt in wenigen Sekunden"
-  waitingForOther: "Warte auf den Zug des Gegenspielers"
-  waitingForMe: "Warte auf deinen Zug"
-  waitingBoth: "Mach dich bereit"
-  ready: "Bereit"
-  cancelReady: "Nicht bereit"
-  opponentTurn: "Zug deines Gegners"
-  myTurn: "Dein Zug"
-  turnOf: "{name} ist am Zug"
-  pastTurnOf: "Zug von {name}"
-  surrender: "Aufgeben"
-  surrendered: "Durch Aufgabe"
-  drawn: "Unentschieden"
-  won: "{name} gewinnt"
-  black: "Schwarz"
-  white: "Weiß"
-  total: "Gesamt"
-  turnCount: " Zug {count}"
-  myGames: "Meine Runden"
-  allGames: "Alle Runden"
-  ended: "Beendet"
-  playing: "Laufend"
-  isLlotheo: "Der mit weniger Steinen gewinnt (Llotheo)"
-  loopedMap: "Wiederholendes Spielbrett"
-  canPutEverywhere: "Steine können überall platziert werden"
 _instanceTicker:
   none: "Nie anzeigen"
   remote: "Für Benutzer fremder Instanzen anzeigen"
@@ -1096,8 +1064,6 @@ _sfx:
   chatBg: "Chat (Hintergrund)"
   antenna: "Antennen"
   channel: "Kanalbenachrichtigung"
-  reversiPutBlack: "Reversi: Schwarz macht einen Zug"
-  reversiPutWhite: "Reversi: Weiß macht einen Zug"
 _ago:
   unknown: "Unbekannt"
   future: "Zukunft"
@@ -1320,68 +1286,6 @@ _timelines:
   local: "Lokal"
   social: "Sozial"
   global: "Global"
-_rooms:
-  roomOf: "{user}'s Raum"
-  addFurniture: "Möbel hinzufügen"
-  translate: "Bewegen"
-  rotate: "Drehen"
-  exit: "Zurück"
-  remove: "Entfernen"
-  clear: "Aufräumen"
-  clearConfirm: "Möchtest du wirklich alle Möbel entfernen?"
-  leaveConfirm: "Es gibt ungespeicherte Änderungen. Möchtest du wirklich gehen?"
-  chooseImage: "Bild auswählen"
-  roomType: "Raumart"
-  carpetColor: "Teppichfarbe"
-  _roomType:
-    default: "Standard"
-    washitsu: "Japanischer Stil"
-  _furnitures:
-    milk: "Milchkarton"
-    bed: "Bett"
-    low-table: "Niedrigtisch"
-    desk: "Schreibtisch"
-    chair: "Stuhl"
-    chair2: "Stuhl 2"
-    fan: "Ventilator"
-    pc: "Computer"
-    plant: "Deko-Pflanze"
-    plant2: "Deko-Pflanze 2"
-    eraser: "Radiergummi"
-    pencil: "Bleistift"
-    pudding: "Pudding"
-    cardboard-box: "Pappkarton"
-    cardboard-box2: "Pappkarton 2"
-    cardboard-box3: "Pappkarton 3"
-    book: "Buch"
-    book2: "Buch 2"
-    piano: "Piano"
-    facial-tissue: "Taschentücher"
-    server: "Server"
-    moon: "Mond"
-    corkboard: "Pinnwand"
-    mousepad: "Mauspad"
-    monitor: "Monitor"
-    keyboard: "Tastatur"
-    carpet-stripe: "Gestreifter Teppich"
-    mat: "Matte"
-    color-box: "Regal"
-    wall-clock: "Wanduhr"
-    photoframe: "Bilderrahmen"
-    cube: "Würfel"
-    tv: "Fernseher"
-    pinguin: "Pinguin"
-    rubik-cube: "Zauberwürfel"
-    poster-h: "Poster (Horizontal)"
-    poster-v: "Poster (Vertikal)"
-    sofa: "Sofa"
-    spiral: "Spiraltreppe"
-    bin: "Papierkorb"
-    cup-noodle: "Instantnudeln"
-    holo-display: "Holographischer Bildschirm"
-    energy-drink: "Energy Drink"
-    doll-ai: "Ai-Puppe"
-    banknote: "Geldscheine"
 _pages:
   newPage: "Seite erstellen"
   editPage: "Seite bearbeiten"
diff --git a/locales/en-US.yml b/locales/en-US.yml
index 9286e78cc3..6bbe848210 100644
--- a/locales/en-US.yml
+++ b/locales/en-US.yml
@@ -242,7 +242,6 @@ uploadFromUrlDescription: "URL of the file you want to upload"
 uploadFromUrlRequested: "Upload requested"
 uploadFromUrlMayTakeTime: "It may take some time until the upload is complete."
 explore: "Explore"
-games: "Misskey Games"
 messageRead: "Read"
 noMoreHistory: "There is no further history"
 startMessaging: "Start a new chat"
@@ -537,7 +536,6 @@ yourAccountSuspendedDescription: "This account has been suspended due to breakin
 menu: "Menu"
 divider: "Divider"
 addItem: "Add Item"
-rooms: "Room"
 relays: "Relays"
 addRelay: "Add Relay"
 inboxUrl: "Inbox URL"
@@ -621,8 +619,11 @@ reportAbuse: "Report"
 reportAbuseOf: "Report {name}"
 fillAbuseReportDescription: "Please fill in details regarding this report. If it is about a specific note, please include its URL."
 abuseReported: "Your report has been sent. Thank you very much."
+reporter: "Reporter"
 reporteeOrigin: "Reportee Origin"
 reporterOrigin: "Reporter Origin"
+forwardReport: "Forward report to remote instance"
+forwardReportIsAnonymous: "Instead of your account, an anonymous system account will be displayed as reporter at the remote instance."
 send: "Send"
 abuseMarkAsResolved: "Mark report as resolved"
 openInNewTab: "Open in new tab"
@@ -670,7 +671,6 @@ emailVerified: "Email has been verified"
 noteFavoritesCount: "Number of favorite notes"
 pageLikesCount: "Number of liked Pages"
 pageLikedCount: "Number of received Page likes"
-reversiCount: "Number of Reversi matches"
 contact: "Contact"
 useSystemFont: "Use the system's default font"
 clips: "Clips"
@@ -746,6 +746,7 @@ notRecommended: "Not recommended"
 botProtection: "Bot Protection"
 instanceBlocking: "Blocked Instances"
 selectAccount: "Select account"
+switchAccount: "Switch account"
 enabled: "Enabled"
 disabled: "Disabled"
 quickAction: "Quick actions"
@@ -794,6 +795,7 @@ pubSub: "Pub/Sub Accounts"
 lastCommunication: "Last communication"
 resolved: "Resolved"
 unresolved: "Unresolved"
+breakFollow: "Unfollow"
 itsOn: "Enabled"
 itsOff: "Disabled"
 emailRequiredForSignup: "Require email address for sign-up"
@@ -943,39 +945,6 @@ _mfm:
   sparkleDescription: "Gives content a sparkling particle effect."
   rotate: "Rotate"
   rotateDescription: "Turns content by a specified angle."
-_reversi:
-  reversi: "Reversi"
-  gameSettings: "Game settings"
-  chooseBoard: "Choose a board"
-  blackOrWhite: "Black/White"
-  blackIs: "{name} is playing Black"
-  rules: "Rules"
-  botSettings: "Bot options"
-  thisGameIsStartedSoon: "The game will start in a few seconds"
-  waitingForOther: "Waiting for the opponent's turn"
-  waitingForMe: "Waiting for your turn"
-  waitingBoth: "Get ready"
-  ready: "Ready"
-  cancelReady: "Cancel ready"
-  opponentTurn: "Opponent's turn"
-  myTurn: "Your turn"
-  turnOf: "It's {name}'s turn"
-  pastTurnOf: "{name}'s turn"
-  surrender: "Surrender"
-  surrendered: "By surrender"
-  drawn: "Draw"
-  won: "{name} wins"
-  black: "Black"
-  white: "White"
-  total: "Total"
-  turnCount: "Turn {count}"
-  myGames: "My rounds"
-  allGames: "All rounds"
-  ended: "Ended"
-  playing: "Currently playing"
-  isLlotheo: "The one with fewer stones wins (Llotheo)"
-  loopedMap: "Looping map"
-  canPutEverywhere: "Tiles are placeable everywhere"
 _instanceTicker:
   none: "Never show"
   remote: "Show for remote users"
@@ -1095,8 +1064,6 @@ _sfx:
   chatBg: "Chat (Background)"
   antenna: "Antennas"
   channel: "Channel notifications"
-  reversiPutBlack: "Reversi: Black makes a move"
-  reversiPutWhite: "Reversi: White makes a move"
 _ago:
   unknown: "Unknown"
   future: "Future"
@@ -1319,68 +1286,6 @@ _timelines:
   local: "Local"
   social: "Social"
   global: "Global"
-_rooms:
-  roomOf: "{user}'s room"
-  addFurniture: "Place furniture"
-  translate: "Move"
-  rotate: "Rotate"
-  exit: "Back"
-  remove: "Remove"
-  clear: "Remove All"
-  clearConfirm: "Do you really want to remove all furniture from your room?"
-  leaveConfirm: "There are unsaved changes. Do you really want to leave?"
-  chooseImage: "Select an image"
-  roomType: "Room type"
-  carpetColor: "Carpet color"
-  _roomType:
-    default: "Default"
-    washitsu: "Japanese-style"
-  _furnitures:
-    milk: "Milk carton"
-    bed: "Bed"
-    low-table: "Low Table"
-    desk: "Desk"
-    chair: "Chair"
-    chair2: "Chair 2"
-    fan: "Fan"
-    pc: "Computer"
-    plant: "Houseplant"
-    plant2: "Houseplant 2"
-    eraser: "Eraser"
-    pencil: "Pencil"
-    pudding: "Pudding"
-    cardboard-box: "Cardboard Box"
-    cardboard-box2: "Cardboard Box 2"
-    cardboard-box3: "Cardboard Box 3"
-    book: "Book"
-    book2: "Book 2"
-    piano: "Piano"
-    facial-tissue: "Tissues"
-    server: "Server"
-    moon: "Moon"
-    corkboard: "Cork board"
-    mousepad: "Mousepad"
-    monitor: "Monitor"
-    keyboard: "Keyboard"
-    carpet-stripe: "Carpet (striped)"
-    mat: "Mat"
-    color-box: "Bookshelf"
-    wall-clock: "Wall clock"
-    photoframe: "Picture frame"
-    cube: "Cube"
-    tv: "TV"
-    pinguin: "Penguin"
-    rubik-cube: "Puzzle Cube"
-    poster-h: "Poster (Horizontal)"
-    poster-v: "Poster (Vertical)"
-    sofa: "Sofa"
-    spiral: "Spiral Staircase"
-    bin: "Garbage can"
-    cup-noodle: "Cup noodles"
-    holo-display: "Holographic display"
-    energy-drink: "Energy drink"
-    doll-ai: "Ai doll"
-    banknote: "Pile of money"
 _pages:
   newPage: "Create a new Page"
   editPage: "Edit this Page"
diff --git a/locales/eo-UY.yml b/locales/eo-UY.yml
index 15a8440a23..0689834a1e 100644
--- a/locales/eo-UY.yml
+++ b/locales/eo-UY.yml
@@ -1,8 +1,8 @@
 ---
 _lang_: "Esperanto"
 headlineMisskey: "Reto konektita per notoj"
-introMisskey: "Bonvenon! Misskey estas malfermitkoda malcentraliza etbloga servo.\nKreu \"noto\"n por diskonigi nunan aferon, aŭ por paroli vian penson al ĉiuj ĉirkaŭ vi. 📡\nLa funkcion \"reago\" ebligas esprimi rapide vian senton pri la noto de la alia en la Fediverso. 👍\nBonvole esploru novan mondon. 🚀"
-monthAndDay: "La {day}a de la {month}a monato"
+introMisskey: "Bonvenon! Misskey estas malfermitkoda malcentraliza etbloga servo.\nKreu \"noto\"n por diskonigi nunan aferon, aŭ por paroli vian penson al ĉiuj ĉirkaŭ vi. 📡\nLa funkcio \"reago\" ebligas esprimi rapide vian senton pri la noto de la alia en la Fediverso. 👍\nBonvole esploru novan mondon. 🚀"
+monthAndDay: "La {day}a de l' {month}a"
 search: "Serĉi"
 notifications: "Sciigoj"
 username: "Uzantnomo"
@@ -23,7 +23,7 @@ otherSettings: "Aliaj agordoj"
 openInWindow: "Malfermi en nova fenestro"
 profile: "Profilo"
 timeline: "Templinio"
-noAccountDescription: "La uzanto ankoraŭ ne skribis la sinprezenton en sia profilo."
+noAccountDescription: "La uzanto ankoraŭ ne skribis la prion de sia profilo."
 login: "Saluti"
 loggingIn: "Salutado…"
 logout: "Adiaŭi"
@@ -41,10 +41,10 @@ cantFavorite: "Oni ne povis aldoni al viaj preferaĵoj."
 pin: "Alpingli"
 unpin: "Depingli"
 copyContent: "Kopii enhavon"
-copyLink: "Kopii ligilon"
+copyLink: "Kopii la ligilon"
 delete: "Forviŝi"
 deleteAndEdit: "Forviŝi kaj redakti"
-deleteAndEditConfirm: "Ĉu vi certas ke vi volas redakti foriginte la noton? Tio forviŝos reagojn, plusendojn, kaj respondojn ĉiujn apartenantajn al ĝi."
+deleteAndEditConfirm: "Ĉu vi certas ke vi volas foriginte redakti la noton? Vi perdos ĉiujn reagojn, plusendojn, kaj respondojn je ĝi."
 addToList: "Aldoni al listo"
 sendMessage: "Sendi mesaĝon"
 copyUsername: "Kopii uzantnomon"
@@ -69,7 +69,7 @@ lists: "Listoj"
 noLists: "Neniu listo"
 note: "Noti"
 notes: "Notoj"
-following: "Sekvatoj"
+following: "Sekvata"
 followers: "Sekvantoj"
 followsYou: "Sekvas vin"
 createList: "Krei liston"
@@ -97,7 +97,7 @@ quote: "Citi"
 pinnedNote: "Alpinglita noto"
 pinned: "Alpingli"
 you: "Vi"
-clickToShow: "Klaku por malkaŝu"
+clickToShow: "Klaki por malkaŝi"
 sensitive: "Enhavo ne estas deca por laborejo (NSFW)"
 add: "Aldoni"
 reaction: "Reagoj"
@@ -122,7 +122,7 @@ selectAntenna: "Elekti antenon"
 selectWidget: "Elekti enestraĵon"
 editWidgets: "Redakti fenestraĵon"
 editWidgetsExit: "Fini la redaktadon"
-customEmojis: "Federaj emoĵioj"
+customEmojis: "Propraj emoĝioj"
 emoji: "Emoĵio"
 emojis: "Emoĵio"
 emojiName: "Nomo de la emoĵio"
@@ -132,11 +132,10 @@ settingGuide: "Agordaj rekomendoj"
 cacheRemoteFiles: "Stapli forajn dosierojn"
 flagAsBot: "Marki kiel esti uzanto de roboto"
 flagAsCat: "Marki kiel esti kato"
-flagAsCatDescription: "Se vi estas kato, faru ĉi tiun flagon"
 autoAcceptFollowed: "Aŭtomate akcepti la peton de sekvado far uzantoj kiujn vi sekvas"
 addAccount: "Aldoni konton"
 loginFailed: "Saluto malsukcesis"
-showOnRemote: "Vidi ĉe la surloka nodo"
+showOnRemote: "Vidi pli al la originala profilo"
 general: "Ĝenerala"
 wallpaper: "Ekranfonoj"
 setWallpaper: "Apliki ekranfonon"
@@ -167,7 +166,7 @@ withNFiles: "{n} dosiero(j)"
 monitor: "Monitoro"
 network: "Reto"
 disk: "Disko"
-instanceInfo: "Informoj pri la nodo"
+instanceInfo: "Informoj sur la nodo"
 statistics: "Statistikoj"
 clearCachedFiles: "Malplenigi la staplon"
 clearCachedFilesConfirm: "Ĉu vi certas, ke vi volas forviŝi ĉiujn forajn dosierojn en la staplo?"
@@ -223,7 +222,6 @@ uploadFromUrl: "Alŝuti de URL"
 uploadFromUrlDescription: "URL de la dosiero kiun vi volas alŝuti"
 uploadFromUrlRequested: "La alŝutado estis patita"
 explore: "Esplori"
-games: "Miskiaj Ludoj"
 messageRead: "Legita"
 noMoreHistory: "Ne plu de la historio"
 startMessaging: "Komenci babiladon"
@@ -232,7 +230,7 @@ agreeTo: "Mi akceptas {0}"
 tos: "Kondiĉoj de uzado"
 start: "Komenciĝi"
 home: "Hejma"
-remoteUserCaution: "Ĉi tiuj infomoj ne estas kompletaj, ĉar ili estas pri uzanto el la fora."
+remoteUserCaution: "Pro fora uzanto, la infomoj ne estas tuto."
 activity: "Aktiveco"
 images: "Bildoj"
 birthday: "Naskiĝdato"
@@ -283,7 +281,7 @@ normal: "Normala"
 instanceName: "Nomo de la nodo"
 instanceDescription: "Priskribo de la nodo "
 maintainerName: "Nomo de la administranto"
-maintainerEmail: "Retpoŝta adreso de la administranto"
+maintainerEmail: "Retpoŝtadreso de la administranto"
 tosUrl: "URL de kondiĉoj de uzado"
 thisYear: "Ĉi-jare"
 thisMonth: "Ĉi-monate"
@@ -307,9 +305,9 @@ bannerUrl: "URL de standardo"
 backgroundImageUrl: "URL de la fona bildo"
 basicInfo: "Baza informo"
 pinnedUsers: "Alpinglita uzanto"
-pinnedUsersDescription: "Listigu uzantnomojn apartige en ĉiu linio por alpingli al la paĝoj ekz \"Esplori\"."
+pinnedUsersDescription: "Laŭlinigu uzantnomojn en ĉiu linio, por alpingli al la paĝoj ekz \"Esplori\"."
 pinnedPages: "Alpinglitaj paĝoj"
-pinnedPagesDescription: "Listigu dosierindiko apartige en ĉiu linio por alpingli al la ĉefpaĝo de la nodo."
+pinnedPagesDescription: "Laŭlinigu dosierindikojn de paĝo en ĉiu linio, por alpingli al la ĉefpaĝo de la nodo."
 pinnedNotes: "Alpinglita noto"
 hcaptcha: "hCaptcha"
 enableHcaptcha: "Ebligi hCaptcha"
@@ -367,7 +365,7 @@ cacheClear: "Malplenigi staplon"
 markAsReadAllNotifications: "Marki ĉiujn sciigojn kiel legita"
 markAsReadAllTalkMessages: "Marki ĉiujn retbabiladojn kiel legita"
 help: "Manlibro de uzado"
-inputMessageHere: "Entajpu masaĝo tie ĉi"
+inputMessageHere: "Entajpu mesaĝon tie"
 close: "Fermi"
 group: "Grupo"
 groups: "Grupoj"
@@ -378,7 +376,7 @@ invites: "Inviti"
 groupName: "Grupa nomo"
 members: "Membroj"
 transfer: "Movi"
-messagingWithUser: "Babili private"
+messagingWithUser: "Private babili "
 messagingWithGroup: "Babili grupe"
 title: "Titolo"
 text: "Teksto"
@@ -434,6 +432,7 @@ clientSettings: "Agordoj de kliento"
 accountSettings: "Agordoj de konto"
 numberOfDays: "Nombro de tagoj"
 hideThisNote: "Kaŝi la noton"
+showFeaturedNotesInTimeline: "Montri en via templinio notojn de la tendenco"
 objectStorageBaseUrl: "Baza URL"
 objectStoragePrefix: "Prefix"
 objectStorageRegion: "Regiono"
@@ -469,11 +468,10 @@ disablePagesScript: "Malebligi AiScript en la paĝoj"
 deleteAllFiles: "Forviŝi ĉiujn dosierojn"
 deleteAllFilesConfirm: "Ĉu vi certas, ke vi volas forviŝi ĉiujn dosierojn?"
 removeAllFollowing: "Ĉesi sekvi ĉiujn sekvatojn"
-userSuspended: "Ĉi tiu uzanto estas flostigita."
-userSilenced: "Ĉi tiu uzanto estas mutigita."
+userSuspended: "La uzanto estas flostigita."
+userSilenced: "La uzanto estas mutigita."
 menu: "Menuo"
 addItem: "Aldoni novaĵon"
-rooms: "Ĉambro"
 deletedNote: "Forviŝita noto"
 invisibleNote: "Malpublikigita noto"
 enableInfiniteScroll: "Ebligi infinitan rulumon"
@@ -504,9 +502,10 @@ disableAll: "Malebligi ĉiujn"
 notificationType: "Tipo de sciigoj"
 edit: "Redakti"
 emailServer: "Retpoŝta servilo"
-enableEmail: "Ebligi dissendon el retpoŝto"
+enableEmail: "Ebligi dissendon de retpoŝto"
+emailConfigInfo: "Uzata por konfirmi vian retadreson kiam registri  kaj por restarigi vian pasvorton"
 email: "Retpoŝto"
-emailAddress: "Retpoŝta adreso"
+emailAddress: "Retpoŝtadreso"
 smtpConfig: "Agordoj de SMTP servilo"
 smtpHost: "Transa servilo"
 smtpPort: "Pordo"
@@ -531,12 +530,9 @@ regenerateLoginToken: "Regeneri la aŭtentikigan pecon"
 fileIdOrUrl: "Dosiera identigilo aŭ URL"
 behavior: "Konduto"
 sample: "Ekzemplo"
-abuseReports: "Signaloj"
-reportAbuse: "Signalo"
-reportAbuseOf: "Signali kontraŭ {name}"
 send: "Sendi"
 openInNewTab: "Malfermi en nova langeto"
-editTheseSettingsMayBreakAccount: "Redakti ĉi tiujn agordojn povas damaĝi vian konton."
+editTheseSettingsMayBreakAccount: "Redakti tiujn agordojn povas damaĝi vian konton."
 instanceTicker: "Nomo de la nodo sendinta notojn"
 waitingFor: "Atendado pro {x}"
 random: "Hazarde"
@@ -559,17 +555,17 @@ sentReactionsCount: "La nombro de la reagoj senditaj"
 receivedReactionsCount: "La nombro de la reagoj ricevitaj"
 yes: "Jes"
 no: "Ne"
-driveFilesCount: "La nombro de la dosieroj ĉe la disko"
+driveFilesCount: "La nombro de la dosieroj sur la disko"
 notSet: "Ne elektita"
-emailVerified: "Via retpoŝto estis kontrolita."
+emailVerified: "Via retpoŝtadreso estis kontrolita."
 noteFavoritesCount: "La nombro de notoj preferataj"
-pageLikesCount: "La nombro de paĝoj kiun la uzanto preferas"
-pageLikedCount: "La nombro de uzantoj, kiuj preferas paĝon de ĉi tiu uzanto"
+pageLikesCount: "La nombro de paĝa plaĉon"
+pageLikedCount: "La nombro de la ricevita \"Mi plaĉas\""
 contact: "Kontakto"
 useSystemFont: "Uzi la tiparon implicitan de la sistemo"
 developer: "Evoluiganto"
-makeExplorable: "Videbligi konton sur la paĝo \"Esplori\""
-makeExplorableDescription: "Se vi elŝaltas tiun, via konto ne montros en la paĝo \"Esplori\"."
+makeExplorable: "La konton videbligi sur la paĝo \"Esplori\""
+makeExplorableDescription: "Se vi elŝaltas tiun, via konto ne montros sur la paĝo \"Esplori\"."
 duplicate: "Duobligi"
 left: "Maldekstra"
 center: "Centra"
@@ -592,7 +588,7 @@ updatedAt: "Laste ĝisdatigita"
 saveConfirm: "Ĉu vi konservas la ŝanĝon?"
 deleteConfirm: "Ĉu certas forviŝi?"
 closeAccount: "Forigi konton"
-currentVersion: "Nuna versio"
+currentVersion: "La aktuala versio"
 latestVersion: "La plej nova versio"
 youAreRunningUpToDateClient: "Vi uzas la plej novan version de via kliento."
 newVersionOfClientAvailable: "Nova versio de via kliento estas disponebla."
@@ -624,6 +620,7 @@ memo: "Memorigilo"
 high: "Alta"
 middle: "Meza"
 low: "Malalta"
+emailNotConfiguredWarning: "Vi ne agordis retpoŝtadreso."
 customCss: "Personecigita CSS"
 global: "Malloka"
 sent: "Sendi"
@@ -636,19 +633,21 @@ translate: "Traduki"
 translatedFrom: "Tradukita el {x}"
 breakFollow: "Ĉesigi la sekvadon al vi"
 itsOn: "Ŝaltita"
+emailRequiredForSignup: "Registri konton devas konformi retpoŝtadreson"
 unread: "Nelegita"
 controlPanel: "Ŝaltpodio"
 manageAccounts: "Bonteni la kontojn"
 classic: "Klasika"
 muteThread: "Silentigi la mesaĝaron"
 unmuteThread: "Malsilentigi la mesaĝaron"
-ffVisibility: "Videbleco de la sekvadoj pri vi"
-ffVisibilityDescription: "Tie ĉi vi povas agordi la videblecon pri kiuj povas vidi tiujn, kiujn vi sekvas kaj kiuj sekvas vin."
+ffVisibility: "Videbleco de viaj sekvatoj/sekvantoj"
+ffVisibilityDescription: "Oni permesas agordi tiuln kiuj povas vidi la homojn kiujn vi sekvas, kaj la homojn kiuj sekvas vin."
 continueThread: "Pli vidi la mesaĝaron"
 incorrectPassword: "Nevalida pasvorto"
 leaveGroup: "Eliĝi el la grupo"
 leaveGroupConfirm: "Ĉu vi certas ke vi volas eliĝi el la grupo {name}?"
-welcomeBackWithName: "Alvenbenon! {name}"
+welcomeBackWithName: "Bonrevenon, {name}!"
+clickToFinishEmailVerification: "Volu klaki [{ok}] por fini la konfirmon de vian retadreson"
 _emailUnavailable:
   used: "La retpoŝto jam estas uzita."
   format: "Nevalida formato."
@@ -665,7 +664,7 @@ _accountDelete:
 _ad:
   back: "Nuligi"
 _forgotPassword:
-  enterEmail: "Entajpu la retpoŝton kiun vi registrigis al via konto. Ligilo por restarigi pasvorton estos sendita al la retpoŝto."
+  enterEmail: "Entajpu la retpoŝton kiun vi registrigis al via konto. Ligilo por restarigi pasvorton estos sendita al la retadreso."
 _gallery:
   liked: "Ŝatitaj notoj"
   like: "Ŝati"
@@ -703,7 +702,7 @@ _mfm:
   inlineMath: "Formulo (en linio)"
   blockMath: "Formulo (bloko)"
   quote: "Citi"
-  emoji: "Federaj emoĵioj"
+  emoji: "Propraj emoĝioj"
   search: "Serĉi"
   flip: "Inversa"
   x2: "Granda"
@@ -711,8 +710,6 @@ _mfm:
   x4: "Pli grandega"
   font: "Presliteraro"
   rotate: "Orientiĝo"
-_reversi:
-  total: "Entute"
 _instanceTicker:
   none: "Ne montri"
   remote: "Montri al foraj uzantoj"
@@ -725,7 +722,7 @@ _channel:
   setBanner: "Apliki standardan bildon"
   removeBanner: "Forviŝi la standardan bildon"
   owned: "Bontenitaj de vi"
-  following: "Sekvante"
+  following: "Sekvado"
   usersCount: "{n} partoprenantoj"
 _menuDisplay:
   sideFull: "Flanko"
@@ -783,12 +780,12 @@ _time:
 _tutorial:
   title: "Uzado de Misskey"
   step1_1: "Bonvenon."
-  step7_2: "Se vi volas scii pli pri Misskey, rigardu la fakon {help}."
+  step7_2: "Se vi volas pli scii pri Misskey, vidu la fakon {help}."
   step7_3: "Do, bonvolu amuziĝi sur Misskey🚀"
 _2fa:
   registerKey: "Nove registri ŝlosilon"
 _permissions:
-  "read:account": "Legado de la informoj pri via konto"
+  "read:account": "Vidi la informojn de via konto"
   "write:account": "Redatado de la informoj de via konto"
   "read:blocks": "Vidi vian liston de uzantoj blokitaj"
   "write:blocks": "Redakti vian liston de blokitoj"
@@ -796,8 +793,8 @@ _permissions:
   "write:drive": "Ĉia operacio por skribi, forviŝi, aŭ alimaniere ŝanĝi la informon de dosiero en via disko de Misskey"
   "read:favorites": "Vidi vian liston de preferaĵoj"
   "write:favorites": "Redakti vian liston de preferaĵoj"
-  "read:following": "Vidi la infomojn pri la sekvadoj pri vi"
-  "write:following": "Sekvi/Malsekvi alian uzanton"
+  "read:following": "Vidi la informojn de sekvo"
+  "write:following": "Sekvi/ Ĉesi sekvi alian uzanton"
   "read:messaging": "Vidi viajn retbabiladojn"
   "write:messaging": "Administri viajn retbabiladojn"
   "read:mutes": "Vidi vian liston de silentigitoj"
@@ -812,7 +809,7 @@ _permissions:
   "read:channels": "Vidi kanalojn"
 _antennaSources:
   all: "Ĉiuj notoj"
-  homeTimeline: "Notoj de uzantoj kiujn vi sekvas"
+  homeTimeline: "Notoj de la uzantoj kiujn vi sekvas"
 _weekday:
   sunday: "Dimanĉo"
   monday: "Lundo"
@@ -856,18 +853,19 @@ _visibility:
 _postForm:
   replyPlaceholder: "Respondi la noton…"
   quotePlaceholder: "Citi la noton…"
-  channelPlaceholder: "Mencii en la kanalo…"
+  channelPlaceholder: "Afiŝi en la kanalo…"
   _placeholders:
     a: "Kiel vi fartas?"
     b: "Kio okazis ĉirkaŭ vi?"
-    c: "Kion vi pensas?"
-    d: "Kion vi parolos?"
-    e: "Komencu skribi…"
+    c: "Kio estas sur via penso?"
+    d: "Kion vi volas diri?"
+    e: "Komencu skribi tie"
+    f: "Atendanta de vi skribon…"
 _profile:
   name: "Nomo"
   username: "Uzantnomo"
   description: "Sinprezento"
-  metadata: "Kromaj informoj"
+  metadata: "Kromaj Informoj"
   metadataEdit: "Redakti kromajn informojn"
   changeAvatar: "Ŝanĝi profilbildon"
   changeBanner: "Ŝanĝi standardon"
@@ -888,42 +886,18 @@ _timelines:
   local: "Loka"
   social: "Sociala"
   global: "Malloka"
-_rooms:
-  translate: "Movi"
-  chooseImage: "Elekti bildon"
-  _roomType:
-    default: "Implicitaĵo"
-  _furnitures:
-    bed: "Lito"
-    low-table: "Malaltotablo"
-    desk: "Skribotablo"
-    chair: "Seĝo"
-    chair2: "Seĝo 2"
-    pc: "Komputilo"
-    eraser: "Skrapileto"
-    pencil: "Krajono"
-    pudding: "Flaŭno"
-    book: "Libro"
-    book2: "Libro 2"
-    piano: "Piano"
-    facial-tissue: "Tualetpaperejo"
-    server: "Servilo"
-    moon: "Luno"
-    monitor: "Monitoro"
-    keyboard: "Klavaro"
-    doll-ai: "Pupa Ai"
 _pages:
   newPage: "Krei novan paĝon"
   editPage: "Redakti paĝon"
   deleted: "Oni forviŝis la paĝon."
   editThisPage: "Redakti la paĝon"
-  viewPage: "Vidi viajn paĝojn"
+  viewPage: "Vidi paĝojn"
   my: "Miaj paĝoj"
   featured: "Ravaĵoj"
   contents: "Enhavo"
-  content: "Paĝo en bloko"
+  content: "Bloko de paĝo"
   title: "Temlinio"
-  url: "URL de paĝo"
+  url: "URL de la paĝo"
   alignCenter: "Centrigi"
   hideTitleWhenPinned: "Kaŝi la titolon de la paĝo kiam alpinglita"
   chooseBlock: "Aldoni blokon"
@@ -1039,7 +1013,7 @@ _notification:
   youWereInvitedToGroup: "Invitita al grupo"
   _types:
     all: "Ĉio"
-    follow: "Novaj sekvatoj"
+    follow: "Novaj sekvantoj"
     mention: "Mencioj"
     reply: "Respondoj"
     renote: "Plusendoj"
diff --git a/locales/es-ES.yml b/locales/es-ES.yml
index a8b2f5b720..a9339acf7b 100644
--- a/locales/es-ES.yml
+++ b/locales/es-ES.yml
@@ -81,6 +81,8 @@ somethingHappened: "Ocurrió un error"
 retry: "Reintentar"
 pageLoadError: "Error al leer la página"
 pageLoadErrorDescription: "Normalmente es debido a la red o al caché del navegador. Por favor limpie el caché o intente más tarde."
+serverIsDead: "No hay respuesta del servidor. Espere un momento y vuelva a intentarlo."
+youShouldUpgradeClient: "Para ver esta página, por favor refrezca el navegador y utiliza una versión más reciente del cliente."
 enterListName: "Ingrese nombre de lista"
 privacy: "Privacidad"
 makeFollowManuallyApprove: "Aprobar manualmente las solicitudes de seguimiento"
@@ -104,6 +106,7 @@ clickToShow: "Click para ver"
 sensitive: "Marcado como sensible"
 add: "Agregar"
 reaction: "Reacción"
+reactionSetting: "Reacciones para mostrar en el menú de reacciones"
 reactionSettingDescription2: "Arrastre para reordenar, click para borrar, apriete la tecla + para añadir."
 rememberNoteVisibility: "Recordar visibilidad"
 attachCancel: "Quitar adjunto"
@@ -239,7 +242,6 @@ uploadFromUrlDescription: "URL del fichero que quieres subir"
 uploadFromUrlRequested: "Subida solicitada"
 uploadFromUrlMayTakeTime: "Subir el fichero puede tardar un tiempo."
 explore: "Explorar"
-games: "Misskey Games"
 messageRead: "Ya leído"
 noMoreHistory: "El historial se ha acabado"
 startMessaging: "Iniciar chat"
@@ -533,7 +535,6 @@ yourAccountSuspendedDescription: "Esta cuenta ha sido suspendida debido a violac
 menu: "Menú"
 divider: "Divisor"
 addItem: "Agregar elemento"
-rooms: "Cuartos"
 relays: "Relés"
 addRelay: "Agregar relé"
 inboxUrl: "Inbox URL"
@@ -589,6 +590,7 @@ smtpSecure: "Usar SSL/TLS implícito en la conexión SMTP"
 smtpSecureInfo: "Apagar cuando se use STARTTLS"
 testEmail: "Prueba de envío"
 wordMute: "Silenciar palabras"
+instanceMute: "Instancias silenciadas"
 userSaysSomething: "{name} dijo algo"
 makeActive: "Activar"
 display: "Apariencia"
@@ -663,7 +665,6 @@ emailVerified: "Su dirección de correo electrónico ha sido verificada."
 noteFavoritesCount: "Número de notas favoritas"
 pageLikesCount: "Número de favoritos en la página"
 pageLikedCount: "Número de favoritos de su página"
-reversiCount: "Numero de partidas Reversi"
 contact: "Contacto"
 useSystemFont: "Utilizar la tipografía por defecto del sistema"
 clips: "Clip"
@@ -707,12 +708,27 @@ usageAmount: "Uso"
 capacity: "Capacidad"
 inUse: "Usado"
 editCode: "Editar código"
+apply: "Aplicar"
+publish: "Publicar"
+inChannelSearch: "Buscar en el canal"
+markAllAsRead: "Marcar todo como leído"
 goBack: "Deseleccionar"
 info: "Información"
+online: "En línea"
+offline: "Sin conexión"
 user: "Usuarios"
 administration: "Administrar"
+gallery: "Galería"
+recentPosts: "Posts recientes"
+popularPosts: "Más vistos"
 expiration: "Termina el"
+high: "Alta"
 middle: "Mediano"
+low: "Baja"
+emailNotConfiguredWarning: "No se ha configurado una dirección de correo electrónico."
+ratio: "Proporción"
+previewNoteText: "Mostrar vista preliminar"
+customCss: "CSS personalizado"
 customCssWarn: "Este ajuste sólo debe utilizarse si se sabe lo que hace. Introducir valores inadecuados puede hacer que el cliente deje de funcionar con normalidad."
 global: "Global"
 squareAvatars: "Mostrar iconos cuadrados"
@@ -735,13 +751,28 @@ pubSub: "Cuentas Pub/Sub"
 lastCommunication: "Última comunicación"
 resolved: "Resuelto"
 unresolved: "Sin resolver"
+itsOn: "¡Está encendido!"
+itsOff: "¡Está apagado!"
+emailRequiredForSignup: "Se requere una dirección de correo electrónico para el registro de la cuenta"
+unread: "No leído"
+filter: "Filtro"
 controlPanel: "Panel de control"
+manageAccounts: "Administrar cuenta"
+makeReactionsPublic: "Hacer el historial de reacciones público"
+makeReactionsPublicDescription: "Todas las reacciones que hayas hecho serán públicamente visibles."
+classic: "Clásico"
+muteThread: "Ocultar hilo"
+unmuteThread: "Mostrar hilo"
+ffVisibility: "Visibilidad de seguidores y seguidos"
 hide: "Ocultar"
+_ffVisibility:
+  public: "Publicar"
 _accountDelete:
   accountDelete: "Eliminar Cuenta"
 _ad:
   back: "Deseleccionar"
 _gallery:
+  my: "Mi galería"
   unlike: "Quitar me gusta"
 _email:
   _follow:
@@ -768,39 +799,6 @@ _mfm:
   flipDescription: "Voltea el contenido hacia arriba / abajo o hacia la izquierda / derecha."
   font: "Fuente"
   rotate: "Rotar"
-_reversi:
-  reversi: "Reversi"
-  gameSettings: "Configuración del juego"
-  chooseBoard: "Elegir tablero"
-  blackOrWhite: "Blancas/Negras"
-  blackIs: "{name} juega con fichas negras"
-  rules: "Reglas"
-  botSettings: "Opciones del bot"
-  thisGameIsStartedSoon: "El juego empezará en segundos"
-  waitingForOther: "Esperando el turno del adversario"
-  waitingForMe: "Esperando mi turno"
-  waitingBoth: "Prepárate"
-  ready: "Listo"
-  cancelReady: "No estoy listo"
-  opponentTurn: "Turno del adversario"
-  myTurn: "Mi turno"
-  turnOf: "Turno de {name}"
-  pastTurnOf: "Turno de {name}"
-  surrender: "Rendirse"
-  surrendered: "Por rendirse"
-  drawn: "Empate"
-  won: "{name} ha ganado"
-  black: "Negro"
-  white: "Blanco"
-  total: "Total"
-  turnCount: "Turno {count}"
-  myGames: "Mis juegos"
-  allGames: "Todos los juegos"
-  ended: "Finalizado"
-  playing: "Jugando"
-  isLlotheo: "El que tenga menos fichas gana (LLoTheO)"
-  loopedMap: "Mapa en bucle"
-  canPutEverywhere: "Puedes colocar donde quieras"
 _instanceTicker:
   none: "No mostrar"
   remote: "Mostrar a usuarios remotos"
@@ -820,6 +818,8 @@ _channel:
   usersCount: "{n} participantes"
   notesCount: "{n} notas"
 _menuDisplay:
+  sideFull: "Horizontal"
+  sideIcon: "Horizontal (ícono)"
   hide: "Ocultar"
 _wordMute:
   muteWords: "Palabras que silenciar"
@@ -830,6 +830,11 @@ _wordMute:
   soft: "Suave"
   hard: "Duro"
   mutedNotes: "Notas silenciadas"
+_instanceMute:
+  instanceMuteDescription: "Silencia todas las notas y reposts de la instancias seleccionadas, incluyendo respuestas a los usuarios de las mismas"
+  instanceMuteDescription2: "Separar por líneas"
+  title: "Oculta las notas de las instancias listadas."
+  heading: "Instancias a silenciar"
 _theme:
   explore: "Explorar temas"
   install: "Instalar tema"
@@ -1116,68 +1121,6 @@ _timelines:
   local: "Local"
   social: "Social"
   global: "Global"
-_rooms:
-  roomOf: "Cuarto de {user}"
-  addFurniture: "Colocar muebles"
-  translate: "Mover"
-  rotate: "Rotar"
-  exit: "Deseleccionar"
-  remove: "Quitar"
-  clear: "Quitar todo"
-  clearConfirm: "¿Quiere quitar todos los muebles?"
-  leaveConfirm: "Hay modificaciones sin guardar. ¿Desea irse?"
-  chooseImage: "Escoger una imagen"
-  roomType: "Estilo de cuarto"
-  carpetColor: "Color de piso"
-  _roomType:
-    default: "Predeterminado"
-    washitsu: "Estilo japonés"
-  _furnitures:
-    milk: "Cartón de leche"
-    bed: "Cama"
-    low-table: "Mesa chica"
-    desk: "Escritorio"
-    chair: "Silla"
-    chair2: "Silla 2"
-    fan: "Ventilador"
-    pc: "Computadora"
-    plant: "Planta decorativa"
-    plant2: "Planta decorativa 2"
-    eraser: "Goma de borrar"
-    pencil: "lápiz"
-    pudding: "Pudín"
-    cardboard-box: "Caja de cartón"
-    cardboard-box2: "Caja de cartón 2"
-    cardboard-box3: "Caja de cartón 3"
-    book: "Libro"
-    book2: "Libro 2"
-    piano: "Piano"
-    facial-tissue: "Caja de pañuelos"
-    server: "Servidor"
-    moon: "Luna"
-    corkboard: "Pizarra de corcho"
-    mousepad: "Alfombrilla de ratón"
-    monitor: "Monitor"
-    keyboard: "Teclado"
-    carpet-stripe: "Alfombra (a rayas)"
-    mat: "Tapete"
-    color-box: "Caja de colores"
-    wall-clock: "Reloj de pared"
-    photoframe: "Fotograma"
-    cube: "Cubo"
-    tv: "Televisor"
-    pinguin: "Pinguino"
-    rubik-cube: "Cubo rubik"
-    poster-h: "Poster (horizontal)"
-    poster-v: "Poster (vertical)"
-    sofa: "Sillón"
-    spiral: "Escalera en espiral"
-    bin: "Papelera"
-    cup-noodle: "Taza de sopa de fideos"
-    holo-display: "Poster holográfico"
-    energy-drink: "Bebida energética"
-    doll-ai: "Muñeca"
-    banknote: "Billetes"
 _pages:
   newPage: "Crear página"
   editPage: "Editar página"
diff --git a/locales/fr-FR.yml b/locales/fr-FR.yml
index d7e436492d..58dd000ccc 100644
--- a/locales/fr-FR.yml
+++ b/locales/fr-FR.yml
@@ -19,7 +19,7 @@ noNotifications: "Aucune notification"
 instance: "Instance"
 settings: "Paramètres"
 basicSettings: "Paramètres généraux"
-otherSettings: "Autres paramètres"
+otherSettings: "Paramètres avancés"
 openInWindow: "Ouvrir dans une nouvelle fenêtre"
 profile: "Profil"
 timeline: "Fil"
@@ -106,6 +106,7 @@ clickToShow: "Cliquer pour afficher"
 sensitive: "Contenu sensible"
 add: "Ajouter"
 reaction: "Réactions"
+reactionSetting: "Réactions à afficher dans le sélecteur de réactions"
 reactionSettingDescription2: "Déplacer pour réorganiser, cliquer pour effacer, utiliser « + » pour ajouter."
 rememberNoteVisibility: "Activer l'option \" se souvenir de la visibilité des notes \" vous permet de réutiliser automatiquement la visibilité utilisée lors de la publication de votre note précédente."
 attachCancel: "Supprimer le fichier attaché"
@@ -241,7 +242,6 @@ uploadFromUrlDescription: "URL du fichier que vous souhaitez téléverser"
 uploadFromUrlRequested: "Téléversement demandé"
 uploadFromUrlMayTakeTime: "Le téléversement de votre fichier peut prendre un certain temps."
 explore: "Découvrir"
-games: "Jeux de Misskey"
 messageRead: "Lu"
 noMoreHistory: "Il n’y a plus d’historique"
 startMessaging: "Commencer à discuter"
@@ -535,7 +535,6 @@ yourAccountSuspendedDescription: "Ce compte est suspendu car vous avez enfreint
 menu: "Menu"
 divider: "Séparateur"
 addItem: "Ajouter un élément"
-rooms: "Chambre"
 relays: "Relais"
 addRelay: "Ajouter un relais"
 inboxUrl: "Inbox URL"
@@ -591,6 +590,7 @@ smtpSecure: "Utiliser SSL/TLS implicitement dans les connexions SMTP"
 smtpSecureInfo: "Désactiver cette option lorsque STARTTLS est utilisé"
 testEmail: "Tester la distribution de courriel"
 wordMute: "Filtre de mots"
+instanceMute: "Instance en sourdine"
 userSaysSomething: "{name} a dit quelque chose"
 makeActive: "Activer"
 display: "Affichage"
@@ -618,6 +618,9 @@ reportAbuse: "Signaler"
 reportAbuseOf: "Signaler {name}"
 fillAbuseReportDescription: "Veuillez expliquer les raisons du signalement. S'il s'agit d'une note précise, veuillez en donner le lien."
 abuseReported: "Le rapport est envoyé. Merci."
+reporteeOrigin: "Origine du signalement"
+reporterOrigin: "Signalé par"
+forwardReport: "Transférer le signalement à l’instance distante"
 send: "Envoyer"
 abuseMarkAsResolved: "Marquer le signalement comme résolu"
 openInNewTab: "Ouvrir dans un nouvel onglet"
@@ -665,7 +668,6 @@ emailVerified: "Votre adresse e-mail a été vérifiée."
 noteFavoritesCount: "Nombre de notes dans les favoris"
 pageLikesCount: "Nombre de pages aimées"
 pageLikedCount: "Nombre de vos pages aimées"
-reversiCount: "Nombre de parties de Reversi"
 contact: "Contact"
 useSystemFont: "Utiliser la police par défaut du système"
 clips: "Clips"
@@ -680,6 +682,7 @@ center: "Centrer"
 wide: "Large"
 narrow: "Condensé"
 reloadToApplySetting: "Vos paramètres seront appliqués lorsque vous rechargerez la page. Souhaitez-vous recharger ?"
+needReloadToApply: "Ce paramètre s'appliquera après un rechargement."
 showTitlebar: "Afficher la barre de titre"
 clearCache: "Vider le cache"
 onlineUsersCount: "{n} utilisateur(s) en ligne"
@@ -788,6 +791,7 @@ pubSub: "Comptes Pub/Sub"
 lastCommunication: "Dernière communication"
 resolved: "Résolu"
 unresolved: "En attente"
+breakFollow: "Ne plus suivre"
 itsOn: "Activé"
 itsOff: "Désactivé"
 emailRequiredForSignup: "Une adresse e-mail est nécessaire pour créer un compte"
@@ -795,9 +799,22 @@ unread: "Non lu"
 filter: "Filtre"
 controlPanel: "Panneau de contrôle"
 manageAccounts: "Gérer les comptes"
+makeReactionsPublic: "Rendre les réactions publiques"
+makeReactionsPublicDescription: "Ceci rendra la liste de toutes vos réactions données publique."
 classic: "Classique"
+muteThread: "Mettre ce thread en sourdine"
+ffVisibility: "Visibilité des abonnés/abonnements"
+ffVisibilityDescription: "Permet de configurer qui peut voir les personnes que tu suis et les personnes qui te suivent."
+continueThread: "Afficher la suite du fil"
+deleteAccountConfirm: "Votre compte sera supprimé. Êtes vous certain ?"
+incorrectPassword: "Le mot de passe est incorrect."
 hide: "Masquer"
+leaveGroup: "Quitter le groupe"
+leaveGroupConfirm: "Êtes vous sûr de vouloir quitter \"{name}\" ?"
+welcomeBackWithName: "Heureux de vous revoir, {name}"
+clickToFinishEmailVerification: "Veuillez cliquer sur [{ok}] afin de compléter la vérification par courriel."
 _emailUnavailable:
+  used: "Non disponible"
   format: "Le format de cette adresse de courriel est invalide"
   mx: "Ce serveur de courriels est invalide"
   smtp: "Ce serveur de courriels ne répond pas"
@@ -919,39 +936,6 @@ _mfm:
   sparkle: "Paillettes"
   sparkleDescription: "Ajoute un effet scintillant au contenu."
   rotate: "Pivoter"
-_reversi:
-  reversi: "Reversi"
-  gameSettings: "Réglages de la partie"
-  chooseBoard: "Choix du plateau"
-  blackOrWhite: "Pions blancs/Pions noirs"
-  blackIs: "{name} joue les pions noirs"
-  rules: "Règles"
-  botSettings: "Options du bot"
-  thisGameIsStartedSoon: "La partie commencera dans quelques secondes"
-  waitingForOther: "En attente que l'adversaire soit prêt"
-  waitingForMe: "En attente que vous soyez prêt"
-  waitingBoth: "Préparez-vous"
-  ready: "Prêt"
-  cancelReady: "Recommencer la préparation"
-  opponentTurn: "Tour de l’adversaire"
-  myTurn: "C’est votre tour"
-  turnOf: "Tour de {name}"
-  pastTurnOf: "Tour de {name}"
-  surrender: "Abandonner"
-  surrendered: "Par abandon"
-  drawn: "Match nul"
-  won: "{name} a gagné"
-  black: "Noirs"
-  white: "Blancs"
-  total: "Total"
-  turnCount: "Tour {count}"
-  myGames: "Mes parties"
-  allGames: "Toutes les parties"
-  ended: "Fin de partie"
-  playing: "En cours"
-  isLlotheo: "Celui ou celle qui a le moins de pièces gagne (Llotheo)"
-  loopedMap: "Carte en boucle"
-  canPutEverywhere: "Les pions peuvent être placés partout "
 _instanceTicker:
   none: "Cacher "
   remote: "Montrer pour les utilisateur·ice·s distant·e·s"
@@ -984,6 +968,8 @@ _wordMute:
   soft: "Doux"
   hard: "Strict"
   mutedNotes: "Notes filtrées"
+_instanceMute:
+  heading: "Instances à mettre en sourdine"
 _theme:
   explore: "Explorer les thèmes"
   install: "Installer un thème"
@@ -1066,8 +1052,6 @@ _sfx:
   chatBg: "Discussion (arrière-plan)"
   antenna: "Réception de l’antenne"
   channel: "Notifications de canal"
-  reversiPutBlack: "Reversi : les pions noirs ont joué"
-  reversiPutWhite: "Reversi : les pions blancs ont joué"
 _ago:
   unknown: "Inconnu"
   future: "Futur"
@@ -1257,6 +1241,7 @@ _exportOrImport:
   muteList: "Comptes masqués"
   blockingList: "Comptes bloqués"
   userLists: "Listes"
+  excludeInactiveUsers: "Exclure les utilisateur·rice·s inactifs"
 _charts:
   federationInstancesIncDec: "Variation du nombre d'instances fédérées"
   federationInstancesTotal: "Nombre total d'instances fédérées"
@@ -1288,68 +1273,6 @@ _timelines:
   local: "Local"
   social: "Social"
   global: "Global"
-_rooms:
-  roomOf: "Chambre de {user}"
-  addFurniture: "Placer des meubles"
-  translate: "Déplacer"
-  rotate: "Pivoter"
-  exit: "Retour"
-  remove: "Enlever"
-  clear: "Tout enlever"
-  clearConfirm: "Souhaitez-vous enlever tous les meubles de votre chambre ?"
-  leaveConfirm: "Vous avez des modifications non-sauvegardées. Voulez-vous vraiment quitter ?"
-  chooseImage: "Sélectionnez une image"
-  roomType: "Type de chambre"
-  carpetColor: "Couleur du tapis"
-  _roomType:
-    default: "Par défaut"
-    washitsu: "Style japonnais"
-  _furnitures:
-    milk: "Brique de lait"
-    bed: "Lit"
-    low-table: "Table basse"
-    desk: "Bureau"
-    chair: "Chaise"
-    chair2: "Chaise 2"
-    fan: "Ventilateur"
-    pc: "Ordinateur"
-    plant: "Plante d’intérieur"
-    plant2: "Plante d’intérieur 2"
-    eraser: "Gomme"
-    pencil: "Crayon"
-    pudding: "Pudding"
-    cardboard-box: "Boîte en carton"
-    cardboard-box2: "Boîte en carton 2"
-    cardboard-box3: "Boîte en carton 3"
-    book: "Livre"
-    book2: "Livre 2"
-    piano: "Piano"
-    facial-tissue: "Boîte de mouchoirs"
-    server: "Serveurs"
-    moon: "Lune"
-    corkboard: "Tableau en liège"
-    mousepad: "Tapis de souris"
-    monitor: "Écran de contrôle"
-    keyboard: "Clavier"
-    carpet-stripe: "Tapis (zébré)"
-    mat: "Tapis"
-    color-box: "Étagère"
-    wall-clock: "Horloge murale"
-    photoframe: "Cadre photo"
-    cube: "Cube"
-    tv: "Télé"
-    pinguin: "Pingouin"
-    rubik-cube: "Cube de Rubik"
-    poster-h: "Affiche (horizontale)"
-    poster-v: "Affiche (verticale)"
-    sofa: "Canapé"
-    spiral: "Escaliers en spirale"
-    bin: "Corbeille"
-    cup-noodle: "Bol de nouilles"
-    holo-display: "Affichage holographique"
-    energy-drink: "Boisson énergétique"
-    doll-ai: "Poupée Ai"
-    banknote: "Billets de banque"
 _pages:
   newPage: "Créer une page"
   editPage: "Modifier une page"
diff --git a/locales/id-ID.yml b/locales/id-ID.yml
index f4f0ec8ce4..a1d52f6ef1 100644
--- a/locales/id-ID.yml
+++ b/locales/id-ID.yml
@@ -241,7 +241,6 @@ uploadFromUrlDescription: "URL berkas yang ingin kamu unggah"
 uploadFromUrlRequested: "Pengunggahan telah diminta"
 uploadFromUrlMayTakeTime: "Membutuhkan beberapa waktu hingga pengunggahan selesai"
 explore: "Jelajahi"
-games: "Permainan Misskey"
 messageRead: "Telah dibaca"
 noMoreHistory: "Tidak ada sejarah lagi"
 startMessaging: "Mulai mengirim pesan"
@@ -535,7 +534,6 @@ yourAccountSuspendedDescription: "Akun ini dibekukan karena melanggar ketentuan
 menu: "Menu"
 divider: "Pembagi"
 addItem: "Tambahkan item"
-rooms: "Ruang"
 relays: "Relay"
 addRelay: "Tambahkan relay"
 inboxUrl: "URL Kotak masuk"
@@ -667,7 +665,6 @@ emailVerified: "Surel telah diverifikasi"
 noteFavoritesCount: "Jumlah catatan yang difavoritkan"
 pageLikesCount: "Jumlah suka yang diterima Halaman"
 pageLikedCount: "Jumlah Halaman yang disukai"
-reversiCount: "Jumlah pertandingan Reversi"
 contact: "Kontak"
 useSystemFont: "Gunakan font bawaan sistem operasi"
 clips: "Klip"
@@ -934,39 +931,6 @@ _mfm:
   sparkleDescription: "Memberikan konten efek partikel kelap-kelip."
   rotate: "Putar"
   rotateDescription: "Putar konten sesuai sudut yang ditentukan."
-_reversi:
-  reversi: "Reversi"
-  gameSettings: "Pengaturan permainan"
-  chooseBoard: "Pilih papan"
-  blackOrWhite: "Hitam/Putih"
-  blackIs: "{name} bermain Hitam"
-  rules: "Peraturan"
-  botSettings: "Opsi bot"
-  thisGameIsStartedSoon: "Permainan akan mulai dalam beberapa detik"
-  waitingForOther: "Menunggu giliran lawan"
-  waitingForMe: "Menunggu giliran kamu"
-  waitingBoth: "Bersiap"
-  ready: "Siap"
-  cancelReady: "Batalkan siap"
-  opponentTurn: "Giliran lawan"
-  myTurn: "Giliran kamu"
-  turnOf: "Giliran {name}"
-  pastTurnOf: "Giliran {name}"
-  surrender: "Menyerah"
-  surrendered: "Karena menyerah"
-  drawn: "Seri"
-  won: "Kemenangan {name}"
-  black: "Hitam"
-  white: "Putih"
-  total: "Jumlah"
-  turnCount: "Giliran {count}"
-  myGames: "Rondeku"
-  allGames: "Semua ronde"
-  ended: "Selesai"
-  playing: "Sedang bermain"
-  isLlotheo: "Pemain dengan batu paling sedikitlah yang menang (Llotheo)"
-  loopedMap: "Peta melingkar"
-  canPutEverywhere: "Keping dapat ditaruh dimana saja"
 _instanceTicker:
   none: "Jangan tampilkan"
   remote: "Tampilkan untuk pengguna luar"
@@ -1081,8 +1045,6 @@ _sfx:
   chatBg: "Obrolan (Latar Belakang)"
   antenna: "Penerimaan Antenna"
   channel: "Pemberitahuan saluran"
-  reversiPutBlack: "Reversi: Hitam bergerak"
-  reversiPutWhite: "Reversi: Putih bergerak"
 _ago:
   unknown: "Tidak diketahui"
   future: "Masa depan"
@@ -1303,68 +1265,6 @@ _timelines:
   local: "Lokal"
   social: "Sosial"
   global: "Global"
-_rooms:
-  roomOf: "Ruangan {user}"
-  addFurniture: "Letakkan perabotan"
-  translate: "Pindah"
-  rotate: "Putar"
-  exit: "Kembali"
-  remove: "Hapus"
-  clear: "Bersihkan"
-  clearConfirm: "Apakah kamu yakin ingin menghapus semua perabotan di ruanganmu?"
-  leaveConfirm: "Ada perubahan yang belum tersimpan. Apakah kamu ingin pergi?"
-  chooseImage: "Pilih gambar"
-  roomType: "Tipe ruangan"
-  carpetColor: "Warna karpet"
-  _roomType:
-    default: "Bawaan"
-    washitsu: "Gaya Jepang"
-  _furnitures:
-    milk: "Kardus susu"
-    bed: "Tempat tidur"
-    low-table: "Meja pendek"
-    desk: "Meja tulis"
-    chair: "Kursi"
-    chair2: "Kursi 2"
-    fan: "Kipas angin"
-    pc: "Komputer"
-    plant: "Tanaman"
-    plant2: "Tanaman 2"
-    eraser: "Karet Penghapus"
-    pencil: "Pensil"
-    pudding: "Puding"
-    cardboard-box: "Kotak Kardus"
-    cardboard-box2: "Kotak Kardus 2"
-    cardboard-box3: "Kotak Kardus 3"
-    book: "Buku"
-    book2: "Buku 2"
-    piano: "Piano"
-    facial-tissue: "Tisu Wajah"
-    server: "Server"
-    moon: "Bulan"
-    corkboard: "Papan buletin"
-    mousepad: "Mousepad"
-    monitor: "Layar Monitor"
-    keyboard: "Papan tombol"
-    carpet-stripe: "Karpet (Bergaris)"
-    mat: "Keset"
-    color-box: "Rak buku"
-    wall-clock: "Jam dinding"
-    photoframe: "Bingkai foto"
-    cube: "Kubus"
-    tv: "Televisi"
-    pinguin: "Pinguin"
-    rubik-cube: "Rubik"
-    poster-h: "Poster (Horizontal)"
-    poster-v: "Poster (Vertical)"
-    sofa: "Sofa"
-    spiral: "Tangga spiral"
-    bin: "Tempat sampah"
-    cup-noodle: "Migelas"
-    holo-display: "Layar hologram"
-    energy-drink: "Minuman energi"
-    doll-ai: "Boneka Ai"
-    banknote: "Uang"
 _pages:
   newPage: "Buat halaman baru"
   editPage: "Sunting halaman"
diff --git a/locales/it-IT.yml b/locales/it-IT.yml
index ff5b01a585..d13e53625f 100644
--- a/locales/it-IT.yml
+++ b/locales/it-IT.yml
@@ -80,6 +80,9 @@ error: "Errore"
 somethingHappened: "Si è verificato un problema"
 retry: "Riprova"
 pageLoadError: "Caricamento pagina non riuscito. "
+pageLoadErrorDescription: "Questo viene normalmente causato dalla rete o dalla cache del browser. Si prega di pulire la cache, o di attendere e riprovare più tardi."
+serverIsDead: "Il server non risponde. Si prega di attendere e riprovare più tardi."
+youShouldUpgradeClient: "Per visualizzare la pagina è necessario aggiornare il client alla nuova versione e ricaricare."
 enterListName: "Nome della lista"
 privacy: "Privacy"
 makeFollowManuallyApprove: "Richiedi di approvare i follower manualmente"
@@ -103,6 +106,7 @@ clickToShow: "Clicca per visualizzare"
 sensitive: "Contenuto sensibile"
 add: "Aggiungi"
 reaction: "Reazione"
+reactionSetting: "Reazioni visualizzate sul pannello"
 reactionSettingDescription2: "Trascina per riorganizzare, clicca per cancellare, usa il pulsante \"+\" per aggiungere."
 rememberNoteVisibility: "Ricordare le impostazioni di visibilità delle note"
 attachCancel: "Rimuovi allegato"
@@ -132,6 +136,7 @@ emojiUrl: "URL dell'emoji"
 addEmoji: "Aggiungi un emoji"
 settingGuide: "Configurazione suggerita"
 cacheRemoteFiles: "Memorizzazione nella cache dei file remoti"
+cacheRemoteFilesDescription: "Disabilitando questa opzione, i file remoti verranno linkati direttamente senza essere memorizzati nella cache. Sarà possibile risparmiare spazio di archiviazione sul server, ma il traffico aumenterà in quanto non verranno generate anteprime."
 flagAsBot: "Io sono un robot"
 flagAsBotDescription: "Se l'account esegue principalmente operazioni automatiche, attiva quest'opzione. Quando attivata, opera come un segnalatore per gli altri sviluppatori allo scopo di prevenire catene d’interazione senza fine con altri bot, e di adeguare i sistemi interni di Misskey perché trattino questo account come un bot."
 flagAsCat: "Io sono un gatto"
@@ -148,12 +153,14 @@ searchWith: "Cerca: {q}"
 youHaveNoLists: "Non hai ancora creato nessuna lista"
 followConfirm: "Sei sicur@ di voler seguire {name}?"
 proxyAccount: "Account proxy"
+proxyAccountDescription: "Un account proxy è un account che funziona da follower remoto per gli utenti sotto certe condizioni. Ad esempio, quando un utente aggiunge un utente remoto alla lista, dato che se nessun utente locale segue quell'utente le sue attività non verranno distribuite, al suo posto lo seguirà un account proxy."
 host: "Server remoto"
 selectUser: "Seleziona utente"
 recipient: "Destinatario"
 annotation: "Descrizione"
 federation: "Federazione"
 instances: "Istanza"
+registeredAt: "Registrato presso"
 latestRequestSentAt: "Ultima richiesta inviata"
 latestRequestReceivedAt: "Ultima richiesta ricevuta"
 latestStatus: "Ultimo stato"
@@ -161,6 +168,7 @@ storageUsage: "Volume di dischi"
 charts: "Grafici"
 perHour: "All'ora"
 perDay: "al giorno"
+stopActivityDelivery: "Interrompi la distribuzione di attività"
 blockThisInstance: "Blocca l'istanza"
 operations: "Operazioni"
 software: "Software"
@@ -234,7 +242,6 @@ uploadFromUrlDescription: "URL del file che vuoi caricare"
 uploadFromUrlRequested: "Caricamento richiesto"
 uploadFromUrlMayTakeTime: "Il caricamento del file può richiedere tempo."
 explore: "Esplora"
-games: "Misskey Giochi"
 messageRead: "Visualizzato"
 noMoreHistory: "Non c'è più cronologia da visualizzare"
 startMessaging: "Nuovo messaggio"
@@ -315,11 +322,13 @@ registration: "Iscriviti"
 enableRegistration: "Permettere nuove registrazioni"
 invite: "Invita"
 proxyRemoteFiles: "Usare file remoti come proxy"
+proxyRemoteFilesDescription: "Attivando questa opzione i file remoti non salvati o cancellati perché eccedenti il limite di archiviazione verranno inoltrati tramite proxy, inclusa la generazione di anteprime. Non ha effetto sullo spazio di archiviazione del server."
 driveCapacityPerLocalAccount: "Volume del Drive per utente locale"
 driveCapacityPerRemoteAccount: "Volume del Drive per utente remoto"
 inMb: "in Megabytes"
 iconUrl: "URL di icona (favicon, ecc.)"
 bannerUrl: "URL dell'immagine d'intestazione"
+backgroundImageUrl: "URL dello sfondo"
 basicInfo: "Informazioni fondamentali"
 pinnedUsers: "Utenti in evidenza"
 pinnedUsersDescription: "Elenca gli/le utenti che vuoi fissare in cima alla pagina \"Esplora\", un@ per riga."
@@ -438,10 +447,12 @@ uiLanguage: "Lingua di visualizzazione dell'interfaccia"
 groupInvited: "Invitat@ al gruppo"
 aboutX: "Informazioni su {x}"
 useOsNativeEmojis: "Usare le emoji native del sistema operativo"
+disableDrawer: "Non mostrare il menù sul drawer"
 youHaveNoGroups: "Nessun gruppo"
 joinOrCreateGroup: "Puoi creare il tuo gruppo o essere invitat@ a gruppi che già esistono."
 noHistory: "Nessuna cronologia"
 signinHistory: "Cronologia di accesso all'account"
+disableAnimatedMfm: "Disabilità i MFM animati"
 doing: "In corso..."
 category: "Categoria"
 tags: "Tag"
@@ -469,12 +480,17 @@ showFeaturedNotesInTimeline: "Mostrare le note di tendenza nella tua timeline"
 objectStorage: "Stoccaggio oggetti"
 useObjectStorage: "Utilizza stoccaggio oggetti"
 objectStorageBaseUrl: "Base URL"
+objectStorageBaseUrlDesc: "URL di riferimento. In caso di utilizzo di proxy o CDN l'URL è 'https://<bucket>.s3.amazonaws.com' per S3, 'https://storage.googleapis.com/<bucket>' per GCS eccetera. "
 objectStorageBucket: "Bucket"
+objectStorageBucketDesc: "Specificare il nome del bucket utilizzato dal provider."
 objectStoragePrefix: "Prefix"
 objectStoragePrefixDesc: "I file saranno conservati sotto la directory di questo prefisso."
 objectStorageEndpoint: "Endpoint"
+objectStorageEndpointDesc: "Lasciare vuoto se si sta utilizzando S3. In caso contrario si prega di specificare l'endpoint come '<host>' oppure '<host>:<port>' a seconda del servizio utilizzato."
 objectStorageRegion: "Region"
+objectStorageRegionDesc: "Specificate una regione, quale 'xx-east-1'. Se il servizio in utilizzo non distingue tra regioni, lasciate vuoto o inserite 'us-east-1'."
 objectStorageUseSSL: "Usare SSL"
+objectStorageUseSSLDesc: "Disabilita quest'opzione se non utilizzi HTTPS per le connessioni API."
 objectStorageUseProxy: "Usa proxy"
 objectStorageUseProxyDesc: "Disabilita quest'opzione se non usi proxy per la connessione API."
 objectStorageSetPublicRead: "Imposta \"visibilità pubblica\" al momento di caricare"
@@ -504,6 +520,7 @@ sort: "Ordina per"
 ascendingOrder: "Ascendente"
 descendingOrder: "Discendente"
 scratchpad: "ScratchPad"
+scratchpadDescription: "Lo Scratchpad offre un ambiente per esperimenti di AiScript. È possibile scrivere, eseguire e confermare i risultati dell'interazione del codice con Misskey."
 output: "Uscita"
 script: "Script"
 disablePagesScript: "Disabilita AiScript nelle pagine"
@@ -514,9 +531,11 @@ removeAllFollowing: "Cancella tutti i follows"
 removeAllFollowingDescription: "Cancella tutti i follows del server {host}. Per favore, esegui se, ad esempio, l'istanza non esiste più."
 userSuspended: "L'utente è sospes@."
 userSilenced: "L'utente è silenziat@."
+yourAccountSuspendedTitle: "Questo account è sospeso."
+yourAccountSuspendedDescription: "Questo account è stato sospeso a causa di una violazione dei termini di servizio del server. Contattare l'amministrazione per i dettagli. Si prega di non creare un nuovo account."
+menu: "Menù"
 divider: "Linea di separazione"
 addItem: "Aggiungi elemento"
-rooms: "Camera"
 relays: "Ripetitori"
 addRelay: "Aggiungi ripetitore"
 inboxUrl: "Inbox URL"
@@ -541,6 +560,7 @@ manage: "Gestione"
 plugins: "Estensioni"
 deck: "Deck"
 undeck: "Esci dal deck"
+useBlurEffectForModal: "Utilizza effetto sfocatura per i modali"
 useFullReactionPicker: "Usa la totalità del pannello di reazioni"
 width: "Larghezza"
 height: "Altezza"
@@ -571,6 +591,7 @@ smtpSecure: "Usare la porta SSL/TLS implicito per le connessioni SMTP"
 smtpSecureInfo: "Disabilitare quando è attivo STARTTLS."
 testEmail: "Testare la consegna di posta elettronica"
 wordMute: "Filtri parole"
+instanceMute: "Silenzia l'istanza"
 userSaysSomething: "{name} ha detto qualcosa"
 makeActive: "Attiva"
 display: "Visualizza"
@@ -589,13 +610,18 @@ useGlobalSettingDesc: "Se abilitato, le impostazioni notifiche dell'account verr
 other: "Avanzate"
 regenerateLoginToken: "Genera di nuovo un token di connessione"
 regenerateLoginTokenDescription: "Genera un nuovo token di autenticazione. Solitamente questa operazione non è necessaria: quando si genera un nuovo token, tutti i dispositivi vanno disconnessi."
+setMultipleBySeparatingWithSpace: "È possibile creare multiple voci separate da spazi."
 fileIdOrUrl: "ID o URL del file"
 behavior: "Comportamento"
+sample: "Esempio"
 abuseReports: "Segnalazioni"
 reportAbuse: "Segnalazioni"
 reportAbuseOf: "Segnala {name}"
 fillAbuseReportDescription: "Si prega di spiegare il motivo della segnalazione. Se riguarda una nota precisa, si prega di collegare anche l'URL della nota."
 abuseReported: "La segnalazione è stata inviata. Grazie."
+reporter: "il corrispondente"
+reporteeOrigin: "Origine del segnalato"
+reporterOrigin: "Origine del segnalatore"
 send: "Inviare"
 abuseMarkAsResolved: "Contrassegna la segnalazione come risolta"
 openInNewTab: "Apri in una nuova scheda"
@@ -643,7 +669,6 @@ emailVerified: "Il tuo indirizzo email è stato verificato"
 noteFavoritesCount: "Conteggio note tra i preferiti"
 pageLikesCount: "Numero di pagine che ti piacciono"
 pageLikedCount: "Numero delle tue pagine che hanno ricevuto \"Mi piace\""
-reversiCount: "Numero di partite a Reversi"
 contact: "Contatti"
 useSystemFont: "Usa il carattere predefinito del sistema"
 clips: "Clip"
@@ -657,6 +682,7 @@ left: "Sinistra"
 center: "Centro"
 wide: "Largo"
 reloadToApplySetting: "Le tue preferenze verranno impostate dopo il ricaricamento della pagina. Vuoi ricaricare adesso?"
+needReloadToApply: "È necessario riavviare per rendere effettive le modifiche."
 showTitlebar: "Visualizza la barra del titolo"
 clearCache: "Svuota cache"
 onlineUsersCount: "{n} utenti online"
@@ -739,13 +765,65 @@ middle: "Media"
 low: "Bassa"
 emailNotConfiguredWarning: "Non hai impostato nessun indirizzo e-mail."
 ratio: "Rapporto"
+previewNoteText: "Anteprima del testo"
+customCss: "CSS personalizzato"
 global: "Federata"
+squareAvatars: "Mostra l'immagine del profilo come quadrato"
 sent: "Inviare"
+searchResult: "Risultati della Ricerca"
 hashtags: "Hashtag"
 troubleshooting: "Risoluzione problemi"
+useBlurEffect: "Utilizza effetto sfocatura per l'interfaccia utente"
+learnMore: "Più dettagli"
+misskeyUpdated: "Misskey è stato aggiornato!"
+whatIsNew: "Visualizza le informazioni sull'aggiornamento"
+translate: "Traduzione"
+translatedFrom: "Tradotto da {x}"
+accountDeletionInProgress: "La cancellazione dell'account è in corso"
+usernameInfo: "Un nome per identificare univocamente il tuo account sul server. È possibile utilizzare caratteri alfanumerici (a~z, A~Z, 0~9) e il trattino basso (_). Non sarà possibile cambiare il nome utente in seguito."
+aiChanMode: "Modalità Ai"
+keepCw: "Mantieni il CW"
+resolved: "Risolto"
+unresolved: "Non risolto"
+breakFollow: "Smetti di seguire"
+itsOn: "Abilitato"
+itsOff: "Disabilitato"
+emailRequiredForSignup: "È necessario un indirizzo mail per registrare un account"
+unread: "Non letto"
+filter: "Filtri"
+controlPanel: "Pannello di controllo"
+manageAccounts: "Gestisci account"
+classic: "Classico"
+muteThread: "Silenzia la discussione"
+unmuteThread: "Riattiva la discussione"
+deleteAccountConfirm: "L'account verrà cancellato. Procedere?"
+incorrectPassword: "La password è errata."
+voteConfirm: "Votare per「{choice}」?"
 hide: "Nascondere"
+leaveGroup: "Esci dal gruppo"
+leaveGroupConfirm: "Uscire da「{name}」?"
+useDrawerReactionPickerForMobile: "Mostra sul drawer da dispositivo mobile"
+welcomeBackWithName: "Bentornato/a, {name}"
+clickToFinishEmailVerification: "Fai click su [{ok}] per completare la verifica dell'indirizzo email."
+_emailUnavailable:
+  used: "Email già in uso"
+  format: "Formato email non valido"
+  disposable: "Email non riutilizzabile"
+  mx: "Server email non corretto"
+  smtp: "Il server email non risponde"
 _ffVisibility:
   public: "Pubblico"
+  followers: "Mostra solo ai follower"
+  private: "Invisibile"
+_signup:
+  almostThere: "Quasi completo"
+  emailAddressInfo: "Inserisci il tuo indirizzo email. Non verrà reso pubblico."
+_accountDelete:
+  accountDelete: "Cancellazione account"
+  sendEmail: "Al termine della cancellazione dell'account, verrà inviata una mail all'indirizzo a cui era registrato."
+  requestAccountDelete: "Richiesta di cancellazione account"
+  started: "Il processo di cancellazione è iniziato."
+  inProgress: "Cancellazione in corso"
 _ad:
   back: "Indietro"
   reduceFrequencyOfThisAd: "Visualizza questa pubblicità meno spesso"
@@ -801,19 +879,27 @@ _mfm:
   quote: "Cita il nota"
   emoji: "Emoji personalizzati"
   search: "Cerca"
+  flip: "Inverti"
+  jump: "Animazione(salto)"
+  jumpDescription: "Da un animazione che salta su e giù."
+  bounce: "Animazione(rimbalzo)"
+  bounceDescription: "Rende il testo rimbalzante"
+  shake: "rimbalzante"
+  shakeDescription: "Rende il testo traballante"
+  twitch: "testo"
+  twitchDescription: "Fa tremare il testo"
+  x2: "Più grande"
+  x2Description: "Mostra il contenuto ingrandito."
+  x3: "Molto più grande"
+  x3Description: "Mostra il contenuto molto più ingrandito."
+  x4: "Estremamente più grande"
+  x4Description: "Mostra il contenuto estremamente più ingrandito."
   blur: "Sfocatura"
+  blurDescription: "È possibile rendere sfocato il contenuto. Spostando il cursore su di esso tornerà visibile chiaramente."
   font: "Tipo di carattere"
   fontDescription: "Puoi scegliere il tipo di carattere per il contenuto."
   rainbow: "Arcobaleno"
   rotate: "Ruota"
-_reversi:
-  reversi: "Reversi"
-  gameSettings: "Impostazioni di gioco"
-  botSettings: "Opzioni del bot"
-  black: "Nero"
-  white: "Bianco"
-  total: "Totale"
-  ended: "Esci"
 _instanceTicker:
   none: "Nascondi"
   remote: "Mostra solo per gli/le utenti remotə"
@@ -865,6 +951,7 @@ _theme:
   func: "Funzione"
   funcKind: "Tipo di funzione"
   argument: "Argomento"
+  alpha: "Opacità"
   darken: "Scuro"
   lighten: "Chiaro"
   inputConstantName: "Inserisci un nome per la costante"
@@ -902,6 +989,7 @@ _theme:
     inputBorder: "Inquadra casella di testo"
     listItemHoverBg: "Sfondo della voce di elenco (sorvolato)"
     driveFolderBg: "Sfondo della cartella di disco"
+    badge: "Distintivo"
     messageBg: "Sfondo della chat"
 _sfx:
   note: "Nota"
@@ -1119,68 +1207,6 @@ _timelines:
   local: "Locale"
   social: "Sociale"
   global: "Federata"
-_rooms:
-  roomOf: "Camera di {user}"
-  addFurniture: "Disponi mobilia"
-  translate: "Sposta"
-  rotate: "Ruota"
-  exit: "Indietro"
-  remove: "Togli"
-  clear: "Rimuovi tutto"
-  clearConfirm: "Sei sicur@ di voler rimuovere tutti i mobili dalla tua camera?"
-  leaveConfirm: "Hai fatto modifiche ancora non salvate. Vuoi davvero uscire?"
-  chooseImage: "Seleziona immagine"
-  roomType: "Tipo di stanza"
-  carpetColor: "Colore del suolo"
-  _roomType:
-    default: "Predefinito"
-    washitsu: "Washitsu"
-  _furnitures:
-    milk: "Cartone del latte"
-    bed: "Letto"
-    low-table: "Tavolino"
-    desk: "Tavolo"
-    chair: "Sedia"
-    chair2: "Sedia 2"
-    fan: "Ventilatore"
-    pc: "Computer"
-    plant: "Pianta da appartamento"
-    plant2: "Pianta da appartamento2"
-    eraser: "Gomma"
-    pencil: "Matita"
-    pudding: "Pudding"
-    cardboard-box: "Scatola di cartone"
-    cardboard-box2: "Scatola di cartone 2"
-    cardboard-box3: "Scatola di cartone 3"
-    book: "Libro"
-    book2: "Libro2"
-    piano: "Pianoforte"
-    facial-tissue: "Scatola di fazzolettini"
-    server: "Server"
-    moon: "Luna"
-    corkboard: "Bacheca"
-    mousepad: "Tappetino per il mouse"
-    monitor: "Monitor "
-    keyboard: "Tastiera"
-    carpet-stripe: "Tappeto (a strisce)"
-    mat: "Zerbino"
-    color-box: "Libreria"
-    wall-clock: "Orologio da parete"
-    photoframe: "Cornice"
-    cube: "Cubo"
-    tv: "TV"
-    pinguin: "Pinguino"
-    rubik-cube: "Cubo di Rubik"
-    poster-h: "Poster (orizzontale)"
-    poster-v: "Poster (verticale)"
-    sofa: "Divano"
-    spiral: "Scale a chiocciola"
-    bin: "Cestino"
-    cup-noodle: "Noodle istantanei"
-    holo-display: "Visualizzazione olografica"
-    energy-drink: "Bevanda energetica"
-    doll-ai: "Bambola Ai"
-    banknote: "Mazzetta di banconote"
 _pages:
   newPage: "Crea pagina"
   editPage: "Modifica pagina"
@@ -1378,6 +1404,10 @@ _pages:
       string: "Testo"
       array: "Liste"
       stringArray: "Lista di testo"
+_relayStatus:
+  requesting: "In attesa di approvazione"
+  accepted: "Approvato"
+  rejected: "Respinto"
 _notification:
   fileUploaded: "File caricato correttamente"
   youGotMention: "{name} ti ha menzionato"
diff --git a/locales/ja-KS.yml b/locales/ja-KS.yml
index 25cc93b28a..45ab9684d2 100644
--- a/locales/ja-KS.yml
+++ b/locales/ja-KS.yml
@@ -239,7 +239,6 @@ uploadFromUrlDescription: "このURLのファイルをアップロードした
 uploadFromUrlRequested: "アップロードしたい言うといたで"
 uploadFromUrlMayTakeTime: "アップロード終わるんにちょい時間かかるかもしれへんわ。"
 explore: "みつける"
-games: "Misskey Games"
 messageRead: "もう読んだ"
 noMoreHistory: "これより過去の履歴はあらへんで"
 startMessaging: "チャットやるで"
@@ -515,7 +514,6 @@ removeAllFollowingDescription: "{host}からのフォローをすべて解除す
 userSuspended: "このユーザーは...凍結されとる。"
 userSilenced: "このユーザーは...サイレンスされとる。"
 divider: "分割線"
-rooms: "ルーム"
 relays: "リレー"
 addRelay: "リレーの追加"
 inboxUrl: "inboxのURL"
@@ -701,29 +699,6 @@ _mfm:
   blur: "ぼかし"
   font: "フォント"
   rotate: "回転"
-_reversi:
-  reversi: "リバーシ"
-  gameSettings: "対局の設定"
-  chooseBoard: "ボードを選択"
-  blackOrWhite: "先行/後攻"
-  blackIs: "{name}が黒(先行)"
-  rules: "ルール"
-  botSettings: "Botのオプション"
-  pastTurnOf: "{name}のターン"
-  surrender: "投了"
-  surrendered: "投了により"
-  drawn: "引き分け"
-  won: "{name}の勝ち"
-  black: "黒"
-  white: "白"
-  total: "合計"
-  turnCount: "{count}ターン目"
-  myGames: "自分の対局"
-  allGames: "みんなの対局"
-  ended: "終了"
-  playing: "対局中"
-  isLlotheo: "石の少ない方が勝ち(ロセオ)"
-  loopedMap: "ループマップ"
 _instanceTicker:
   none: "表示せん"
   remote: "リモートユーザーに表示"
@@ -936,68 +911,6 @@ _timelines:
   local: "ローカル"
   social: "ソーシャル"
   global: "グローバル"
-_rooms:
-  roomOf: "{user}のルーム"
-  addFurniture: "家具を置く"
-  translate: "移動"
-  rotate: "回転"
-  exit: "戻る"
-  remove: "しまう"
-  clear: "片付け"
-  clearConfirm: "家具ぜんぶしまうけど、ホンマにええん?"
-  leaveConfirm: "未保存の変更があるけど、移動してええか?"
-  chooseImage: "画像を選ぶ"
-  roomType: "部屋のタイプ"
-  carpetColor: "床の色"
-  _roomType:
-    default: "デフォルト"
-    washitsu: "和室"
-  _furnitures:
-    milk: "牛乳パック"
-    bed: "ベッド"
-    low-table: "ローテーブル"
-    desk: "デスク"
-    chair: "チェア"
-    chair2: "チェア2"
-    fan: "換気扇"
-    pc: "パソコン"
-    plant: "観葉植物"
-    plant2: "観葉植物2"
-    eraser: "消しゴム"
-    pencil: "鉛筆"
-    pudding: "プリン"
-    cardboard-box: "段ボール箱"
-    cardboard-box2: "段ボール箱2"
-    cardboard-box3: "段ボール箱3"
-    book: "本"
-    book2: "本2"
-    piano: "ピアノ"
-    facial-tissue: "ティッシュボックス"
-    server: "サーバー"
-    moon: "月"
-    corkboard: "コルクボード"
-    mousepad: "マウスパッド"
-    monitor: "モニター"
-    keyboard: "キーボード"
-    carpet-stripe: "カーペット(縞)"
-    mat: "マット"
-    color-box: "カラーボックス"
-    wall-clock: "壁掛け時計"
-    photoframe: "額縁"
-    cube: "キューブ"
-    tv: "テレビ"
-    pinguin: "ピンギン"
-    rubik-cube: "ルービックキューブ"
-    poster-h: "ルービックキューブ"
-    poster-v: "ポスター(縦長)"
-    sofa: "ソファ"
-    spiral: "螺旋階段"
-    bin: "ゴミ箱"
-    cup-noodle: "カップ麺"
-    holo-display: "ホログラフィックディスプレイ"
-    energy-drink: "エナジードリンク"
-    doll-ai: "藍ちゃん人形"
-    banknote: "札束"
 _pages:
   newPage: "ページを作る"
   editPage: "ページの編集"
diff --git a/locales/ko-KR.yml b/locales/ko-KR.yml
index 1fee5f5728..7451603a66 100644
--- a/locales/ko-KR.yml
+++ b/locales/ko-KR.yml
@@ -241,7 +241,6 @@ uploadFromUrlDescription: "업로드하려는 파일의 URL"
 uploadFromUrlRequested: "업로드를 요청했습니다"
 uploadFromUrlMayTakeTime: "업로드가 완료될 때까지 시간이 소요될 수 있습니다."
 explore: "발견하기"
-games: "Misskey Games"
 messageRead: "읽음"
 noMoreHistory: "이것보다 과거의 기록이 없습니다"
 startMessaging: "대화 시작하기"
@@ -535,7 +534,6 @@ yourAccountSuspendedDescription: "이 계정은 서버의 이용 약관을 위
 menu: "메뉴"
 divider: "구분선"
 addItem: "항목 추가"
-rooms: "방"
 relays: "릴레이"
 addRelay: "릴레이 추가"
 inboxUrl: "Inbox 주소"
@@ -668,7 +666,6 @@ emailVerified: "메일 주소가 확인되었습니다."
 noteFavoritesCount: "즐겨찾기한 노트 수"
 pageLikesCount: "좋아요 한 Page 수"
 pageLikedCount: "Page에 받은 좋아요 수"
-reversiCount: "리버시 대국 횟수"
 contact: "연락처"
 useSystemFont: "시스템 기본 글꼴을 사용"
 clips: "클립"
@@ -936,39 +933,6 @@ _mfm:
   sparkleDescription: "반짝이는 파티클 효과를 추가합니다."
   rotate: "회전"
   rotateDescription: "지정한 각도로 회전시킵니다."
-_reversi:
-  reversi: "리버시"
-  gameSettings: "대국 설정"
-  chooseBoard: "보드 선택"
-  blackOrWhite: "선공/후공"
-  blackIs: "{name}님이 흑(선공)"
-  rules: "규칙"
-  botSettings: "Bot 설정"
-  thisGameIsStartedSoon: "잠시 후에 대국이 시작됩니다"
-  waitingForOther: "상대의 준비가 완료될 때까지 기다리고 있습니다"
-  waitingForMe: "당신의 준비 완료를 기다리고 있습니다"
-  waitingBoth: "준비해 주세요"
-  ready: "준비 완료"
-  cancelReady: "준비 취소"
-  opponentTurn: "상대의 차례입니다"
-  myTurn: "당신의 차례입니다"
-  turnOf: "{name}님의 차례입니다"
-  pastTurnOf: "{name}님의 차례"
-  surrender: "기권"
-  surrendered: "기권에 의해"
-  drawn: "무승부"
-  won: "{name}님의 승리"
-  black: "흑"
-  white: "백"
-  total: "합계"
-  turnCount: "{count}턴 째"
-  myGames: "내 대국"
-  allGames: "모두의 대국"
-  ended: "종료"
-  playing: "지금 대국 중"
-  isLlotheo: "돌이 적은 사람이 승리 (llotheo)"
-  loopedMap: "루프 지도"
-  canPutEverywhere: "어디에나 놓을 수 있음"
 _instanceTicker:
   none: "보이지 않음"
   remote: "리모트 유저에게만 보이기"
@@ -1088,8 +1052,6 @@ _sfx:
   chatBg: "대화 (백그라운드)"
   antenna: "안테나 수신"
   channel: "채널 알림"
-  reversiPutBlack: "리버시: 흑돌을 두었을 때"
-  reversiPutWhite: "리버시: 백돌을 두었을 때"
 _ago:
   unknown: "알 수 없음"
   future: "미래"
@@ -1312,68 +1274,6 @@ _timelines:
   local: "로컬"
   social: "소셜"
   global: "글로벌"
-_rooms:
-  roomOf: "{user}의 방"
-  addFurniture: "가구를 배치"
-  translate: "이동"
-  rotate: "회전"
-  exit: "뒤로"
-  remove: "치우기"
-  clear: "모두 치우기"
-  clearConfirm: "정말 방 안의 모든 가구를 치우시겠습니까?"
-  leaveConfirm: "저장되지 않은 변경 사항이 있습니다. 정말 나가시겠습니까?"
-  chooseImage: "이미지 선택"
-  roomType: "방 스타일"
-  carpetColor: "바닥 색상"
-  _roomType:
-    default: "기본값"
-    washitsu: "일본식"
-  _furnitures:
-    milk: "우유 팩"
-    bed: "침대"
-    low-table: "낮은 테이블"
-    desk: "책상"
-    chair: "의자"
-    chair2: "의자 2"
-    fan: "환기구"
-    pc: "컴퓨터"
-    plant: "관엽식물"
-    plant2: "관엽식물 2"
-    eraser: "지우개"
-    pencil: "연필"
-    pudding: "푸딩"
-    cardboard-box: "골판지 상자"
-    cardboard-box2: "골판지 상자 2"
-    cardboard-box3: "골판지 상자 3"
-    book: "책"
-    book2: "책 2"
-    piano: "피아노"
-    facial-tissue: "휴지 상자"
-    server: "서버"
-    moon: "달"
-    corkboard: "게시판"
-    mousepad: "마우스 패드"
-    monitor: "모니터"
-    keyboard: "키보드"
-    carpet-stripe: "카페트 (줄무늬)"
-    mat: "매트"
-    color-box: "책장"
-    wall-clock: "벽걸이 시계"
-    photoframe: "액자"
-    cube: "큐브"
-    tv: "TV"
-    pinguin: "펭귄"
-    rubik-cube: "루빅스 큐브"
-    poster-h: "포스터 (가로)"
-    poster-v: "포스터 (세로)"
-    sofa: "소파"
-    spiral: "나선형 계단"
-    bin: "휴지통"
-    cup-noodle: "컵라면"
-    holo-display: "홀로그램"
-    energy-drink: "에너지 드링크"
-    doll-ai: "아이쨩 인형"
-    banknote: "지폐뭉치"
 _pages:
   newPage: "페이지 만들기"
   editPage: "페이지 수정"
diff --git a/locales/nl-NL.yml b/locales/nl-NL.yml
index 09e0fb627f..386357f2d3 100644
--- a/locales/nl-NL.yml
+++ b/locales/nl-NL.yml
@@ -212,7 +212,6 @@ uploadFromUrlDescription: "URL van het bestand dat je wil uploaden"
 uploadFromUrlRequested: "Uploadverzoek"
 uploadFromUrlMayTakeTime: "Het kan even duren voordat het uploaden voltooid is."
 explore: "Verkennen"
-games: "Misskey spellen"
 messageRead: "Lezen"
 noMoreHistory: "Er is geen verdere geschiedenis"
 startMessaging: "Start een gesprek"
@@ -294,11 +293,6 @@ _exportOrImport:
   excludeInactiveUsers: "Negeer inactieve gebruikers"
 _timelines:
   home: "Startpagina"
-_rooms:
-  _roomType:
-    default: "Standaard"
-  _furnitures:
-    monitor: "Monitor"
 _pages:
   blocks:
     image: "Afbeeldingen"
diff --git a/locales/pl-PL.yml b/locales/pl-PL.yml
index a4f8f3efea..27772663bc 100644
--- a/locales/pl-PL.yml
+++ b/locales/pl-PL.yml
@@ -241,7 +241,6 @@ uploadFromUrlDescription: "Adres URL pliku, który chcesz wysłać"
 uploadFromUrlRequested: "Zażądano wysłania"
 uploadFromUrlMayTakeTime: "Wysyłanie może chwilę potrwać."
 explore: "Eksploruj"
-games: "Gry Misskey"
 messageRead: "Przeczytano"
 noMoreHistory: "Nie ma dalszej historii"
 startMessaging: "Rozpocznij czat"
@@ -528,7 +527,6 @@ userSuspended: "To konto zostało zawieszone."
 userSilenced: "Ten użytkownik został wyciszony."
 divider: "Rozdzielacz"
 addItem: "Dodaj element"
-rooms: "Pokój"
 relays: "Przekaźniki"
 addRelay: "Dodaj przekaźnik"
 inboxUrl: "Adres URL skrzynki nadawczej"
@@ -656,7 +654,6 @@ emailVerified: "Adres e-mail został potwierdzony"
 noteFavoritesCount: "Liczba polubionych wpisów"
 pageLikesCount: "Liczba otrzymanych polubień stron"
 pageLikedCount: "Liczba polubionych stron"
-reversiCount: "Liczba rozgrywek Reversi"
 contact: "Kontakt"
 useSystemFont: "Używaj domyślnej czcionki systemu"
 experimentalFeatures: "Eksperymentalne funkcje"
@@ -842,36 +839,6 @@ _mfm:
   font: "Czcionka"
   fontDescription: "Wybiera czcionkę do wyświetlania treści."
   rotate: "Obróć"
-_reversi:
-  reversi: "Reversi"
-  gameSettings: "Ustawienia gry"
-  chooseBoard: "Wybierz tablicę"
-  blackOrWhite: "Czarne/białe"
-  blackIs: "{name} gra czarnymi"
-  rules: "Zasady"
-  botSettings: "Opcje bota"
-  thisGameIsStartedSoon: "Gra rozpocznie się za kilka sekund"
-  waitingForOther: "Oczekiwanie na ruch przeciwnika"
-  waitingForMe: "Oczekiwanie na Twój ruch"
-  waitingBoth: "Przygotuj się"
-  ready: "Gotowy(-a)"
-  cancelReady: "Anuluj gotowość"
-  opponentTurn: "Kolej przeciwnika"
-  myTurn: "Twoja kolej"
-  turnOf: "Kolej {name}"
-  pastTurnOf: "Kolej {name}"
-  surrender: "Poddaj się"
-  surrendered: "Przez poddanie się"
-  drawn: "Remis"
-  won: "{name} wygrał(a)"
-  black: "Czarny"
-  white: "Biały"
-  total: "Łącznie"
-  turnCount: "Ruch {count}"
-  myGames: "Moje gry"
-  allGames: "Wszystkie gry"
-  ended: "Zakończono"
-  playing: "W trakcie gry"
 _instanceTicker:
   none: "Nigdy nie pokazuj"
   remote: "Pokaż dla zdalnych użytkowników"
@@ -979,8 +946,6 @@ _sfx:
   chat: "Wiadomości"
   chatBg: "Rozmowy (tło)"
   channel: "Powiadomienia kanału"
-  reversiPutBlack: "Reversi: Czarny wykonuje ruch"
-  reversiPutWhite: "Reversi: Biały wykonuje ruch"
 _ago:
   unknown: "Nieznane"
   future: "W przyszłości"
@@ -1136,65 +1101,6 @@ _timelines:
   local: "Lokalne"
   social: "Społeczność"
   global: "Globalna"
-_rooms:
-  roomOf: "Pokój {user}"
-  addFurniture: "Umieść meble"
-  translate: "Przenieś"
-  rotate: "Obróć"
-  exit: "Wróć"
-  remove: "Usuń"
-  clear: "Usuń wszystkie"
-  clearConfirm: "Czy na pewno chcesz usunąć wszystkie meble ze swojego pokoju?"
-  leaveConfirm: "Masz niezapisane zmiany. Czy na pewno chcesz wyjść?"
-  chooseImage: "Wybierz obraz"
-  roomType: "Typ pokoju"
-  carpetColor: "Kolor dywanu"
-  _roomType:
-    default: "Domyślne"
-    washitsu: "W japońskim stylu"
-  _furnitures:
-    milk: "Karton mleka"
-    bed: "Łóżko"
-    low-table: "Niski stolik"
-    desk: "Biurko"
-    chair: "Krzesło"
-    chair2: "Krzesło 2"
-    fan: "Chłodzenie"
-    pc: "Komputer"
-    plant: "Roślina domowa"
-    plant2: "Roślina domowa 2"
-    eraser: "Gumka"
-    pencil: "Ołówek"
-    pudding: "Budyń"
-    cardboard-box: "Pudło tekturowe"
-    cardboard-box2: "Pudło tekturowe 2"
-    cardboard-box3: "Pudło tekturowe 3"
-    book: "Książka"
-    book2: "Książka 2"
-    piano: "Fortepian"
-    server: "Serwery"
-    moon: "Księżyc"
-    corkboard: "Tablica korkowa"
-    mousepad: "Podkładka pod mysz"
-    monitor: "Monitor"
-    keyboard: "Klawiatura"
-    carpet-stripe: "Dywan (w paski)"
-    color-box: "Biblioteczka"
-    wall-clock: "Zegar ścienny"
-    photoframe: "Ramka do zdjęć"
-    cube: "Kostka"
-    tv: "Telewizor"
-    pinguin: "Pingwin"
-    rubik-cube: "Kostka Rubika"
-    poster-h: "Plakat (poziomy)"
-    poster-v: "Plakat (pionowy)"
-    sofa: "Kanapa"
-    spiral: "Schody spiralne"
-    bin: "Kosz"
-    holo-display: "Wyświetlacz holograficzny"
-    energy-drink: "Napój energetyczny"
-    doll-ai: "Lalka AI"
-    banknote: "Banknot"
 _pages:
   newPage: "Utwórz stronę"
   editPage: "Edytuj tę stronę"
diff --git a/locales/ru-RU.yml b/locales/ru-RU.yml
index 588fb91c40..7de9f8ff95 100644
--- a/locales/ru-RU.yml
+++ b/locales/ru-RU.yml
@@ -106,6 +106,7 @@ clickToShow: "Нажмите для просмотра"
 sensitive: "Содержимое не для всех"
 add: "Добавить"
 reaction: "Реакции"
+reactionSetting: "Реакции, отображаемые в палитре"
 reactionSettingDescription2: "Расставляйте перетаскиванием, удаляйте нажатием, добавляйте кнопкой «+»."
 rememberNoteVisibility: "Запоминать видимость заметок"
 attachCancel: "Удалить вложение"
@@ -127,7 +128,7 @@ selectAntenna: "Выберите антенну"
 selectWidget: "Выберите виджет"
 editWidgets: "Редактировать виджеты"
 editWidgetsExit: "Готово"
-customEmojis: "Эмодзи пользователя"
+customEmojis: "Собственные эмодзи"
 emoji: "Эмодзи"
 emojis: "Эмодзи"
 emojiName: "Название эмодзи"
@@ -200,7 +201,7 @@ done: "Готово"
 processing: "Обработка"
 preview: "Предпросмотр"
 default: "По умолчанию"
-noCustomEmojis: "Эмодзи пользователя отсутствуют"
+noCustomEmojis: "Собственные эмодзи отсутствуют"
 noJobs: "Нет заданий"
 federating: "Федерируется"
 blocked: "Заблокировано"
@@ -241,7 +242,6 @@ uploadFromUrlDescription: "Ссылка на файл, который хотит
 uploadFromUrlRequested: "Загрузка выбранного"
 uploadFromUrlMayTakeTime: "Загрузка может занять некоторое время."
 explore: "Обзор"
-games: "Игры Misskey"
 messageRead: "Прочитали"
 noMoreHistory: "История закончилась"
 startMessaging: "Начать общение"
@@ -447,6 +447,7 @@ uiLanguage: "Язык интерфейса"
 groupInvited: "Приглашение в группу"
 aboutX: "Описание {x}"
 useOsNativeEmojis: "Использовать эмодзи операционной системы"
+disableDrawer: "Не использовать выдвижные меню"
 youHaveNoGroups: "У вас нет ни одной группы"
 joinOrCreateGroup: "Получайте приглашения в группы или создавайте свои собственные"
 noHistory: "История пока пуста"
@@ -535,7 +536,6 @@ yourAccountSuspendedDescription: "Эта учетная запись была з
 menu: "Меню"
 divider: "Линия-разделитель"
 addItem: "Добавить элемент"
-rooms: "Комната"
 relays: "Ретрансляторы"
 addRelay: "Добавить ретранслятор"
 inboxUrl: "URL ящика входящих сообщений"
@@ -591,6 +591,7 @@ smtpSecure: "Использовать SSL/TLS для SMTP-соединений"
 smtpSecureInfo: "Выключите при использовании STARTTLS."
 testEmail: "Проверка доставки электронной почты"
 wordMute: "Скрытие слов"
+instanceMute: "Глушение инстансов"
 userSaysSomething: "{name} что-то сообщает"
 makeActive: "Активировать"
 display: "Отображение"
@@ -618,8 +619,8 @@ reportAbuse: "Жалоба"
 reportAbuseOf: "Пожаловаться на пользователя {name}"
 fillAbuseReportDescription: "Опишите, пожалуйста, причину жалобы подробнее. Если речь о конкретной заметке, будьте добры приложить ссылку на неё."
 abuseReported: "Жалоба отправлена. Большое спасибо за информацию."
-reporteeOrigin: "Куда сообщать"
-reporterOrigin: "Сообщено"
+reporteeOrigin: "О ком сообщено"
+reporterOrigin: "Кто сообщил"
 send: "Отправить"
 abuseMarkAsResolved: "Отметить жалобу как решённую"
 openInNewTab: "Открыть в новой вкладке"
@@ -667,7 +668,6 @@ emailVerified: "Адрес электронной почты подтвержд
 noteFavoritesCount: "Количество добавленного в избранное"
 pageLikesCount: "Количество понравившихся страниц"
 pageLikedCount: "Количество страниц, понравившихся другим"
-reversiCount: "Количество сыгранных игр в реверси"
 contact: "Как связаться"
 useSystemFont: "Использовать шрифт, предлагаемый системой"
 clips: "Подборки"
@@ -682,7 +682,7 @@ center: "По центру"
 wide: "Толстый"
 narrow: "Тонкий"
 reloadToApplySetting: "Это настройка вступает в силу при загрузке страницы. Перезагрузить сейчас?"
-needReloadToApply: "Чтобы это вступило в силу, требуется перезагрузка."
+needReloadToApply: "Изменения вступят в силу после перезагрузки страницы."
 showTitlebar: "Показать заголовок"
 clearCache: "Очистить кэш"
 onlineUsersCount: "Пользователей сейчас в сети: {n}"
@@ -767,7 +767,7 @@ middle: "Средне"
 low: "Низкий"
 emailNotConfiguredWarning: "Не указан адрес электронной почты"
 ratio: "Соотношение"
-previewNoteText: "Предварительный просмотр текста"
+previewNoteText: "Предварительный просмотр"
 customCss: "Индивидуальный CSS"
 customCssWarn: "Используйте эту настройку только если знаете, что делаете. Ошибки здесь чреваты тем, что сайт перестанет нормально работать у вас."
 global: "Всеобщая"
@@ -782,16 +782,19 @@ learnMore: "Подробнее"
 misskeyUpdated: "Misskey обновился!"
 whatIsNew: "Что новенького?"
 translate: "Перевод"
-translatedFrom: "{x}Перевод с английского"
+translatedFrom: "Перевод. Язык оригинала — {x}"
 accountDeletionInProgress: "В настоящее время выполняется удаление учетной записи"
 usernameInfo: "Имя, которое отличает вашу учетную запись от других на этом сервере. Вы можете использовать алфавит (a~z, A~Z), цифры (0~9) или символы подчеркивания (_). Имена пользователей не могут быть изменены позже."
 aiChanMode: "ИИ режим"
 keepCw: "Сохраняйте Предупреждения о содержимом"
+pubSub: "Учётные записи Pub/Sub"
 lastCommunication: "Последнее сообщение"
-resolved: "Решен"
-unresolved: "Неразрешенные"
-itsOff: "Он выключен!"
-emailRequiredForSignup: "Требуется адрес электронной почты для регистрации аккаунта"
+resolved: "Решено"
+unresolved: "Без решения"
+breakFollow: "Отписка"
+itsOn: "Включено"
+itsOff: "Выключено"
+emailRequiredForSignup: "Для регистрации учётной записи нужен адрес электронной почты"
 unread: "Непрочитанное"
 filter: "Фильтры"
 controlPanel: "Панель управления"
@@ -799,30 +802,38 @@ manageAccounts: "Управление аккаунтом"
 makeReactionsPublic: "Опубликовать список реакций"
 makeReactionsPublicDescription: "Список сделанных вами реакций доступен для просмотра всем желающим."
 classic: "Классика"
-unmuteThread: "Отключить звук"
-ffVisibilityDescription: "Вы можете установить объем вашей следующей/последней информации."
-voteConfirm: "Вы бы проголосовали за \"{choice}\"?"
+muteThread: "Заглушить цепочку"
+unmuteThread: "Отменить глушение цепочки"
+ffVisibility: "Видимость подписок и подписчиков"
+ffVisibilityDescription: "Здесь можно настроить, кто будет видеть ваши подписки и подписчиков."
+continueThread: "Показать следующие ответы"
+deleteAccountConfirm: "Учётная запись будет безвозвратно удалена. Подтверждаете?"
+incorrectPassword: "Пароль неверен."
+voteConfirm: "Отдать голос за «{choice}»?"
 hide: "Спрятать"
 leaveGroup: "Покинуть группу"
-leaveGroupConfirm: "Вы хотите оставить \"{name}\"?"
+leaveGroupConfirm: "Покинуть группу «{name}»?"
+useDrawerReactionPickerForMobile: "Выдвижная палитра на мобильном устройстве"
 welcomeBackWithName: "С возвращением, {name}!"
 clickToFinishEmailVerification: "Пожалуйста, нажмите [{ok}], чтобы завершить подтверждение адреса электронной почты."
 _emailUnavailable:
   used: "Уже используется"
-  format: "Неправильный формат"
-  mx: "Это неправильный почтовый сервер!"
+  format: "Неверный формат"
+  disposable: "Временный адрес электронной почты не принимается"
+  mx: "Неверный почтовый сервер"
   smtp: "Почтовый сервер не отвечает"
 _ffVisibility:
-  public: "Опубликовать"
-  private: "Частный"
+  public: "Общедоступны"
+  followers: "Показываются только подписчикам"
+  private: "Показываются только вам"
 _signup:
   almostThere: "Почти готово!"
-  emailAddressInfo: "Пожалуйста, введите адрес электронной почты, который вы используете."
-  emailSent: "На указанный вами адрес электронной почты ({email}) было отправлено письмо с подтверждением. Перейдите по ссылке в электронном письме, чтобы завершить создание учетной записи."
+  emailAddressInfo: "Введите ваш адрес электронной почты."
+  emailSent: "На указанный вами адрес электронной почты ({email}) отправлено письмо. Перейдите по ссылке в письме, чтобы завершить регистрацию."
 _accountDelete:
-  accountDelete: "Удалить свой аккаунт"
-  mayTakeTime: "Удаление учетной записи - это тяжелый процесс, который может занять много времени, если у вас создано много контента или загружено много файлов."
-  sendEmail: "Мы отправим уведомление на зарегистрированный вами адрес электронной почты, когда ваша учетная запись будет удалена."
+  accountDelete: "Удалить свою учётную запись"
+  mayTakeTime: "Удаление учётной записи — ресурсозатратный процесс. Он может занять много времени, если вы много писали и загружали файлов."
+  sendEmail: "Когда ваша учетная запись будет удалена, мы сообщим на указанную вами электронную почту."
   requestAccountDelete: "Запросить удаление вашей учетной записи"
   started: "Процесс удаления начался."
   inProgress: "Удаление в процессе"
@@ -894,7 +905,7 @@ _mfm:
   blockMathDescription: "Оформляет математическое выражение (KaTeX) на отдельной строке."
   quote: "Цитата"
   quoteDescription: "Так можно процитировать чей-то текст."
-  emoji: "Эмодзи пользователя"
+  emoji: "Собственные эмодзи"
   emojiDescription: "Можно вставить эмодзи в текст, окружив название двоеточиями."
   search: "Поиск"
   searchDescription: "Можно добавить форму для поиска, сразу задав, что искать."
@@ -926,43 +937,10 @@ _mfm:
   fontDescription: "Так можно писать произвольным шрифтом."
   rainbow: "Радуга"
   rainbowDescription: "Заставлять содержимое отображаться в цветах радуги."
-  sparkle: "Блеск"
-  sparkleDescription: "Добавьте эффект искрящихся частиц."
+  sparkle: "Искры"
+  sparkleDescription: "Добавляет эффект искрящихся частиц."
   rotate: "Повернуть"
-  rotateDescription: "Повернуть на указанный угол."
-_reversi:
-  reversi: "Реверси"
-  gameSettings: "Настройки игры"
-  chooseBoard: "Выберите доску"
-  blackOrWhite: "Черные/Белые"
-  blackIs: "{name} за чёрных"
-  rules: "Правила"
-  botSettings: "Настройки бота"
-  thisGameIsStartedSoon: "Игра скоро начнётся."
-  waitingForOther: "Ожидание соперника..."
-  waitingForMe: "В ожидании, когда будете готовы."
-  waitingBoth: "Приготовьтесь."
-  ready: "Готово"
-  cancelReady: "Возврат к подготовке"
-  opponentTurn: "Ход соперника"
-  myTurn: "Ваш ход"
-  turnOf: "Ходит {name}."
-  pastTurnOf: "Ходит {name}."
-  surrender: "Сдаться"
-  surrendered: "Противник сдался"
-  drawn: "Ничья"
-  won: "Победитель — {name}"
-  black: "Чёрные"
-  white: "Белые"
-  total: "Всего"
-  turnCount: "Ход {count}"
-  myGames: "Сыгранное вами"
-  allGames: "Все игры"
-  ended: "Завершена"
-  playing: "Идёт игра"
-  isLlotheo: "Выигрывает меньшее число камней (LLoTheO)"
-  loopedMap: "Замкнутая в кольцо доска"
-  canPutEverywhere: "Камни можно ставить везде"
+  rotateDescription: "Поворачивает на заданный угол."
 _instanceTicker:
   none: "Не показывать"
   remote: "Только для других сайтов"
@@ -995,6 +973,8 @@ _wordMute:
   soft: "Мягкий"
   hard: "Жёсткий"
   mutedNotes: "Скрытые заметки"
+_instanceMute:
+  heading: "Список заглушенных инстансов"
 _theme:
   explore: "Обзор"
   install: "Установить тему"
@@ -1077,8 +1057,6 @@ _sfx:
   chatBg: "Сообщения (фон)"
   antenna: "Антенна"
   channel: "Канал"
-  reversiPutBlack: "Реверси — ход чёрных"
-  reversiPutWhite: "Реверси — ход белых"
 _ago:
   unknown: "Когда-то"
   future: "Из будущего"
@@ -1156,10 +1134,10 @@ _permissions:
   "write:user-groups": "Изменять и удалять группы пользователей"
   "read:channels": "Смотреть каналы"
   "write:channels": "Изменять каналы"
-  "read:gallery": "Смотреть галерею"
-  "write:gallery": "Работа с галереей"
-  "read:gallery-likes": "Посмотреть галерею лайков"
-  "write:gallery-likes": "Манипулируйте понравившейся галереей"
+  "read:gallery": "Просмотр галереи"
+  "write:gallery": "Редактирование галереи"
+  "read:gallery-likes": "Просмотр списка понравившегося в галерее"
+  "write:gallery-likes": "Изменение списка понравившегося в галерее"
 _auth:
   shareAccess: "Дать доступ для «{name}» к вашей учётной записи?"
   shareAccessAsk: "Уверены, что хотите дать приложению доступ к своей учётной записи?"
@@ -1268,7 +1246,8 @@ _exportOrImport:
   muteList: "Скрытые"
   blockingList: "Заблокированные"
   userLists: "Списки"
-  excludeMutingUsers: "Исключение отключенных пользователей"
+  excludeMutingUsers: "За исключением заглушенных пользователей"
+  excludeInactiveUsers: "Без неактивных учётных записей"
 _charts:
   federationInstancesIncDec: "Изменение внешних связей"
   federationInstancesTotal: "Количество внешних связей"
@@ -1300,68 +1279,6 @@ _timelines:
   local: "Местная"
   social: "Социальная"
   global: "Всеобщая"
-_rooms:
-  roomOf: "Комната {user}"
-  addFurniture: "Добавить обстановку"
-  translate: "Передвинуть"
-  rotate: "Повернуть"
-  exit: "Выход"
-  remove: "Выбросить"
-  clear: "Очистить"
-  clearConfirm: "Уверены что стоит убрать всю обстановку из вашей комнаты?"
-  leaveConfirm: "Изменения не сохранены, правда хотите покинуть комнату?"
-  chooseImage: "Выберите изображение"
-  roomType: "Стиль комнаты"
-  carpetColor: "Цвет ковра"
-  _roomType:
-    default: "По умолчанию"
-    washitsu: "Японская"
-  _furnitures:
-    milk: "Пакет молока"
-    bed: "Кровать"
-    low-table: "Журнальный стол"
-    desk: "Письменный стол"
-    chair: "Стул"
-    chair2: "Стул 2"
-    fan: "Вентилятор"
-    pc: "Системный блок"
-    plant: "Растение в горшке"
-    plant2: "Растение в горшке 2"
-    eraser: "Ластик"
-    pencil: "Карандаш"
-    pudding: "Пудинг"
-    cardboard-box: "Картонная коробка"
-    cardboard-box2: "Картонная коробка 2"
-    cardboard-box3: "Картонная коробка 3"
-    book: "Книга"
-    book2: "Книга про Misskey"
-    piano: "Пианино"
-    facial-tissue: "Салфетки"
-    server: "Сервер"
-    moon: "Луна"
-    corkboard: "Пробковая доска"
-    mousepad: "Коврик для мыши"
-    monitor: "Монитор"
-    keyboard: "Клавиатура"
-    carpet-stripe: "Полосатый ковёр"
-    mat: "Мат"
-    color-box: "Книжная полка"
-    wall-clock: "Настенные часы"
-    photoframe: "Картина в раме"
-    cube: "Куб"
-    tv: "Телевизор"
-    pinguin: "Пингвин"
-    rubik-cube: "Кубик Рубика"
-    poster-h: "Плакат (альбомная ориентация)"
-    poster-v: "Плакат (портретная ориентация)"
-    sofa: "Диван"
-    spiral: "Спиральная лестница"
-    bin: "Мусорное ведро"
-    cup-noodle: "Стакан лапши"
-    holo-display: "Голографический проектор"
-    energy-drink: "Банка энергетического напитка"
-    doll-ai: "Кукла Ай-тян"
-    banknote: "Пачка денег"
 _pages:
   newPage: "Создать страницу"
   editPage: "Править страницу"
diff --git a/locales/uk-UA.yml b/locales/uk-UA.yml
index 8cb0e2e2aa..2dd6056011 100644
--- a/locales/uk-UA.yml
+++ b/locales/uk-UA.yml
@@ -236,7 +236,6 @@ uploadFromUrlDescription: "Посилання на файл для завант
 uploadFromUrlRequested: "Завантаження розпочалось"
 uploadFromUrlMayTakeTime: "Завантаження може зайняти деякий час."
 explore: "Огляд"
-games: "Ігри Misskey"
 messageRead: "Прочитано"
 noMoreHistory: "Подальшої історії немає"
 startMessaging: "Розпочати діалог"
@@ -519,7 +518,6 @@ userSuspended: "Обліковий запис заблокований."
 userSilenced: "Обліковий запис приглушений."
 divider: "Розділювач"
 addItem: "Додати елемент"
-rooms: "Кімнати"
 relays: "Ретранслятори"
 addRelay: "Додати ретранслятор"
 inboxUrl: "Inbox URL"
@@ -646,7 +644,6 @@ emailVerified: "Електронну пошту підтверджено."
 noteFavoritesCount: "Кількість улюблених нотаток"
 pageLikesCount: "Кількість отриманих вподобань сторінки"
 pageLikedCount: "Кількість вподобаних сторінок"
-reversiCount: "Кількість матчів \"Реверсі\""
 contact: "Контакт"
 useSystemFont: "Використовувати стандартний шрифт системи"
 clips: "Добірка"
@@ -771,37 +768,6 @@ _mfm:
   font: "Шрифт"
   fontDescription: "Встановлює шрифт для контенту."
   rotate: "Обертати"
-_reversi:
-  reversi: "Реверсі"
-  gameSettings: "Налаштування гри"
-  chooseBoard: "Вибір дошки"
-  blackOrWhite: "Чорні / Білі"
-  blackIs: "{name} грає чорними"
-  rules: "Правила"
-  botSettings: "Параметри бота"
-  thisGameIsStartedSoon: "Гра розпочнеться через кілька секунд"
-  waitingForOther: "Чекаємо на хід суперника"
-  waitingForMe: "Чекаємо на ваш хід"
-  waitingBoth: "Приготуйтесь"
-  ready: "Готовність"
-  cancelReady: "Скасувати готовність"
-  opponentTurn: "Хід суперника"
-  myTurn: "Ваш хід"
-  turnOf: "Хід {name}"
-  pastTurnOf: "Хід {name}"
-  surrender: "Здатися"
-  drawn: "Нічия"
-  won: "Перемога {name}"
-  black: "Чорні"
-  white: "Білі"
-  total: "Всього"
-  turnCount: "Хід {count}"
-  myGames: "Мої ігри"
-  allGames: "Усі ігри"
-  ended: "Завершено"
-  playing: "В даний момент у процесі гри"
-  isLlotheo: "Гравець з найменшою кількістю фігур виграє (Llotheo)"
-  canPutEverywhere: "Фігури можна ставити в будь якії позиції"
 _instanceTicker:
   none: "Не відображати"
   remote: "Відображати для віддалених користувачів"
@@ -901,8 +867,6 @@ _sfx:
   chatBg: "Чати (фон)"
   antenna: "Прийом антени"
   channel: "Повідомлення каналу"
-  reversiPutBlack: "Реверсі: хід Чорного"
-  reversiPutWhite: "Реверсі: хід Білого"
 _ago:
   unknown: "Невідомо"
   future: "Майбутнє"
@@ -1095,68 +1059,6 @@ _timelines:
   local: "Локальна"
   social: "Соціальна"
   global: "Глобальна"
-_rooms:
-  roomOf: "Кімната {user}"
-  addFurniture: "Розмістити меблі"
-  translate: "Пересунути"
-  rotate: "Обертати"
-  exit: "Назад"
-  remove: "Видалити"
-  clear: "Видалити все"
-  clearConfirm: "Ви дійсно хочете позбутись усіх речей у вашій кімнаті?"
-  leaveConfirm: "Є незбережені зміни. Ви дійсно хочете вийти?"
-  chooseImage: "Виберіть зображення"
-  roomType: "Тип кімнати"
-  carpetColor: "Колір килима"
-  _roomType:
-    default: "За замовчуванням"
-    washitsu: "В японському стилі"
-  _furnitures:
-    milk: "Пакет молока"
-    bed: "Ліжко"
-    low-table: "Журнальний стіл"
-    desk: "Письмовий стіл"
-    chair: "Стілець"
-    chair2: "Стілець 2"
-    fan: "Вентилятор"
-    pc: "Комп’ютер"
-    plant: "Кімнатна рослина"
-    plant2: "Кімнатна рослина 2"
-    eraser: "Ластик"
-    pencil: "Олівець"
-    pudding: "Пудинг"
-    cardboard-box: "Картонна коробка"
-    cardboard-box2: "Картонна коробка 2"
-    cardboard-box3: "Картонна коробка 3"
-    book: "Книга"
-    book2: "Книга 2"
-    piano: "Піаніно"
-    facial-tissue: "Серветки"
-    server: "Сервер"
-    moon: "Місяць"
-    corkboard: "Коркова дошка"
-    mousepad: "Килимок для миші"
-    monitor: "Монітор"
-    keyboard: "Клавіатура"
-    carpet-stripe: "Смугастий килим"
-    mat: "Мат"
-    color-box: "Книжкова полиця"
-    wall-clock: "Настінний годинник"
-    photoframe: "Фоторамка"
-    cube: "Куб"
-    tv: "Телевізор"
-    pinguin: "Пінгвін"
-    rubik-cube: "Кубик Рубіка"
-    poster-h: "Плакат (горизонтальний)"
-    poster-v: "Плакат (вертикальний)"
-    sofa: "Диван"
-    spiral: "Гвинтові сходи"
-    bin: "Смітник"
-    cup-noodle: "Локшина в чашці"
-    holo-display: "Голографічний дисплей"
-    energy-drink: "Енергетичний напій"
-    doll-ai: "Лялька Аі-тян"
-    banknote: "Пачка грошей"
 _pages:
   newPage: "Створити сторінку"
   editPage: "Редагувати сторінку"
diff --git a/locales/zh-CN.yml b/locales/zh-CN.yml
index c842b065a4..5815c92f43 100644
--- a/locales/zh-CN.yml
+++ b/locales/zh-CN.yml
@@ -242,7 +242,6 @@ uploadFromUrlDescription: "输入文件的URL"
 uploadFromUrlRequested: "请求上传"
 uploadFromUrlMayTakeTime: "上传可能需要一些时间完成。"
 explore: "发现"
-games: "Misskey游戏"
 messageRead: "已读"
 noMoreHistory: "没有更多的历史记录"
 startMessaging: "添加聊天"
@@ -537,7 +536,6 @@ yourAccountSuspendedDescription: "由于违反了服务器的服务条款或其
 menu: "菜单"
 divider: "分割线"
 addItem: "添加项目"
-rooms: "房间"
 relays: "中继"
 addRelay: "添加中继"
 inboxUrl: "Inbox URL"
@@ -670,7 +668,6 @@ emailVerified: "电子邮件地址已验证"
 noteFavoritesCount: "收藏的帖子数"
 pageLikesCount: "页面点赞次数"
 pageLikedCount: "页面被点赞次数"
-reversiCount: "黑白棋对战次数"
 contact: "联系人"
 useSystemFont: "使用系统默认字体"
 clips: "书签"
@@ -746,6 +743,7 @@ notRecommended: "不推荐"
 botProtection: "Bot防御"
 instanceBlocking: "被阻拦的实例"
 selectAccount: "选择账户"
+switchAccount: "切换账户"
 enabled: "已启用"
 disabled: "已禁用 "
 quickAction: "快捷操作"
@@ -944,39 +942,6 @@ _mfm:
   sparkleDescription: "添加发光粒子效果。"
   rotate: "旋转"
   rotateDescription: "旋转指定的角度。"
-_reversi:
-  reversi: "黑白棋"
-  gameSettings: "对局设置"
-  chooseBoard: "棋盘选择"
-  blackOrWhite: "先手/后手"
-  blackIs: "{name}执黑(先走)"
-  rules: "规则"
-  botSettings: "机器人设置"
-  thisGameIsStartedSoon: "对局在几秒后开始"
-  waitingForOther: "等待对手准备"
-  waitingForMe: "等待您的准备"
-  waitingBoth: "请准备"
-  ready: "准备就绪"
-  cancelReady: "重新准备"
-  opponentTurn: "对手的会合"
-  myTurn: "您的回合"
-  turnOf: "{name}的回合"
-  pastTurnOf: "{name}的回合"
-  surrender: "认输 "
-  surrendered: "对手认输"
-  drawn: "平局"
-  won: "{name}获胜"
-  black: "黑"
-  white: "白"
-  total: "总计"
-  turnCount: "{count}回合"
-  myGames: "我的对局"
-  allGames: "所有对局"
-  ended: "结束"
-  playing: "对局中"
-  isLlotheo: "棋子较少一方获胜(LLoTheO规则)"
-  loopedMap: "循环棋盘"
-  canPutEverywhere: "可以下在任意位置"
 _instanceTicker:
   none: "不显示"
   remote: "显示给远程用户"
@@ -1096,8 +1061,6 @@ _sfx:
   chatBg: "聊天背景"
   antenna: "天线接收"
   channel: "频道通知"
-  reversiPutBlack: "黑白棋:黑方下子时"
-  reversiPutWhite: "黑白棋:白方下子时"
 _ago:
   unknown: "未知"
   future: "未来"
@@ -1320,68 +1283,6 @@ _timelines:
   local: "本地"
   social: "社交"
   global: "全局"
-_rooms:
-  roomOf: "{user}的房间"
-  addFurniture: "放置家具"
-  translate: "移动"
-  rotate: "旋转"
-  exit: "返回"
-  remove: "移除"
-  clear: "清理"
-  clearConfirm: "是否清除所有家具?"
-  leaveConfirm: "有尚未保存的修改。是否离开?"
-  chooseImage: "选择图片"
-  roomType: "房间类型"
-  carpetColor: "地板颜色"
-  _roomType:
-    default: "默认"
-    washitsu: "和式房间"
-  _furnitures:
-    milk: "牛奶纸箱"
-    bed: "床"
-    low-table: "矮桌"
-    desk: "书桌"
-    chair: "椅子"
-    chair2: "椅子2"
-    fan: "换气扇"
-    pc: "电脑"
-    plant: "观叶植物"
-    plant2: "观叶植物2"
-    eraser: "橡皮擦"
-    pencil: "铅笔"
-    pudding: "布丁"
-    cardboard-box: "纸箱"
-    cardboard-box2: "纸箱2"
-    cardboard-box3: "纸箱3"
-    book: "书"
-    book2: "书2"
-    piano: "钢琴"
-    facial-tissue: "纸巾盒"
-    server: "服务器"
-    moon: "月亮"
-    corkboard: "软木板"
-    mousepad: "鼠标垫"
-    monitor: "显示器"
-    keyboard: "键盘"
-    carpet-stripe: "地毯(条纹)"
-    mat: "垫子"
-    color-box: "收纳柜"
-    wall-clock: "挂钟"
-    photoframe: "相框"
-    cube: "立方体"
-    tv: "电视"
-    pinguin: "企鹅君"
-    rubik-cube: "魔方"
-    poster-h: "海报(横向)"
-    poster-v: "海报(纵向)"
-    sofa: "沙发"
-    spiral: "螺旋楼梯"
-    bin: "垃圾箱"
-    cup-noodle: "杯面"
-    holo-display: "全息显示器"
-    energy-drink: "能量饮料"
-    doll-ai: "小蓝的玩偶"
-    banknote: "钞票"
 _pages:
   newPage: "创建页面"
   editPage: "编辑页面"
diff --git a/locales/zh-TW.yml b/locales/zh-TW.yml
index ad264822fa..798398a6a9 100644
--- a/locales/zh-TW.yml
+++ b/locales/zh-TW.yml
@@ -239,7 +239,6 @@ uploadFromUrlDescription: "您要上傳的文件的URL"
 uploadFromUrlRequested: "已請求上傳"
 uploadFromUrlMayTakeTime: "還需要一些時間才能完成上傳。"
 explore: "探索"
-games: "Misskey 遊戲"
 messageRead: "已讀"
 noMoreHistory: "沒有更多歷史紀錄"
 startMessaging: "開始傳送訊息"
@@ -525,7 +524,6 @@ userSuspended: "該使用者已被停用"
 userSilenced: "該用戶已被禁言。"
 divider: "分割線"
 addItem: "新增項目"
-rooms: "房間"
 relays: "中繼"
 addRelay: "新增中繼"
 inboxUrl: "收件夾URL"
@@ -651,7 +649,6 @@ emailVerified: "已成功驗證您的電郵"
 noteFavoritesCount: "我的最愛貼文的數目"
 pageLikesCount: "頁面被按讚次數"
 pageLikedCount: "頁面被按讚次數"
-reversiCount: "黑白棋對戰次數"
 contact: "聯絡人"
 useSystemFont: "使用系統預設的字型"
 clips: "摘錄"
@@ -840,37 +837,6 @@ _mfm:
   font: "字型"
   fontDescription: "您可以設定顯示內容的字型"
   rotate: "旋轉"
-_reversi:
-  reversi: "黑白棋"
-  gameSettings: "對弈設定"
-  chooseBoard: "選擇棋盤"
-  blackOrWhite: "黑棋/白棋"
-  blackIs: "{name}在玩黑棋"
-  rules: "規則"
-  botSettings: "機器人設定"
-  thisGameIsStartedSoon: "遊戲即將開始"
-  waitingForOther: "等待對手準備"
-  waitingForMe: "等待您的準備"
-  waitingBoth: "請準備"
-  ready: "已就緒"
-  cancelReady: "重新準備"
-  opponentTurn: "對手回合"
-  myTurn: "你的回合"
-  turnOf: "{name}的回合"
-  pastTurnOf: "{name}的回合"
-  surrender: "認輸"
-  surrendered: "對手認輸"
-  drawn: "平手"
-  won: "{name}獲勝"
-  black: "黑"
-  white: "白"
-  total: "合計"
-  turnCount: "{count}回合"
-  myGames: "我的對弈"
-  allGames: "所有對弈"
-  ended: "已結束"
-  playing: "正在對弈"
-  loopedMap: "循環棋盤"
 _instanceTicker:
   none: "隱藏"
   remote: "向遠端使用者顯示"
@@ -1176,67 +1142,6 @@ _timelines:
   local: "本地"
   social: "社群"
   global: "公開"
-_rooms:
-  roomOf: "{user}的房間"
-  addFurniture: "擺放家具"
-  translate: "移動 "
-  rotate: "旋轉"
-  exit: "返回"
-  remove: "移除"
-  clear: "全部移除"
-  clearConfirm: "確定要移除全部家具嗎?"
-  leaveConfirm: "修改未儲存,是否要離開?"
-  chooseImage: "選擇圖像"
-  roomType: "房間種類"
-  carpetColor: "地板顏色"
-  _roomType:
-    default: "預設"
-    washitsu: "和室"
-  _furnitures:
-    milk: "牛奶盒"
-    bed: "床"
-    low-table: "咖啡桌"
-    desk: "書桌"
-    chair: "椅子"
-    chair2: "椅子2"
-    fan: "通風機"
-    pc: "電腦"
-    plant: "觀葉植物"
-    plant2: "觀葉植物2"
-    eraser: "橡皮擦"
-    pencil: "鉛筆"
-    pudding: "布丁"
-    cardboard-box: "紙板箱"
-    cardboard-box2: "紙板箱2"
-    cardboard-box3: "紙板箱3"
-    book: "讀物"
-    book2: "讀物2"
-    piano: "鋼琴"
-    server: "伺服器"
-    moon: "月亮"
-    corkboard: "木栓板"
-    mousepad: "滑鼠墊"
-    monitor: "監視器"
-    keyboard: "鍵盤"
-    carpet-stripe: "條紋地毯"
-    mat: "地毯"
-    color-box: "層架"
-    wall-clock: "壁鐘"
-    photoframe: "相框"
-    cube: "立方體"
-    tv: "電視"
-    pinguin: "企鵝蠟像"
-    rubik-cube: "魔術方塊"
-    poster-h: "海報(橫向)"
-    poster-v: "海報(直向)"
-    sofa: " 沙發"
-    spiral: "螺旋式樓梯"
-    bin: "垃圾箱"
-    cup-noodle: "杯面"
-    holo-display: "投影機"
-    energy-drink: "能量飲料"
-    doll-ai: "小藍的人偶公仔"
-    banknote: "大疊鈔票"
 _pages:
   newPage: "建立頁面"
   editPage: "編輯頁面"

From 2f67ec2f843bc05f37b960399f7f6e37ac37d54b Mon Sep 17 00:00:00 2001
From: tamaina <tamaina@hotmail.co.jp>
Date: Thu, 27 Jan 2022 00:08:48 +0900
Subject: [PATCH 23/46] =?UTF-8?q?enhance:=20MediaList=E3=81=A7=E3=81=AF?=
 =?UTF-8?q?=E3=80=81=E3=82=B5=E3=83=BC=E3=83=90=E3=83=BC=E3=81=A7=E8=A8=B1?=
 =?UTF-8?q?=E5=8F=AF=E3=81=95=E3=82=8C=E3=81=9F=E5=BD=A2=E5=BC=8F=E3=81=97?=
 =?UTF-8?q?=E3=81=8B=E8=A1=A8=E7=A4=BA=E3=81=97=E3=81=AA=E3=81=84=E3=82=88?=
 =?UTF-8?q?=E3=81=86=E3=81=AB=20(#8113)?=
MIME-Version: 1.0
Content-Type: text/plain; charset=UTF-8
Content-Transfer-Encoding: 8bit

* wip

* fix
---
 packages/client/src/components/media-list.vue | 36 +++++++++------
 packages/client/src/const.ts                  | 44 +++++++++++++++++++
 2 files changed, 66 insertions(+), 14 deletions(-)
 create mode 100644 packages/client/src/const.ts

diff --git a/packages/client/src/components/media-list.vue b/packages/client/src/components/media-list.vue
index 2970d06c97..efcbb12922 100644
--- a/packages/client/src/components/media-list.vue
+++ b/packages/client/src/components/media-list.vue
@@ -3,7 +3,7 @@
 	<XBanner v-for="media in mediaList.filter(media => !previewable(media))" :key="media.id" :media="media"/>
 	<div v-if="mediaList.filter(media => previewable(media)).length > 0" class="gird-container">
 		<div ref="gallery" :data-count="mediaList.filter(media => previewable(media)).length">
-			<template v-for="media in mediaList">
+			<template v-for="media in mediaList.filter(media => previewable(media))">
 				<XVideo v-if="media.type.startsWith('video')" :key="media.id" :video="media"/>
 				<XImage v-else-if="media.type.startsWith('image')" :key="media.id" class="image" :data-id="media.id" :image="media" :raw="raw"/>
 			</template>
@@ -22,6 +22,7 @@ import XBanner from './media-banner.vue';
 import XImage from './media-image.vue';
 import XVideo from './media-video.vue';
 import * as os from '@/os';
+import { FILE_TYPE_BROWSERSAFE } from '@/const';
 import { defaultStore } from '@/store';
 
 export default defineComponent({
@@ -44,18 +45,23 @@ export default defineComponent({
 
 		onMounted(() => {
 			const lightbox = new PhotoSwipeLightbox({
-				dataSource: props.mediaList.filter(media => media.type.startsWith('image')).map(media => {
-					const item = {
-						src: media.url,
-						w: media.properties.width,
-						h: media.properties.height,
-						alt: media.name,
-					};
-					if (media.properties.orientation != null && media.properties.orientation >= 5) {
-						[item.w, item.h] = [item.h, item.w];
-					}
-					return item;
-				}),
+				dataSource: props.mediaList
+					.filter(media => {
+						if (media.type === 'image/svg+xml') return true; // svgのwebpublicはpngなのでtrue
+						return media.type.startsWith('image') && FILE_TYPE_BROWSERSAFE.includes(media.type);
+					})
+					.map(media => {
+						const item = {
+							src: media.url,
+							w: media.properties.width,
+							h: media.properties.height,
+							alt: media.name,
+						};
+						if (media.properties.orientation != null && media.properties.orientation >= 5) {
+							[item.w, item.h] = [item.h, item.w];
+						}
+						return item;
+					}),
 				gallery: gallery.value,
 				children: '.image',
 				thumbSelector: '.image',
@@ -99,7 +105,9 @@ export default defineComponent({
 		});
 
 		const previewable = (file: misskey.entities.DriveFile): boolean => {
-			return file.type.startsWith('video') || file.type.startsWith('image');
+			if (file.type === 'image/svg+xml') return true; // svgのwebpublic/thumbnailはpngなのでtrue
+			// FILE_TYPE_BROWSERSAFEに適合しないものはブラウザで表示するのに不適切
+			return (file.type.startsWith('video') || file.type.startsWith('image')) && FILE_TYPE_BROWSERSAFE.includes(file.type);
 		};
 
 		return {
diff --git a/packages/client/src/const.ts b/packages/client/src/const.ts
new file mode 100644
index 0000000000..505cf2748e
--- /dev/null
+++ b/packages/client/src/const.ts
@@ -0,0 +1,44 @@
+// ブラウザで直接表示することを許可するファイルの種類のリスト
+// ここに含まれないものは application/octet-stream としてレスポンスされる
+// SVGはXSSを生むので許可しない
+export const FILE_TYPE_BROWSERSAFE = [
+	// Images
+	'image/png',
+	'image/gif',
+	'image/jpeg',
+	'image/webp',
+	'image/apng',
+	'image/bmp',
+	'image/tiff',
+	'image/x-icon',
+
+	// OggS
+	'audio/opus',
+	'video/ogg',
+	'audio/ogg',
+	'application/ogg',
+
+	// ISO/IEC base media file format
+	'video/quicktime',
+	'video/mp4',
+	'audio/mp4',
+	'video/x-m4v',
+	'audio/x-m4a',
+	'video/3gpp',
+	'video/3gpp2',
+
+	'video/mpeg',
+	'audio/mpeg',
+
+	'video/webm',
+	'audio/webm',
+
+	'audio/aac',
+	'audio/x-flac',
+	'audio/vnd.wave',
+];
+/*
+https://github.com/sindresorhus/file-type/blob/main/supported.js
+https://github.com/sindresorhus/file-type/blob/main/core.js
+https://developer.mozilla.org/en-US/docs/Web/Media/Formats/Containers
+*/

From f3be43c39c77e4c7ca18e7be60f1e242f1bdc8fc Mon Sep 17 00:00:00 2001
From: syuilo <Syuilotan@yahoo.co.jp>
Date: Thu, 27 Jan 2022 00:13:36 +0900
Subject: [PATCH 24/46] update vue

---
 packages/client/package.json |   4 +-
 packages/client/yarn.lock    | 136 +++++++++++++++++------------------
 2 files changed, 70 insertions(+), 70 deletions(-)

diff --git a/packages/client/package.json b/packages/client/package.json
index 6c4cf764d0..71dd89bea4 100644
--- a/packages/client/package.json
+++ b/packages/client/package.json
@@ -38,7 +38,7 @@
 		"@types/websocket": "1.0.4",
 		"@types/ws": "8.2.2",
 		"@typescript-eslint/parser": "5.10.0",
-		"@vue/compiler-sfc": "3.2.28",
+		"@vue/compiler-sfc": "3.2.29",
 		"abort-controller": "3.0.0",
 		"autobind-decorator": "2.4.0",
 		"autosize": "5.0.1",
@@ -111,7 +111,7 @@
 		"uuid": "8.3.2",
 		"v-debounce": "0.1.2",
 		"vanilla-tilt": "1.7.2",
-		"vue": "3.2.28",
+		"vue": "3.2.29",
 		"vue-loader": "17.0.0",
 		"vue-prism-editor": "2.0.0-alpha.2",
 		"vue-router": "4.0.5",
diff --git a/packages/client/yarn.lock b/packages/client/yarn.lock
index d983d01ba8..a45a24ce0e 100644
--- a/packages/client/yarn.lock
+++ b/packages/client/yarn.lock
@@ -699,95 +699,95 @@
   resolved "https://registry.yarnpkg.com/@ungap/promise-all-settled/-/promise-all-settled-1.1.2.tgz#aa58042711d6e3275dd37dc597e5d31e8c290a44"
   integrity sha512-sL/cEvJWAnClXw0wHk85/2L0G6Sj8UB0Ctc1TEMbKSsmpRosqhwj9gWgFRZSrBr2f9tiXISwNhCPmlfqUqyb9Q==
 
-"@vue/compiler-core@3.2.28":
-  version "3.2.28"
-  resolved "https://registry.yarnpkg.com/@vue/compiler-core/-/compiler-core-3.2.28.tgz#7f6aa4b167f0ae0413f3c36e507c898db06e8fe8"
-  integrity sha512-mQpfEjmHVxmWKaup0HL6tLMv2HqjjJu7XT4/q0IoUXYXC4xKG8lIVn5YChJqxBTLPuQjzas7u7i9L4PAWJZRtA==
+"@vue/compiler-core@3.2.29":
+  version "3.2.29"
+  resolved "https://registry.yarnpkg.com/@vue/compiler-core/-/compiler-core-3.2.29.tgz#b06097ab8ff0493177c68c5ea5b63d379a061097"
+  integrity sha512-RePZ/J4Ub3sb7atQw6V6Rez+/5LCRHGFlSetT3N4VMrejqJnNPXKUt5AVm/9F5MJriy2w/VudEIvgscCfCWqxw==
   dependencies:
     "@babel/parser" "^7.16.4"
-    "@vue/shared" "3.2.28"
+    "@vue/shared" "3.2.29"
     estree-walker "^2.0.2"
     source-map "^0.6.1"
 
-"@vue/compiler-dom@3.2.28":
-  version "3.2.28"
-  resolved "https://registry.yarnpkg.com/@vue/compiler-dom/-/compiler-dom-3.2.28.tgz#cc32a987fee50673f25430df35ea943f252c23e6"
-  integrity sha512-KA4yXceLteKC7VykvPnViUixemQw3A+oii+deSbZJOQKQKVh1HLosI10qxa8ImPCyun41+wG3uGR+tW7eu1W6Q==
+"@vue/compiler-dom@3.2.29":
+  version "3.2.29"
+  resolved "https://registry.yarnpkg.com/@vue/compiler-dom/-/compiler-dom-3.2.29.tgz#ad0ead405bd2f2754161335aad9758aa12430715"
+  integrity sha512-y26vK5khdNS9L3ckvkqJk/78qXwWb75Ci8iYLb67AkJuIgyKhIOcR1E8RIt4mswlVCIeI9gQ+fmtdhaiTAtrBQ==
   dependencies:
-    "@vue/compiler-core" "3.2.28"
-    "@vue/shared" "3.2.28"
+    "@vue/compiler-core" "3.2.29"
+    "@vue/shared" "3.2.29"
 
-"@vue/compiler-sfc@3.2.28":
-  version "3.2.28"
-  resolved "https://registry.yarnpkg.com/@vue/compiler-sfc/-/compiler-sfc-3.2.28.tgz#0a576c09abc72d6a76b153133de6fd7599c182c3"
-  integrity sha512-zB0WznfEBb4CbGBHzhboHDKVO5nxbkbxxFo9iVlxObP7a9/qvA5kkZEuT7nXP52f3b3qEfmVTjIT23Lo1ndZdQ==
+"@vue/compiler-sfc@3.2.29":
+  version "3.2.29"
+  resolved "https://registry.yarnpkg.com/@vue/compiler-sfc/-/compiler-sfc-3.2.29.tgz#f76d556cd5fca6a55a3ea84c88db1a2a53a36ead"
+  integrity sha512-X9+0dwsag2u6hSOP/XsMYqFti/edvYvxamgBgCcbSYuXx1xLZN+dS/GvQKM4AgGS4djqo0jQvWfIXdfZ2ET68g==
   dependencies:
     "@babel/parser" "^7.16.4"
-    "@vue/compiler-core" "3.2.28"
-    "@vue/compiler-dom" "3.2.28"
-    "@vue/compiler-ssr" "3.2.28"
-    "@vue/reactivity-transform" "3.2.28"
-    "@vue/shared" "3.2.28"
+    "@vue/compiler-core" "3.2.29"
+    "@vue/compiler-dom" "3.2.29"
+    "@vue/compiler-ssr" "3.2.29"
+    "@vue/reactivity-transform" "3.2.29"
+    "@vue/shared" "3.2.29"
     estree-walker "^2.0.2"
     magic-string "^0.25.7"
     postcss "^8.1.10"
     source-map "^0.6.1"
 
-"@vue/compiler-ssr@3.2.28":
-  version "3.2.28"
-  resolved "https://registry.yarnpkg.com/@vue/compiler-ssr/-/compiler-ssr-3.2.28.tgz#411e8b3bdc3183b2acd35e6551734b34366d64e5"
-  integrity sha512-z8rck1PDTu20iLyip9lAvIhaO40DUJrw3Zv0mS4Apfh3PlfWpF5dhsO5g0dgt213wgYsQIYVIlU9cfrYapqRgg==
+"@vue/compiler-ssr@3.2.29":
+  version "3.2.29"
+  resolved "https://registry.yarnpkg.com/@vue/compiler-ssr/-/compiler-ssr-3.2.29.tgz#37b15b32dcd2f6b410bb61fca3f37b1a92b7eb1e"
+  integrity sha512-LrvQwXlx66uWsB9/VydaaqEpae9xtmlUkeSKF6aPDbzx8M1h7ukxaPjNCAXuFd3fUHblcri8k42lfimHfzMICA==
   dependencies:
-    "@vue/compiler-dom" "3.2.28"
-    "@vue/shared" "3.2.28"
+    "@vue/compiler-dom" "3.2.29"
+    "@vue/shared" "3.2.29"
 
-"@vue/reactivity-transform@3.2.28":
-  version "3.2.28"
-  resolved "https://registry.yarnpkg.com/@vue/reactivity-transform/-/reactivity-transform-3.2.28.tgz#e0abf86694f4d182f974fbac934fc3e23e0a6d9b"
-  integrity sha512-zE8idNkOPnBDd2tKSIk84hOQZ+jXKvSy5FoIIVlcNEJHnCFnQ3maqeSJ9KoB2Rf6EXUhFTiTDNRlYlXmT2uHbQ==
+"@vue/reactivity-transform@3.2.29":
+  version "3.2.29"
+  resolved "https://registry.yarnpkg.com/@vue/reactivity-transform/-/reactivity-transform-3.2.29.tgz#a08d606e10016b7cf588d1a43dae4db2953f9354"
+  integrity sha512-YF6HdOuhdOw6KyRm59+3rML8USb9o8mYM1q+SH0G41K3/q/G7uhPnHGKvspzceD7h9J3VR1waOQ93CUZj7J7OA==
   dependencies:
     "@babel/parser" "^7.16.4"
-    "@vue/compiler-core" "3.2.28"
-    "@vue/shared" "3.2.28"
+    "@vue/compiler-core" "3.2.29"
+    "@vue/shared" "3.2.29"
     estree-walker "^2.0.2"
     magic-string "^0.25.7"
 
-"@vue/reactivity@3.2.28":
-  version "3.2.28"
-  resolved "https://registry.yarnpkg.com/@vue/reactivity/-/reactivity-3.2.28.tgz#1c3c7f434372edd867f937151897fca7efc4be18"
-  integrity sha512-WamM5LGv7JIarW+EYAzYFqYonZXjTnOjNW0sBO93jRE9I1ReAwfH8NvQXkPA3JZ3fuF6SGDdG8Y9/+dKjd/1Gw==
+"@vue/reactivity@3.2.29":
+  version "3.2.29"
+  resolved "https://registry.yarnpkg.com/@vue/reactivity/-/reactivity-3.2.29.tgz#afdc9c111d4139b14600be17ad80267212af6052"
+  integrity sha512-Ryhb6Gy62YolKXH1gv42pEqwx7zs3n8gacRVZICSgjQz8Qr8QeCcFygBKYfJm3o1SccR7U+bVBQDWZGOyG1k4g==
   dependencies:
-    "@vue/shared" "3.2.28"
+    "@vue/shared" "3.2.29"
 
-"@vue/runtime-core@3.2.28":
-  version "3.2.28"
-  resolved "https://registry.yarnpkg.com/@vue/runtime-core/-/runtime-core-3.2.28.tgz#69d8eede42957a1660b964004aa002982ae36a41"
-  integrity sha512-sVbBMFUt42JatTlXbdH6tVcLPw1eEOrrVQWI+j6/nJVzR852RURaT6DhdR0azdYscxq4xmmBctE0VQmlibBOFw==
+"@vue/runtime-core@3.2.29":
+  version "3.2.29"
+  resolved "https://registry.yarnpkg.com/@vue/runtime-core/-/runtime-core-3.2.29.tgz#fb8577b2fcf52e8d967bd91cdf49ab9fb91f9417"
+  integrity sha512-VMvQuLdzoTGmCwIKTKVwKmIL0qcODIqe74JtK1pVr5lnaE0l25hopodmPag3RcnIcIXe+Ye3B2olRCn7fTCgig==
   dependencies:
-    "@vue/reactivity" "3.2.28"
-    "@vue/shared" "3.2.28"
+    "@vue/reactivity" "3.2.29"
+    "@vue/shared" "3.2.29"
 
-"@vue/runtime-dom@3.2.28":
-  version "3.2.28"
-  resolved "https://registry.yarnpkg.com/@vue/runtime-dom/-/runtime-dom-3.2.28.tgz#b5a0cf38daed5534edbc95790f4eeac97dff2003"
-  integrity sha512-Jg7cxZanEXXGu1QnZILFLnDrM+MIFN8VAullmMZiJEZziHvhygRMpi0ahNy/8OqGwtTze1JNhLdHRBO+q2hbmg==
+"@vue/runtime-dom@3.2.29":
+  version "3.2.29"
+  resolved "https://registry.yarnpkg.com/@vue/runtime-dom/-/runtime-dom-3.2.29.tgz#35e9a2bf04ef80b86ac2ca0e7b2ceaccf1e18f01"
+  integrity sha512-YJgLQLwr+SQyORzTsBQLL5TT/5UiV83tEotqjL7F9aFDIQdFBTCwpkCFvX9jqwHoyi9sJqM9XtTrMcc8z/OjPA==
   dependencies:
-    "@vue/runtime-core" "3.2.28"
-    "@vue/shared" "3.2.28"
+    "@vue/runtime-core" "3.2.29"
+    "@vue/shared" "3.2.29"
     csstype "^2.6.8"
 
-"@vue/server-renderer@3.2.28":
-  version "3.2.28"
-  resolved "https://registry.yarnpkg.com/@vue/server-renderer/-/server-renderer-3.2.28.tgz#235944dc4d969fadd387f62acc2eb8b8d50008a2"
-  integrity sha512-S+MhurgkPabRvhdDl8R6efKBmniJqBbbWIYTXADaJIKFLFLQCW4gcYUTbxuebzk6j3z485vpekhrHHymTF52Pg==
+"@vue/server-renderer@3.2.29":
+  version "3.2.29"
+  resolved "https://registry.yarnpkg.com/@vue/server-renderer/-/server-renderer-3.2.29.tgz#ea6afa361b9c781a868c8da18c761f9b7bc89102"
+  integrity sha512-lpiYx7ciV7rWfJ0tPkoSOlLmwqBZ9FTmQm33S+T4g0j1fO/LmhJ9b9Ctl1o5xvIFVDk9QkSUWANZn7H2pXuxVw==
   dependencies:
-    "@vue/compiler-ssr" "3.2.28"
-    "@vue/shared" "3.2.28"
+    "@vue/compiler-ssr" "3.2.29"
+    "@vue/shared" "3.2.29"
 
-"@vue/shared@3.2.28":
-  version "3.2.28"
-  resolved "https://registry.yarnpkg.com/@vue/shared/-/shared-3.2.28.tgz#5b0b1840432031d0ea1adff633b356a503e87048"
-  integrity sha512-eMQ8s9j8FpbGHlgUAaj/coaG3Q8YtMsoWL/RIHTsE3Ex7PUTyr7V91vB5HqWB5Sn8m4RXTHGO22/skoTUYvp0A==
+"@vue/shared@3.2.29":
+  version "3.2.29"
+  resolved "https://registry.yarnpkg.com/@vue/shared/-/shared-3.2.29.tgz#07dac7051117236431d2f737d16932aa38bbb925"
+  integrity sha512-BjNpU8OK6Z0LVzGUppEk0CMYm/hKDnZfYdjSmPOs0N+TR1cLKJAkDwW8ASZUvaaSLEi6d3hVM7jnWnX+6yWnHw==
 
 "@webassemblyjs/ast@1.11.0":
   version "1.11.0"
@@ -6153,16 +6153,16 @@ vue-svg-loader@0.17.0-beta.2:
     semver "^7.3.2"
     svgo "^1.3.2"
 
-vue@3.2.28:
-  version "3.2.28"
-  resolved "https://registry.yarnpkg.com/vue/-/vue-3.2.28.tgz#840d193bf9713f57a365ef115c4b1286d43e0e5d"
-  integrity sha512-U+jBwVh3RQ9AgceLFdT7i2FFujoC+kYuGrKo5y8aLluWKZWPS40WgA2pyYHaiSX9ydCbEGr3rc/JzdqskzD95g==
+vue@3.2.29:
+  version "3.2.29"
+  resolved "https://registry.yarnpkg.com/vue/-/vue-3.2.29.tgz#3571b65dbd796d3a6347e2fd45a8e6e11c13d56a"
+  integrity sha512-cFIwr7LkbtCRanjNvh6r7wp2yUxfxeM2yPpDQpAfaaLIGZSrUmLbNiSze9nhBJt5MrZ68Iqt0O5scwAMEVxF+Q==
   dependencies:
-    "@vue/compiler-dom" "3.2.28"
-    "@vue/compiler-sfc" "3.2.28"
-    "@vue/runtime-dom" "3.2.28"
-    "@vue/server-renderer" "3.2.28"
-    "@vue/shared" "3.2.28"
+    "@vue/compiler-dom" "3.2.29"
+    "@vue/compiler-sfc" "3.2.29"
+    "@vue/runtime-dom" "3.2.29"
+    "@vue/server-renderer" "3.2.29"
+    "@vue/shared" "3.2.29"
 
 vuedraggable@4.0.1:
   version "4.0.1"

From 0befca370429956a22641b62447a9fa26de48fca Mon Sep 17 00:00:00 2001
From: syuilo <Syuilotan@yahoo.co.jp>
Date: Thu, 27 Jan 2022 00:14:54 +0900
Subject: [PATCH 25/46] Update CHANGELOG.md

---
 CHANGELOG.md | 1 +
 1 file changed, 1 insertion(+)

diff --git a/CHANGELOG.md b/CHANGELOG.md
index 835c504637..ca969a0e31 100644
--- a/CHANGELOG.md
+++ b/CHANGELOG.md
@@ -23,6 +23,7 @@
 - カスタム絵文字一括インポート
 - 投稿フォームで一時的に投稿するアカウントを切り替えられるように
 - Unifying Misskey-specific IRIs in JSON-LD `@context`
+- クライアントのパフォーマンス向上
 - セキュリティの向上
 
 ### Bugfixes

From 40b7230bd4e0f7a615cdad8481cadba4a00e9954 Mon Sep 17 00:00:00 2001
From: syuilo <Syuilotan@yahoo.co.jp>
Date: Thu, 27 Jan 2022 00:16:27 +0900
Subject: [PATCH 26/46] 12.102.0

---
 CHANGELOG.md | 2 +-
 package.json | 2 +-
 2 files changed, 2 insertions(+), 2 deletions(-)

diff --git a/CHANGELOG.md b/CHANGELOG.md
index ca969a0e31..718c7d97c3 100644
--- a/CHANGELOG.md
+++ b/CHANGELOG.md
@@ -7,7 +7,7 @@
 
 -->
 
-## 12.x.x (unreleased)
+## 12.102.0 (2022/01/27)
 
 ### Changes
 - Room機能が削除されました
diff --git a/package.json b/package.json
index 5a4ea315ba..c8fc90c821 100644
--- a/package.json
+++ b/package.json
@@ -1,6 +1,6 @@
 {
 	"name": "misskey",
-	"version": "12.101.1",
+	"version": "12.102.0",
 	"codename": "indigo",
 	"repository": {
 		"type": "git",

From 1e48d9e15b5037316cd6694a72e99a40b4710ef0 Mon Sep 17 00:00:00 2001
From: syuilo <Syuilotan@yahoo.co.jp>
Date: Thu, 27 Jan 2022 01:01:27 +0900
Subject: [PATCH 27/46] Update CHANGELOG.md

---
 CHANGELOG.md | 3 +++
 1 file changed, 3 insertions(+)

diff --git a/CHANGELOG.md b/CHANGELOG.md
index 718c7d97c3..e834e80034 100644
--- a/CHANGELOG.md
+++ b/CHANGELOG.md
@@ -9,6 +9,9 @@
 
 ## 12.102.0 (2022/01/27)
 
+### NOTE
+アップデート後、一部カスタム絵文字が表示できなくなる場合があります。その場合、一旦絵文字管理ページから絵文字を一括エクスポートし、再度コントロールパネルから一括インポートすると直ります。
+
 ### Changes
 - Room機能が削除されました
   - 後日別リポジトリとして復活予定です

From 47edc18931f015039350fb5c0001055fce379d0a Mon Sep 17 00:00:00 2001
From: syuilo <Syuilotan@yahoo.co.jp>
Date: Thu, 27 Jan 2022 01:47:02 +0900
Subject: [PATCH 28/46] Update CHANGELOG.md

---
 CHANGELOG.md | 1 +
 1 file changed, 1 insertion(+)

diff --git a/CHANGELOG.md b/CHANGELOG.md
index e834e80034..ad7c53fdb4 100644
--- a/CHANGELOG.md
+++ b/CHANGELOG.md
@@ -11,6 +11,7 @@
 
 ### NOTE
 アップデート後、一部カスタム絵文字が表示できなくなる場合があります。その場合、一旦絵文字管理ページから絵文字を一括エクスポートし、再度コントロールパネルから一括インポートすると直ります。
+⚠ 12.102.0以前にエクスポートされたzipとは互換性がありません。アップデートしてからエクスポートを行なってください。
 
 ### Changes
 - Room機能が削除されました

From 779bd244a64e93a6d22dddf876f2611e7e4451ef Mon Sep 17 00:00:00 2001
From: syuilo <Syuilotan@yahoo.co.jp>
Date: Thu, 27 Jan 2022 17:38:02 +0900
Subject: [PATCH 29/46] chore: fix instant form handling

---
 packages/client/src/components/post-form.vue | 4 ++--
 packages/client/src/pages/about-misskey.vue  | 1 +
 packages/client/src/pages/share.vue          | 2 +-
 3 files changed, 4 insertions(+), 3 deletions(-)

diff --git a/packages/client/src/components/post-form.vue b/packages/client/src/components/post-form.vue
index ed78c5a3fb..2eda97e14d 100644
--- a/packages/client/src/components/post-form.vue
+++ b/packages/client/src/components/post-form.vue
@@ -102,7 +102,7 @@ const props = withDefaults(defineProps<{
 	initialLocalOnly?: boolean;
 	initialVisibleUsers?: misskey.entities.User[];
 	initialNote?: misskey.entities.Note;
-	share?: boolean;
+	instant?: boolean;
 	fixed?: boolean;
 	autofocus?: boolean;
 }>(), {
@@ -641,7 +641,7 @@ onMounted(() => {
 
 	nextTick(() => {
 		// 書きかけの投稿を復元
-		if (!props.share && !props.mention && !props.specified) {
+		if (!props.instant && !props.mention && !props.specified) {
 			const draft = JSON.parse(localStorage.getItem('drafts') || '{}')[draftKey];
 			if (draft) {
 				text = draft.data.text;
diff --git a/packages/client/src/pages/about-misskey.vue b/packages/client/src/pages/about-misskey.vue
index 8119f33051..f887e29cc0 100644
--- a/packages/client/src/pages/about-misskey.vue
+++ b/packages/client/src/pages/about-misskey.vue
@@ -182,6 +182,7 @@ function gravity() {
 function iLoveMisskey() {
 	os.post({
 		initialText: 'I $[jelly ❤] #Misskey',
+		instant: true,
 	});
 }
 
diff --git a/packages/client/src/pages/share.vue b/packages/client/src/pages/share.vue
index 5df6256fb2..4d77de5819 100644
--- a/packages/client/src/pages/share.vue
+++ b/packages/client/src/pages/share.vue
@@ -5,7 +5,7 @@
 			<XPostForm
 				v-if="state === 'writing'"
 				fixed
-				:share="true"
+				:instant="true"
 				:initial-text="initialText"
 				:initial-visibility="visibility"
 				:initial-files="files"

From e5d56a7cfeac02b8244c27fabba6d3c029b5eefa Mon Sep 17 00:00:00 2001
From: syuilo <Syuilotan@yahoo.co.jp>
Date: Thu, 27 Jan 2022 17:55:11 +0900
Subject: [PATCH 30/46] =?UTF-8?q?fix(client):=20=E3=83=81=E3=83=A3?=
 =?UTF-8?q?=E3=83=83=E3=83=88=E3=81=8C=E8=A6=8B=E3=82=8C=E3=81=AA=E3=81=84?=
MIME-Version: 1.0
Content-Type: text/plain; charset=UTF-8
Content-Transfer-Encoding: 8bit

Fix #8203
---
 packages/client/src/pages/messaging/messaging-room.form.vue | 5 +++--
 packages/client/src/pages/messaging/messaging-room.vue      | 2 +-
 2 files changed, 4 insertions(+), 3 deletions(-)

diff --git a/packages/client/src/pages/messaging/messaging-room.form.vue b/packages/client/src/pages/messaging/messaging-room.form.vue
index 0fc7c8a5df..1b9421ca9a 100644
--- a/packages/client/src/pages/messaging/messaging-room.form.vue
+++ b/packages/client/src/pages/messaging/messaging-room.form.vue
@@ -24,7 +24,7 @@
 <script lang="ts">
 import { defineComponent, defineAsyncComponent } from 'vue';
 import insertTextAtCursor from 'insert-text-at-cursor';
-import * as autosize from 'autosize';
+import autosize from 'autosize';
 import { formatTimeString } from '@/scripts/format-time-string';
 import { selectFile } from '@/scripts/select-file';
 import * as os from '@/os';
@@ -76,7 +76,8 @@ export default defineComponent({
 		autosize(this.$refs.text);
 
 		// TODO: detach when unmount
-		new Autocomplete(this.$refs.text, this, { model: 'text' });
+		// TODO
+		//new Autocomplete(this.$refs.text, this, { model: 'text' });
 
 		// 書きかけの投稿を復元
 		const draft = JSON.parse(localStorage.getItem('message_drafts') || '{}')[this.draftKey];
diff --git a/packages/client/src/pages/messaging/messaging-room.vue b/packages/client/src/pages/messaging/messaging-room.vue
index a715dad6de..65c44ce113 100644
--- a/packages/client/src/pages/messaging/messaging-room.vue
+++ b/packages/client/src/pages/messaging/messaging-room.vue
@@ -11,7 +11,7 @@
 			<button v-show="existMoreMessages" ref="loadMore" class="more _button" :class="{ fetching: fetchingMoreMessages }" :disabled="fetchingMoreMessages" @click="fetchMoreMessages">
 				<template v-if="fetchingMoreMessages"><i class="fas fa-spinner fa-pulse fa-fw"></i></template>{{ fetchingMoreMessages ? $ts.loading : $ts.loadMore }}
 			</button>
-			<XList v-slot="{ item: message }" class="messages" :items="messages" direction="up" reversed>
+			<XList v-if="messages.length > 0" v-slot="{ item: message }" class="messages" :items="messages" direction="up" reversed>
 				<XMessage :key="message.id" :message="message" :is-group="group != null"/>
 			</XList>
 		</div>

From 990fef59938f81dbc8c749317c78c36f585060e7 Mon Sep 17 00:00:00 2001
From: syuilo <Syuilotan@yahoo.co.jp>
Date: Thu, 27 Jan 2022 18:00:23 +0900
Subject: [PATCH 31/46] 12.102.1

---
 CHANGELOG.md | 4 ++++
 package.json | 2 +-
 2 files changed, 5 insertions(+), 1 deletion(-)

diff --git a/CHANGELOG.md b/CHANGELOG.md
index ad7c53fdb4..cabb4ad46e 100644
--- a/CHANGELOG.md
+++ b/CHANGELOG.md
@@ -7,6 +7,10 @@
 
 -->
 
+## 12.102.1 (2022/01/27)
+### Bugfixes
+- チャットが表示できない問題を修正
+
 ## 12.102.0 (2022/01/27)
 
 ### NOTE
diff --git a/package.json b/package.json
index c8fc90c821..d945672987 100644
--- a/package.json
+++ b/package.json
@@ -1,6 +1,6 @@
 {
 	"name": "misskey",
-	"version": "12.102.0",
+	"version": "12.102.1",
 	"codename": "indigo",
 	"repository": {
 		"type": "git",

From 2a4f2fba09be1c0e38c3848718080595d89ada47 Mon Sep 17 00:00:00 2001
From: syuilo <Syuilotan@yahoo.co.jp>
Date: Fri, 28 Jan 2022 00:46:49 +0900
Subject: [PATCH 32/46] refactor(client): use composition api

---
 .../client/src/components/media-image.vue     | 62 +++++++------------
 1 file changed, 21 insertions(+), 41 deletions(-)

diff --git a/packages/client/src/components/media-image.vue b/packages/client/src/components/media-image.vue
index 3e2cabae0a..43639f6771 100644
--- a/packages/client/src/components/media-image.vue
+++ b/packages/client/src/components/media-image.vue
@@ -20,52 +20,32 @@
 </div>
 </template>
 
-<script lang="ts">
-import { defineComponent } from 'vue';
+<script lang="ts" setup>
+import { watch } from 'vue';
+import * as misskey from 'misskey-js';
 import { getStaticImageUrl } from '@/scripts/get-static-image-url';
 import ImgWithBlurhash from '@/components/img-with-blurhash.vue';
-import * as os from '@/os';
+import { defaultStore } from '@/store';
 
-export default defineComponent({
-	components: {
-		ImgWithBlurhash
-	},
-	props: {
-		image: {
-			type: Object,
-			required: true
-		},
-		raw: {
-			default: false
-		}
-	},
-	data() {
-		return {
-			hide: true,
-		};
-	},
-	computed: {
-		url(): any {
-			let url = this.$store.state.disableShowingAnimatedImages
-				? getStaticImageUrl(this.image.thumbnailUrl)
-				: this.image.thumbnailUrl;
+const props = defineProps<{
+	image: misskey.entities.DriveFile;
+	raw?: boolean;
+}>();
 
-			if (this.raw || this.$store.state.loadRawImages) {
-				url = this.image.url;
-			}
+let hide = $ref(true);
 
-			return url;
-		}
-	},
-	created() {
-		// Plugin:register_note_view_interruptor を使って書き換えられる可能性があるためwatchする
-		this.$watch('image', () => {
-			this.hide = (this.$store.state.nsfw === 'force') ? true : this.image.isSensitive && (this.$store.state.nsfw !== 'ignore');
-		}, {
-			deep: true,
-			immediate: true,
-		});
-	},
+const url = (props.raw || defaultStore.state.loadRawImages)
+	? props.image.url
+	: defaultStore.state.disableShowingAnimatedImages
+			? getStaticImageUrl(props.image.thumbnailUrl)
+			: props.image.thumbnailUrl;
+
+// Plugin:register_note_view_interruptor を使って書き換えられる可能性があるためwatchする
+watch(() => props.image, () => {
+	hide = (defaultStore.state.nsfw === 'force') ? true : props.image.isSensitive && (defaultStore.state.nsfw !== 'ignore');
+}, {
+	deep: true,
+	immediate: true,
 });
 </script>
 

From a9960ac63a9b036dd2c3b8231dddeb924afb0d49 Mon Sep 17 00:00:00 2001
From: syuilo <Syuilotan@yahoo.co.jp>
Date: Fri, 28 Jan 2022 00:52:05 +0900
Subject: [PATCH 33/46] refactor(client): use composition api

---
 packages/client/src/components/media-list.vue | 156 ++++++++----------
 1 file changed, 69 insertions(+), 87 deletions(-)

diff --git a/packages/client/src/components/media-list.vue b/packages/client/src/components/media-list.vue
index efcbb12922..532627edbd 100644
--- a/packages/client/src/components/media-list.vue
+++ b/packages/client/src/components/media-list.vue
@@ -12,8 +12,8 @@
 </div>
 </template>
 
-<script lang="ts">
-import { defineComponent, onMounted, PropType, ref } from 'vue';
+<script lang="ts" setup>
+import { onMounted, ref } from 'vue';
 import * as misskey from 'misskey-js';
 import PhotoSwipeLightbox from 'photoswipe/dist/photoswipe-lightbox.esm.js';
 import PhotoSwipe from 'photoswipe/dist/photoswipe.esm.js';
@@ -25,98 +25,80 @@ import * as os from '@/os';
 import { FILE_TYPE_BROWSERSAFE } from '@/const';
 import { defaultStore } from '@/store';
 
-export default defineComponent({
-	components: {
-		XBanner,
-		XImage,
-		XVideo,
-	},
-	props: {
-		mediaList: {
-			type: Array as PropType<misskey.entities.DriveFile[]>,
-			required: true,
-		},
-		raw: {
-			default: false
-		},
-	},
-	setup(props) {
-		const gallery = ref(null);
+const props = defineProps<{
+	mediaList: misskey.entities.DriveFile[];
+	raw?: boolean;
+}>();
 
-		onMounted(() => {
-			const lightbox = new PhotoSwipeLightbox({
-				dataSource: props.mediaList
-					.filter(media => {
-						if (media.type === 'image/svg+xml') return true; // svgのwebpublicはpngなのでtrue
-						return media.type.startsWith('image') && FILE_TYPE_BROWSERSAFE.includes(media.type);
-					})
-					.map(media => {
-						const item = {
-							src: media.url,
-							w: media.properties.width,
-							h: media.properties.height,
-							alt: media.name,
-						};
-						if (media.properties.orientation != null && media.properties.orientation >= 5) {
-							[item.w, item.h] = [item.h, item.w];
-						}
-						return item;
-					}),
-				gallery: gallery.value,
-				children: '.image',
-				thumbSelector: '.image',
-				loop: false,
-				padding: window.innerWidth > 500 ? {
-					top: 32,
-					bottom: 32,
-					left: 32,
-					right: 32,
-				} : {
-					top: 0,
-					bottom: 0,
-					left: 0,
-					right: 0,
-				},
-				imageClickAction: 'close',
-				tapAction: 'toggle-controls',
-				pswpModule: PhotoSwipe,
-			});
+const gallery = ref(null);
+const pswpZIndex = os.claimZIndex('middle');
 
-			lightbox.on('itemData', (e) => {
-				const { itemData } = e;
-
-				// element is children
-				const { element } = itemData;
-
-				const id = element.dataset.id;
-				const file = props.mediaList.find(media => media.id === id);
-
-				itemData.src = file.url;
-				itemData.w = Number(file.properties.width);
-				itemData.h = Number(file.properties.height);
-				if (file.properties.orientation != null && file.properties.orientation >= 5) {
-					[itemData.w, itemData.h] = [itemData.h, itemData.w];
+onMounted(() => {
+	const lightbox = new PhotoSwipeLightbox({
+		dataSource: props.mediaList
+			.filter(media => {
+				if (media.type === 'image/svg+xml') return true; // svgのwebpublicはpngなのでtrue
+				return media.type.startsWith('image') && FILE_TYPE_BROWSERSAFE.includes(media.type);
+			})
+			.map(media => {
+				const item = {
+					src: media.url,
+					w: media.properties.width,
+					h: media.properties.height,
+					alt: media.name,
+				};
+				if (media.properties.orientation != null && media.properties.orientation >= 5) {
+					[item.w, item.h] = [item.h, item.w];
 				}
-				itemData.msrc = file.thumbnailUrl;
-				itemData.thumbCropped = true;
-			});
+				return item;
+			}),
+		gallery: gallery.value,
+		children: '.image',
+		thumbSelector: '.image',
+		loop: false,
+		padding: window.innerWidth > 500 ? {
+			top: 32,
+			bottom: 32,
+			left: 32,
+			right: 32,
+		} : {
+			top: 0,
+			bottom: 0,
+			left: 0,
+			right: 0,
+		},
+		imageClickAction: 'close',
+		tapAction: 'toggle-controls',
+		pswpModule: PhotoSwipe,
+	});
 
-			lightbox.init();
-		});
+	lightbox.on('itemData', (ev) => {
+		const { itemData } = ev;
 
-		const previewable = (file: misskey.entities.DriveFile): boolean => {
-			if (file.type === 'image/svg+xml') return true; // svgのwebpublic/thumbnailはpngなのでtrue
-			// FILE_TYPE_BROWSERSAFEに適合しないものはブラウザで表示するのに不適切
-			return (file.type.startsWith('video') || file.type.startsWith('image')) && FILE_TYPE_BROWSERSAFE.includes(file.type);
-		};
+		// element is children
+		const { element } = itemData;
 
-		return {
-			previewable,
-			gallery,
-			pswpZIndex: os.claimZIndex('middle'),
-		};
-	},
+		const id = element.dataset.id;
+		const file = props.mediaList.find(media => media.id === id);
+
+		itemData.src = file.url;
+		itemData.w = Number(file.properties.width);
+		itemData.h = Number(file.properties.height);
+		if (file.properties.orientation != null && file.properties.orientation >= 5) {
+			[itemData.w, itemData.h] = [itemData.h, itemData.w];
+		}
+		itemData.msrc = file.thumbnailUrl;
+		itemData.thumbCropped = true;
+	});
+
+	lightbox.init();
 });
+
+const previewable = (file: misskey.entities.DriveFile): boolean => {
+	if (file.type === 'image/svg+xml') return true; // svgのwebpublic/thumbnailはpngなのでtrue
+	// FILE_TYPE_BROWSERSAFEに適合しないものはブラウザで表示するのに不適切
+	return (file.type.startsWith('video') || file.type.startsWith('image')) && FILE_TYPE_BROWSERSAFE.includes(file.type);
+};
 </script>
 
 <style lang="scss" scoped>

From 389350ba770f9332b0abb6f651e39450c6048a2f Mon Sep 17 00:00:00 2001
From: syuilo <Syuilotan@yahoo.co.jp>
Date: Fri, 28 Jan 2022 01:05:31 +0900
Subject: [PATCH 34/46] =?UTF-8?q?fix(client):=20=E6=8A=95=E7=A8=BF?=
 =?UTF-8?q?=E3=81=AENSFW=E7=94=BB=E5=83=8F=E3=82=92=E8=A1=A8=E7=A4=BA?=
 =?UTF-8?q?=E3=81=97=E3=81=9F=E3=81=82=E3=81=A8=E3=81=AB=E3=83=AA=E3=82=A2?=
 =?UTF-8?q?=E3=82=AF=E3=82=B7=E3=83=A7=E3=83=B3=E3=81=8C=E6=9B=B4=E6=96=B0?=
 =?UTF-8?q?=E3=81=95=E3=82=8C=E3=82=8B=E3=81=A8=E7=94=BB=E5=83=8F=E3=81=8C?=
 =?UTF-8?q?=E9=9D=9E=E8=A1=A8=E7=A4=BA=E3=81=AB=E3=81=AA=E3=82=8B=E5=95=8F?=
 =?UTF-8?q?=E9=A1=8C=E3=82=92=E4=BF=AE=E6=AD=A3?=
MIME-Version: 1.0
Content-Type: text/plain; charset=UTF-8
Content-Transfer-Encoding: 8bit

Fix #8208
---
 CHANGELOG.md                                  |  7 +++++
 .../client/src/scripts/use-note-capture.ts    | 28 +++++--------------
 2 files changed, 14 insertions(+), 21 deletions(-)

diff --git a/CHANGELOG.md b/CHANGELOG.md
index cabb4ad46e..f60cbd1295 100644
--- a/CHANGELOG.md
+++ b/CHANGELOG.md
@@ -7,6 +7,13 @@
 
 -->
 
+## 12.x.x (unreleased)
+
+### Improvements
+
+### Bugfixes
+- 投稿のNSFW画像を表示したあとにリアクションが更新されると画像が非表示になる問題を修正
+
 ## 12.102.1 (2022/01/27)
 ### Bugfixes
 - チャットが表示できない問題を修正
diff --git a/packages/client/src/scripts/use-note-capture.ts b/packages/client/src/scripts/use-note-capture.ts
index bb00e464e3..b7cf99d5e1 100644
--- a/packages/client/src/scripts/use-note-capture.ts
+++ b/packages/client/src/scripts/use-note-capture.ts
@@ -19,51 +19,41 @@ export function useNoteCapture(props: {
 			case 'reacted': {
 				const reaction = body.reaction;
 
-				const updated = JSON.parse(JSON.stringify(appearNote.value));
-
 				if (body.emoji) {
 					const emojis = appearNote.value.emojis || [];
 					if (!emojis.includes(body.emoji)) {
-						updated.emojis = [...emojis, body.emoji];
+						appearNote.value.emojis = [...emojis, body.emoji];
 					}
 				}
 
 				// TODO: reactionsプロパティがない場合ってあったっけ? なければ || {} は消せる
 				const currentCount = (appearNote.value.reactions || {})[reaction] || 0;
 
-				updated.reactions[reaction] = currentCount + 1;
+				appearNote.value.reactions[reaction] = currentCount + 1;
 
 				if ($i && (body.userId === $i.id)) {
-					updated.myReaction = reaction;
+					appearNote.value.myReaction = reaction;
 				}
-
-				appearNote.value = updated;
 				break;
 			}
 
 			case 'unreacted': {
 				const reaction = body.reaction;
 
-				const updated = JSON.parse(JSON.stringify(appearNote.value));
-
 				// TODO: reactionsプロパティがない場合ってあったっけ? なければ || {} は消せる
 				const currentCount = (appearNote.value.reactions || {})[reaction] || 0;
 
-				updated.reactions[reaction] = Math.max(0, currentCount - 1);
+				appearNote.value.reactions[reaction] = Math.max(0, currentCount - 1);
 
 				if ($i && (body.userId === $i.id)) {
-					updated.myReaction = null;
+					appearNote.value.myReaction = null;
 				}
-
-				appearNote.value = updated;
 				break;
 			}
 
 			case 'pollVoted': {
 				const choice = body.choice;
 
-				const updated = JSON.parse(JSON.stringify(appearNote.value));
-
 				const choices = [...appearNote.value.poll.choices];
 				choices[choice] = {
 					...choices[choice],
@@ -73,16 +63,12 @@ export function useNoteCapture(props: {
 					} : {})
 				};
 
-				updated.poll.choices = choices;
-
-				appearNote.value = updated;
+				appearNote.value.poll.choices = choices;
 				break;
 			}
 
 			case 'deleted': {
-				const updated = JSON.parse(JSON.stringify(appearNote.value));
-				updated.value = true;
-				appearNote.value = updated;
+				appearNote.value.deletedAt = new Date();
 				break;
 			}
 		}

From 86931bdafdb23415818adbd25ae20f4896f0b6a5 Mon Sep 17 00:00:00 2001
From: syuilo <Syuilotan@yahoo.co.jp>
Date: Fri, 28 Jan 2022 01:09:46 +0900
Subject: [PATCH 35/46] =?UTF-8?q?fix(client):=20=E3=80=8C=E3=82=AF?=
 =?UTF-8?q?=E3=83=AA=E3=83=83=E3=83=97=E3=80=8D=E3=83=9A=E3=83=BC=E3=82=B8?=
 =?UTF-8?q?=E3=81=8C=E9=96=8B=E3=81=8B=E3=81=AA=E3=81=84=E5=95=8F=E9=A1=8C?=
 =?UTF-8?q?=E3=82=92=E4=BF=AE=E6=AD=A3?=
MIME-Version: 1.0
Content-Type: text/plain; charset=UTF-8
Content-Transfer-Encoding: 8bit

---
 CHANGELOG.md                                 | 1 +
 packages/client/src/pages/my-clips/index.vue | 2 +-
 2 files changed, 2 insertions(+), 1 deletion(-)

diff --git a/CHANGELOG.md b/CHANGELOG.md
index f60cbd1295..6a30c15922 100644
--- a/CHANGELOG.md
+++ b/CHANGELOG.md
@@ -13,6 +13,7 @@
 
 ### Bugfixes
 - 投稿のNSFW画像を表示したあとにリアクションが更新されると画像が非表示になる問題を修正
+- 「クリップ」ページが開かない問題を修正
 
 ## 12.102.1 (2022/01/27)
 ### Bugfixes
diff --git a/packages/client/src/pages/my-clips/index.vue b/packages/client/src/pages/my-clips/index.vue
index 97b563f6f8..4b31e6c8ba 100644
--- a/packages/client/src/pages/my-clips/index.vue
+++ b/packages/client/src/pages/my-clips/index.vue
@@ -19,7 +19,7 @@ import MkPagination from '@/components/ui/pagination.vue';
 import MkButton from '@/components/ui/button.vue';
 import * as os from '@/os';
 import * as symbols from '@/symbols';
-import i18n from '@/components/global/i18n';
+import { i18n } from '@/i18n';
 
 const pagination = {
 	endpoint: 'clips/list' as const,

From 2752858c7c00e59243f3edb3845ff7f00febb10f Mon Sep 17 00:00:00 2001
From: syuilo <Syuilotan@yahoo.co.jp>
Date: Fri, 28 Jan 2022 01:13:52 +0900
Subject: [PATCH 36/46] =?UTF-8?q?fix(client):=20=E3=83=88=E3=83=AC?=
 =?UTF-8?q?=E3=83=B3=E3=83=89=E3=82=A6=E3=82=A3=E3=82=B8=E3=82=A7=E3=83=83?=
 =?UTF-8?q?=E3=83=88=E3=81=8C=E5=8B=95=E4=BD=9C=E3=81=97=E3=81=AA=E3=81=84?=
 =?UTF-8?q?=E3=81=AE=E3=82=92=E4=BF=AE=E6=AD=A3?=
MIME-Version: 1.0
Content-Type: text/plain; charset=UTF-8
Content-Transfer-Encoding: 8bit

---
 CHANGELOG.md                           | 1 +
 packages/client/src/widgets/trends.vue | 4 ++--
 2 files changed, 3 insertions(+), 2 deletions(-)

diff --git a/CHANGELOG.md b/CHANGELOG.md
index 6a30c15922..d65cc8ebd5 100644
--- a/CHANGELOG.md
+++ b/CHANGELOG.md
@@ -14,6 +14,7 @@
 ### Bugfixes
 - 投稿のNSFW画像を表示したあとにリアクションが更新されると画像が非表示になる問題を修正
 - 「クリップ」ページが開かない問題を修正
+- トレンドウィジェットが動作しないのを修正
 
 ## 12.102.1 (2022/01/27)
 ### Bugfixes
diff --git a/packages/client/src/widgets/trends.vue b/packages/client/src/widgets/trends.vue
index eb5eb4049f..a34710eae7 100644
--- a/packages/client/src/widgets/trends.vue
+++ b/packages/client/src/widgets/trends.vue
@@ -52,8 +52,8 @@ const stats = ref([]);
 const fetching = ref(true);
 
 const fetch = () => {
-	os.api('hashtags/trend').then(stats => {
-		stats.value = stats;
+	os.api('hashtags/trend').then(res => {
+		stats.value = res;
 		fetching.value = false;
 	});
 };

From 6ebab5f57737842b609688c89a0063f10d247b9a Mon Sep 17 00:00:00 2001
From: syuilo <Syuilotan@yahoo.co.jp>
Date: Fri, 28 Jan 2022 11:19:18 +0900
Subject: [PATCH 37/46] chore(client): improve chart rendering

---
 packages/client/src/components/chart.vue | 22 ++++++++++++++++++++++
 1 file changed, 22 insertions(+)

diff --git a/packages/client/src/components/chart.vue b/packages/client/src/components/chart.vue
index 1959271f5d..d17c0c9f3e 100644
--- a/packages/client/src/components/chart.vue
+++ b/packages/client/src/components/chart.vue
@@ -143,6 +143,7 @@ export default defineComponent({
 			}
 
 			const gridColor = defaultStore.state.darkMode ? 'rgba(255, 255, 255, 0.1)' : 'rgba(0, 0, 0, 0.1)';
+			const vLineColor = defaultStore.state.darkMode ? 'rgba(255, 255, 255, 0.2)' : 'rgba(0, 0, 0, 0.2)';
 
 			// フォントカラー
 			Chart.defaults.color = getComputedStyle(document.documentElement).getPropertyValue('--fg');
@@ -255,6 +256,27 @@ export default defineComponent({
 						},
 					},
 				},
+				plugins: [{
+					id: 'vLine',
+					beforeDraw(chart, args, options) {
+						if (chart.tooltip._active && chart.tooltip._active.length) {
+							const activePoint = chart.tooltip._active[0];
+							const ctx = chart.ctx;
+							const x = activePoint.element.x;
+							const topY = chart.scales.y.top;
+							const bottomY = chart.scales.y.bottom;
+
+							ctx.save();
+							ctx.beginPath();
+							ctx.moveTo(x, bottomY);
+							ctx.lineTo(x, topY);
+							ctx.lineWidth = 1;
+							ctx.strokeStyle = vLineColor;
+							ctx.stroke();
+							ctx.restore();
+						}
+					}
+				}]
 			});
 		};
 

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 38/46] 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(() => {

From b946d89ec1bfb3f10fd0f1c13cc160ac202a6e5d Mon Sep 17 00:00:00 2001
From: syuilo <Syuilotan@yahoo.co.jp>
Date: Fri, 28 Jan 2022 11:53:12 +0900
Subject: [PATCH 39/46] refactor(client): better semantics

---
 packages/client/src/account.ts                            | 6 +++---
 packages/client/src/components/drive.file.vue             | 2 +-
 packages/client/src/components/drive.vue                  | 2 +-
 packages/client/src/components/emoji-picker.vue           | 2 +-
 packages/client/src/components/global/header.vue          | 4 ++--
 packages/client/src/components/page-window.vue            | 2 +-
 packages/client/src/components/post-form-attaches.vue     | 2 +-
 packages/client/src/components/post-form.vue              | 6 +++---
 packages/client/src/components/sample.vue                 | 2 +-
 packages/client/src/menu.ts                               | 6 +++---
 packages/client/src/pages/admin/emojis.vue                | 8 ++++----
 packages/client/src/pages/admin/index.vue                 | 2 +-
 packages/client/src/pages/channel-editor.vue              | 2 +-
 packages/client/src/pages/clip.vue                        | 2 +-
 packages/client/src/pages/emojis.emoji.vue                | 2 +-
 packages/client/src/pages/emojis.vue                      | 2 +-
 packages/client/src/pages/gallery/edit.vue                | 2 +-
 packages/client/src/pages/messaging/index.vue             | 2 +-
 .../client/src/pages/messaging/messaging-room.form.vue    | 4 ++--
 packages/client/src/pages/messaging/messaging-room.vue    | 2 +-
 packages/client/src/pages/notifications.vue               | 2 +-
 packages/client/src/pages/page-editor/page-editor.vue     | 2 +-
 packages/client/src/pages/settings/accounts.vue           | 4 ++--
 packages/client/src/pages/settings/import-export.vue      | 8 ++++----
 packages/client/src/pages/settings/profile.vue            | 4 ++--
 packages/client/src/pages/settings/theme.vue              | 2 +-
 packages/client/src/pages/timeline.vue                    | 6 +++---
 packages/client/src/pages/user/index.vue                  | 2 +-
 packages/client/src/pages/welcome.entrance.a.vue          | 2 +-
 packages/client/src/pages/welcome.entrance.b.vue          | 2 +-
 packages/client/src/pages/welcome.entrance.c.vue          | 2 +-
 packages/client/src/widgets/timeline.vue                  | 2 +-
 32 files changed, 50 insertions(+), 50 deletions(-)

diff --git a/packages/client/src/account.ts b/packages/client/src/account.ts
index a04d0378c8..4aeceeccab 100644
--- a/packages/client/src/account.ts
+++ b/packages/client/src/account.ts
@@ -205,18 +205,18 @@ export async function openAccountMenu(opts: {
 				}, {
 					text: i18n.ts.createAccount,
 					action: () => { createAccount(); },
-				}], ev.currentTarget || ev.target);
+				}], ev.currentTarget ?? ev.target);
 			},
 		}, {
 			type: 'link',
 			icon: 'fas fa-users',
 			text: i18n.ts.manageAccounts,
 			to: `/settings/accounts`,
-		}]], ev.currentTarget || ev.target, {
+		}]], ev.currentTarget ?? ev.target, {
 			align: 'left'
 		});
 	} else {
-		popupMenu([...(opts.includeCurrentAccount ? [createItem($i)] : []), ...accountItemPromises], ev.currentTarget || ev.target, {
+		popupMenu([...(opts.includeCurrentAccount ? [createItem($i)] : []), ...accountItemPromises], ev.currentTarget ?? ev.target, {
 			align: 'left'
 		});
 	}
diff --git a/packages/client/src/components/drive.file.vue b/packages/client/src/components/drive.file.vue
index 669c0d7db1..209e9b7c47 100644
--- a/packages/client/src/components/drive.file.vue
+++ b/packages/client/src/components/drive.file.vue
@@ -95,7 +95,7 @@ function onClick(ev: MouseEvent) {
 	if (props.selectMode) {
 		emit('chosen', props.file);
 	} else {
-		os.popupMenu(getMenu(), (ev.currentTarget || ev.target || undefined) as HTMLElement | undefined);
+		os.popupMenu(getMenu(), (ev.currentTarget ?? ev.target || undefined) as HTMLElement | undefined);
 	}
 }
 
diff --git a/packages/client/src/components/drive.vue b/packages/client/src/components/drive.vue
index b706839540..0d3f29d4c3 100644
--- a/packages/client/src/components/drive.vue
+++ b/packages/client/src/components/drive.vue
@@ -591,7 +591,7 @@ function getMenu() {
 }
 
 function showMenu(ev: MouseEvent) {
-	os.popupMenu(getMenu(), (ev.currentTarget || ev.target || undefined) as HTMLElement | undefined);
+	os.popupMenu(getMenu(), (ev.currentTarget ?? ev.target || undefined) as HTMLElement | undefined);
 }
 
 function onContextmenu(ev: MouseEvent) {
diff --git a/packages/client/src/components/emoji-picker.vue b/packages/client/src/components/emoji-picker.vue
index f291510555..6999ad6517 100644
--- a/packages/client/src/components/emoji-picker.vue
+++ b/packages/client/src/components/emoji-picker.vue
@@ -280,7 +280,7 @@ function getKey(emoji: string | Misskey.entities.CustomEmoji | UnicodeEmojiDef):
 }
 
 function chosen(emoji: any, ev?: MouseEvent) {
-	const el = ev && (ev.currentTarget || ev.target) as HTMLElement | null | undefined;
+	const el = ev && (ev.currentTarget ?? ev.target) as HTMLElement | null | undefined;
 	if (el) {
 		const rect = el.getBoundingClientRect();
 		const x = rect.left + (el.offsetWidth / 2);
diff --git a/packages/client/src/components/global/header.vue b/packages/client/src/components/global/header.vue
index 02598d95b3..e558614c12 100644
--- a/packages/client/src/components/global/header.vue
+++ b/packages/client/src/components/global/header.vue
@@ -113,7 +113,7 @@ export default defineComponent({
 				if (menu.length > 0) menu.push(null);
 				menu = menu.concat(props.menu);
 			}
-			popupMenu(menu, ev.currentTarget || ev.target);
+			popupMenu(menu, ev.currentTarget ?? ev.target);
 		};
 
 		const showTabsPopup = (ev: MouseEvent) => {
@@ -126,7 +126,7 @@ export default defineComponent({
 				icon: tab.icon,
 				action: tab.onClick,
 			}));
-			popupMenu(menu, ev.currentTarget || ev.target);
+			popupMenu(menu, ev.currentTarget ?? ev.target);
 		};
 
 		const preventDrag = (ev: TouchEvent) => {
diff --git a/packages/client/src/components/page-window.vue b/packages/client/src/components/page-window.vue
index ec7451d5aa..7455236bad 100644
--- a/packages/client/src/components/page-window.vue
+++ b/packages/client/src/components/page-window.vue
@@ -160,7 +160,7 @@ export default defineComponent({
 				action: () => {
 					copyToClipboard(this.url);
 				}
-			}], ev.currentTarget || ev.target);
+			}], ev.currentTarget ?? ev.target);
 		},
 
 		back() {
diff --git a/packages/client/src/components/post-form-attaches.vue b/packages/client/src/components/post-form-attaches.vue
index 0c8181b481..9dd69a0ee5 100644
--- a/packages/client/src/components/post-form-attaches.vue
+++ b/packages/client/src/components/post-form-attaches.vue
@@ -127,7 +127,7 @@ export default defineComponent({
 				text: this.$ts.attachCancel,
 				icon: 'fas fa-times-circle',
 				action: () => { this.detachMedia(file.id) }
-			}], ev.currentTarget || ev.target).then(() => this.menu = null);
+			}], ev.currentTarget ?? ev.target).then(() => this.menu = null);
 		}
 	}
 });
diff --git a/packages/client/src/components/post-form.vue b/packages/client/src/components/post-form.vue
index a8882fc05f..8c5027f8e7 100644
--- a/packages/client/src/components/post-form.vue
+++ b/packages/client/src/components/post-form.vue
@@ -342,7 +342,7 @@ function focus() {
 }
 
 function chooseFileFrom(ev) {
-	selectFiles(ev.currentTarget || ev.target, i18n.ts.attachFile).then(files_ => {
+	selectFiles(ev.currentTarget ?? ev.target, i18n.ts.attachFile).then(files_ => {
 		for (const file of files_) {
 			files.push(file);
 		}
@@ -592,7 +592,7 @@ function insertMention() {
 }
 
 async function insertEmoji(ev: MouseEvent) {
-	os.openEmojiPicker(ev.currentTarget || ev.target, {}, textareaEl);
+	os.openEmojiPicker(ev.currentTarget ?? ev.target, {}, textareaEl);
 }
 
 function showActions(ev) {
@@ -605,7 +605,7 @@ function showActions(ev) {
 				if (key === 'text') { text = value; }
 			});
 		}
-	})), ev.currentTarget || ev.target);
+	})), ev.currentTarget ?? ev.target);
 }
 
 let postAccount = $ref<misskey.entities.UserDetailed | null>(null);
diff --git a/packages/client/src/components/sample.vue b/packages/client/src/components/sample.vue
index 03ad6a9838..65249ff7e9 100644
--- a/packages/client/src/components/sample.vue
+++ b/packages/client/src/components/sample.vue
@@ -109,7 +109,7 @@ export default defineComponent({
 				text: 'Delete some bananas',
 				danger: true,
 				action: () => {},
-			}], ev.currentTarget || ev.target);
+			}], ev.currentTarget ?? ev.target);
 		},
 	}
 });
diff --git a/packages/client/src/menu.ts b/packages/client/src/menu.ts
index 5f7a527095..ebc7898101 100644
--- a/packages/client/src/menu.ts
+++ b/packages/client/src/menu.ts
@@ -78,7 +78,7 @@ export const menuDef = reactive({
 				}];
 				items.value = _items;
 			});
-			os.popupMenu(items, ev.currentTarget || ev.target);
+			os.popupMenu(items, ev.currentTarget ?? ev.target);
 		},
 	},
 	groups: {
@@ -109,7 +109,7 @@ export const menuDef = reactive({
 				}];
 				items.value = _items;
 			});
-			os.popupMenu(items, ev.currentTarget || ev.target);
+			os.popupMenu(items, ev.currentTarget ?? ev.target);
 		},
 	},
 	mentions: {
@@ -200,7 +200,7 @@ export const menuDef = reactive({
 					localStorage.setItem('ui', 'desktop');
 					unisonReload();
 				}
-			}*/], ev.currentTarget || ev.target);
+			}*/], ev.currentTarget ?? ev.target);
 		},
 	},
 });
diff --git a/packages/client/src/pages/admin/emojis.vue b/packages/client/src/pages/admin/emojis.vue
index f6fbf7dbd9..a080ee9c23 100644
--- a/packages/client/src/pages/admin/emojis.vue
+++ b/packages/client/src/pages/admin/emojis.vue
@@ -118,7 +118,7 @@ const toggleSelect = (emoji) => {
 };
 
 const add = async (ev: MouseEvent) => {
-	const files = await selectFiles(ev.currentTarget || ev.target, null);
+	const files = await selectFiles(ev.currentTarget ?? ev.target, null);
 
 	const promise = Promise.all(files.map(file => os.api('admin/emoji/add', {
 		fileId: file.id,
@@ -160,7 +160,7 @@ const remoteMenu = (emoji, ev: MouseEvent) => {
 		text: i18n.ts.import,
 		icon: 'fas fa-plus',
 		action: () => { im(emoji) }
-	}], ev.currentTarget || ev.target);
+	}], ev.currentTarget ?? ev.target);
 };
 
 const menu = (ev: MouseEvent) => {
@@ -186,7 +186,7 @@ const menu = (ev: MouseEvent) => {
 		icon: 'fas fa-upload',
 		text: i18n.ts.import,
 		action: async () => {
-			const file = await selectFile(ev.currentTarget || ev.target);
+			const file = await selectFile(ev.currentTarget ?? ev.target);
 			os.api('admin/emoji/import-zip', {
 				fileId: file.id,
 			})
@@ -202,7 +202,7 @@ const menu = (ev: MouseEvent) => {
 				});
 			});
 		}
-	}], ev.currentTarget || ev.target);
+	}], ev.currentTarget ?? ev.target);
 };
 
 const setCategoryBulk = async () => {
diff --git a/packages/client/src/pages/admin/index.vue b/packages/client/src/pages/admin/index.vue
index 6b1b5b86a9..6b11650f48 100644
--- a/packages/client/src/pages/admin/index.vue
+++ b/packages/client/src/pages/admin/index.vue
@@ -298,7 +298,7 @@ export default defineComponent({
 				action: () => {
 					alert('TODO');
 				}
-			}], ev.currentTarget || ev.target);
+			}], ev.currentTarget ?? ev.target);
 		};
 
 		return {
diff --git a/packages/client/src/pages/channel-editor.vue b/packages/client/src/pages/channel-editor.vue
index 58c644be62..3818c7481a 100644
--- a/packages/client/src/pages/channel-editor.vue
+++ b/packages/client/src/pages/channel-editor.vue
@@ -112,7 +112,7 @@ export default defineComponent({
 		},
 
 		setBannerImage(e) {
-			selectFile(e.currentTarget || e.target, null).then(file => {
+			selectFile(e.currentTarget ?? e.target, null).then(file => {
 				this.bannerId = file.id;
 			});
 		},
diff --git a/packages/client/src/pages/clip.vue b/packages/client/src/pages/clip.vue
index 6b49221d32..c999f1bfc9 100644
--- a/packages/client/src/pages/clip.vue
+++ b/packages/client/src/pages/clip.vue
@@ -127,7 +127,7 @@ export default defineComponent({
 						clipId: this.clip.id,
 					});
 				}
-			} : undefined], ev.currentTarget || ev.target);
+			} : undefined], ev.currentTarget ?? ev.target);
 		}
 	}
 });
diff --git a/packages/client/src/pages/emojis.emoji.vue b/packages/client/src/pages/emojis.emoji.vue
index 9e4deb9ceb..b2801694db 100644
--- a/packages/client/src/pages/emojis.emoji.vue
+++ b/packages/client/src/pages/emojis.emoji.vue
@@ -29,7 +29,7 @@ function menu(ev) {
 			copyToClipboard(`:${props.emoji.name}:`);
 			os.success();
 		}
-	}], ev.currentTarget || ev.target);
+	}], ev.currentTarget ?? ev.target);
 }
 </script>
 
diff --git a/packages/client/src/pages/emojis.vue b/packages/client/src/pages/emojis.vue
index 69e3147750..886b5f7119 100644
--- a/packages/client/src/pages/emojis.vue
+++ b/packages/client/src/pages/emojis.vue
@@ -32,7 +32,7 @@ function menu(ev) {
 				});
 			});
 		}
-	}], ev.currentTarget || ev.target);
+	}], ev.currentTarget ?? ev.target);
 }
 
 defineExpose({
diff --git a/packages/client/src/pages/gallery/edit.vue b/packages/client/src/pages/gallery/edit.vue
index e3fa1a0fcd..25ee513186 100644
--- a/packages/client/src/pages/gallery/edit.vue
+++ b/packages/client/src/pages/gallery/edit.vue
@@ -92,7 +92,7 @@ export default defineComponent({
 
 	methods: {
 		selectFile(e) {
-			selectFiles(e.currentTarget || e.target, null).then(files => {
+			selectFiles(e.currentTarget ?? e.target, null).then(files => {
 				this.files = this.files.concat(files);
 			});
 		},
diff --git a/packages/client/src/pages/messaging/index.vue b/packages/client/src/pages/messaging/index.vue
index 554ebc4b6b..88a1e07afc 100644
--- a/packages/client/src/pages/messaging/index.vue
+++ b/packages/client/src/pages/messaging/index.vue
@@ -128,7 +128,7 @@ export default defineComponent({
 				text: this.$ts.messagingWithGroup,
 				icon: 'fas fa-users',
 				action: () => { this.startGroup() }
-			}], ev.currentTarget || ev.target);
+			}], ev.currentTarget ?? ev.target);
 		},
 
 		async startUser() {
diff --git a/packages/client/src/pages/messaging/messaging-room.form.vue b/packages/client/src/pages/messaging/messaging-room.form.vue
index 1b9421ca9a..3863c8f82b 100644
--- a/packages/client/src/pages/messaging/messaging-room.form.vue
+++ b/packages/client/src/pages/messaging/messaging-room.form.vue
@@ -154,7 +154,7 @@ export default defineComponent({
 		},
 
 		chooseFile(e) {
-			selectFile(e.currentTarget || e.target, this.$ts.selectFile).then(file => {
+			selectFile(e.currentTarget ?? e.target, this.$ts.selectFile).then(file => {
 				this.file = file;
 			});
 		},
@@ -214,7 +214,7 @@ export default defineComponent({
 		},
 
 		async insertEmoji(ev) {
-			os.openEmojiPicker(ev.currentTarget || ev.target, {}, this.$refs.text);
+			os.openEmojiPicker(ev.currentTarget ?? ev.target, {}, this.$refs.text);
 		}
 	}
 });
diff --git a/packages/client/src/pages/messaging/messaging-room.vue b/packages/client/src/pages/messaging/messaging-room.vue
index 65c44ce113..2ecc68eb54 100644
--- a/packages/client/src/pages/messaging/messaging-room.vue
+++ b/packages/client/src/pages/messaging/messaging-room.vue
@@ -335,7 +335,7 @@ const Component = defineComponent({
 					popout(path);
 					this.$router.back();
 				},
-			}], ev.currentTarget || ev.target);
+			}], ev.currentTarget ?? ev.target);
 		}
 	}
 });
diff --git a/packages/client/src/pages/notifications.vue b/packages/client/src/pages/notifications.vue
index 96c5b3ca85..36e423e534 100644
--- a/packages/client/src/pages/notifications.vue
+++ b/packages/client/src/pages/notifications.vue
@@ -32,7 +32,7 @@ function setFilter(ev) {
 			includeTypes = null;
 		}
 	}, null, ...typeItems] : typeItems;
-	os.popupMenu(items, ev.currentTarget || ev.target);
+	os.popupMenu(items, ev.currentTarget ?? ev.target);
 }
 
 defineExpose({
diff --git a/packages/client/src/pages/page-editor/page-editor.vue b/packages/client/src/pages/page-editor/page-editor.vue
index fe207555f8..f302ac4f90 100644
--- a/packages/client/src/pages/page-editor/page-editor.vue
+++ b/packages/client/src/pages/page-editor/page-editor.vue
@@ -448,7 +448,7 @@ export default defineComponent({
 		},
 
 		setEyeCatchingImage(e) {
-			selectFile(e.currentTarget || e.target, null).then(file => {
+			selectFile(e.currentTarget ?? e.target, null).then(file => {
 				this.eyeCatchingImageId = file.id;
 			});
 		},
diff --git a/packages/client/src/pages/settings/accounts.vue b/packages/client/src/pages/settings/accounts.vue
index c795ede8ac..a744a031d4 100644
--- a/packages/client/src/pages/settings/accounts.vue
+++ b/packages/client/src/pages/settings/accounts.vue
@@ -64,7 +64,7 @@ export default defineComponent({
 				icon: 'fas fa-trash-alt',
 				danger: true,
 				action: () => this.removeAccount(account),
-			}], ev.currentTarget || ev.target);
+			}], ev.currentTarget ?? ev.target);
 		},
 
 		addAccount(ev) {
@@ -74,7 +74,7 @@ export default defineComponent({
 			}, {
 				text: this.$ts.createAccount,
 				action: () => { this.createAccount(); },
-			}], ev.currentTarget || ev.target);
+			}], ev.currentTarget ?? ev.target);
 		},
 
 		addExistingAccount() {
diff --git a/packages/client/src/pages/settings/import-export.vue b/packages/client/src/pages/settings/import-export.vue
index 7b554dcd88..c153b4d28c 100644
--- a/packages/client/src/pages/settings/import-export.vue
+++ b/packages/client/src/pages/settings/import-export.vue
@@ -114,22 +114,22 @@ export default defineComponent({
 		};
 
 		const importFollowing = async (ev) => {
-			const file = await selectFile(ev.currentTarget || ev.target);
+			const file = await selectFile(ev.currentTarget ?? ev.target);
 			os.api('i/import-following', { fileId: file.id }).then(onImportSuccess).catch(onError);
 		};
 
 		const importUserLists = async (ev) => {
-			const file = await selectFile(ev.currentTarget || ev.target);
+			const file = await selectFile(ev.currentTarget ?? ev.target);
 			os.api('i/import-user-lists', { fileId: file.id }).then(onImportSuccess).catch(onError);
 		};
 
 		const importMuting = async (ev) => {
-			const file = await selectFile(ev.currentTarget || ev.target);
+			const file = await selectFile(ev.currentTarget ?? ev.target);
 			os.api('i/import-muting', { fileId: file.id }).then(onImportSuccess).catch(onError);
 		};
 
 		const importBlocking = async (ev) => {
-			const file = await selectFile(ev.currentTarget || ev.target);
+			const file = await selectFile(ev.currentTarget ?? ev.target);
 			os.api('i/import-blocking', { fileId: file.id }).then(onImportSuccess).catch(onError);
 		};
 
diff --git a/packages/client/src/pages/settings/profile.vue b/packages/client/src/pages/settings/profile.vue
index 0786e7f4ae..66b654d87f 100644
--- a/packages/client/src/pages/settings/profile.vue
+++ b/packages/client/src/pages/settings/profile.vue
@@ -102,7 +102,7 @@ function save() {
 }
 
 function changeAvatar(ev) {
-	selectFile(ev.currentTarget || ev.target, i18n.ts.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.ts.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,
 		});
diff --git a/packages/client/src/pages/settings/theme.vue b/packages/client/src/pages/settings/theme.vue
index fefd72777a..3e4ec1b2af 100644
--- a/packages/client/src/pages/settings/theme.vue
+++ b/packages/client/src/pages/settings/theme.vue
@@ -184,7 +184,7 @@ export default defineComponent({
 			themesCount,
 			wallpaper,
 			setWallpaper(e) {
-				selectFile(e.currentTarget || e.target, null).then(file => {
+				selectFile(e.currentTarget ?? e.target, null).then(file => {
 					wallpaper.value = file.url;
 				});
 			},
diff --git a/packages/client/src/pages/timeline.vue b/packages/client/src/pages/timeline.vue
index a55fe1eb91..b2266d22c3 100644
--- a/packages/client/src/pages/timeline.vue
+++ b/packages/client/src/pages/timeline.vue
@@ -64,7 +64,7 @@ async function chooseList(ev: MouseEvent): Promise<void> {
 		text: list.name,
 		to: `/timeline/list/${list.id}`,
 	}));
-	os.popupMenu(items, ev.currentTarget || ev.target);
+	os.popupMenu(items, ev.currentTarget ?? ev.target);
 }
 
 async function chooseAntenna(ev: MouseEvent): Promise<void> {
@@ -75,7 +75,7 @@ async function chooseAntenna(ev: MouseEvent): Promise<void> {
 		indicate: antenna.hasUnreadNote,
 		to: `/timeline/antenna/${antenna.id}`,
 	}));
-	os.popupMenu(items, ev.currentTarget || ev.target);
+	os.popupMenu(items, ev.currentTarget ?? ev.target);
 }
 
 async function chooseChannel(ev: MouseEvent): Promise<void> {
@@ -86,7 +86,7 @@ async function chooseChannel(ev: MouseEvent): Promise<void> {
 		indicate: channel.hasUnreadNote,
 		to: `/channels/${channel.id}`,
 	}));
-	os.popupMenu(items, ev.currentTarget || ev.target);
+	os.popupMenu(items, ev.currentTarget ?? ev.target);
 }
 
 function saveSrc(): void {
diff --git a/packages/client/src/pages/user/index.vue b/packages/client/src/pages/user/index.vue
index 599e24d81c..10a86243f9 100644
--- a/packages/client/src/pages/user/index.vue
+++ b/packages/client/src/pages/user/index.vue
@@ -264,7 +264,7 @@ export default defineComponent({
 		},
 
 		menu(ev) {
-			os.popupMenu(getUserMenu(this.user), ev.currentTarget || ev.target);
+			os.popupMenu(getUserMenu(this.user), ev.currentTarget ?? ev.target);
 		},
 
 		parallaxLoop() {
diff --git a/packages/client/src/pages/welcome.entrance.a.vue b/packages/client/src/pages/welcome.entrance.a.vue
index efdc038b7e..47e1f12342 100644
--- a/packages/client/src/pages/welcome.entrance.a.vue
+++ b/packages/client/src/pages/welcome.entrance.a.vue
@@ -135,7 +135,7 @@ export default defineComponent({
 				action: () => {
 					window.open(`https://misskey-hub.net/help.md`, '_blank');
 				}
-			}], ev.currentTarget || ev.target);
+			}], ev.currentTarget ?? ev.target);
 		},
 
 		number
diff --git a/packages/client/src/pages/welcome.entrance.b.vue b/packages/client/src/pages/welcome.entrance.b.vue
index 93344dc9a8..053087fda0 100644
--- a/packages/client/src/pages/welcome.entrance.b.vue
+++ b/packages/client/src/pages/welcome.entrance.b.vue
@@ -119,7 +119,7 @@ export default defineComponent({
 				action: () => {
 					window.open(`https://misskey-hub.net/help.md`, '_blank');
 				}
-			}], ev.currentTarget || ev.target);
+			}], ev.currentTarget ?? ev.target);
 		},
 
 		number
diff --git a/packages/client/src/pages/welcome.entrance.c.vue b/packages/client/src/pages/welcome.entrance.c.vue
index 36b61647a6..6bf487e16e 100644
--- a/packages/client/src/pages/welcome.entrance.c.vue
+++ b/packages/client/src/pages/welcome.entrance.c.vue
@@ -139,7 +139,7 @@ export default defineComponent({
 				action: () => {
 					window.open(`https://misskey-hub.net/help.md`, '_blank');
 				}
-			}], ev.currentTarget || ev.target);
+			}], ev.currentTarget ?? ev.target);
 		},
 
 		number
diff --git a/packages/client/src/widgets/timeline.vue b/packages/client/src/widgets/timeline.vue
index 0e4396c133..34e3b20e36 100644
--- a/packages/client/src/widgets/timeline.vue
+++ b/packages/client/src/widgets/timeline.vue
@@ -116,7 +116,7 @@ const choose = async (ev) => {
 		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(() => {
+	}, antennaItems.length > 0 ? null : undefined, ...antennaItems, listItems.length > 0 ? null : undefined, ...listItems], ev.currentTarget ?? ev.target).then(() => {
 		menuOpened.value = false;
 	});
 };

From bfc9873fb9e6906b8db124aeda6144a1d79fc4c1 Mon Sep 17 00:00:00 2001
From: syuilo <Syuilotan@yahoo.co.jp>
Date: Fri, 28 Jan 2022 12:14:21 +0900
Subject: [PATCH 40/46] refactor(client): use setup sugar

---
 packages/client/src/components/ui/modal.vue | 394 +++++++++-----------
 1 file changed, 182 insertions(+), 212 deletions(-)

diff --git a/packages/client/src/components/ui/modal.vue b/packages/client/src/components/ui/modal.vue
index c691c8c6d0..cb46d38742 100644
--- a/packages/client/src/components/ui/modal.vue
+++ b/packages/client/src/components/ui/modal.vue
@@ -1,5 +1,5 @@
 <template>
-<transition :name="$store.state.animation ? (type === 'drawer') ? 'modal-drawer' : (type === 'popup') ? 'modal-popup' : 'modal' : ''" :duration="$store.state.animation ? 200 : 0" appear @after-leave="$emit('closed')" @enter="$emit('opening')" @after-enter="childRendered">
+<transition :name="$store.state.animation ? (type === 'drawer') ? 'modal-drawer' : (type === 'popup') ? 'modal-popup' : 'modal' : ''" :duration="$store.state.animation ? 200 : 0" appear @after-leave="emit('closed')" @enter="emit('opening')" @after-enter="childRendered">
 	<div v-show="manualShowing != null ? manualShowing : showing" v-hotkey.global="keymap" class="qzhlnise" :class="{ drawer: type === 'drawer', dialog: type === 'dialog' || type === 'dialog:top', popup: type === 'popup' }" :style="{ zIndex, pointerEvents: (manualShowing != null ? manualShowing : showing) ? 'auto' : 'none', '--transformOrigin': transformOrigin }">
 		<div class="bg _modalBg" :class="{ transparent: transparentBg && (type === 'popup') }" :style="{ zIndex }" @click="onBgClick" @contextmenu.prevent.stop="() => {}"></div>
 		<div ref="content" class="content" :class="{ fixed, top: type === 'dialog:top' }" :style="{ zIndex }" @click.self="onBgClick">
@@ -9,8 +9,8 @@
 </transition>
 </template>
 
-<script lang="ts">
-import { defineComponent, nextTick, onMounted, computed, PropType, ref, watch } from 'vue';
+<script lang="ts" setup>
+import { nextTick, onMounted, computed, ref, watch, provide } from 'vue';
 import * as os from '@/os';
 import { isTouchUsing } from '@/scripts/touch';
 import { defaultStore } from '@/store';
@@ -25,234 +25,204 @@ function getFixedContainer(el: Element | null): Element | null {
 	}
 }
 
-export default defineComponent({
-	provide: {
-		modal: true
-	},
+const props = withDefaults(defineProps<{
+	manualShowing?: boolean;
+	srcCenter?: boolean;
+	src?: HTMLElement;
+	preferType?: string;
+	zPriority?: 'low' | 'middle' | 'high';
+	noOverlap?: boolean;
+	transparentBg?: boolean;
+}>(), {
+	manualShowing: null,
+	src: null,
+	preferType: 'auto',
+	zPriority: 'low',
+	noOverlap: true,
+	transparentBg: false,
+});
 
-	props: {
-		manualShowing: {
-			type: Boolean,
-			required: false,
-			default: null,
-		},
-		srcCenter: {
-			type: Boolean,
-			required: false
-		},
-		src: {
-			type: Object as PropType<HTMLElement>,
-			required: false,
-			default: null,
-		},
-		preferType: {
-			required: false,
-			type: String,
-			default: 'auto',
-		},
-		zPriority: {
-			type: String as PropType<'low' | 'middle' | 'high'>,
-			required: false,
-			default: 'low',
-		},
-		noOverlap: {
-			type: Boolean,
-			required: false,
-			default: true,
-		},
-		transparentBg: {
-			type: Boolean,
-			required: false,
-			default: false,
-		},
-	},
+const emit = defineEmits<{
+	(ev: 'opening'): void;
+	(ev: 'click'): void;
+	(ev: 'esc'): void;
+	(ev: 'close'): void;
+	(ev: 'closed'): void;
+}>();
 
-	emits: ['opening', 'click', 'esc', 'close', 'closed'],
+provide('modal', true);
 
-	setup(props, context) {
-		const maxHeight = ref<number>();
-		const fixed = ref(false);
-		const transformOrigin = ref('center');
-		const showing = ref(true);
-		const content = ref<HTMLElement>();
-		const zIndex = os.claimZIndex(props.zPriority);
-		const type = computed(() => {
-			if (props.preferType === 'auto') {
-				if (!defaultStore.state.disableDrawer && isTouchUsing && window.innerWidth < 500 && window.innerHeight < 1000) {
-					return 'drawer';
-				} else {
-					return props.src != null ? 'popup' : 'dialog';
-				}
-			} else {
-				return props.preferType;
-			}
-		});
-		
-		let contentClicking = false;
+const maxHeight = ref<number>();
+const fixed = ref(false);
+const transformOrigin = ref('center');
+const showing = ref(true);
+const content = ref<HTMLElement>();
+const zIndex = os.claimZIndex(props.zPriority);
+const type = computed(() => {
+	if (props.preferType === 'auto') {
+		if (!defaultStore.state.disableDrawer && isTouchUsing && window.innerWidth < 500 && window.innerHeight < 1000) {
+			return 'drawer';
+		} else {
+			return props.src != null ? 'popup' : 'dialog';
+		}
+	} else {
+		return props.preferType;
+	}
+});
 
-		const close = () => {
-			// eslint-disable-next-line vue/no-mutating-props
-			if (props.src) props.src.style.pointerEvents = 'auto';
-			showing.value = false;
-			context.emit('close');
-		};
+let contentClicking = false;
 
-		const onBgClick = () => {
-			if (contentClicking) return;
-			context.emit('click');
-		};
+const close = () => {
+	// eslint-disable-next-line vue/no-mutating-props
+	if (props.src) props.src.style.pointerEvents = 'auto';
+	showing.value = false;
+	emit('close');
+};
 
-		if (type.value === 'drawer') {
-			maxHeight.value = window.innerHeight / 2;
+const onBgClick = () => {
+	if (contentClicking) return;
+	emit('click');
+};
+
+if (type.value === 'drawer') {
+	maxHeight.value = window.innerHeight / 2;
+}
+
+const keymap = {
+	'esc': () => emit('esc'),
+};
+
+const MARGIN = 16;
+
+const align = () => {
+	if (props.src == null) return;
+	if (type.value === 'drawer') return;
+
+	const popover = content.value!;
+
+	if (popover == null) return;
+
+	const rect = props.src.getBoundingClientRect();
+	
+	const width = popover.offsetWidth;
+	const height = popover.offsetHeight;
+
+	let left;
+	let top;
+
+	if (props.srcCenter) {
+		const x = rect.left + (fixed.value ? 0 : window.pageXOffset) + (props.src.offsetWidth / 2);
+		const y = rect.top + (fixed.value ? 0 : window.pageYOffset) + (props.src.offsetHeight / 2);
+		left = (x - (width / 2));
+		top = (y - (height / 2));
+	} else {
+		const x = rect.left + (fixed.value ? 0 : window.pageXOffset) + (props.src.offsetWidth / 2);
+		const y = rect.top + (fixed.value ? 0 : window.pageYOffset) + props.src.offsetHeight;
+		left = (x - (width / 2));
+		top = y;
+	}
+
+	if (fixed.value) {
+		// 画面から横にはみ出る場合
+		if (left + width > window.innerWidth) {
+			left = window.innerWidth - width;
 		}
 
-		const keymap = {
-			'esc': () => context.emit('esc'),
-		};
-
-		const MARGIN = 16;
-
-		const align = () => {
-			if (props.src == null) return;
-			if (type.value === 'drawer') return;
-
-			const popover = content.value!;
-
-			if (popover == null) return;
-
-			const rect = props.src.getBoundingClientRect();
-			
-			const width = popover.offsetWidth;
-			const height = popover.offsetHeight;
-
-			let left;
-			let top;
-
-			if (props.srcCenter) {
-				const x = rect.left + (fixed.value ? 0 : window.pageXOffset) + (props.src.offsetWidth / 2);
-				const y = rect.top + (fixed.value ? 0 : window.pageYOffset) + (props.src.offsetHeight / 2);
-				left = (x - (width / 2));
-				top = (y - (height / 2));
-			} else {
-				const x = rect.left + (fixed.value ? 0 : window.pageXOffset) + (props.src.offsetWidth / 2);
-				const y = rect.top + (fixed.value ? 0 : window.pageYOffset) + props.src.offsetHeight;
-				left = (x - (width / 2));
-				top = y;
-			}
-
-			if (fixed.value) {
-				// 画面から横にはみ出る場合
-				if (left + width > window.innerWidth) {
-					left = window.innerWidth - width;
-				}
-
-				// 画面から縦にはみ出る場合
-				if (top + height > (window.innerHeight - MARGIN)) {
-					if (props.noOverlap) {
-						const underSpace = (window.innerHeight - MARGIN) - top;
-						const upperSpace = (rect.top - MARGIN);
-						if (underSpace >= (upperSpace / 3)) {
-							maxHeight.value =  underSpace;
-						} else {
-							maxHeight.value =  upperSpace;
-							top = (upperSpace + MARGIN) - height;
-						}
-					} else {
-						top = (window.innerHeight - MARGIN) - height;
-					}
+		// 画面から縦にはみ出る場合
+		if (top + height > (window.innerHeight - MARGIN)) {
+			if (props.noOverlap) {
+				const underSpace = (window.innerHeight - MARGIN) - top;
+				const upperSpace = (rect.top - MARGIN);
+				if (underSpace >= (upperSpace / 3)) {
+					maxHeight.value =  underSpace;
+				} else {
+					maxHeight.value =  upperSpace;
+					top = (upperSpace + MARGIN) - height;
 				}
 			} else {
-				// 画面から横にはみ出る場合
-				if (left + width - window.pageXOffset > window.innerWidth) {
-					left = window.innerWidth - width + window.pageXOffset - 1;
+				top = (window.innerHeight - MARGIN) - height;
+			}
+		}
+	} else {
+		// 画面から横にはみ出る場合
+		if (left + width - window.pageXOffset > window.innerWidth) {
+			left = window.innerWidth - width + window.pageXOffset - 1;
+		}
+
+		// 画面から縦にはみ出る場合
+		if (top + height - window.pageYOffset > (window.innerHeight - MARGIN)) {
+			if (props.noOverlap) {
+				const underSpace = (window.innerHeight - MARGIN) - (top - window.pageYOffset);
+				const upperSpace = (rect.top - MARGIN);
+				if (underSpace >= (upperSpace / 3)) {
+					maxHeight.value =  underSpace;
+				} else {
+					maxHeight.value =  upperSpace;
+					top = window.pageYOffset + ((upperSpace + MARGIN) - height);
 				}
-
-				// 画面から縦にはみ出る場合
-				if (top + height - window.pageYOffset > (window.innerHeight - MARGIN)) {
-					if (props.noOverlap) {
-						const underSpace = (window.innerHeight - MARGIN) - (top - window.pageYOffset);
-						const upperSpace = (rect.top - MARGIN);
-						if (underSpace >= (upperSpace / 3)) {
-							maxHeight.value =  underSpace;
-						} else {
-							maxHeight.value =  upperSpace;
-							top = window.pageYOffset + ((upperSpace + MARGIN) - height);
-						}
-					} else {
-						top = (window.innerHeight - MARGIN) - height + window.pageYOffset - 1;
-					}
-				}
-			}
-
-			if (top < 0) {
-				top = MARGIN;
-			}
-
-			if (left < 0) {
-				left = 0;
-			}
-
-			if (top > rect.top + (fixed.value ? 0 : window.pageYOffset)) {
-				transformOrigin.value = 'center top';
-			} else if ((top + height) <= rect.top + (fixed.value ? 0 : window.pageYOffset)) {
-				transformOrigin.value = 'center bottom';
 			} else {
-				transformOrigin.value = 'center';
+				top = (window.innerHeight - MARGIN) - height + window.pageYOffset - 1;
 			}
+		}
+	}
 
-			popover.style.left = left + 'px';
-			popover.style.top = top + 'px';
-		};
+	if (top < 0) {
+		top = MARGIN;
+	}
 
-		const childRendered = () => {
-			// モーダルコンテンツにマウスボタンが押され、コンテンツ外でマウスボタンが離されたときにモーダルバックグラウンドクリックと判定させないためにマウスイベントを監視しフラグ管理する
-			const el = content.value!.children[0];
-			el.addEventListener('mousedown', e => {
-				contentClicking = true;
-				window.addEventListener('mouseup', e => {
-					// click イベントより先に mouseup イベントが発生するかもしれないのでちょっと待つ
-					window.setTimeout(() => {
-						contentClicking = false;
-					}, 100);
-				}, { passive: true, once: true });
-			}, { passive: true });
-		};
+	if (left < 0) {
+		left = 0;
+	}
 
-		onMounted(() => {
-			watch(() => props.src, async () => {
-				if (props.src) {
-					// eslint-disable-next-line vue/no-mutating-props
-					props.src.style.pointerEvents = 'none';
-				}
-				fixed.value = (type.value === 'drawer') || (getFixedContainer(props.src) != null);
+	if (top > rect.top + (fixed.value ? 0 : window.pageYOffset)) {
+		transformOrigin.value = 'center top';
+	} else if ((top + height) <= rect.top + (fixed.value ? 0 : window.pageYOffset)) {
+		transformOrigin.value = 'center bottom';
+	} else {
+		transformOrigin.value = 'center';
+	}
 
-				await nextTick()
-				
-				align();
-			}, { immediate: true, });
+	popover.style.left = left + 'px';
+	popover.style.top = top + 'px';
+};
 
-			nextTick(() => {
-				const popover = content.value;
-				new ResizeObserver((entries, observer) => {
-					align();
-				}).observe(popover!);
-			});
-		});
+const childRendered = () => {
+	// モーダルコンテンツにマウスボタンが押され、コンテンツ外でマウスボタンが離されたときにモーダルバックグラウンドクリックと判定させないためにマウスイベントを監視しフラグ管理する
+	const el = content.value!.children[0];
+	el.addEventListener('mousedown', ev => {
+		contentClicking = true;
+		window.addEventListener('mouseup', ev => {
+			// click イベントより先に mouseup イベントが発生するかもしれないのでちょっと待つ
+			window.setTimeout(() => {
+				contentClicking = false;
+			}, 100);
+		}, { passive: true, once: true });
+	}, { passive: true });
+};
 
-		return {
-			showing,
-			type,
-			fixed,
-			content,
-			transformOrigin,
-			maxHeight,
-			close,
-			zIndex,
-			keymap,
-			onBgClick,
-			childRendered,
-		};
-	},
+onMounted(() => {
+	watch(() => props.src, async () => {
+		if (props.src) {
+			// eslint-disable-next-line vue/no-mutating-props
+			props.src.style.pointerEvents = 'none';
+		}
+		fixed.value = (type.value === 'drawer') || (getFixedContainer(props.src) != null);
+
+		await nextTick()
+		
+		align();
+	}, { immediate: true, });
+
+	nextTick(() => {
+		const popover = content.value;
+		new ResizeObserver((entries, observer) => {
+			align();
+		}).observe(popover!);
+	});
+});
+
+defineExpose({
+	close,
 });
 </script>
 

From 974269b8f16ab06b2bfa7fab48a9d0da56ce9f88 Mon Sep 17 00:00:00 2001
From: syuilo <Syuilotan@yahoo.co.jp>
Date: Fri, 28 Jan 2022 12:20:42 +0900
Subject: [PATCH 41/46] refactor

---
 packages/client/src/components/drive.file.vue          | 2 +-
 packages/client/src/components/drive.vue               | 2 +-
 packages/client/src/components/emoji-picker-dialog.vue | 6 +++---
 packages/client/src/components/ui/modal.vue            | 6 ++++--
 packages/client/src/os.ts                              | 2 +-
 5 files changed, 10 insertions(+), 8 deletions(-)

diff --git a/packages/client/src/components/drive.file.vue b/packages/client/src/components/drive.file.vue
index 209e9b7c47..262eae0de1 100644
--- a/packages/client/src/components/drive.file.vue
+++ b/packages/client/src/components/drive.file.vue
@@ -95,7 +95,7 @@ function onClick(ev: MouseEvent) {
 	if (props.selectMode) {
 		emit('chosen', props.file);
 	} else {
-		os.popupMenu(getMenu(), (ev.currentTarget ?? ev.target || undefined) as HTMLElement | undefined);
+		os.popupMenu(getMenu(), (ev.currentTarget ?? ev.target ?? undefined) as HTMLElement | undefined);
 	}
 }
 
diff --git a/packages/client/src/components/drive.vue b/packages/client/src/components/drive.vue
index 0d3f29d4c3..e044c67523 100644
--- a/packages/client/src/components/drive.vue
+++ b/packages/client/src/components/drive.vue
@@ -591,7 +591,7 @@ function getMenu() {
 }
 
 function showMenu(ev: MouseEvent) {
-	os.popupMenu(getMenu(), (ev.currentTarget ?? ev.target || undefined) as HTMLElement | undefined);
+	os.popupMenu(getMenu(), (ev.currentTarget ?? ev.target ?? undefined) as HTMLElement | undefined);
 }
 
 function onContextmenu(ev: MouseEvent) {
diff --git a/packages/client/src/components/emoji-picker-dialog.vue b/packages/client/src/components/emoji-picker-dialog.vue
index f06a24636c..9a755be0b9 100644
--- a/packages/client/src/components/emoji-picker-dialog.vue
+++ b/packages/client/src/components/emoji-picker-dialog.vue
@@ -43,9 +43,9 @@ withDefaults(defineProps<{
 });
 
 const emit = defineEmits<{
-	(e: 'done', v: any): void;
-	(e: 'close'): void;
-	(e: 'closed'): void;
+	(ev: 'done', v: any): void;
+	(ev: 'close'): void;
+	(ev: 'closed'): void;
 }>();
 
 const modal = ref<InstanceType<typeof MkModal>>();
diff --git a/packages/client/src/components/ui/modal.vue b/packages/client/src/components/ui/modal.vue
index cb46d38742..c8bf348fdc 100644
--- a/packages/client/src/components/ui/modal.vue
+++ b/packages/client/src/components/ui/modal.vue
@@ -25,11 +25,13 @@ function getFixedContainer(el: Element | null): Element | null {
 	}
 }
 
+type ModalTypes = 'popup' | 'dialog' | 'drawer';
+
 const props = withDefaults(defineProps<{
 	manualShowing?: boolean;
 	srcCenter?: boolean;
 	src?: HTMLElement;
-	preferType?: string;
+	preferType?: ModalTypes | 'auto';
 	zPriority?: 'low' | 'middle' | 'high';
 	noOverlap?: boolean;
 	transparentBg?: boolean;
@@ -66,7 +68,7 @@ const type = computed(() => {
 			return props.src != null ? 'popup' : 'dialog';
 		}
 	} else {
-		return props.preferType;
+		return props.preferType!;
 	}
 });
 
diff --git a/packages/client/src/os.ts b/packages/client/src/os.ts
index c16ea717ad..f3be5c68fb 100644
--- a/packages/client/src/os.ts
+++ b/packages/client/src/os.ts
@@ -403,7 +403,7 @@ export async function selectDriveFolder(multiple: boolean) {
 	});
 }
 
-export async function pickEmoji(src?: HTMLElement, opts) {
+export async function pickEmoji(src: HTMLElement | null, opts) {
 	return new Promise((resolve, reject) => {
 		popup(import('@/components/emoji-picker-dialog.vue'), {
 			src,

From bb6b912aef48c00c407cbea9606aeb5cf0d54833 Mon Sep 17 00:00:00 2001
From: syuilo <Syuilotan@yahoo.co.jp>
Date: Fri, 28 Jan 2022 12:21:35 +0900
Subject: [PATCH 42/46] fix

---
 packages/client/src/components/ui/modal.vue | 2 +-
 1 file changed, 1 insertion(+), 1 deletion(-)

diff --git a/packages/client/src/components/ui/modal.vue b/packages/client/src/components/ui/modal.vue
index c8bf348fdc..e2f6996b16 100644
--- a/packages/client/src/components/ui/modal.vue
+++ b/packages/client/src/components/ui/modal.vue
@@ -25,7 +25,7 @@ function getFixedContainer(el: Element | null): Element | null {
 	}
 }
 
-type ModalTypes = 'popup' | 'dialog' | 'drawer';
+type ModalTypes = 'popup' | 'dialog' | 'dialog:top' | 'drawer';
 
 const props = withDefaults(defineProps<{
 	manualShowing?: boolean;

From 9ffab33037db0d83ce0aa0dd5d04ff0c70b815b6 Mon Sep 17 00:00:00 2001
From: syuilo <Syuilotan@yahoo.co.jp>
Date: Fri, 28 Jan 2022 12:30:47 +0900
Subject: [PATCH 43/46] =?UTF-8?q?fix(client):=20=E3=83=AA=E3=82=A2?=
 =?UTF-8?q?=E3=82=AF=E3=82=B7=E3=83=A7=E3=83=B3=E8=A8=AD=E5=AE=9A=E3=81=A7?=
 =?UTF-8?q?=E7=B5=B5=E6=96=87=E5=AD=97=E3=83=94=E3=83=83=E3=82=AB=E3=83=BC?=
 =?UTF-8?q?=E3=81=8C=E9=96=8B=E3=81=8B=E3=81=AA=E3=81=84=E3=81=AE=E3=82=92?=
 =?UTF-8?q?=E4=BF=AE=E6=AD=A3?=
MIME-Version: 1.0
Content-Type: text/plain; charset=UTF-8
Content-Transfer-Encoding: 8bit

---
 CHANGELOG.md                                           | 1 +
 packages/client/src/components/emoji-picker-dialog.vue | 4 ++--
 packages/client/src/components/ui/modal.vue            | 2 +-
 3 files changed, 4 insertions(+), 3 deletions(-)

diff --git a/CHANGELOG.md b/CHANGELOG.md
index d65cc8ebd5..5af091908a 100644
--- a/CHANGELOG.md
+++ b/CHANGELOG.md
@@ -15,6 +15,7 @@
 - 投稿のNSFW画像を表示したあとにリアクションが更新されると画像が非表示になる問題を修正
 - 「クリップ」ページが開かない問題を修正
 - トレンドウィジェットが動作しないのを修正
+- リアクション設定で絵文字ピッカーが開かないのを修正
 
 ## 12.102.1 (2022/01/27)
 ### Bugfixes
diff --git a/packages/client/src/components/emoji-picker-dialog.vue b/packages/client/src/components/emoji-picker-dialog.vue
index 9a755be0b9..2c0b2e9a8b 100644
--- a/packages/client/src/components/emoji-picker-dialog.vue
+++ b/packages/client/src/components/emoji-picker-dialog.vue
@@ -32,12 +32,12 @@ import MkEmojiPicker from '@/components/emoji-picker.vue';
 import { defaultStore } from '@/store';
 
 withDefaults(defineProps<{
-	manualShowing?: boolean;
+	manualShowing?: boolean | null;
 	src?: HTMLElement;
 	showPinned?: boolean;
 	asReactionPicker?: boolean;
 }>(), {
-	manualShowing: false,
+	manualShowing: null,
 	showPinned: true,
 	asReactionPicker: false,
 });
diff --git a/packages/client/src/components/ui/modal.vue b/packages/client/src/components/ui/modal.vue
index e2f6996b16..3c3bb5c226 100644
--- a/packages/client/src/components/ui/modal.vue
+++ b/packages/client/src/components/ui/modal.vue
@@ -28,7 +28,7 @@ function getFixedContainer(el: Element | null): Element | null {
 type ModalTypes = 'popup' | 'dialog' | 'dialog:top' | 'drawer';
 
 const props = withDefaults(defineProps<{
-	manualShowing?: boolean;
+	manualShowing?: boolean | null;
 	srcCenter?: boolean;
 	src?: HTMLElement;
 	preferType?: ModalTypes | 'auto';

From 82e81a0984cf078bfe2b9a47a4e8418ecf39dd45 Mon Sep 17 00:00:00 2001
From: syuilo <Syuilotan@yahoo.co.jp>
Date: Fri, 28 Jan 2022 12:30:59 +0900
Subject: [PATCH 44/46] refactor(client): use composition api

---
 .../client/src/pages/settings/reaction.vue    | 139 ++++++++----------
 1 file changed, 59 insertions(+), 80 deletions(-)

diff --git a/packages/client/src/pages/settings/reaction.vue b/packages/client/src/pages/settings/reaction.vue
index e5b1189947..ae3e1a1187 100644
--- a/packages/client/src/pages/settings/reaction.vue
+++ b/packages/client/src/pages/settings/reaction.vue
@@ -44,8 +44,8 @@
 </div>
 </template>
 
-<script lang="ts">
-import { defineComponent } from 'vue';
+<script lang="ts" setup>
+import { watch } from 'vue';
 import XDraggable from 'vuedraggable';
 import FormInput from '@/components/form/input.vue';
 import FormRadios from '@/components/form/radios.vue';
@@ -56,91 +56,70 @@ import FormSwitch from '@/components/form/switch.vue';
 import * as os from '@/os';
 import { defaultStore } from '@/store';
 import * as symbols from '@/symbols';
+import { i18n } from '@/i18n';
 
-export default defineComponent({
-	components: {
-		FormInput,
-		FormButton,
-		FromSlot,
-		FormRadios,
-		FormSection,
-		FormSwitch,
-		XDraggable,
-	},
+let reactions = $ref(JSON.parse(JSON.stringify(defaultStore.state.reactions)));
 
-	emits: ['info'],
-	
-	data() {
-		return {
-			[symbols.PAGE_INFO]: {
-				title: this.$ts.reaction,
-				icon: 'fas fa-laugh',
-				action: {
-					icon: 'fas fa-eye',
-					handler: this.preview
-				},
-				bg: 'var(--bg)',
-			},
-			reactions: JSON.parse(JSON.stringify(this.$store.state.reactions)),
+const reactionPickerWidth = $computed(defaultStore.makeGetterSetter('reactionPickerWidth'));
+const reactionPickerHeight = $computed(defaultStore.makeGetterSetter('reactionPickerHeight'));
+const reactionPickerUseDrawerForMobile = $computed(defaultStore.makeGetterSetter('reactionPickerUseDrawerForMobile'));
+
+function save() {
+	defaultStore.set('reactions', reactions);
+}
+
+function remove(reaction, ev: MouseEvent) {
+	os.popupMenu([{
+		text: i18n.ts.remove,
+		action: () => {
+			reactions = reactions.filter(x => x !== reaction);
 		}
-	},
+	}], ev.currentTarget ?? ev.target);
+}
 
-	computed: {
-		reactionPickerWidth: defaultStore.makeGetterSetter('reactionPickerWidth'),
-		reactionPickerHeight: defaultStore.makeGetterSetter('reactionPickerHeight'),
-		reactionPickerUseDrawerForMobile: defaultStore.makeGetterSetter('reactionPickerUseDrawerForMobile'),
-	},
+function preview(ev: MouseEvent) {
+	os.popup(import('@/components/emoji-picker-dialog.vue'), {
+		asReactionPicker: true,
+		src: ev.currentTarget ?? ev.target,
+	}, {}, 'closed');
+}
 
-	watch: {
-		reactions: {
-			handler() {
-				this.save();
-			},
-			deep: true
+async function setDefault() {
+	const { canceled } = await os.confirm({
+		type: 'warning',
+		text: i18n.ts.resetAreYouSure,
+	});
+	if (canceled) return;
+
+	reactions = JSON.parse(JSON.stringify(defaultStore.def.reactions.default));
+}
+
+function chooseEmoji(ev: MouseEvent) {
+	os.pickEmoji(ev.currentTarget ?? ev.target, {
+		showPinned: false
+	}).then(emoji => {
+		if (!reactions.includes(emoji)) {
+			reactions.push(emoji);
 		}
+	});
+}
+
+watch($$(reactions), () => {
+	save();
+}, {
+	deep: true,
+});
+
+defineExpose({
+	[symbols.PAGE_INFO]: {
+		title: i18n.ts.reaction,
+		icon: 'fas fa-laugh',
+		action: {
+			icon: 'fas fa-eye',
+			handler: preview,
+		},
+		bg: 'var(--bg)',
 	},
-
-	methods: {
-		save() {
-			this.$store.set('reactions', this.reactions);
-		},
-
-		remove(reaction, ev) {
-			os.popupMenu([{
-				text: this.$ts.remove,
-				action: () => {
-					this.reactions = this.reactions.filter(x => x !== reaction)
-				}
-			}], ev.currentTarget || ev.target);
-		},
-
-		preview(ev) {
-			os.popup(import('@/components/emoji-picker-dialog.vue'), {
-				asReactionPicker: true,
-				src: ev.currentTarget || ev.target,
-			}, {}, 'closed');
-		},
-
-		async setDefault() {
-			const { canceled } = await os.confirm({
-				type: 'warning',
-				text: this.$ts.resetAreYouSure,
-			});
-			if (canceled) return;
-
-			this.reactions = JSON.parse(JSON.stringify(this.$store.def.reactions.default));
-		},
-
-		chooseEmoji(ev) {
-			os.pickEmoji(ev.currentTarget || ev.target, {
-				showPinned: false
-			}).then(emoji => {
-				if (!this.reactions.includes(emoji)) {
-					this.reactions.push(emoji);
-				}
-			});
-		}
-	}
 });
 </script>
 

From a6d4868ff0335b9d71841c3d688896488b0e89e4 Mon Sep 17 00:00:00 2001
From: syuilo <Syuilotan@yahoo.co.jp>
Date: Fri, 28 Jan 2022 13:39:54 +0900
Subject: [PATCH 45/46] =?UTF-8?q?fix(client):=20DM=E3=83=9A=E3=83=BC?=
 =?UTF-8?q?=E3=82=B8=E3=81=A7=E3=83=A1=E3=83=B3=E3=82=B7=E3=83=A7=E3=83=B3?=
 =?UTF-8?q?=E3=81=8C=E5=90=AB=E3=81=BE=E3=82=8C=E3=82=8B=E5=95=8F=E9=A1=8C?=
 =?UTF-8?q?=E3=82=92=E4=BF=AE=E6=AD=A3?=
MIME-Version: 1.0
Content-Type: text/plain; charset=UTF-8
Content-Transfer-Encoding: 8bit

Fix #8211
---
 CHANGELOG.md                           | 1 +
 packages/client/src/pages/messages.vue | 4 ++--
 2 files changed, 3 insertions(+), 2 deletions(-)

diff --git a/CHANGELOG.md b/CHANGELOG.md
index 5af091908a..09b5a2ac87 100644
--- a/CHANGELOG.md
+++ b/CHANGELOG.md
@@ -16,6 +16,7 @@
 - 「クリップ」ページが開かない問題を修正
 - トレンドウィジェットが動作しないのを修正
 - リアクション設定で絵文字ピッカーが開かないのを修正
+- DMページでメンションが含まれる問題を修正
 
 ## 12.102.1 (2022/01/27)
 ### Bugfixes
diff --git a/packages/client/src/pages/messages.vue b/packages/client/src/pages/messages.vue
index 09d51abf75..9c5fb9b341 100644
--- a/packages/client/src/pages/messages.vue
+++ b/packages/client/src/pages/messages.vue
@@ -12,9 +12,9 @@ import { i18n } from '@/i18n';
 const pagination = {
 	endpoint: 'notes/mentions' as const,
 	limit: 10,
-	params: () => ({
+	params: {
 		visibility: 'specified'
-	}),
+	},
 };
 
 defineExpose({

From 29b33b37eef2c34318a5da002f4705467a82c1de Mon Sep 17 00:00:00 2001
From: Johann150 <johann.galle@protonmail.com>
Date: Fri, 28 Jan 2022 07:29:24 +0100
Subject: [PATCH 46/46] round relative time (#8199)

---
 packages/client/src/components/global/time.vue | 10 +++++-----
 1 file changed, 5 insertions(+), 5 deletions(-)

diff --git a/packages/client/src/components/global/time.vue b/packages/client/src/components/global/time.vue
index 19199fd408..5748d9de61 100644
--- a/packages/client/src/components/global/time.vue
+++ b/packages/client/src/components/global/time.vue
@@ -24,11 +24,11 @@ let now = $ref(new Date());
 const relative = $computed(() => {
 	const ago = (now.getTime() - _time.getTime()) / 1000/*ms*/;
 	return (
-		ago >= 31536000 ? i18n.t('_ago.yearsAgo',   { n: (~~(ago / 31536000)).toString() }) :
-		ago >= 2592000  ? i18n.t('_ago.monthsAgo',  { n: (~~(ago / 2592000)).toString() }) :
-		ago >= 604800   ? i18n.t('_ago.weeksAgo',   { n: (~~(ago / 604800)).toString() }) :
-		ago >= 86400    ? i18n.t('_ago.daysAgo',    { n: (~~(ago / 86400)).toString() }) :
-		ago >= 3600     ? i18n.t('_ago.hoursAgo',   { n: (~~(ago / 3600)).toString() }) :
+		ago >= 31536000 ? i18n.t('_ago.yearsAgo',   { n: Math.round(ago / 31536000).toString() }) :
+		ago >= 2592000  ? i18n.t('_ago.monthsAgo',  { n: Math.round(ago / 2592000).toString() }) :
+		ago >= 604800   ? i18n.t('_ago.weeksAgo',   { n: Math.round(ago / 604800).toString() }) :
+		ago >= 86400    ? i18n.t('_ago.daysAgo',    { n: Math.round(ago / 86400).toString() }) :
+		ago >= 3600     ? i18n.t('_ago.hoursAgo',   { n: Math.round(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.ts._ago.justNow :