diff --git a/gulpfile.ts b/gulpfile.ts
index 262c0a5030af41f9df9596b79151c05dd7060310..880adb51de02928bdf5c9e99ee464307f349c3d9 100644
--- a/gulpfile.ts
+++ b/gulpfile.ts
@@ -11,7 +11,7 @@ const cleanCSS = require('gulp-clean-css');
 const sass = require('gulp-dart-sass');
 const fiber = require('fibers');
 
-const locales = require('./locales');
+const locales: { [x: string]: any } = require('./locales');
 const meta = require('./package.json');
 
 gulp.task('build:ts', () => {
@@ -31,8 +31,10 @@ gulp.task('build:copy:views', () =>
 gulp.task('build:copy:locales', cb => {
 	fs.mkdirSync('./built/client/assets/locales', { recursive: true });
 
+	const v = { '_version_': meta.version };
+
 	for (const [lang, locale] of Object.entries(locales)) {
-		fs.writeFileSync(`./built/client/assets/locales/${lang}.${meta.version}.json`, JSON.stringify(locale), 'utf-8');
+		fs.writeFileSync(`./built/client/assets/locales/${lang}.${meta.version}.json`, JSON.stringify({ ...locale, ...v }), 'utf-8');
 	}
 
 	cb();
diff --git a/locales/ja-JP.yml b/locales/ja-JP.yml
index ab293eb89de1c0b8e22f78217fba6cb8c842c97c..3c7dc6640bc2cce7b69aa42c2d0c9e0a9d732247 100644
--- a/locales/ja-JP.yml
+++ b/locales/ja-JP.yml
@@ -507,6 +507,8 @@ addRelay: "リレーの追加"
 inboxUrl: "inboxのURL"
 addedRelays: "追加済みのリレー"
 serviceworkerInfo: "プッシュ通知を行うには有効する必要があります。"
+deletedNote: "削除された投稿"
+invisibleNote: "非公開の投稿"
 
 _theme:
   explore: "テーマを探す"
@@ -1102,3 +1104,17 @@ _relayStatus:
   requesting: "承認待ち"
   accepted: "承認済み"
   rejected: "拒否済み"
+
+_notification:
+  fileUploaded: "ファイルがアップロードされました"
+  youGotMention: "{name}からのメンション"
+  youGotReply: "{name}からのリプライ"
+  youGotQuote: "{name}による引用"
+  youRenoted: "{name}がRenoteしました"
+  youGotPoll: "{name}が投票しました"
+  youGotMessagingMessageFromUser: "{name}からのチャットがあります"
+  youGotMessagingMessageFromGroup: "{name}のチャットがあります"
+  youWereFollowed: "フォローされました"
+  youReceivedFollowRequest: "フォローリクエストが来ました"
+  yourFollowRequestAccepted: "フォローリクエストが承認されました"
+  youWereInvitedToGroup: "グループに招待されました"
diff --git a/package.json b/package.json
index 15c1478dbdbbb4712d93e1a756f030dd1ab64aed..d34394d138c701a0fa50a468f071d10ddd2d7fca 100644
--- a/package.json
+++ b/package.json
@@ -125,6 +125,7 @@
 		"css-loader": "3.5.3",
 		"cssnano": "4.1.10",
 		"dateformat": "3.0.3",
+		"deep-entries": "3.1.0",
 		"diskusage": "1.1.3",
 		"double-ended-queue": "2.1.0-0",
 		"escape-regexp": "0.0.1",
@@ -151,6 +152,7 @@
 		"http-proxy-agent": "4.0.1",
 		"http-signature": "1.3.4",
 		"https-proxy-agent": "5.0.0",
+		"idb-keyval": "3.2.0",
 		"insert-text-at-cursor": "0.3.0",
 		"is-root": "2.1.0",
 		"is-svg": "4.2.1",
diff --git a/src/client/app.vue b/src/client/app.vue
index 5e7396205bf7e60a4822fee8b86a6fb9c4a8be34..8e192d46338981a24491d71d2887bf86425bd9c2 100644
--- a/src/client/app.vue
+++ b/src/client/app.vue
@@ -136,15 +136,12 @@ import { faGripVertical, faChevronLeft, faHashtag, faBroadcastTower, faFireAlt,
 import { faBell, faEnvelope, faLaugh, faComments } from '@fortawesome/free-regular-svg-icons';
 import { ResizeObserver } from '@juggle/resize-observer';
 import { v4 as uuid } from 'uuid';
-import i18n from './i18n';
 import { host, instanceName } from './config';
 import { search } from './scripts/search';
 
 const DESKTOP_THRESHOLD = 1100;
 
 export default Vue.extend({
-	i18n,
-
 	components: {
 		XClock: () => import('./components/header-clock.vue').then(m => m.default),
 		MkButton: () => import('./components/ui/button.vue').then(m => m.default),
diff --git a/src/client/components/captcha.vue b/src/client/components/captcha.vue
index 6b1ee6f0b265544816dec10b36bf9ab8e7bf7828..1a894d9350b286f10ec966583a65a013b13b7940 100644
--- a/src/client/components/captcha.vue
+++ b/src/client/components/captcha.vue
@@ -7,7 +7,6 @@
 
 <script lang="ts">
 import Vue from 'vue';
-import i18n from '../i18n';
 
 type Captcha = {
 	render(container: string | Node, options: {
@@ -31,7 +30,6 @@ declare global {
 }
 
 export default Vue.extend({
-	i18n,
 	props: {
 		provider: {
 			type: String,
diff --git a/src/client/components/cw-button.vue b/src/client/components/cw-button.vue
index 4516e5210c964c149fdd33af8709559beb9b0d0f..07a44d970f8d2a0493d4b2f795d807560caab25e 100644
--- a/src/client/components/cw-button.vue
+++ b/src/client/components/cw-button.vue
@@ -7,13 +7,10 @@
 
 <script lang="ts">
 import Vue from 'vue';
-import i18n from '../i18n';
 import { length } from 'stringz';
 import { concat } from '../../prelude/array';
 
 export default Vue.extend({
-	i18n,
-
 	props: {
 		value: {
 			type: Boolean,
diff --git a/src/client/components/date-separated-list.vue b/src/client/components/date-separated-list.vue
index b80c6494eda4cd9d0fa57c5b9ae8eecec19cb232..a27e9a05a201b069a5e606ca7489669189b8a449 100644
--- a/src/client/components/date-separated-list.vue
+++ b/src/client/components/date-separated-list.vue
@@ -15,11 +15,8 @@
 <script lang="ts">
 import Vue from 'vue';
 import { faAngleUp, faAngleDown } from '@fortawesome/free-solid-svg-icons';
-import i18n from '../i18n';
 
 export default Vue.extend({
-	i18n,
-
 	props: {
 		items: {
 			type: Array,
diff --git a/src/client/components/dialog.vue b/src/client/components/dialog.vue
index da8e54684b40e6a6205345b027f3b0cb3a8ddd50..58115b47a29e814505f3f9e0722c8d00648e8c9d 100644
--- a/src/client/components/dialog.vue
+++ b/src/client/components/dialog.vue
@@ -57,11 +57,8 @@ import MkInput from './ui/input.vue';
 import MkSelect from './ui/select.vue';
 import MkSignin from './signin.vue';
 import parseAcct from '../../misc/acct/parse';
-import i18n from '../i18n';
 
 export default Vue.extend({
-	i18n,
-
 	components: {
 		MkButton,
 		MkInput,
diff --git a/src/client/components/drive-window.vue b/src/client/components/drive-window.vue
index d63881c0edfbc75ca96d6838cac9c847e2c06a38..c42cb666171d769492f5b9256e43308969f544c8 100644
--- a/src/client/components/drive-window.vue
+++ b/src/client/components/drive-window.vue
@@ -12,13 +12,10 @@
 
 <script lang="ts">
 import Vue from 'vue';
-import i18n from '../i18n';
 import XDrive from './drive.vue';
 import XWindow from './window.vue';
 
 export default Vue.extend({
-	i18n,
-
 	components: {
 		XDrive,
 		XWindow,
diff --git a/src/client/components/drive.file.vue b/src/client/components/drive.file.vue
index a547abf9a07930d710b6f100d4f66d8a83c3189a..1b24c61df53d611ee38580c20dad1c6863145d56 100644
--- a/src/client/components/drive.file.vue
+++ b/src/client/components/drive.file.vue
@@ -32,7 +32,6 @@
 <script lang="ts">
 import Vue from 'vue';
 import { faEye, faEyeSlash } from '@fortawesome/free-regular-svg-icons';
-import i18n from '../i18n';
 import copyToClipboard from '../scripts/copy-to-clipboard';
 //import updateAvatar from '../api/update-avatar';
 //import updateBanner from '../api/update-banner';
@@ -40,8 +39,6 @@ import XFileThumbnail from './drive-file-thumbnail.vue';
 import { faDownload, faLink, faICursor, faTrashAlt } from '@fortawesome/free-solid-svg-icons';
 
 export default Vue.extend({
-	i18n,
-
 	components: {
 		XFileThumbnail
 	},
diff --git a/src/client/components/drive.folder.vue b/src/client/components/drive.folder.vue
index b778acc77f551a961cba82f3046e7b5873e36dab..9e80653194fd0bd99090fd0723b101220ec7bbc8 100644
--- a/src/client/components/drive.folder.vue
+++ b/src/client/components/drive.folder.vue
@@ -28,11 +28,8 @@
 <script lang="ts">
 import Vue from 'vue';
 import { faFolder, faFolderOpen } from '@fortawesome/free-regular-svg-icons';
-import i18n from '../i18n';
 
 export default Vue.extend({
-	i18n,
-
 	props: {
 		folder: {
 			type: Object,
diff --git a/src/client/components/drive.nav-folder.vue b/src/client/components/drive.nav-folder.vue
index 0689faecd24897a6314daf4fc9cb2a860e2fee9c..9e805a5e93fd2a711f14773114c5366fd6b15a94 100644
--- a/src/client/components/drive.nav-folder.vue
+++ b/src/client/components/drive.nav-folder.vue
@@ -15,11 +15,8 @@
 <script lang="ts">
 import Vue from 'vue';
 import { faCloud } from '@fortawesome/free-solid-svg-icons';
-import i18n from '../i18n';
 
 export default Vue.extend({
-	i18n,
-
 	props: {
 		folder: {
 			type: Object,
diff --git a/src/client/components/drive.vue b/src/client/components/drive.vue
index 08c7097a8fb3b6e4b1293e43063ffc966446f259..65eb1cb8163075279704c61d0a79fdb2a34db8cb 100644
--- a/src/client/components/drive.vue
+++ b/src/client/components/drive.vue
@@ -48,7 +48,6 @@
 <script lang="ts">
 import Vue from 'vue';
 import { faAngleRight } from '@fortawesome/free-solid-svg-icons';
-import i18n from '../i18n';
 import XNavFolder from './drive.nav-folder.vue';
 import XFolder from './drive.folder.vue';
 import XFile from './drive.file.vue';
@@ -56,8 +55,6 @@ import XUploader from './uploader.vue';
 import MkButton from './ui/button.vue';
 
 export default Vue.extend({
-	i18n,
-
 	components: {
 		XNavFolder,
 		XFolder,
diff --git a/src/client/components/emoji-picker.vue b/src/client/components/emoji-picker.vue
index 868a6125c43578096a44eaf79e7c3e2273259284..7871b438c9693370b0546f7c0822eea3b7b795ea 100644
--- a/src/client/components/emoji-picker.vue
+++ b/src/client/components/emoji-picker.vue
@@ -64,7 +64,6 @@
 
 <script lang="ts">
 import Vue from 'vue';
-import i18n from '../i18n';
 import { emojilist } from '../../misc/emojilist';
 import { getStaticImageUrl } from '../scripts/get-static-image-url';
 import { faAsterisk, faLeaf, faUtensils, faFutbol, faCity, faDice, faGlobe, faHistory, faUser } from '@fortawesome/free-solid-svg-icons';
@@ -73,8 +72,6 @@ import { groupByX } from '../../prelude/array';
 import XPopup from './popup.vue';
 
 export default Vue.extend({
-	i18n,
-
 	components: {
 		XPopup,
 	},
diff --git a/src/client/components/error.vue b/src/client/components/error.vue
index dd9de43c1664b12a4e1e4d572c094637552efbac..fea81305ed996853db692516f10e59ac6d7a393b 100644
--- a/src/client/components/error.vue
+++ b/src/client/components/error.vue
@@ -9,11 +9,9 @@
 <script lang="ts">
 import Vue from 'vue';
 import { faExclamationTriangle } from '@fortawesome/free-solid-svg-icons';
-import i18n from '../i18n';
 import MkButton from './ui/button.vue';
 
 export default Vue.extend({
-	i18n,
 	components: {
 		MkButton,
 	},
diff --git a/src/client/components/follow-button.vue b/src/client/components/follow-button.vue
index 23cb0cd94527c6b37cacdb531456247f8e25ce40..7967c0e1595f342ef656f648fcb575630a1f5594 100644
--- a/src/client/components/follow-button.vue
+++ b/src/client/components/follow-button.vue
@@ -30,12 +30,9 @@
 
 <script lang="ts">
 import Vue from 'vue';
-import i18n from '../i18n';
 import { faSpinner, faPlus, faMinus, faHourglassHalf } from '@fortawesome/free-solid-svg-icons';
 
 export default Vue.extend({
-	i18n,
-
 	props: {
 		user: {
 			type: Object,
diff --git a/src/client/components/google.vue b/src/client/components/google.vue
index 01dcf24bf8756f1216cdb37f7d7b240ecbc55252..de96cbd16a55041333379712a683c50c25b557cd 100644
--- a/src/client/components/google.vue
+++ b/src/client/components/google.vue
@@ -8,10 +8,8 @@
 <script lang="ts">
 import Vue from 'vue';
 import { faSearch } from '@fortawesome/free-solid-svg-icons';
-import i18n from '../i18n';
 
 export default Vue.extend({
-	i18n,
 	props: ['q'],
 	data() {
 		return {
diff --git a/src/client/components/image-viewer.vue b/src/client/components/image-viewer.vue
index 3359b600da45814ee0bd3aa9d8375eeae7809694..c78112b98896f750f0612ef11660093f1ef8a6f9 100644
--- a/src/client/components/image-viewer.vue
+++ b/src/client/components/image-viewer.vue
@@ -6,12 +6,9 @@
 
 <script lang="ts">
 import Vue from 'vue';
-import i18n from '../i18n';
 import XModal from './modal.vue';
 
 export default Vue.extend({
-	i18n,
-
 	components: {
 		XModal,
 	},
diff --git a/src/client/components/instance-stats.vue b/src/client/components/instance-stats.vue
index 378e9ce3919c96c434d68e01ed7c03e1e0524c2b..552e3523f738995b1dd6767f82026de2a7291471 100644
--- a/src/client/components/instance-stats.vue
+++ b/src/client/components/instance-stats.vue
@@ -125,7 +125,6 @@
 import Vue from 'vue';
 import { faChartBar, faUser, faPencilAlt } from '@fortawesome/free-solid-svg-icons';
 import Chart from 'chart.js';
-import i18n from '../i18n';
 import MkSelect from './ui/select.vue';
 
 const chartLimit = 90;
@@ -140,8 +139,6 @@ const alpha = (hex, a) => {
 };
 
 export default Vue.extend({
-	i18n,
-
 	components: {
 		MkSelect
 	},
diff --git a/src/client/components/media-banner.vue b/src/client/components/media-banner.vue
index 088c11fab7a9f113f278afb16df3dcb8d9da9845..0f746d4340ebfb137434e705c97ed947130602f7 100644
--- a/src/client/components/media-banner.vue
+++ b/src/client/components/media-banner.vue
@@ -28,10 +28,8 @@
 <script lang="ts">
 import Vue from 'vue';
 import { faExclamationTriangle } from '@fortawesome/free-solid-svg-icons';
-import i18n from '../i18n';
 
 export default Vue.extend({
-	i18n,
 	props: {
 		media: {
 			type: Object,
diff --git a/src/client/components/media-image.vue b/src/client/components/media-image.vue
index 6c33b657ffce9b729e04ce0d5296ec4c3f92b838..6d1b5345de56be4f991aee96feede7f9d87d35b2 100644
--- a/src/client/components/media-image.vue
+++ b/src/client/components/media-image.vue
@@ -21,12 +21,10 @@
 <script lang="ts">
 import Vue from 'vue';
 import { faExclamationTriangle, faEyeSlash } from '@fortawesome/free-solid-svg-icons';
-import i18n from '../i18n';
 import { getStaticImageUrl } from '../scripts/get-static-image-url';
 import ImageViewer from './image-viewer.vue';
 
 export default Vue.extend({
-	i18n,
 	props: {
 		image: {
 			type: Object,
diff --git a/src/client/components/media-video.vue b/src/client/components/media-video.vue
index d9b4415cbf1df8c18ed8f202cd986978ddec6e4c..a5e06bfaa9bd177f8f0dbbb74e8c1c3fb475b2d1 100644
--- a/src/client/components/media-video.vue
+++ b/src/client/components/media-video.vue
@@ -23,10 +23,8 @@
 import Vue from 'vue';
 import { faPlayCircle } from '@fortawesome/free-regular-svg-icons';
 import { faExclamationTriangle, faEyeSlash } from '@fortawesome/free-solid-svg-icons';
-import i18n from '../i18n';
 
 export default Vue.extend({
-	i18n,
 	props: {
 		video: {
 			type: Object,
diff --git a/src/client/components/mention.vue b/src/client/components/mention.vue
index 06dcf1288787ee9a896ae888c049de457660cea9..8c939f839a2a81a6ea43ec13c532f6470ce41dd1 100644
--- a/src/client/components/mention.vue
+++ b/src/client/components/mention.vue
@@ -16,12 +16,10 @@
 
 <script lang="ts">
 import Vue from 'vue';
-import i18n from '../i18n';
 import { toUnicode } from 'punycode';
 import { host as localHost } from '../config';
 
 export default Vue.extend({
-	i18n,
 	props: {
 		username: {
 			type: String,
diff --git a/src/client/components/note.vue b/src/client/components/note.vue
index fd895ad5aed4878591250a7bbf624e2033687c5f..6e513a4b2a4712c1e2f9ece2dbc6ac2cda291cfb 100644
--- a/src/client/components/note.vue
+++ b/src/client/components/note.vue
@@ -93,7 +93,6 @@ import { faBolt, faTimes, faBullhorn, faStar, faLink, faExternalLinkSquareAlt, f
 import { faCopy, faTrashAlt, faEdit, faEye, faEyeSlash } from '@fortawesome/free-regular-svg-icons';
 import { parse } from '../../mfm/parse';
 import { sum, unique } from '../../prelude/array';
-import i18n from '../i18n';
 import XSub from './note.sub.vue';
 import XNoteHeader from './note-header.vue';
 import XNotePreview from './note-preview.vue';
@@ -109,7 +108,6 @@ import { url } from '../config';
 import copyToClipboard from '../scripts/copy-to-clipboard';
 
 export default Vue.extend({
-	i18n,
 	
 	components: {
 		XSub,
diff --git a/src/client/components/notes.vue b/src/client/components/notes.vue
index 0cf4dee2ddda365470a30dd193af2f90423d9c6b..515bc58e2e8eec716b5f1092293e2fd120533726 100644
--- a/src/client/components/notes.vue
+++ b/src/client/components/notes.vue
@@ -29,15 +29,12 @@
 
 <script lang="ts">
 import Vue from 'vue';
-import i18n from '../i18n';
 import paging from '../scripts/paging';
 import XNote from './note.vue';
 import XList from './date-separated-list.vue';
 import MkButton from './ui/button.vue';
 
 export default Vue.extend({
-	i18n,
-
 	components: {
 		XNote, XList, MkButton
 	},
diff --git a/src/client/components/notification.vue b/src/client/components/notification.vue
index d3ebc8f1790b6c361fae0b59d31979c8dfc0690d..de233d14ac9b4fd2cfda96f43d70abcfb8df6fb2 100644
--- a/src/client/components/notification.vue
+++ b/src/client/components/notification.vue
@@ -61,13 +61,11 @@
 import Vue from 'vue';
 import { faIdCardAlt, faPlus, faQuoteLeft, faQuoteRight, faRetweet, faReply, faAt, faCheck, faPollH } from '@fortawesome/free-solid-svg-icons';
 import { faClock } from '@fortawesome/free-regular-svg-icons';
-import getNoteSummary from '../../misc/get-note-summary';
+import noteSummary from '../../misc/get-note-summary';
 import XReactionIcon from './reaction-icon.vue';
 import MkFollowButton from './follow-button.vue';
-import i18n from '../i18n';
 
 export default Vue.extend({
-	i18n,
 	components: {
 		XReactionIcon, MkFollowButton
 	},
@@ -89,7 +87,7 @@ export default Vue.extend({
 	},
 	data() {
 		return {
-			getNoteSummary,
+			getNoteSummary: (text: string) => noteSummary(text, this.$root.i18n.messages[this.$root.i18n.locale]),
 			followRequestDone: false,
 			groupInviteDone: false,
 			faIdCardAlt, faPlus, faQuoteLeft, faQuoteRight, faRetweet, faReply, faAt, faClock, faCheck, faPollH
diff --git a/src/client/components/notifications.vue b/src/client/components/notifications.vue
index ecf5268983c6ed16d6239a2e32b38ce4bbd1417e..3ed198a04cd1d7f760c0a3507f5af46974ffd587 100644
--- a/src/client/components/notifications.vue
+++ b/src/client/components/notifications.vue
@@ -18,15 +18,12 @@
 
 <script lang="ts">
 import Vue from 'vue';
-import i18n from '../i18n';
 import paging from '../scripts/paging';
 import XNotification from './notification.vue';
 import XList from './date-separated-list.vue';
 import XNote from './note.vue';
 
 export default Vue.extend({
-	i18n,
-
 	components: {
 		XNotification,
 		XList,
diff --git a/src/client/components/page/page.post.vue b/src/client/components/page/page.post.vue
index 6f79374f3478c98dfc1c153d8d8917ce1ddbbdfc..da5bc8bfab7951da2a0d62e36d8d0d77b80c9a8f 100644
--- a/src/client/components/page/page.post.vue
+++ b/src/client/components/page/page.post.vue
@@ -8,13 +8,11 @@
 <script lang="ts">
 import Vue from 'vue';
 import { faCheck, faPaperPlane } from '@fortawesome/free-solid-svg-icons';
-import i18n from '../../i18n';
 import MkTextarea from '../ui/textarea.vue';
 import MkButton from '../ui/button.vue';
 import { apiUrl } from '../../config';
 
 export default Vue.extend({
-	i18n,
 	components: {
 		MkTextarea,
 		MkButton,
diff --git a/src/client/components/page/page.vue b/src/client/components/page/page.vue
index e3b04d7fd66e977e544f53217772d763972b4969..b3cc01ec22820b428b7d4fe63fd5555f2c708cac 100644
--- a/src/client/components/page/page.vue
+++ b/src/client/components/page/page.vue
@@ -9,14 +9,11 @@ import Vue from 'vue';
 import { parse } from '@syuilo/aiscript';
 import { faHeart as faHeartS } from '@fortawesome/free-solid-svg-icons';
 import { faHeart } from '@fortawesome/free-regular-svg-icons';
-import i18n from '../../i18n';
 import XBlock from './page.block.vue';
 import { Hpml } from '../../scripts/hpml/evaluator';
 import { url } from '../../config';
 
 export default Vue.extend({
-	i18n,
-
 	components: {
 		XBlock
 	},
diff --git a/src/client/components/poll-editor.vue b/src/client/components/poll-editor.vue
index 91c7dab59806b46daa466c1a1fcbe64f9df4b68a..0687e999b5fcbf4b1aebd295959158bf0ae43438 100644
--- a/src/client/components/poll-editor.vue
+++ b/src/client/components/poll-editor.vue
@@ -51,7 +51,6 @@
 <script lang="ts">
 import Vue from 'vue';
 import { faExclamationTriangle, faTimes } from '@fortawesome/free-solid-svg-icons';
-import i18n from '../i18n';
 import { erase } from '../../prelude/array';
 import { addTime } from '../../prelude/time';
 import { formatDateTimeString } from '../../misc/format-time-string';
@@ -61,7 +60,6 @@ import MkSwitch from './ui/switch.vue';
 import MkButton from './ui/button.vue';
 
 export default Vue.extend({
-	i18n,
 	components: {
 		MkInput,
 		MkSelect,
diff --git a/src/client/components/poll.vue b/src/client/components/poll.vue
index c748b6b099c38f6db95322b2ef4a1b9061cb184c..e0c42cd008aaf4c5f37c50b87ed3b17f4fba8e9f 100644
--- a/src/client/components/poll.vue
+++ b/src/client/components/poll.vue
@@ -24,11 +24,9 @@
 <script lang="ts">
 import Vue from 'vue';
 import { faCheck } from '@fortawesome/free-solid-svg-icons';
-import i18n from '../i18n';
 import { sum } from '../../prelude/array';
 
 export default Vue.extend({
-	i18n,
 	props: {
 		note: {
 			type: Object,
diff --git a/src/client/components/post-form-attaches.vue b/src/client/components/post-form-attaches.vue
index d9c0653617f59f572d820fbc3b7b186589aaae7d..2415bf28ec24f7eea924423b7859e7e52553c9da 100644
--- a/src/client/components/post-form-attaches.vue
+++ b/src/client/components/post-form-attaches.vue
@@ -14,15 +14,12 @@
 
 <script lang="ts">
 import Vue from 'vue';
-import i18n from '../i18n';
 import * as XDraggable from 'vuedraggable';
 import { faTimesCircle, faEye, faEyeSlash } from '@fortawesome/free-regular-svg-icons';
 import { faExclamationTriangle, faICursor } from '@fortawesome/free-solid-svg-icons';
 import XFileThumbnail from './drive-file-thumbnail.vue'
 
 export default Vue.extend({
-	i18n,
-
 	components: {
 		XDraggable,
 		XFileThumbnail
diff --git a/src/client/components/post-form.vue b/src/client/components/post-form.vue
index 05faea51466eb65361bf794ace019044b9adac72..cdb61f51d500615e89826f208c645c1cd742b570 100644
--- a/src/client/components/post-form.vue
+++ b/src/client/components/post-form.vue
@@ -57,7 +57,6 @@ import { faEyeSlash, faLaughSquint } from '@fortawesome/free-regular-svg-icons';
 import insertTextAtCursor from 'insert-text-at-cursor';
 import { length } from 'stringz';
 import { toASCII } from 'punycode';
-import i18n from '../i18n';
 import MkVisibilityChooser from './visibility-chooser.vue';
 import MkUserSelect from './user-select.vue';
 import XNotePreview from './note-preview.vue';
@@ -70,8 +69,6 @@ import { formatTimeString } from '../../misc/format-time-string';
 import { selectDriveFile } from '../scripts/select-drive-file';
 
 export default Vue.extend({
-	i18n,
-
 	components: {
 		XNotePreview,
 		XUploader: () => import('./uploader.vue').then(m => m.default),
diff --git a/src/client/components/reaction-icon.vue b/src/client/components/reaction-icon.vue
index 3c6d56b80a1b62c4dd6adaf15d8a0f04b8defe89..fe2b5283689b41a4f1e2dd1778c0a40d2f443429 100644
--- a/src/client/components/reaction-icon.vue
+++ b/src/client/components/reaction-icon.vue
@@ -4,9 +4,7 @@
 
 <script lang="ts">
 import Vue from 'vue';
-import i18n from '../i18n';
 export default Vue.extend({
-	i18n,
 	props: {
 		reaction: {
 			type: String,
diff --git a/src/client/components/reaction-picker.vue b/src/client/components/reaction-picker.vue
index 99b27ad9c937e784a03ae4d1c31aa950cb72be9a..e331410c394567b4ca1de7f147fb20e85dd767eb 100644
--- a/src/client/components/reaction-picker.vue
+++ b/src/client/components/reaction-picker.vue
@@ -11,14 +11,11 @@
 
 <script lang="ts">
 import Vue from 'vue';
-import i18n from '../i18n';
 import { emojiRegex } from '../../misc/emoji-regex';
 import XReactionIcon from './reaction-icon.vue';
 import XPopup from './popup.vue';
 
 export default Vue.extend({
-	i18n,
-
 	components: {
 		XPopup,
 		XReactionIcon,
diff --git a/src/client/components/reactions-viewer.details.vue b/src/client/components/reactions-viewer.details.vue
index ea2523a11f495b4a7970915ddc5127a51db37a03..67c8b261be2f9d6976e35c82c6344388707460d4 100644
--- a/src/client/components/reactions-viewer.details.vue
+++ b/src/client/components/reactions-viewer.details.vue
@@ -20,10 +20,8 @@
 
 <script lang="ts">
 import Vue from 'vue';
-import i18n from '../i18n';
 
 export default Vue.extend({
-	i18n,
 	props: {
 		reaction: {
 			type: String,
diff --git a/src/client/components/remote-caution.vue b/src/client/components/remote-caution.vue
index 95b37d3053546cefedca2366b19cab2f12e990cc..21af9f766a096ffc5146d034bfc76223c8d24c53 100644
--- a/src/client/components/remote-caution.vue
+++ b/src/client/components/remote-caution.vue
@@ -5,10 +5,8 @@
 <script lang="ts">
 import Vue from 'vue';
 import { faExclamationTriangle } from '@fortawesome/free-solid-svg-icons';
-import i18n from '../i18n';
 
 export default Vue.extend({
-	i18n,
 	props: {
 		href: {
 			type: String,
diff --git a/src/client/components/signin-dialog.vue b/src/client/components/signin-dialog.vue
index a356c3ccdbedcd915fe22a92dce3d6cdc431b3ca..98b75e627cc6250c236548f3f5f07b2a3f42fb6f 100644
--- a/src/client/components/signin-dialog.vue
+++ b/src/client/components/signin-dialog.vue
@@ -7,13 +7,10 @@
 
 <script lang="ts">
 import Vue from 'vue';
-import i18n from '../i18n';
 import XWindow from './window.vue';
 import MkSignin from './signin.vue';
 
 export default Vue.extend({
-	i18n,
-
 	components: {
 		MkSignin,
 		XWindow,
diff --git a/src/client/components/signin.vue b/src/client/components/signin.vue
index dc73ad8a0fc0a7e8f87ad6078c40c8af801b3255..a7653b17b0c3e4893c9993c2c411b0df6d13f820 100755
--- a/src/client/components/signin.vue
+++ b/src/client/components/signin.vue
@@ -49,13 +49,10 @@ import { faLock, faGavel } from '@fortawesome/free-solid-svg-icons';
 import { faTwitter, faDiscord, faGithub } from '@fortawesome/free-brands-svg-icons';
 import MkButton from './ui/button.vue';
 import MkInput from './ui/input.vue';
-import i18n from '../i18n';
 import { apiUrl, host } from '../config';
 import { byteify, hexify } from '../scripts/2fa';
 
 export default Vue.extend({
-	i18n,
-
 	components: {
 		MkButton,
 		MkInput,
diff --git a/src/client/components/signup-dialog.vue b/src/client/components/signup-dialog.vue
index 4db79af5121f135a3887b127b0dd0ed0e51d8aa3..eff1f79c48774f095740bc2641d61cdcd1f244fe 100644
--- a/src/client/components/signup-dialog.vue
+++ b/src/client/components/signup-dialog.vue
@@ -7,13 +7,10 @@
 
 <script lang="ts">
 import Vue from 'vue';
-import i18n from '../i18n';
 import XWindow from './window.vue';
 import XSignup from './signup.vue';
 
 export default Vue.extend({
-	i18n,
-
 	components: {
 		XSignup,
 		XWindow,
diff --git a/src/client/components/signup.vue b/src/client/components/signup.vue
index acb6a745ab5a123c3f14590efe12acaf4cdacec2..ff1932b42d14526e51a8722fa458d59dcd539431 100644
--- a/src/client/components/signup.vue
+++ b/src/client/components/signup.vue
@@ -53,15 +53,12 @@ import Vue from 'vue';
 import { faLock, faExclamationTriangle, faSpinner, faCheck, faKey } from '@fortawesome/free-solid-svg-icons';
 const getPasswordStrength = require('syuilo-password-strength');
 import { toUnicode } from 'punycode';
-import i18n from '../i18n';
 import { host, url } from '../config';
 import MkButton from './ui/button.vue';
 import MkInput from './ui/input.vue';
 import MkSwitch from './ui/switch.vue';
 
 export default Vue.extend({
-	i18n,
-
 	components: {
 		MkButton,
 		MkInput,
diff --git a/src/client/components/stream-indicator.vue b/src/client/components/stream-indicator.vue
index dd7a5d07c14640571c4c2b5e5116eb5f5cc76d03..ec00f4cbfe7983aec45fa4ff7a0bb8383cf33213 100644
--- a/src/client/components/stream-indicator.vue
+++ b/src/client/components/stream-indicator.vue
@@ -10,10 +10,8 @@
 
 <script lang="ts">
 import Vue from 'vue';
-import i18n from '../i18n';
 
 export default Vue.extend({
-	i18n,
 	data() {
 		return {
 			hasDisconnected: false,
diff --git a/src/client/components/sub-note-content.vue b/src/client/components/sub-note-content.vue
index e60c19744231a071328a9babc5da59ae3517743e..a14c832ea85dd3ab3c8cfefb75cf039693ba8c41 100644
--- a/src/client/components/sub-note-content.vue
+++ b/src/client/components/sub-note-content.vue
@@ -21,12 +21,10 @@
 <script lang="ts">
 import Vue from 'vue';
 import { faReply } from '@fortawesome/free-solid-svg-icons';
-import i18n from '../i18n';
 import XPoll from './poll.vue';
 import XMediaList from './media-list.vue';
 
 export default Vue.extend({
-	i18n,
 	components: {
 		XPoll,
 		XMediaList,
diff --git a/src/client/components/time.vue b/src/client/components/time.vue
index 6d092cf4f8fd9056ed375d7ca95019933b5bb930..2a871d6d8187fdb589cbf4f0eca87a1fff6040bf 100644
--- a/src/client/components/time.vue
+++ b/src/client/components/time.vue
@@ -8,10 +8,8 @@
 
 <script lang="ts">
 import Vue from 'vue';
-import i18n from '../i18n';
 
 export default Vue.extend({
-	i18n,
 	props: {
 		time: {
 			type: [Date, String],
diff --git a/src/client/components/uploader.vue b/src/client/components/uploader.vue
index 4ceb5e2877f8727f3e1d70dbbd11d346bbfc713a..6ebdf123b1f9ea3e2a7ddf5c50031d662f031b7c 100644
--- a/src/client/components/uploader.vue
+++ b/src/client/components/uploader.vue
@@ -21,13 +21,11 @@
 
 <script lang="ts">
 import Vue from 'vue';
-import i18n from '../i18n';
 import { apiUrl } from '../config';
 //import getMD5 from '../../scripts/get-md5';
 import { faSpinner } from '@fortawesome/free-solid-svg-icons';
 
 export default Vue.extend({
-	i18n,
 	data() {
 		return {
 			uploads: [],
diff --git a/src/client/components/url-preview-popup.vue b/src/client/components/url-preview-popup.vue
index acd9b1aa9ae1c7f76baa36f1463f74cc6d6dc241..52731296cb392c2a2014e0c62b5ee4d27dd2e470 100644
--- a/src/client/components/url-preview-popup.vue
+++ b/src/client/components/url-preview-popup.vue
@@ -6,12 +6,9 @@
 
 <script lang="ts">
 import Vue from 'vue';
-import i18n from '../i18n';
 import MkUrlPreview from './url-preview.vue';
 
 export default Vue.extend({
-	i18n,
-
 	components: {
 		MkUrlPreview
 	},
diff --git a/src/client/components/url-preview.vue b/src/client/components/url-preview.vue
index c2dd0038bec32c26aba478926a098d68a8befd7e..d77cfafd1e52eb5f84f10b66953a13bd54846db7 100644
--- a/src/client/components/url-preview.vue
+++ b/src/client/components/url-preview.vue
@@ -32,12 +32,9 @@
 <script lang="ts">
 import Vue from 'vue';
 import { faPlayCircle } from '@fortawesome/free-regular-svg-icons';
-import i18n from '../i18n';
 import { url as local, lang } from '../config';
 
 export default Vue.extend({
-	i18n,
-
 	props: {
 		url: {
 			type: String,
diff --git a/src/client/components/user-list.vue b/src/client/components/user-list.vue
index bde3af69066058d302b238154dbb40ff6f0b23e1..7a9cd58a481e0f1919501af44212c4eb0c7e8474 100644
--- a/src/client/components/user-list.vue
+++ b/src/client/components/user-list.vue
@@ -31,14 +31,11 @@
 
 <script lang="ts">
 import Vue from 'vue';
-import i18n from '../i18n';
 import paging from '../scripts/paging';
 import MkContainer from './ui/container.vue';
 import MkFollowButton from './follow-button.vue';
 
 export default Vue.extend({
-	i18n,
-
 	components: {
 		MkContainer,
 		MkFollowButton,
diff --git a/src/client/components/user-menu.vue b/src/client/components/user-menu.vue
index a2275197d89bde3464bb998d9db84adc71642698..25937fb3c04427eedb806239f30cf5e89adc7fc6 100644
--- a/src/client/components/user-menu.vue
+++ b/src/client/components/user-menu.vue
@@ -6,15 +6,12 @@
 import Vue from 'vue';
 import { faAt, faListUl, faEye, faEyeSlash, faBan, faPencilAlt, faComments, faUsers, faMicrophoneSlash } from '@fortawesome/free-solid-svg-icons';
 import { faSnowflake, faEnvelope } from '@fortawesome/free-regular-svg-icons';
-import i18n from '../i18n';
 import XMenu from './menu.vue';
 import copyToClipboard from '../scripts/copy-to-clipboard';
 import { host } from '../config';
 import getAcct from '../../misc/acct/render';
 
 export default Vue.extend({
-	i18n,
-
 	components: {
 		XMenu
 	},
diff --git a/src/client/components/user-preview.vue b/src/client/components/user-preview.vue
index 89150eaaccd3eb139bd36d56674751b5ab7eedaf..8c8eee2a3458b93be05bd95a39df1694cc57751b 100644
--- a/src/client/components/user-preview.vue
+++ b/src/client/components/user-preview.vue
@@ -28,13 +28,10 @@
 
 <script lang="ts">
 import Vue from 'vue';
-import i18n from '../i18n';
 import parseAcct from '../../misc/acct/parse';
 import MkFollowButton from './follow-button.vue';
 
 export default Vue.extend({
-	i18n,
-
 	components: {
 		MkFollowButton
 	},
diff --git a/src/client/components/user-select.vue b/src/client/components/user-select.vue
index a82626652d6207ba100782a27bbc4d1f9230eb28..9b4a68ddb35af45caeaa3c3d031d8e266b27459b 100644
--- a/src/client/components/user-select.vue
+++ b/src/client/components/user-select.vue
@@ -21,14 +21,11 @@
 
 <script lang="ts">
 import Vue from 'vue';
-import i18n from '../i18n';
 import { faTimes, faCheck } from '@fortawesome/free-solid-svg-icons';
 import MkInput from './ui/input.vue';
 import XWindow from './window.vue';
 
 export default Vue.extend({
-	i18n,
-
 	components: {
 		MkInput,
 		XWindow,
diff --git a/src/client/components/users-dialog.vue b/src/client/components/users-dialog.vue
index 9d0c5e4251cdc1acb8730bc6950a09ea59fd09ea..0e0cc36c2a31b30efe4a6b3e2d0d1175e6d348ff 100644
--- a/src/client/components/users-dialog.vue
+++ b/src/client/components/users-dialog.vue
@@ -31,13 +31,10 @@
 <script lang="ts">
 import Vue from 'vue';
 import { faTimes } from '@fortawesome/free-solid-svg-icons';
-import i18n from '../i18n';
 import paging from '../scripts/paging';
 import XModal from './modal.vue';
 
 export default Vue.extend({
-	i18n,
-
 	components: {
 		XModal,
 	},
diff --git a/src/client/components/visibility-chooser.vue b/src/client/components/visibility-chooser.vue
index dc7b41e2863216674cefeada2767fbb3c6286bdd..0f7e37a0884f44db383b0ac0d958663729bed951 100644
--- a/src/client/components/visibility-chooser.vue
+++ b/src/client/components/visibility-chooser.vue
@@ -37,11 +37,9 @@
 import Vue from 'vue';
 import { faGlobe, faUnlock, faHome } from '@fortawesome/free-solid-svg-icons';
 import { faEnvelope } from '@fortawesome/free-regular-svg-icons';
-import i18n from '../i18n';
 import XPopup from './popup.vue';
 
 export default Vue.extend({
-	i18n,
 	components: {
 		XPopup
 	},
diff --git a/src/client/components/window.vue b/src/client/components/window.vue
index 0b2ba248bfe6b526af068a23c111c7814a5211b5..db13985181039031743304add2af5bf1febe12e2 100644
--- a/src/client/components/window.vue
+++ b/src/client/components/window.vue
@@ -20,12 +20,9 @@
 <script lang="ts">
 import Vue from 'vue';
 import { faTimes, faCheck } from '@fortawesome/free-solid-svg-icons';
-import i18n from '../i18n';
 import XModal from './modal.vue';
 
 export default Vue.extend({
-	i18n,
-
 	components: {
 		XModal,
 	},
diff --git a/src/client/config.ts b/src/client/config.ts
index 0d4a96964ec3a4113399aa62f184f80bc2f437ff..f71647a05c1ff42aa6a6ca91fe075f825755f820 100644
--- a/src/client/config.ts
+++ b/src/client/config.ts
@@ -1,3 +1,6 @@
+import { clientDb, entries } from './db';
+import { fromEntries } from '../prelude/array';
+
 declare const _LANGS_: string[];
 declare const _VERSION_: string;
 declare const _ENV_: string;
@@ -12,7 +15,7 @@ export const apiUrl = url + '/api';
 export const wsUrl = url.replace('http://', 'ws://').replace('https://', 'wss://') + '/streaming';
 export const lang = localStorage.getItem('lang');
 export const langs = _LANGS_;
-export const locale = JSON.parse(localStorage.getItem('locale'));
+export const getLocale = async () => fromEntries((await entries(clientDb.i18n)) as [string, string][]);
 export const version = _VERSION_;
 export const env = _ENV_;
 export const instanceName = siteName === 'Misskey' ? null : siteName;
diff --git a/src/client/db.ts b/src/client/db.ts
new file mode 100644
index 0000000000000000000000000000000000000000..3000a0c96836a4c6a25f82a5fde836c0cbd55620
--- /dev/null
+++ b/src/client/db.ts
@@ -0,0 +1,68 @@
+import { Store } from 'idb-keyval';
+// Provide functions from idb-keyval
+export { get, set, del, clear, keys } from 'idb-keyval';
+
+//#region Construct DB
+export const clientDb = {
+	i18n: new Store('MisskeyClient', 'i18n')
+};
+//#endregion
+
+//#region Provide some tool functions
+function openTransaction(store: Store, mode: IDBTransactionMode): Promise<IDBTransaction>{
+	return store._dbp.then(db => db.transaction(store.storeName, mode));
+}
+
+export function entries(store: Store): Promise<[IDBValidKey, unknown][]> {
+	const entries: [IDBValidKey, unknown][] = [];
+
+	return store._withIDBStore('readonly', store => {
+		store.openCursor().onsuccess = function () {
+			if (!this.result) return;
+			entries.push([this.result.key, this.result.value]);
+			this.result.continue();
+		};
+	}).then(() => entries);
+}
+
+export async function bulkGet(keys: IDBValidKey[], store: Store): Promise<[IDBValidKey, unknown][]> {
+	const valPromises: Promise<[IDBValidKey, unknown]>[] = [];
+
+	const tx = await openTransaction(store, 'readwrite');
+	const st = tx.objectStore(store.storeName);
+	for (const key of keys) {
+		valPromises.push(new Promise((resolve, reject) => {
+			const getting = st.get(key);
+			getting.onsuccess = function (e) {
+				return resolve([key, this.result]);
+			};
+			getting.onerror = function (e) {
+				return reject(this.error);
+			};
+		}));
+	}
+	return new Promise((resolve, reject) => {
+		tx.oncomplete = () => resolve(Promise.all(valPromises));
+		tx.abort = tx.onerror = () => reject(tx.error);
+	});
+}
+
+export async function bulkSet(map: [IDBValidKey, any][], store: Store): Promise<void> {
+	const tx = await openTransaction(store, 'readwrite');
+	const st = tx.objectStore(store.storeName);
+	for (const [key, value] of map) {
+		st.put(value, key);
+	}
+	return new Promise((resolve, reject) => {
+		tx.oncomplete = () => resolve();
+		tx.abort = tx.onerror = () => reject(tx.error);
+	});
+}
+
+export function count(store: Store): Promise<number> {
+	let req: IDBRequest<number>;
+	return store._withIDBStore('readonly', store => {
+		req = store.count();
+	}).then(() => req.result);
+}
+//#endregion
diff --git a/src/client/i18n.ts b/src/client/i18n.ts
deleted file mode 100644
index 05d319fbafd8a6045504080bf76a555ed3f74b6e..0000000000000000000000000000000000000000
--- a/src/client/i18n.ts
+++ /dev/null
@@ -1,12 +0,0 @@
-import Vue from 'vue';
-import VueI18n from 'vue-i18n';
-import { lang, locale } from './config';
-
-Vue.use(VueI18n);
-
-export default new VueI18n({
-	locale: lang,
-	messages: {
-		[lang]: locale
-	}
-});
diff --git a/src/client/init.ts b/src/client/init.ts
index 500092061a8d830c10aae629d5ee6534098784ea..e2772502f662b485851bf1c3b24997f4ee786693 100644
--- a/src/client/init.ts
+++ b/src/client/init.ts
@@ -7,13 +7,13 @@ import Vuex from 'vuex';
 import VueMeta from 'vue-meta';
 import PortalVue from 'portal-vue';
 import VAnimateCss from 'v-animate-css';
+import VueI18n from 'vue-i18n';
 import { FontAwesomeIcon } from '@fortawesome/vue-fontawesome';
 
-import i18n from './i18n';
 import VueHotkey from './scripts/hotkey';
 import App from './app.vue';
 import MiOS from './mios';
-import { version, langs, instanceName } from './config';
+import { version, langs, instanceName, getLocale } from './config';
 import PostFormDialog from './components/post-form-dialog.vue';
 import Dialog from './components/dialog.vue';
 import Menu from './components/menu.vue';
@@ -21,12 +21,15 @@ import { router } from './router';
 import { applyTheme, lightTheme } from './theme';
 import { isDeviceDarkmode } from './scripts/is-device-darkmode';
 import createStore from './store';
+import { clientDb, get, count } from './db';
+import { setI18nContexts } from './scripts/set-i18n-contexts';
 
 Vue.use(Vuex);
 Vue.use(VueHotkey);
 Vue.use(VueMeta);
 Vue.use(PortalVue);
 Vue.use(VAnimateCss);
+Vue.use(VueI18n);
 Vue.component('fa', FontAwesomeIcon);
 
 require('./directives');
@@ -96,27 +99,6 @@ if (isMobile || window.innerWidth <= 1024) {
 	head.appendChild(viewport);
 }
 
-//#region Fetch locale data
-const cachedLocale = localStorage.getItem('locale');
-
-if (cachedLocale == null) {
-	fetch(`/assets/locales/${lang}.${version}.json`)
-		.then(response => response.json()).then(locale => {
-			localStorage.setItem('locale', JSON.stringify(locale));
-			i18n.locale = lang;
-			i18n.setLocaleMessage(lang, locale);
-		});
-} else {
-	// TODO: 古い時だけ更新
-	setTimeout(() => {
-		fetch(`/assets/locales/${lang}.${version}.json`)
-			.then(response => response.json()).then(locale => {
-				localStorage.setItem('locale', JSON.stringify(locale));
-			});
-	}, 1000 * 5);
-}
-//#endregion
-
 //#region Set lang attr
 const html = document.documentElement;
 html.setAttribute('lang', lang);
@@ -167,6 +149,18 @@ os.init(async () => {
 	});
 	//#endregion
 
+	//#region Fetch locale data
+	const i18n = new VueI18n();
+
+	await count(clientDb.i18n).then(async n => {
+		if (n === 0) return setI18nContexts(lang, version, i18n);
+		if ((await get('_version_', clientDb.i18n) !== version)) return setI18nContexts(lang, version, i18n, true);
+
+		i18n.locale = lang;
+		i18n.setLocaleMessage(lang, await getLocale());
+	});
+	//#endregion
+
 	if ('Notification' in window && store.getters.isSignedIn) {
 		// 許可を得ていなかったらリクエスト
 		if (Notification.permission === 'default') {
@@ -176,6 +170,7 @@ os.init(async () => {
 
 	const app = new Vue({
 		store: store,
+		i18n,
 		metaInfo: {
 			title: null,
 			titleTemplate: title => title ? `${title} | ${(instanceName || 'Misskey')}` : (instanceName || 'Misskey')
@@ -183,7 +178,8 @@ os.init(async () => {
 		data() {
 			return {
 				stream: os.stream,
-				isMobile: isMobile
+				isMobile: isMobile,
+				i18n // TODO: 消せないか考える SEE: https://github.com/syuilo/misskey/pull/6396#discussion_r429511030
 			};
 		},
 		methods: {
diff --git a/src/client/pages/about-misskey.vue b/src/client/pages/about-misskey.vue
index 84cd5d5e9c62d6a2fe76f0cf314f3f464a6da358..2c4a257b156b3e274b95ec91440d51fc4f9565ba 100644
--- a/src/client/pages/about-misskey.vue
+++ b/src/client/pages/about-misskey.vue
@@ -63,12 +63,9 @@
 import Vue from 'vue';
 import { faInfoCircle } from '@fortawesome/free-solid-svg-icons';
 import { version } from '../config';
-import i18n from '../i18n';
 import MkLink from '../components/link.vue';
 
 export default Vue.extend({
-	i18n,
-
 	components: {
 		MkLink
 	},
diff --git a/src/client/pages/about.vue b/src/client/pages/about.vue
index a3a4b6ac736b172d2de43436df0185210aa6df5d..25fb0ca13e79b93ca51b8bff178f845712d46eb2 100644
--- a/src/client/pages/about.vue
+++ b/src/client/pages/about.vue
@@ -25,12 +25,9 @@
 import Vue from 'vue';
 import { faInfoCircle } from '@fortawesome/free-solid-svg-icons';
 import { version } from '../config';
-import i18n from '../i18n';
 import MkInstanceStats from '../components/instance-stats.vue';
 
 export default Vue.extend({
-	i18n,
-
 	metaInfo() {
 		return {
 			title: this.$t('instance') as string
diff --git a/src/client/pages/announcements.vue b/src/client/pages/announcements.vue
index 5c6d4f58af9c7d87b9a3348671148cfc5249c8b7..089475ed60728a40c11afea5126a2070e8dd89c9 100644
--- a/src/client/pages/announcements.vue
+++ b/src/client/pages/announcements.vue
@@ -21,13 +21,10 @@
 <script lang="ts">
 import Vue from 'vue';
 import { faCheck, faBroadcastTower } from '@fortawesome/free-solid-svg-icons';
-import i18n from '../i18n';
 import MkPagination from '../components/ui/pagination.vue';
 import MkButton from '../components/ui/button.vue';
 
 export default Vue.extend({
-	i18n,
-
 	metaInfo() {
 		return {
 			title: this.$t('announcements') as string
diff --git a/src/client/pages/auth.form.vue b/src/client/pages/auth.form.vue
index e6f61f52f144accef2994a3cb8e43b769deb02ea..c5a9b769aca56917bac935f6ddbb040c491b8fdb 100644
--- a/src/client/pages/auth.form.vue
+++ b/src/client/pages/auth.form.vue
@@ -23,11 +23,9 @@
 
 <script lang="ts">
 import Vue from 'vue';
-import i18n from '../i18n';
 import MkButton from '../components/ui/button.vue';
 
 export default Vue.extend({
-	i18n,
 	components: {
 		MkButton
 	},
diff --git a/src/client/pages/auth.vue b/src/client/pages/auth.vue
index e025924fe025be10868aa6bfaf162d3feda42e0e..5c40842da1008190a2574ac7602efc2d8547dff7 100755
--- a/src/client/pages/auth.vue
+++ b/src/client/pages/auth.vue
@@ -30,12 +30,10 @@
 
 <script lang="ts">
 import Vue from 'vue';
-import i18n from '../i18n';
 import XForm from './auth.form.vue';
 import MkSignin from '../components/signin.vue';
 
 export default Vue.extend({
-	i18n,
 	components: {
 		XForm,
 		MkSignin,
diff --git a/src/client/pages/doc.vue b/src/client/pages/doc.vue
index 7c4f7ebccfac41ca863d87a17420fbc4ea3d1ac7..e4c4ef5c6c6d370d4ac26117cf6e812ffde87cc6 100644
--- a/src/client/pages/doc.vue
+++ b/src/client/pages/doc.vue
@@ -19,7 +19,6 @@ import Vue from 'vue';
 import { faFileAlt } from '@fortawesome/free-solid-svg-icons'
 import MarkdownIt from 'markdown-it';
 import MarkdownItAnchor from 'markdown-it-anchor';
-import i18n from '../i18n';
 import { url, lang } from '../config';
 import MkLink from '../components/link.vue';
 
@@ -32,8 +31,6 @@ markdown.use(MarkdownItAnchor, {
 });
 
 export default Vue.extend({
-	i18n,
-
 	metaInfo() {
 		return {
 			title: this.title,
diff --git a/src/client/pages/explore.vue b/src/client/pages/explore.vue
index 7ff4b5ed606f3325d7658fc0ffced83d4da8d330..39904846cf195e389d2b0cceb675aef3ff709b1e 100644
--- a/src/client/pages/explore.vue
+++ b/src/client/pages/explore.vue
@@ -57,13 +57,10 @@
 import Vue from 'vue';
 import { faChartLine, faPlus, faHashtag, faRocket } from '@fortawesome/free-solid-svg-icons';
 import { faBookmark, faCommentAlt } from '@fortawesome/free-regular-svg-icons';
-import i18n from '../i18n';
 import XUserList from '../components/user-list.vue';
 import MkContainer from '../components/ui/container.vue';
 
 export default Vue.extend({
-	i18n,
-
 	metaInfo() {
 		return {
 			title: this.$t('explore') as string
diff --git a/src/client/pages/follow.vue b/src/client/pages/follow.vue
index d765259737bb1b11868ef079e0fd0d3733af1751..8659763bb7880c18b16395f2f79f757316998e68 100644
--- a/src/client/pages/follow.vue
+++ b/src/client/pages/follow.vue
@@ -5,11 +5,8 @@
 
 <script lang="ts">
 import Vue from 'vue';
-import i18n from '../i18n';
 
 export default Vue.extend({
-	i18n,
-
 	created() {
 		const acct = new URL(location.href).searchParams.get('acct');
 		if (acct == null) return;
diff --git a/src/client/pages/index.welcome.entrance.vue b/src/client/pages/index.welcome.entrance.vue
index a9343e87ccc1f569056f9aa64adbe0f5cd678114..9bb2e85fc32dc3c3cd13102beb698f63b4f89715 100644
--- a/src/client/pages/index.welcome.entrance.vue
+++ b/src/client/pages/index.welcome.entrance.vue
@@ -20,12 +20,9 @@ import XSigninDialog from '../components/signin-dialog.vue';
 import XSignupDialog from '../components/signup-dialog.vue';
 import MkButton from '../components/ui/button.vue';
 import XNotes from '../components/notes.vue';
-import i18n from '../i18n';
 import { host } from '../config';
 
 export default Vue.extend({
-	i18n,
-
 	components: {
 		MkButton,
 		XNotes,
diff --git a/src/client/pages/index.welcome.setup.vue b/src/client/pages/index.welcome.setup.vue
index 6d08f5b5d4a12a149bbc95455d5734574185300a..9a66a4dffbc4f631059e72aaf6a61a62535e9cd7 100644
--- a/src/client/pages/index.welcome.setup.vue
+++ b/src/client/pages/index.welcome.setup.vue
@@ -25,10 +25,8 @@ import { faLock } from '@fortawesome/free-solid-svg-icons';
 import MkButton from '../components/ui/button.vue';
 import MkInput from '../components/ui/input.vue';
 import { host } from '../config';
-import i18n from '../i18n';
 
 export default Vue.extend({
-	i18n,
 	
 	components: {
 		MkButton,
diff --git a/src/client/pages/instance/announcements.vue b/src/client/pages/instance/announcements.vue
index 2889cf8cce43597d0adab4b821a9a5d94c896389..0e11e2932e2c9df7a9f8dcb6deaa15f4a661364f 100644
--- a/src/client/pages/instance/announcements.vue
+++ b/src/client/pages/instance/announcements.vue
@@ -28,14 +28,11 @@
 import Vue from 'vue';
 import { faBroadcastTower, faPlus } from '@fortawesome/free-solid-svg-icons';
 import { faSave, faTrashAlt } from '@fortawesome/free-regular-svg-icons';
-import i18n from '../../i18n';
 import MkButton from '../../components/ui/button.vue';
 import MkInput from '../../components/ui/input.vue';
 import MkTextarea from '../../components/ui/textarea.vue';
 
 export default Vue.extend({
-	i18n,
-
 	metaInfo() {
 		return {
 			title: this.$t('announcements') as string
diff --git a/src/client/pages/instance/federation.instance.vue b/src/client/pages/instance/federation.instance.vue
index 08f4d1b4fb263ef89c5d345e70a2895cba80f800..6b6352a151b78e61721ee75a208a3e240ae9f889 100644
--- a/src/client/pages/instance/federation.instance.vue
+++ b/src/client/pages/instance/federation.instance.vue
@@ -120,7 +120,6 @@
 <script lang="ts">
 import Vue from 'vue';
 import Chart from 'chart.js';
-import i18n from '../../i18n';
 import { faTimes, faCrosshairs, faCloudDownloadAlt, faCloudUploadAlt, faUsers, faPencilAlt, faFileImage, faDatabase, faTrafficLight, faLongArrowAltUp, faLongArrowAltDown, faMinusCircle, faTrashAlt } from '@fortawesome/free-solid-svg-icons';
 import XWindow from '../../components/window.vue';
 import MkUsersDialog from '../../components/users-dialog.vue';
@@ -141,8 +140,6 @@ const alpha = hex => {
 };
 
 export default Vue.extend({
-	i18n,
-
 	components: {
 		XWindow,
 		MkSelect,
diff --git a/src/client/pages/instance/federation.vue b/src/client/pages/instance/federation.vue
index 5babc604539e51c74f601c957a82dd741beab57f..77819235d7d5b1ede0c4bdee5d5b54ee195fd146 100644
--- a/src/client/pages/instance/federation.vue
+++ b/src/client/pages/instance/federation.vue
@@ -62,7 +62,6 @@
 <script lang="ts">
 import Vue from 'vue';
 import { faGlobe, faCircle, faExchangeAlt, faCaretDown, faCaretUp, faTrafficLight } from '@fortawesome/free-solid-svg-icons';
-import i18n from '../../i18n';
 import MkButton from '../../components/ui/button.vue';
 import MkInput from '../../components/ui/input.vue';
 import MkSelect from '../../components/ui/select.vue';
@@ -70,8 +69,6 @@ import MkPagination from '../../components/ui/pagination.vue';
 import MkInstanceInfo from './federation.instance.vue';
 
 export default Vue.extend({
-	i18n,
-
 	metaInfo() {
 		return {
 			title: this.$t('federation') as string
diff --git a/src/client/pages/instance/index.vue b/src/client/pages/instance/index.vue
index 1d90aa5537f46cd19ac9753eaf2bf734ac9a7e09..d21f8d455e854d165c8fa08f906424e502dba0dc 100644
--- a/src/client/pages/instance/index.vue
+++ b/src/client/pages/instance/index.vue
@@ -107,7 +107,6 @@ import MkButton from '../../components/ui/button.vue';
 import MkSelect from '../../components/ui/select.vue';
 import MkInput from '../../components/ui/input.vue';
 import { version, url } from '../../config';
-import i18n from '../../i18n';
 
 const alpha = (hex, a) => {
 	const result = /^#?([a-f\d]{2})([a-f\d]{2})([a-f\d]{2})$/i.exec(hex)!;
@@ -118,8 +117,6 @@ const alpha = (hex, a) => {
 };
 
 export default Vue.extend({
-	i18n,
-
 	metaInfo() {
 		return {
 			title: this.$t('instance') as string
diff --git a/src/client/pages/instance/queue.queue.vue b/src/client/pages/instance/queue.queue.vue
index 7f0fc7d2bcbc1249ad0043b69c9c310232f160e2..1649d1e1725f93aa32427720c84adfdd8bcd49f2 100644
--- a/src/client/pages/instance/queue.queue.vue
+++ b/src/client/pages/instance/queue.queue.vue
@@ -25,7 +25,6 @@
 <script lang="ts">
 import Vue from 'vue';
 import Chart from 'chart.js';
-import i18n from '../../i18n';
 
 const alpha = (hex, a) => {
 	const result = /^#?([a-f\d]{2})([a-f\d]{2})([a-f\d]{2})$/i.exec(hex)!;
@@ -36,8 +35,6 @@ const alpha = (hex, a) => {
 };
 
 export default Vue.extend({
-	i18n,
-
 	props: {
 		domain: {
 			required: true
diff --git a/src/client/pages/instance/queue.vue b/src/client/pages/instance/queue.vue
index c4892e88db629d9bd17ec26fd9f9b4abad426729..7a2204e5198ec8c75b482b6d1404cb4100ee5370 100644
--- a/src/client/pages/instance/queue.vue
+++ b/src/client/pages/instance/queue.vue
@@ -21,13 +21,10 @@
 import Vue from 'vue';
 import { faExchangeAlt } from '@fortawesome/free-solid-svg-icons';
 import { faTrashAlt } from '@fortawesome/free-regular-svg-icons';
-import i18n from '../../i18n';
 import MkButton from '../../components/ui/button.vue';
 import XQueue from './queue.queue.vue';
 
 export default Vue.extend({
-	i18n,
-
 	metaInfo() {
 		return {
 			title: `${this.$t('jobQueue')} | ${this.$t('instance')}`
diff --git a/src/client/pages/instance/relays.vue b/src/client/pages/instance/relays.vue
index 9b523bd0ecb4d366cee4b412df73d84adba65cae..dd18867b6a9e68563c9ed1ab557835108b42db22 100644
--- a/src/client/pages/instance/relays.vue
+++ b/src/client/pages/instance/relays.vue
@@ -28,13 +28,10 @@
 import Vue from 'vue';
 import { faPlus, faProjectDiagram } from '@fortawesome/free-solid-svg-icons';
 import { faSave, faTrashAlt } from '@fortawesome/free-regular-svg-icons';
-import i18n from '../../i18n';
 import MkButton from '../../components/ui/button.vue';
 import MkInput from '../../components/ui/input.vue';
 
 export default Vue.extend({
-	i18n,
-
 	metaInfo() {
 		return {
 			title: this.$t('relays') as string
diff --git a/src/client/pages/instance/settings.vue b/src/client/pages/instance/settings.vue
index afd6d4cc6d4b4b809b47d9bf8b23207b576f6104..0436e87804b6c982f1340933859d431e3e9a3343 100644
--- a/src/client/pages/instance/settings.vue
+++ b/src/client/pages/instance/settings.vue
@@ -210,12 +210,9 @@ import MkSwitch from '../../components/ui/switch.vue';
 import MkInfo from '../../components/ui/info.vue';
 import MkUserSelect from '../../components/user-select.vue';
 import { url } from '../../config';
-import i18n from '../../i18n';
 import getAcct from '../../../misc/acct/render';
 
 export default Vue.extend({
-	i18n,
-
 	metaInfo() {
 		return {
 			title: this.$t('instance') as string
diff --git a/src/client/pages/instance/users.user.vue b/src/client/pages/instance/users.user.vue
index 1fb064f7f08f970034592f2d3c76adcce8be08e7..25f0260637e460fdbde53d14ca174949c709116b 100644
--- a/src/client/pages/instance/users.user.vue
+++ b/src/client/pages/instance/users.user.vue
@@ -39,12 +39,9 @@ import { faTimes, faBookmark, faKey, faSync, faMicrophoneSlash, faExternalLinkSq
 import { faSnowflake, faTrashAlt, faBookmark as farBookmark  } from '@fortawesome/free-regular-svg-icons';
 import MkButton from '../../components/ui/button.vue';
 import MkSwitch from '../../components/ui/switch.vue';
-import i18n from '../../i18n';
 import Progress from '../../scripts/loading';
 
 export default Vue.extend({
-	i18n,
-
 	components: {
 		MkButton,
 		MkSwitch,
diff --git a/src/client/pages/messaging/index.vue b/src/client/pages/messaging/index.vue
index 7a55004cbf9ce0a718e5aaf862c26e22dd679847..7ee782c4a9081b23d117f8d6b4460be58450a110 100644
--- a/src/client/pages/messaging/index.vue
+++ b/src/client/pages/messaging/index.vue
@@ -42,14 +42,11 @@
 <script lang="ts">
 import Vue from 'vue';
 import { faUser, faUsers, faComments, faPlus } from '@fortawesome/free-solid-svg-icons';
-import i18n from '../../i18n';
 import getAcct from '../../../misc/acct/render';
 import MkButton from '../../components/ui/button.vue';
 import MkUserSelect from '../../components/user-select.vue';
 
 export default Vue.extend({
-	i18n,
-
 	components: {
 		MkButton
 	},
diff --git a/src/client/pages/messaging/messaging-room.form.vue b/src/client/pages/messaging/messaging-room.form.vue
index 0cd3dfcc8580a395d331a7a51f980ffeaebf4acf..be47efd2ce5af234b257e9a080fe7e6f860f0fc2 100644
--- a/src/client/pages/messaging/messaging-room.form.vue
+++ b/src/client/pages/messaging/messaging-room.form.vue
@@ -27,12 +27,10 @@ import Vue from 'vue';
 import { faPaperPlane, faPhotoVideo, faLaughSquint } from '@fortawesome/free-solid-svg-icons';
 import insertTextAtCursor from 'insert-text-at-cursor';
 import * as autosize from 'autosize';
-import i18n from '../../i18n';
 import { formatTimeString } from '../../../misc/format-time-string';
 import { selectFile } from '../../scripts/select-file';
 
 export default Vue.extend({
-	i18n,
 	components: {
 		XUploader: () => import('../../components/uploader.vue').then(m => m.default),
 	},
diff --git a/src/client/pages/messaging/messaging-room.message.vue b/src/client/pages/messaging/messaging-room.message.vue
index 67756572ff4faa98b2beddb26fd645e92eaeca17..58e1e54ad85b7fa52126af07bef4f1772ee8b89f 100644
--- a/src/client/pages/messaging/messaging-room.message.vue
+++ b/src/client/pages/messaging/messaging-room.message.vue
@@ -38,13 +38,11 @@
 
 <script lang="ts">
 import Vue from 'vue';
-import i18n from '../../i18n';
 import { parse } from '../../../mfm/parse';
 import { unique } from '../../../prelude/array';
 import MkUrlPreview from '../../components/url-preview.vue';
 
 export default Vue.extend({
-	i18n,
 	components: {
 		MkUrlPreview
 	},
diff --git a/src/client/pages/messaging/messaging-room.vue b/src/client/pages/messaging/messaging-room.vue
index 317ad087fe04e4992473c3066d94d10328b959e9..e97d5532acd9114893d4626661ee30142bb17763 100644
--- a/src/client/pages/messaging/messaging-room.vue
+++ b/src/client/pages/messaging/messaging-room.vue
@@ -37,7 +37,6 @@
 <script lang="ts">
 import Vue from 'vue';
 import { faArrowCircleDown, faFlag, faUsers, faInfoCircle } from '@fortawesome/free-solid-svg-icons';
-import i18n from '../../i18n';
 import XList from '../../components/date-separated-list.vue';
 import XMessage from './messaging-room.message.vue';
 import XForm from './messaging-room.form.vue';
@@ -45,8 +44,6 @@ import { url } from '../../config';
 import parseAcct from '../../../misc/acct/parse';
 
 export default Vue.extend({
-	i18n,
-
 	components: {
 		XMessage,
 		XForm,
diff --git a/src/client/pages/miauth.vue b/src/client/pages/miauth.vue
index 0e170af11a224ae70742e419b0d2f58e0bf3d737..15cde8bc2512792c03fa7cc48b7be82320ef588c 100644
--- a/src/client/pages/miauth.vue
+++ b/src/client/pages/miauth.vue
@@ -40,12 +40,10 @@
 
 <script lang="ts">
 import Vue from 'vue';
-import i18n from '../i18n';
 import MkSignin from '../components/signin.vue';
 import MkButton from '../components/ui/button.vue';
 
 export default Vue.extend({
-	i18n,
 	components: {
 		MkSignin,
 		MkButton,
diff --git a/src/client/pages/my-antennas/index.antenna.vue b/src/client/pages/my-antennas/index.antenna.vue
index 2a9aebbcbf90be57f900850f4dc09a31ce429feb..6435e4fc9a139b296149b5b9834357f50ba107b1 100644
--- a/src/client/pages/my-antennas/index.antenna.vue
+++ b/src/client/pages/my-antennas/index.antenna.vue
@@ -48,7 +48,6 @@
 <script lang="ts">
 import Vue from 'vue';
 import { faSave, faTrash } from '@fortawesome/free-solid-svg-icons';
-import i18n from '../../i18n';
 import MkButton from '../../components/ui/button.vue';
 import MkInput from '../../components/ui/input.vue';
 import MkTextarea from '../../components/ui/textarea.vue';
@@ -58,8 +57,6 @@ import MkUserSelect from '../../components/user-select.vue';
 import getAcct from '../../../misc/acct/render';
 
 export default Vue.extend({
-	i18n,
-
 	components: {
 		MkButton, MkInput, MkTextarea, MkSelect, MkSwitch
 	},
diff --git a/src/client/pages/my-groups/group.vue b/src/client/pages/my-groups/group.vue
index c8170a2a575aaff477b5c1eb10ff7232ee9c1d1e..0132bc2c33ed635de190d94747dadbcaa85bf3e7 100644
--- a/src/client/pages/my-groups/group.vue
+++ b/src/client/pages/my-groups/group.vue
@@ -41,14 +41,11 @@
 <script lang="ts">
 import Vue from 'vue';
 import { faTimes, faUsers } from '@fortawesome/free-solid-svg-icons';
-import i18n from '../../i18n';
 import Progress from '../../scripts/loading';
 import MkButton from '../../components/ui/button.vue';
 import MkUserSelect from '../../components/user-select.vue';
 
 export default Vue.extend({
-	i18n,
-
 	metaInfo() {
 		return {
 			title: this.group ? `${this.group.name} | ${this.$t('manageGroups')}` : this.$t('manageGroups')
diff --git a/src/client/pages/my-lists/list.vue b/src/client/pages/my-lists/list.vue
index cf85a80ccb6de8cff8a7c20c50999dea77917bbc..7052c55160c8007bf73eb3d76a96d27152b79b7d 100644
--- a/src/client/pages/my-lists/list.vue
+++ b/src/client/pages/my-lists/list.vue
@@ -40,14 +40,11 @@
 <script lang="ts">
 import Vue from 'vue';
 import { faTimes, faListUl } from '@fortawesome/free-solid-svg-icons';
-import i18n from '../../i18n';
 import Progress from '../../scripts/loading';
 import MkButton from '../../components/ui/button.vue';
 import MkUserSelect from '../../components/user-select.vue';
 
 export default Vue.extend({
-	i18n,
-
 	metaInfo() {
 		return {
 			title: this.list ? `${this.list.name} | ${this.$t('manageLists')}` : this.$t('manageLists')
diff --git a/src/client/pages/my-settings/2fa.vue b/src/client/pages/my-settings/2fa.vue
index 6ceca21fe60edb122472363f76da01204a78cc1f..58ba03c41c1813e5638934891c68dc260e8b99df 100644
--- a/src/client/pages/my-settings/2fa.vue
+++ b/src/client/pages/my-settings/2fa.vue
@@ -65,7 +65,6 @@
 <script lang="ts">
 import Vue from 'vue';
 import { faLock } from '@fortawesome/free-solid-svg-icons';
-import i18n from '../../i18n';
 import { hostname } from '../../config';
 import { byteify, hexify, stringify } from '../../scripts/2fa';
 import MkButton from '../../components/ui/button.vue';
@@ -74,7 +73,6 @@ import MkInput from '../../components/ui/input.vue';
 import MkSwitch from '../../components/ui/switch.vue';
 
 export default Vue.extend({
-	i18n,
 	components: {
 		MkButton, MkInfo, MkInput, MkSwitch
 	},
diff --git a/src/client/pages/my-settings/api.vue b/src/client/pages/my-settings/api.vue
index f394c826de5d1ab4702c09c247c7c1b1005297c4..79b459fb5eeb5fd2d4b2cc7e39a43d9134eccefd 100644
--- a/src/client/pages/my-settings/api.vue
+++ b/src/client/pages/my-settings/api.vue
@@ -13,12 +13,10 @@
 <script lang="ts">
 import Vue from 'vue';
 import { faKey, faSyncAlt } from '@fortawesome/free-solid-svg-icons';
-import i18n from '../../i18n';
 import MkButton from '../../components/ui/button.vue';
 import MkInput from '../../components/ui/input.vue';
 
 export default Vue.extend({
-	i18n,
 	components: {
 		MkButton, MkInput
 	},
diff --git a/src/client/pages/my-settings/drive.vue b/src/client/pages/my-settings/drive.vue
index c3d2d1dc2db6c193e127b3235833aa249890fb00..7612c5011f78d47dc9b68ea5b1fbb9bba663108e 100644
--- a/src/client/pages/my-settings/drive.vue
+++ b/src/client/pages/my-settings/drive.vue
@@ -13,12 +13,9 @@ import Vue from 'vue';
 import { faCloud, faFolderOpen } from '@fortawesome/free-solid-svg-icons';
 import { faClock, faEyeSlash, faTrashAlt } from '@fortawesome/free-regular-svg-icons';
 import MkButton from '../../components/ui/button.vue';
-import i18n from '../../i18n';
 import { selectDriveFolder } from '../../scripts/select-drive-folder';
 
 export default Vue.extend({
-	i18n,
-
 	components: {
 		MkButton,
 	},
diff --git a/src/client/pages/my-settings/import-export.vue b/src/client/pages/my-settings/import-export.vue
index 47957411895b7c79900d1df366282eef6252b03d..cc148d48d49cde3cb028b662bd1e10aa2b1bfe9d 100644
--- a/src/client/pages/my-settings/import-export.vue
+++ b/src/client/pages/my-settings/import-export.vue
@@ -21,12 +21,9 @@ import Vue from 'vue';
 import { faDownload, faUpload, faBoxes } from '@fortawesome/free-solid-svg-icons';
 import MkButton from '../../components/ui/button.vue';
 import MkSelect from '../../components/ui/select.vue';
-import i18n from '../../i18n';
 import { apiUrl } from '../../config';
 
 export default Vue.extend({
-	i18n,
-
 	components: {
 		MkButton,
 		MkSelect,
diff --git a/src/client/pages/my-settings/integration.vue b/src/client/pages/my-settings/integration.vue
index 3dd7783f12f1e9e7c52e1206c800fd8f689084c4..2d6e57e22c8a00fe993fe125447e63647baf69bb 100644
--- a/src/client/pages/my-settings/integration.vue
+++ b/src/client/pages/my-settings/integration.vue
@@ -29,13 +29,10 @@
 import Vue from 'vue';
 import { faShareAlt } from '@fortawesome/free-solid-svg-icons';
 import { faTwitter, faDiscord, faGithub } from '@fortawesome/free-brands-svg-icons';
-import i18n from '../../i18n';
 import { apiUrl } from '../../config';
 import MkButton from '../../components/ui/button.vue';
 
 export default Vue.extend({
-	i18n,
-
 	components: {
 		MkButton
 	},
diff --git a/src/client/pages/my-settings/mute-block.vue b/src/client/pages/my-settings/mute-block.vue
index 03cf4aacc82766cca0420a9cb195743790299a59..8eb43a6e29653d689777380e86caa5369d915bfb 100644
--- a/src/client/pages/my-settings/mute-block.vue
+++ b/src/client/pages/my-settings/mute-block.vue
@@ -34,11 +34,8 @@
 import Vue from 'vue';
 import { faBan } from '@fortawesome/free-solid-svg-icons';
 import MkPagination from '../../components/ui/pagination.vue';
-import i18n from '../../i18n';
 
 export default Vue.extend({
-	i18n,
-
 	components: {
 		MkPagination,
 	},
diff --git a/src/client/pages/my-settings/privacy.vue b/src/client/pages/my-settings/privacy.vue
index 7ac9062d8818bba32a53b8cee7267119b53d6ad8..527ac9ea37a78ab908fe218cc61d8e01405e29ff 100644
--- a/src/client/pages/my-settings/privacy.vue
+++ b/src/client/pages/my-settings/privacy.vue
@@ -24,11 +24,8 @@ import Vue from 'vue';
 import { faLock } from '@fortawesome/free-solid-svg-icons';
 import MkSelect from '../../components/ui/select.vue';
 import MkSwitch from '../../components/ui/switch.vue';
-import i18n from '../../i18n';
 
 export default Vue.extend({
-	i18n,
-
 	components: {
 		MkSelect,
 		MkSwitch,
diff --git a/src/client/pages/my-settings/profile.vue b/src/client/pages/my-settings/profile.vue
index b168c89ec02f827890a3d060f20b80ea51db6aa4..16bba7a2709c1c08c683b1fae983848dd890e2f7 100644
--- a/src/client/pages/my-settings/profile.vue
+++ b/src/client/pages/my-settings/profile.vue
@@ -62,13 +62,10 @@ import MkButton from '../../components/ui/button.vue';
 import MkInput from '../../components/ui/input.vue';
 import MkTextarea from '../../components/ui/textarea.vue';
 import MkSwitch from '../../components/ui/switch.vue';
-import i18n from '../../i18n';
 import { host } from '../../config';
 import { selectFile } from '../../scripts/select-file';
 
 export default Vue.extend({
-	i18n,
-
 	components: {
 		MkButton,
 		MkInput,
diff --git a/src/client/pages/my-settings/reaction.vue b/src/client/pages/my-settings/reaction.vue
index 68c481707cee996c37a177f3ebcaace5ee6ba8d5..ef4f6f6723fc7e48b14c637ffd207d2a562de83a 100644
--- a/src/client/pages/my-settings/reaction.vue
+++ b/src/client/pages/my-settings/reaction.vue
@@ -21,13 +21,10 @@ import { faUndo } from '@fortawesome/free-solid-svg-icons';
 import MkInput from '../../components/ui/input.vue';
 import MkButton from '../../components/ui/button.vue';
 import MkReactionPicker from '../../components/reaction-picker.vue';
-import i18n from '../../i18n';
 import { emojiRegexWithCustom } from '../../../misc/emoji-regex';
 import { defaultSettings } from '../../store';
 
 export default Vue.extend({
-	i18n,
-
 	components: {
 		MkInput,
 		MkButton,
diff --git a/src/client/pages/my-settings/security.vue b/src/client/pages/my-settings/security.vue
index ba670b2f6887b8f8cccb5684b3aabfeaae1e502a..dc77ca12c514a3a3effc3a9ada2ce485edcd46b2 100644
--- a/src/client/pages/my-settings/security.vue
+++ b/src/client/pages/my-settings/security.vue
@@ -11,11 +11,8 @@
 import Vue from 'vue';
 import { faLock } from '@fortawesome/free-solid-svg-icons';
 import MkButton from '../../components/ui/button.vue';
-import i18n from '../../i18n';
 
 export default Vue.extend({
-	i18n,
-
 	components: {
 		MkButton,
 	},
diff --git a/src/client/pages/not-found.vue b/src/client/pages/not-found.vue
index 9608e07786b6c4f543848ea8f722543d91192d0c..7f4c46c23d3c31eec16d637d316613e15df0793e 100644
--- a/src/client/pages/not-found.vue
+++ b/src/client/pages/not-found.vue
@@ -15,11 +15,8 @@
 <script lang="ts">
 import Vue from 'vue';
 import { faExclamationTriangle } from '@fortawesome/free-solid-svg-icons';
-import i18n from '../i18n';
 
 export default Vue.extend({
-	i18n,
-
 	metaInfo() {
 		return {
 			title: this.$t('notFound') as string
diff --git a/src/client/pages/note.vue b/src/client/pages/note.vue
index 37c66833e970a0e34304543801563479481f4551..48629a4ebeec0074899aa538c05f597c88282d8a 100644
--- a/src/client/pages/note.vue
+++ b/src/client/pages/note.vue
@@ -29,14 +29,12 @@
 <script lang="ts">
 import Vue from 'vue';
 import { faChevronUp, faChevronDown } from '@fortawesome/free-solid-svg-icons';
-import i18n from '../i18n';
 import Progress from '../scripts/loading';
 import XNote from '../components/note.vue';
 import XNotes from '../components/notes.vue';
 import MkRemoteCaution from '../components/remote-caution.vue';
 
 export default Vue.extend({
-	i18n,
 	metaInfo() {
 		return {
 			title: this.$t('note') as string
diff --git a/src/client/pages/page-editor/els/page-editor.el.button.vue b/src/client/pages/page-editor/els/page-editor.el.button.vue
index 9ca9fe06f3b7240d3d01a1d06b40161e0041933c..9821201666a0baedc75ac58d32cf412e4a7331e4 100644
--- a/src/client/pages/page-editor/els/page-editor.el.button.vue
+++ b/src/client/pages/page-editor/els/page-editor.el.button.vue
@@ -40,15 +40,12 @@
 <script lang="ts">
 import Vue from 'vue';
 import { faBolt } from '@fortawesome/free-solid-svg-icons';
-import i18n from '../../../i18n';
 import XContainer from '../page-editor.container.vue';
 import MkSelect from '../../../components/ui/select.vue';
 import MkInput from '../../../components/ui/input.vue';
 import MkSwitch from '../../../components/ui/switch.vue';
 
 export default Vue.extend({
-	i18n,
-
 	components: {
 		XContainer, MkSelect, MkInput, MkSwitch
 	},
diff --git a/src/client/pages/page-editor/els/page-editor.el.canvas.vue b/src/client/pages/page-editor/els/page-editor.el.canvas.vue
index 49773189192085d844b828e095f1cfed6637d9e8..a499207806a89a5920dfe38d9c822dac5d431d03 100644
--- a/src/client/pages/page-editor/els/page-editor.el.canvas.vue
+++ b/src/client/pages/page-editor/els/page-editor.el.canvas.vue
@@ -13,13 +13,10 @@
 <script lang="ts">
 import Vue from 'vue';
 import { faPaintBrush, faMagic } from '@fortawesome/free-solid-svg-icons';
-import i18n from '../../../i18n';
 import XContainer from '../page-editor.container.vue';
 import MkInput from '../../../components/ui/input.vue';
 
 export default Vue.extend({
-	i18n,
-
 	components: {
 		XContainer, MkInput
 	},
diff --git a/src/client/pages/page-editor/els/page-editor.el.counter.vue b/src/client/pages/page-editor/els/page-editor.el.counter.vue
index d9a4ddddee3e18d8beac15ffb84168d11c616071..f439f3e6ff4f8cd21b3af5c1369e42d31cbb4bcf 100644
--- a/src/client/pages/page-editor/els/page-editor.el.counter.vue
+++ b/src/client/pages/page-editor/els/page-editor.el.counter.vue
@@ -13,13 +13,10 @@
 <script lang="ts">
 import Vue from 'vue';
 import { faBolt, faMagic } from '@fortawesome/free-solid-svg-icons';
-import i18n from '../../../i18n';
 import XContainer from '../page-editor.container.vue';
 import MkInput from '../../../components/ui/input.vue';
 
 export default Vue.extend({
-	i18n,
-
 	components: {
 		XContainer, MkInput
 	},
diff --git a/src/client/pages/page-editor/els/page-editor.el.if.vue b/src/client/pages/page-editor/els/page-editor.el.if.vue
index 0449b9cf2b949a891bfc18196f7ac61fce8f4c3a..53cb9e2aee8f3af4f1b01d62b41513e16f5b30bf 100644
--- a/src/client/pages/page-editor/els/page-editor.el.if.vue
+++ b/src/client/pages/page-editor/els/page-editor.el.if.vue
@@ -28,13 +28,10 @@
 import Vue from 'vue';
 import { v4 as uuid } from 'uuid';
 import { faPlus, faQuestion } from '@fortawesome/free-solid-svg-icons';
-import i18n from '../../../i18n';
 import XContainer from '../page-editor.container.vue';
 import MkSelect from '../../../components/ui/select.vue';
 
 export default Vue.extend({
-	i18n,
-
 	components: {
 		XContainer, MkSelect
 	},
diff --git a/src/client/pages/page-editor/els/page-editor.el.image.vue b/src/client/pages/page-editor/els/page-editor.el.image.vue
index e22701e5c0a1485c97295b2e7efc876ecbda1b39..dd690da6f15c3abd61bfcdcd521995e944b308fc 100644
--- a/src/client/pages/page-editor/els/page-editor.el.image.vue
+++ b/src/client/pages/page-editor/els/page-editor.el.image.vue
@@ -17,14 +17,11 @@
 import Vue from 'vue';
 import { faPencilAlt } from '@fortawesome/free-solid-svg-icons';
 import { faImage, faFolderOpen } from '@fortawesome/free-regular-svg-icons';
-import i18n from '../../../i18n';
 import XContainer from '../page-editor.container.vue';
 import MkFileThumbnail from '../../../components/drive-file-thumbnail.vue';
 import { selectDriveFile } from '../../../scripts/select-drive-file';
 
 export default Vue.extend({
-	i18n,
-
 	components: {
 		XContainer, MkFileThumbnail
 	},
diff --git a/src/client/pages/page-editor/els/page-editor.el.number-input.vue b/src/client/pages/page-editor/els/page-editor.el.number-input.vue
index 76dd254464aaf553dd11595c5194151ee533deed..62d2e1bf8a6d0def51ef42be59fd3365640a7df8 100644
--- a/src/client/pages/page-editor/els/page-editor.el.number-input.vue
+++ b/src/client/pages/page-editor/els/page-editor.el.number-input.vue
@@ -13,13 +13,10 @@
 <script lang="ts">
 import Vue from 'vue';
 import { faBolt, faMagic } from '@fortawesome/free-solid-svg-icons';
-import i18n from '../../../i18n';
 import XContainer from '../page-editor.container.vue';
 import MkInput from '../../../components/ui/input.vue';
 
 export default Vue.extend({
-	i18n,
-
 	components: {
 		XContainer, MkInput
 	},
diff --git a/src/client/pages/page-editor/els/page-editor.el.post.vue b/src/client/pages/page-editor/els/page-editor.el.post.vue
index 2c6ce24e95de0d50e82fd01748ac59cbc00ad68d..06dea51c1f5057612fa0aac537a5a0ccde3ebb07 100644
--- a/src/client/pages/page-editor/els/page-editor.el.post.vue
+++ b/src/client/pages/page-editor/els/page-editor.el.post.vue
@@ -13,15 +13,12 @@
 <script lang="ts">
 import Vue from 'vue';
 import { faPaperPlane } from '@fortawesome/free-regular-svg-icons';
-import i18n from '../../../i18n';
 import XContainer from '../page-editor.container.vue';
 import MkTextarea from '../../../components/ui/textarea.vue';
 import MkInput from '../../../components/ui/input.vue';
 import MkSwitch from '../../../components/ui/switch.vue';
 
 export default Vue.extend({
-	i18n,
-
 	components: {
 		XContainer, MkTextarea, MkInput, MkSwitch
 	},
diff --git a/src/client/pages/page-editor/els/page-editor.el.radio-button.vue b/src/client/pages/page-editor/els/page-editor.el.radio-button.vue
index 8d404ec0df79c3a9e66f74220259eab225617ad6..34a9366d628bf5cd7e0c2fe4cc315361a28adf26 100644
--- a/src/client/pages/page-editor/els/page-editor.el.radio-button.vue
+++ b/src/client/pages/page-editor/els/page-editor.el.radio-button.vue
@@ -14,13 +14,11 @@
 <script lang="ts">
 import Vue from 'vue';
 import { faBolt, faMagic } from '@fortawesome/free-solid-svg-icons';
-import i18n from '../../../i18n';
 import XContainer from '../page-editor.container.vue';
 import MkTextarea from '../../../components/ui/textarea.vue';
 import MkInput from '../../../components/ui/input.vue';
 
 export default Vue.extend({
-	i18n,
 	components: {
 		XContainer, MkTextarea, MkInput
 	},
diff --git a/src/client/pages/page-editor/els/page-editor.el.section.vue b/src/client/pages/page-editor/els/page-editor.el.section.vue
index a32cf9c753455b8571d8536f7d5d1aa205db9ec3..e89a8b840c4282d955caf50820ecd94ef16da40e 100644
--- a/src/client/pages/page-editor/els/page-editor.el.section.vue
+++ b/src/client/pages/page-editor/els/page-editor.el.section.vue
@@ -21,12 +21,9 @@ import Vue from 'vue';
 import { v4 as uuid } from 'uuid';
 import { faPlus, faPencilAlt } from '@fortawesome/free-solid-svg-icons';
 import { faStickyNote } from '@fortawesome/free-regular-svg-icons';
-import i18n from '../../../i18n';
 import XContainer from '../page-editor.container.vue';
 
 export default Vue.extend({
-	i18n,
-
 	components: {
 		XContainer
 	},
diff --git a/src/client/pages/page-editor/els/page-editor.el.switch.vue b/src/client/pages/page-editor/els/page-editor.el.switch.vue
index 8f169c3d23cee06e59dd5a7d9322a7cf1261f665..5055da4f6f17db35e87a0d3469a247060697d21f 100644
--- a/src/client/pages/page-editor/els/page-editor.el.switch.vue
+++ b/src/client/pages/page-editor/els/page-editor.el.switch.vue
@@ -13,14 +13,11 @@
 <script lang="ts">
 import Vue from 'vue';
 import { faBolt, faMagic } from '@fortawesome/free-solid-svg-icons';
-import i18n from '../../../i18n';
 import XContainer from '../page-editor.container.vue';
 import MkSwitch from '../../../components/ui/switch.vue';
 import MkInput from '../../../components/ui/input.vue';
 
 export default Vue.extend({
-	i18n,
-
 	components: {
 		XContainer, MkSwitch, MkInput
 	},
diff --git a/src/client/pages/page-editor/els/page-editor.el.text-input.vue b/src/client/pages/page-editor/els/page-editor.el.text-input.vue
index 7c9e3d6a0e34ad5fa9c30644a9ca826686f57c70..bd5fb3761727b573b8bfda16ff3243a4ba38c4f2 100644
--- a/src/client/pages/page-editor/els/page-editor.el.text-input.vue
+++ b/src/client/pages/page-editor/els/page-editor.el.text-input.vue
@@ -13,13 +13,10 @@
 <script lang="ts">
 import Vue from 'vue';
 import { faBolt, faMagic } from '@fortawesome/free-solid-svg-icons';
-import i18n from '../../../i18n';
 import XContainer from '../page-editor.container.vue';
 import MkInput from '../../../components/ui/input.vue';
 
 export default Vue.extend({
-	i18n,
-
 	components: {
 		XContainer, MkInput
 	},
diff --git a/src/client/pages/page-editor/els/page-editor.el.text.vue b/src/client/pages/page-editor/els/page-editor.el.text.vue
index c6722236eb51b17b106e7f0176c70c8bd2933f4c..a50b1113bd7714a4a609f324b688d009f573cb6b 100644
--- a/src/client/pages/page-editor/els/page-editor.el.text.vue
+++ b/src/client/pages/page-editor/els/page-editor.el.text.vue
@@ -11,12 +11,9 @@
 <script lang="ts">
 import Vue from 'vue';
 import { faAlignLeft } from '@fortawesome/free-solid-svg-icons';
-import i18n from '../../../i18n';
 import XContainer from '../page-editor.container.vue';
 
 export default Vue.extend({
-	i18n,
-
 	components: {
 		XContainer
 	},
diff --git a/src/client/pages/page-editor/els/page-editor.el.textarea-input.vue b/src/client/pages/page-editor/els/page-editor.el.textarea-input.vue
index 8081e706bcccbc375892a683b0b2367d0de2c74d..33c49c705b5952826746932bd9f76ef127621590 100644
--- a/src/client/pages/page-editor/els/page-editor.el.textarea-input.vue
+++ b/src/client/pages/page-editor/els/page-editor.el.textarea-input.vue
@@ -13,14 +13,11 @@
 <script lang="ts">
 import Vue from 'vue';
 import { faBolt, faMagic } from '@fortawesome/free-solid-svg-icons';
-import i18n from '../../../i18n';
 import XContainer from '../page-editor.container.vue';
 import MkTextarea from '../../../components/ui/textarea.vue';
 import MkInput from '../../../components/ui/input.vue';
 
 export default Vue.extend({
-	i18n,
-
 	components: {
 		XContainer, MkTextarea, MkInput
 	},
diff --git a/src/client/pages/page-editor/els/page-editor.el.textarea.vue b/src/client/pages/page-editor/els/page-editor.el.textarea.vue
index d31da5dfa3923dec8afb1639e2b2afab53b4474c..e2e8848ccfd25eb395d2e54036bfb594a030dfdd 100644
--- a/src/client/pages/page-editor/els/page-editor.el.textarea.vue
+++ b/src/client/pages/page-editor/els/page-editor.el.textarea.vue
@@ -11,12 +11,9 @@
 <script lang="ts">
 import Vue from 'vue';
 import { faAlignLeft } from '@fortawesome/free-solid-svg-icons';
-import i18n from '../../../i18n';
 import XContainer from '../page-editor.container.vue';
 
 export default Vue.extend({
-	i18n,
-
 	components: {
 		XContainer
 	},
diff --git a/src/client/pages/page-editor/page-editor.container.vue b/src/client/pages/page-editor/page-editor.container.vue
index 5bc97446710d4796042485763fbf2b2346597ce4..3fa09f5600bed7d391b4e3f0ec1f48216008c967 100644
--- a/src/client/pages/page-editor/page-editor.container.vue
+++ b/src/client/pages/page-editor/page-editor.container.vue
@@ -28,11 +28,8 @@
 import Vue from 'vue';
 import { faBars, faAngleUp, faAngleDown } from '@fortawesome/free-solid-svg-icons';
 import { faTrashAlt } from '@fortawesome/free-regular-svg-icons';
-import i18n from '../../i18n';
 
 export default Vue.extend({
-	i18n,
-
 	props: {
 		expanded: {
 			type: Boolean,
diff --git a/src/client/pages/page-editor/page-editor.script-block.vue b/src/client/pages/page-editor/page-editor.script-block.vue
index 9eafd5daa08d37e2ac1856a9a81f66720e1b0fbc..f3270f02e3a8440a663349fb3e730d0cffced49f 100644
--- a/src/client/pages/page-editor/page-editor.script-block.vue
+++ b/src/client/pages/page-editor/page-editor.script-block.vue
@@ -59,14 +59,11 @@
 import Vue from 'vue';
 import { faPencilAlt, faPlug } from '@fortawesome/free-solid-svg-icons';
 import { v4 as uuid } from 'uuid';
-import i18n from '../../i18n';
 import XContainer from './page-editor.container.vue';
 import MkTextarea from '../../components/ui/textarea.vue';
 import { isLiteralBlock, funcDefs, blockDefs } from '../../scripts/hpml/index';
 
 export default Vue.extend({
-	i18n,
-
 	components: {
 		XContainer, MkTextarea
 	},
diff --git a/src/client/pages/page-editor/page-editor.vue b/src/client/pages/page-editor/page-editor.vue
index 4437c7716d4e1f8714547e507c66716835e5a2a8..2beb2df389ae5a3b4c4c38bd1a17cf64bac748b5 100644
--- a/src/client/pages/page-editor/page-editor.vue
+++ b/src/client/pages/page-editor/page-editor.vue
@@ -91,7 +91,6 @@ import PrismEditor from 'vue-prism-editor';
 import { faICursor, faPlus, faMagic, faCog, faCode, faExternalLinkSquareAlt } from '@fortawesome/free-solid-svg-icons';
 import { faSave, faStickyNote, faTrashAlt } from '@fortawesome/free-regular-svg-icons';
 import { v4 as uuid } from 'uuid';
-import i18n from '../../i18n';
 import XVariable from './page-editor.script-block.vue';
 import XBlocks from './page-editor.blocks.vue';
 import MkTextarea from '../../components/ui/textarea.vue';
@@ -107,8 +106,6 @@ import { collectPageVars } from '../../scripts/collect-page-vars';
 import { selectDriveFile } from '../../scripts/select-drive-file';
 
 export default Vue.extend({
-	i18n,
-
 	components: {
 		XDraggable, XVariable, XBlocks, MkTextarea, MkContainer, MkButton, MkSelect, MkSwitch, MkInput, PrismEditor
 	},
diff --git a/src/client/pages/pages.vue b/src/client/pages/pages.vue
index dd3d09db0872ea67ab709b9c2fa73a9b98f38187..a2617857157eeeeb6ee1838536ff3c2fb9082a3e 100644
--- a/src/client/pages/pages.vue
+++ b/src/client/pages/pages.vue
@@ -28,14 +28,12 @@
 import Vue from 'vue';
 import { faPlus, faEdit } from '@fortawesome/free-solid-svg-icons';
 import { faStickyNote, faHeart } from '@fortawesome/free-regular-svg-icons';
-import i18n from '../i18n';
 import MkPagePreview from '../components/page-preview.vue';
 import MkPagination from '../components/ui/pagination.vue';
 import MkButton from '../components/ui/button.vue';
 import MkContainer from '../components/ui/container.vue';
 
 export default Vue.extend({
-	i18n,
 	components: {
 		MkPagePreview, MkPagination, MkButton, MkContainer
 	},
diff --git a/src/client/pages/preferences/index.vue b/src/client/pages/preferences/index.vue
index 9f4bb679562dfe7a41fb10e4659f79105ad9549c..14d22bf02da6b8eda81b98694a0947dfa60659c6 100644
--- a/src/client/pages/preferences/index.vue
+++ b/src/client/pages/preferences/index.vue
@@ -99,8 +99,8 @@ import MkRadio from '../../components/ui/radio.vue';
 import MkRange from '../../components/ui/range.vue';
 import XTheme from './theme.vue';
 import XSidebar from './sidebar.vue';
-import i18n from '../../i18n';
 import { langs } from '../../config';
+import { clientDb, set } from '../../db';
 
 const sounds = [
 	null,
@@ -120,8 +120,6 @@ const sounds = [
 ];
 
 export default Vue.extend({
-	i18n,
-
 	metaInfo() {
 		return {
 			title: this.$t('settings') as string
@@ -228,9 +226,23 @@ export default Vue.extend({
 
 	watch: {
 		lang() {
+			const dialog = this.$root.dialog({
+				type: 'waiting',
+				iconOnly: true
+			});
+
 			localStorage.setItem('lang', this.lang);
-			localStorage.removeItem('locale');
-			location.reload();
+
+			return set('_version_', `changeLang-${(new Date()).toJSON()}`, clientDb.i18n)
+				.then(() => location.reload())
+				.catch(() => {
+					dialog.close();
+					this.$root.dialog({
+						type: 'error',
+						iconOnly: true,
+						autoClose: true
+					});
+				});
 		},
 
 		fontSize() {
diff --git a/src/client/pages/preferences/sidebar.vue b/src/client/pages/preferences/sidebar.vue
index 2dced10e7bb7968f8ecd0cc1a8acd95ea544dea3..34c9916cf51de2bf8c03d1ced64d5c76b7a73d61 100644
--- a/src/client/pages/preferences/sidebar.vue
+++ b/src/client/pages/preferences/sidebar.vue
@@ -19,12 +19,9 @@ import Vue from 'vue';
 import { faListUl, faSave, faRedo } from '@fortawesome/free-solid-svg-icons';
 import MkButton from '../../components/ui/button.vue';
 import MkTextarea from '../../components/ui/textarea.vue';
-import i18n from '../../i18n';
 import { defaultDeviceUserSettings } from '../../store';
 
 export default Vue.extend({
-	i18n,
-
 	components: {
 		MkButton,
 		MkTextarea,
diff --git a/src/client/pages/preferences/theme.vue b/src/client/pages/preferences/theme.vue
index f35b5d6ed8ac1092f116504760ff8993f0b87c1e..2111fa22483a65a023b94250fea08d799bc6dd33 100644
--- a/src/client/pages/preferences/theme.vue
+++ b/src/client/pages/preferences/theme.vue
@@ -87,14 +87,11 @@ import MkButton from '../../components/ui/button.vue';
 import MkSelect from '../../components/ui/select.vue';
 import MkSwitch from '../../components/ui/switch.vue';
 import MkTextarea from '../../components/ui/textarea.vue';
-import i18n from '../../i18n';
 import { Theme, builtinThemes, applyTheme, validateTheme } from '../../theme';
 import { selectFile } from '../../scripts/select-file';
 import { isDeviceDarkmode } from '../../scripts/is-device-darkmode';
 
 export default Vue.extend({
-	i18n,
-
 	components: {
 		MkInput,
 		MkButton,
diff --git a/src/client/pages/room/room.vue b/src/client/pages/room/room.vue
index 6ede771c568d410fdee43c2f27f99ab3b6722531..cf6850526f56acaa3335b66fda7bdf4dd9491d74 100644
--- a/src/client/pages/room/room.vue
+++ b/src/client/pages/room/room.vue
@@ -59,7 +59,6 @@
 
 <script lang="ts">
 import Vue from 'vue';
-import i18n from '../../i18n';
 import { Room } from '../../scripts/room/room';
 import parseAcct from '../../../misc/acct/parse';
 import XPreview from './preview.vue';
@@ -74,8 +73,6 @@ import { selectFile } from '../../scripts/select-file';
 let room: Room;
 
 export default Vue.extend({
-	i18n,
-
 	components: {
 		XPreview,
 		MkButton,
diff --git a/src/client/pages/scratchpad.vue b/src/client/pages/scratchpad.vue
index 622c40398f7a208a1809746cee0323eda6137727..81d4e6045993a1e33c44220988141246185b4d82 100644
--- a/src/client/pages/scratchpad.vue
+++ b/src/client/pages/scratchpad.vue
@@ -28,14 +28,11 @@ import "prismjs";
 import 'prismjs/themes/prism-okaidia.css';
 import PrismEditor from 'vue-prism-editor';
 import { AiScript, parse, utils, values } from '@syuilo/aiscript';
-import i18n from '../i18n';
 import MkContainer from '../components/ui/container.vue';
 import MkButton from '../components/ui/button.vue';
 import { createAiScriptEnv } from '../scripts/create-aiscript-env';
 
 export default Vue.extend({
-	i18n,
-
 	metaInfo() {
 		return {
 			title: this.$t('scratchpad') as string
diff --git a/src/client/pages/share.vue b/src/client/pages/share.vue
index 566650e309cc563c825acba802f585b756b7097a..153de76801d2a7040f5bdb2bd16ab7b127a45652 100644
--- a/src/client/pages/share.vue
+++ b/src/client/pages/share.vue
@@ -18,13 +18,10 @@
 <script lang="ts">
 import Vue from 'vue';
 import { faShareAlt } from '@fortawesome/free-solid-svg-icons';
-import i18n from '../i18n';
 import PostFormDialog from '../components/post-form-dialog.vue';
 import MkButton from '../components/ui/button.vue';
 
 export default Vue.extend({
-	i18n,
-
 	metaInfo() {
 		return {
 			title: this.$t('share') as string
diff --git a/src/client/pages/user/follow-list.vue b/src/client/pages/user/follow-list.vue
index 3da0b8359de9388a620d0eefeb733f30d8ad75eb..666e2d04fefcc8b374f6bd815a3c60463fce2b8e 100644
--- a/src/client/pages/user/follow-list.vue
+++ b/src/client/pages/user/follow-list.vue
@@ -19,13 +19,10 @@
 <script lang="ts">
 import Vue from 'vue';
 import parseAcct from '../../../misc/acct/parse';
-import i18n from '../../i18n';
 import MkFollowButton from '../../components/follow-button.vue';
 import MkPagination from '../../components/ui/pagination.vue';
 
 export default Vue.extend({
-	i18n,
-
 	components: {
 		MkPagination,
 		MkFollowButton,
diff --git a/src/client/pages/user/index.photos.vue b/src/client/pages/user/index.photos.vue
index 07b4db0a93cd8ecbf8818d93f96c9d5a816b7ab2..83a2618403a9b4a0cb2e90ab2bc77e4949580f51 100644
--- a/src/client/pages/user/index.photos.vue
+++ b/src/client/pages/user/index.photos.vue
@@ -14,11 +14,9 @@
 
 <script lang="ts">
 import Vue from 'vue';
-import i18n from '../../i18n';
 import { getStaticImageUrl } from '../../scripts/get-static-image-url';
 
 export default Vue.extend({
-	i18n,
 	props: ['user'],
 	data() {
 		return {
diff --git a/src/client/scripts/compose-notification.ts b/src/client/scripts/compose-notification.ts
index bf3255250676b94415f14ccf4b14dd45dbc63a29..29eb515bfe0700a2ce8aa50db288589173a7dbbb 100644
--- a/src/client/scripts/compose-notification.ts
+++ b/src/client/scripts/compose-notification.ts
@@ -1,58 +1,95 @@
 import getNoteSummary from '../../misc/get-note-summary';
 import getUserName from '../../misc/get-user-name';
+import { clientDb, get, bulkGet } from '../db';
+import { fromEntries } from '../../prelude/array';
 
-type Notification = {
-	title: string;
-	body: string;
-	icon: string;
-	onclick?: any;
-};
+const getTranslation = (text: string): Promise<string> => get(text, clientDb.i18n);
 
-// TODO: i18n
+export default async function(type, data): Promise<[string, NotificationOptions]> {
+	const contexts = ['deletedNote', 'invisibleNote', 'withNFiles', '_cw.poll'];
+	const locale = fromEntries(await bulkGet(contexts, clientDb.i18n) as [string, string][]);
 
-export default function(type, data): Notification {
 	switch (type) {
-		case 'driveFileCreated':
-			return {
-				title: 'File uploaded',
+		case 'driveFileCreated': // TODO (Server Side)
+			return [await getTranslation('_notification.fileUploaded'), {
 				body: data.name,
 				icon: data.url
-			};
-
+			}];
 		case 'notification':
 			switch (data.type) {
 				case 'mention':
-					return {
-						title: `${getUserName(data.user)}:`,
-						body: getNoteSummary(data),
+					return [(await getTranslation('_notification.youGotMention')).replace('{name}', getUserName(data.user)), {
+						body: getNoteSummary(data.note, locale),
 						icon: data.user.avatarUrl
-					};
+					}];
 
 				case 'reply':
-					return {
-						title: `You got reply from ${getUserName(data.user)}:`,
-						body: getNoteSummary(data),
+					return [(await getTranslation('_notification.youGotReply')).replace('{name}', getUserName(data.user)), {
+						body: getNoteSummary(data.note, locale),
+						icon: data.user.avatarUrl
+					}];
+
+				case 'renote':
+					return [(await getTranslation('_notification.youRenoted')).replace('{name}', getUserName(data.user)), {
+						body: getNoteSummary(data.note, locale),
 						icon: data.user.avatarUrl
-					};
+					}];
 
 				case 'quote':
-					return {
-						title: `${getUserName(data.user)}:`,
-						body: getNoteSummary(data),
+					return [(await getTranslation('_notification.youGotQuote')).replace('{name}', getUserName(data.user)), {
+						body: getNoteSummary(data.note, locale),
 						icon: data.user.avatarUrl
-					};
+					}];
 
 				case 'reaction':
-					return {
-						title: `${getUserName(data.user)}: ${data.reaction}:`,
-						body: getNoteSummary(data.note),
+					return [`${data.reaction} ${getUserName(data.user)}`, {
+						body: getNoteSummary(data.note, locale),
+						icon: data.user.avatarUrl
+					}];
+
+				case 'pollVote':
+					return [(await getTranslation('_notification.youGotPoll')).replace('{name}', getUserName(data.user)), {
+						body: getNoteSummary(data.note, locale),
+						icon: data.user.avatarUrl
+					}];
+
+				case 'follow':
+					return [await getTranslation('_notification.youWereFollowed'), {
+						body: getUserName(data.user),
 						icon: data.user.avatarUrl
-					};
+					}];
+
+				case 'receiveFollowRequest':
+					return [await getTranslation('_notification.youReceivedFollowRequest'), {
+						body: getUserName(data.user),
+						icon: data.user.avatarUrl
+					}];
+
+				case 'followRequestAccepted':
+					return [await getTranslation('_notification.yourFollowRequestAccepted'), {
+						body: getUserName(data.user),
+						icon: data.user.avatarUrl
+					}];
+
+				case 'groupInvited':
+					return [await getTranslation('_notification.youWereInvitedToGroup'), {
+						body: data.group.name
+					}];
 
 				default:
 					return null;
 			}
-
+		case 'unreadMessagingMessage':
+			if (data.groupId === null) {
+				return [(await getTranslation('_notification.youGotMessagingMessageFromUser')).replace('{name}', getUserName(data.user)), {
+					icon: data.user.avatarUrl,
+					tag: `messaging:user:${data.user.id}`
+				}];
+			}
+			return [(await getTranslation('_notification.youGotMessagingMessageFromGroup')).replace('{name}', data.group.name), {
+				icon: data.user.avatarUrl,
+				tag: `messaging:group:${data.group.id}`
+			}];
 		default:
 			return null;
 	}
diff --git a/src/client/scripts/set-i18n-contexts.ts b/src/client/scripts/set-i18n-contexts.ts
new file mode 100644
index 0000000000000000000000000000000000000000..2eb76047f1e17e487862ddea2aa7278c07a87fa5
--- /dev/null
+++ b/src/client/scripts/set-i18n-contexts.ts
@@ -0,0 +1,18 @@
+import VueI18n from 'vue-i18n';
+import { clientDb, clear, bulkSet } from '../db';
+import { deepEntries, delimitEntry } from 'deep-entries';
+import { fromEntries } from '../../prelude/array';
+
+export function setI18nContexts(lang: string, version: string, i18n: VueI18n, cleardb = false) {
+	return Promise.all([
+		cleardb ? clear(clientDb.i18n) : Promise.resolve(),
+		fetch(`/assets/locales/${lang}.${version}.json`)
+	])
+	.then(([, response]) => response.json())
+	.then(locale => {
+		const flatLocaleEntries = deepEntries(locale, delimitEntry) as [string, string][];
+		bulkSet(flatLocaleEntries, clientDb.i18n);
+		i18n.locale = lang;
+		i18n.setLocaleMessage(lang, fromEntries(flatLocaleEntries));
+	});
+}
diff --git a/src/client/sw.js b/src/client/sw.ts
similarity index 88%
rename from src/client/sw.js
rename to src/client/sw.ts
index 68e43429ac04be6b4ec319aeb8430f4a69c96db6..341198852e4015838ddb61d60923b4a8604325ab 100644
--- a/src/client/sw.js
+++ b/src/client/sw.ts
@@ -1,6 +1,7 @@
 /**
  * Service Worker
  */
+declare var self: ServiceWorkerGlobalScope;
 
 import composeNotification from './scripts/compose-notification';
 
@@ -14,7 +15,7 @@ const apiUrl = `${location.origin}/api/`;
 self.addEventListener('install', ev => {
 	console.info('installed');
 
-  ev.waitUntil(
+	ev.waitUntil(
 		caches.open(cacheName)
 			.then(cache => {
 				return cache.addAll([
@@ -22,7 +23,7 @@ self.addEventListener('install', ev => {
 				]);
 			})
 			.then(() => self.skipWaiting())
-  );
+	);
 });
 
 self.addEventListener('activate', ev => {
@@ -55,16 +56,12 @@ self.addEventListener('push', ev => {
 	// クライアント取得
 	ev.waitUntil(self.clients.matchAll({
 		includeUncontrolled: true
-	}).then(clients => {
+	}).then(async clients => {
 		// クライアントがあったらストリームに接続しているということなので通知しない
 		if (clients.length != 0) return;
 
 		const { type, body } = ev.data.json();
 
-		const n = composeNotification(type, body);
-		return self.registration.showNotification(n.title, {
-			body: n.body,
-			icon: n.icon,
-		});
+		return self.registration.showNotification(...(await composeNotification(type, body)));
 	}));
 });
diff --git a/src/client/tsconfig.json b/src/client/tsconfig.json
index 3ec0271f63af9da39401a1910484dff4bbfeba7e..aac0d1bfe7a30a01776c8c0bbdd3f81fb0c35261 100644
--- a/src/client/tsconfig.json
+++ b/src/client/tsconfig.json
@@ -21,6 +21,11 @@
     "typeRoots": [
       "node_modules/@types",
       "src/@types"
+    ],
+    "lib": [
+      "esnext",
+      "dom",
+      "webworker"
     ]
   },
   "compileOnSave": false,
diff --git a/src/client/widgets/activity.chart.vue b/src/client/widgets/activity.chart.vue
index 0278e02ae751f7dfcab1c543e4fc56e36c164f17..2b70493552ea78810db5059f7fd59090ff0462b8 100644
--- a/src/client/widgets/activity.chart.vue
+++ b/src/client/widgets/activity.chart.vue
@@ -26,7 +26,6 @@
 
 <script lang="ts">
 import Vue from 'vue';
-import i18n from '../i18n';
 
 function dragListen(fn) {
 	window.addEventListener('mousemove',  fn);
@@ -41,7 +40,6 @@ function dragClear(fn) {
 }
 
 export default Vue.extend({
-	i18n,
 	props: ['data'],
 	data() {
 		return {
diff --git a/src/client/widgets/activity.vue b/src/client/widgets/activity.vue
index 6c32642bb675837ea24651ac1a4a7bb61064e651..4fdd81ae52e06da8cc7a4b383f692011406af8e5 100644
--- a/src/client/widgets/activity.vue
+++ b/src/client/widgets/activity.vue
@@ -19,7 +19,6 @@
 import { faChartBar, faSort } from '@fortawesome/free-solid-svg-icons';
 import MkContainer from '../components/ui/container.vue';
 import define from './define';
-import i18n from '../i18n';
 import XCalendar from './activity.calendar.vue';
 import XChart from './activity.chart.vue';
 
@@ -30,7 +29,6 @@ export default define({
 		view: 0
 	})
 }).extend({
-	i18n,
 	components: {
 		MkContainer,
 		XCalendar,
diff --git a/src/client/widgets/calendar.vue b/src/client/widgets/calendar.vue
index c041734a4dabfb20ccca565b207352bdb8ca796a..328e6bc62f0307e110c9f8d9ddf215a508d482c9 100644
--- a/src/client/widgets/calendar.vue
+++ b/src/client/widgets/calendar.vue
@@ -33,7 +33,6 @@
 
 <script lang="ts">
 import define from './define';
-import i18n from '../i18n';
 
 export default define({
 	name: 'calendar',
@@ -41,7 +40,6 @@ export default define({
 		design: 0
 	})
 }).extend({
-	i18n,
 	data() {
 		return {
 			now: new Date(),
diff --git a/src/client/widgets/memo.vue b/src/client/widgets/memo.vue
index 3c170adc4ee774f6315766dca37aeab25565fc8a..cdc716b9fadd082d50b837c347f9a73092b27bf6 100644
--- a/src/client/widgets/memo.vue
+++ b/src/client/widgets/memo.vue
@@ -15,7 +15,6 @@
 import { faStickyNote } from '@fortawesome/free-solid-svg-icons';
 import MkContainer from '../components/ui/container.vue';
 import define from './define';
-import i18n from '../i18n';
 
 export default define({
 	name: 'memo',
@@ -23,7 +22,6 @@ export default define({
 		compact: false
 	})
 }).extend({
-	i18n,
 	
 	components: {
 		MkContainer
diff --git a/src/client/widgets/notifications.vue b/src/client/widgets/notifications.vue
index 9c1bddb2ee29bf2cbf37c6fc7e4990411c38c21b..39fc8a9361cc3c1dcbcb0eaabe5dafa57d9e38e1 100644
--- a/src/client/widgets/notifications.vue
+++ b/src/client/widgets/notifications.vue
@@ -15,7 +15,6 @@ import { faBell } from '@fortawesome/free-solid-svg-icons';
 import MkContainer from '../components/ui/container.vue';
 import XNotifications from '../components/notifications.vue';
 import define from './define';
-import i18n from '../i18n';
 
 const basisSteps = [25, 50, 75, 100]
 const previewHeights = [200, 300, 400, 500]
@@ -27,7 +26,6 @@ export default define({
 		basisStep: 0
 	})
 }).extend({
-	i18n,
 	
 	components: {
 		MkContainer,
diff --git a/src/client/widgets/photos.vue b/src/client/widgets/photos.vue
index 1deb6de62dbf60df030a69867f549331f8985d7a..6e4e43a56507c13e02c175c566f87f9fc1eb191c 100644
--- a/src/client/widgets/photos.vue
+++ b/src/client/widgets/photos.vue
@@ -20,7 +20,6 @@
 import { faCamera } from '@fortawesome/free-solid-svg-icons';
 import MkContainer from '../components/ui/container.vue';
 import define from './define';
-import i18n from '../i18n';
 import { getStaticImageUrl } from '../scripts/get-static-image-url';
 
 export default define({
@@ -29,7 +28,6 @@ export default define({
 		design: 0,
 	})
 }).extend({
-	i18n,
 	components: {
 		MkContainer,
 	},
diff --git a/src/client/widgets/rss.vue b/src/client/widgets/rss.vue
index 61c1e23b6e1b32b9506685144470e3b47220c7c6..4e57281e9ff212ae4a275c38a55a54576e658020 100644
--- a/src/client/widgets/rss.vue
+++ b/src/client/widgets/rss.vue
@@ -18,7 +18,6 @@
 import { faRssSquare, faCog } from '@fortawesome/free-solid-svg-icons';
 import MkContainer from '../components/ui/container.vue';
 import define from './define';
-import i18n from '../i18n';
 
 export default define({
 	name: 'rss',
@@ -27,7 +26,6 @@ export default define({
 		url: 'http://feeds.afpbb.com/rss/afpbb/afpbbnews'
 	})
 }).extend({
-	i18n,
 	components: {
 		MkContainer
 	},
diff --git a/src/client/widgets/timeline.vue b/src/client/widgets/timeline.vue
index 55f78f985f24752d1a832cb70b7b4802a101d650..633131182802d4c6f9d30adbe74e9dfe2d2f0209 100644
--- a/src/client/widgets/timeline.vue
+++ b/src/client/widgets/timeline.vue
@@ -27,7 +27,6 @@ import { faComments } from '@fortawesome/free-regular-svg-icons';
 import MkContainer from '../components/ui/container.vue';
 import XTimeline from '../components/timeline.vue';
 import define from './define';
-import i18n from '../i18n';
 
 const basisSteps = [25, 50, 75, 100]
 const previewHeights = [200, 300, 400, 500]
@@ -41,7 +40,6 @@ export default define({
 		basisStep: 0
 	})
 }).extend({
-	i18n,
 	
 	components: {
 		MkContainer,
diff --git a/src/client/widgets/trends.vue b/src/client/widgets/trends.vue
index 690383d1f9607a39137ca292cb46d3581a84448e..61f5bfbd32528d8fb3b7ba87507d86cc3205c569 100644
--- a/src/client/widgets/trends.vue
+++ b/src/client/widgets/trends.vue
@@ -23,7 +23,6 @@
 import { faHashtag } from '@fortawesome/free-solid-svg-icons';
 import MkContainer from '../components/ui/container.vue';
 import define from './define';
-import i18n from '../i18n';
 import XChart from './trends.chart.vue';
 
 export default define({
@@ -32,7 +31,6 @@ export default define({
 		compact: false
 	})
 }).extend({
-	i18n,
 	components: {
 		MkContainer, XChart
 	},
diff --git a/src/misc/get-note-summary.ts b/src/misc/get-note-summary.ts
index e3458cb189e3fc191d7cc6bac996e923ce475e18..c23306ab1125a608b0532405c66f2d33ccfaddc9 100644
--- a/src/misc/get-note-summary.ts
+++ b/src/misc/get-note-summary.ts
@@ -2,13 +2,13 @@
  * 投稿を表す文字列を取得します。
  * @param {*} note (packされた)投稿
  */
-const summarize = (note: any): string => {
+const summarize = (note: any, locale: any): string => {
 	if (note.deletedAt) {
-		return '(削除された投稿)';
+		return `(${locale['deletedNote']})`;
 	}
 
 	if (note.isHidden) {
-		return '(非公開の投稿)';
+		return `(${locale['invisibleNote']})`;
 	}
 
 	let summary = '';
@@ -22,18 +22,18 @@ const summarize = (note: any): string => {
 
 	// ファイルが添付されているとき
 	if ((note.files || []).length != 0) {
-		summary += ` (${note.files.length}つのファイル)`;
+		summary += ` (${locale['withNFiles'].replace('{n}', note.files.length)})`;
 	}
 
 	// 投票が添付されているとき
 	if (note.poll) {
-		summary += ' (投票)';
+		summary += ` (${locale._cw?.poll || locale['_cw.poll']})`;
 	}
 
 	// 返信のとき
 	if (note.replyId) {
 		if (note.reply) {
-			summary += `\n\nRE: ${summarize(note.reply)}`;
+			summary += `\n\nRE: ${summarize(note.reply, locale)}`;
 		} else {
 			summary += '\n\nRE: ...';
 		}
@@ -42,7 +42,7 @@ const summarize = (note: any): string => {
 	// Renoteのとき
 	if (note.renoteId) {
 		if (note.renote) {
-			summary += `\n\nRN: ${summarize(note.renote)}`;
+			summary += `\n\nRN: ${summarize(note.renote, locale)}`;
 		} else {
 			summary += '\n\nRN: ...';
 		}
diff --git a/src/prelude/array.ts b/src/prelude/array.ts
index f4d684d57430eb15c7f7dea87c7e7a72fbcad576..9e1dfead53564906472c377d47a286aedfff726d 100644
--- a/src/prelude/array.ts
+++ b/src/prelude/array.ts
@@ -130,7 +130,17 @@ export function cumulativeSum(xs: number[]): number[] {
 }
 
 // Object.fromEntries()
-export function fromEntries(xs: [string, any][]): { [x: string]: any; } {
+export function fromEntries<T extends readonly (readonly [PropertyKey, any])[]>(xs: T):
+	T[number] extends infer U
+		?
+			(
+				U extends readonly any[]
+					? (x: { [_ in U[0]]: U[1] }) => any
+					: never
+			) extends (x: infer V) => any
+				? V
+				: never
+		: never {
 	return xs.reduce((obj, [k, v]) => Object.assign(obj, { [k]: v }), {} as { [x: string]: any; });
 }
 
diff --git a/src/server/web/index.ts b/src/server/web/index.ts
index 3da86944d798a0b486f18e3b44f39ad4f10e6156..5bb052a693fe3cec568f26dadc15b4874595ca5e 100644
--- a/src/server/web/index.ts
+++ b/src/server/web/index.ts
@@ -245,7 +245,8 @@ router.get('/notes/:note', async ctx => {
 		const meta = await fetchMeta();
 		await ctx.render('note', {
 			note: _note,
-			summary: getNoteSummary(_note),
+			// TODO: Let locale changeable by instance setting
+			summary: getNoteSummary(_note, locales['ja-JP']),
 			instanceName: meta.name || 'Misskey',
 			icon: meta.iconUrl
 		});
diff --git a/src/server/web/views/flush.pug b/src/server/web/views/flush.pug
index f279c236051a16ddfc15e7935783f4f7b962d08a..59fed1f15de77944725eb5cacff55cfd2b681beb 100644
--- a/src/server/web/views/flush.pug
+++ b/src/server/web/views/flush.pug
@@ -1,20 +1,38 @@
 doctype html
 
 html
+	#msg
 	script.
-		localStorage.removeItem('locale');
+		const msg = document.getElementById('msg');
 
 		try {
-			navigator.serviceWorker.controller.postMessage('clear');
+			localStorage.clear();
+			message('localStorage cleared');
 
-			navigator.serviceWorker.getRegistrations().then(registrations => {
-				return Promise.all(registrations.map(registration => registration.unregister()));
-			}).then(() => {
-				location = '/';
-			});
+			const delidb = indexedDB.deleteDatabase('MisskeyClient');
+			delidb.onsuccess = () => message('indexedDB cleared');
+
+			if (navigator.serviceWorker.controller) {
+				navigator.serviceWorker.controller.postMessage('clear');
+				navigator.serviceWorker.getRegistrations()
+					.then(registrations => {
+						return Promise.all(registrations.map(registration => registration.unregister()));
+					})
+					.then(() => {
+						message('Success Flush! Please reopen Misskey.\n成功しました。Misskeyを開き直してください。');
+					})
+					.catch(e => { throw Error(e) });
+			} else {
+				message('Success Flush! Please reopen Misskey.\n成功しました。Misskeyを開き直してください。');
+			}
 		} catch (e) {
 			console.error(e);
+			message(`${e}¥n¥nFlush Failed. Please reopen Misskey.\n失敗しました。Misskeyを開き直してください。`);
 			setTimeout(() => {
 				location = '/';
 			}, 10000)
 		}
+
+		function message(text) {
+			msg.insertAdjacentHTML('beforeend', `<p>[${(new Date()).toString()}] ${text.replace(/Â¥n/g,'<br>')}</p>`)
+		}
diff --git a/src/services/push-notification.ts b/src/services/push-notification.ts
index f0d9c4e22ccb7edd6f3e1dfec7d9966cc172be66..d0a0c04d62f0eeff73d0577a3ae9e3f74750906c 100644
--- a/src/services/push-notification.ts
+++ b/src/services/push-notification.ts
@@ -2,8 +2,13 @@ import * as push from 'web-push';
 import config from '../config';
 import { SwSubscriptions } from '../models';
 import { fetchMeta } from '../misc/fetch-meta';
+import { PackedNotification } from '../models/repositories/notification';
+import { PackedMessagingMessage } from '../models/repositories/messaging-message';
 
-export default async function(userId: string, type: string, body?: any) {
+type notificationType = 'notification' | 'unreadMessagingMessage';
+type notificationBody = PackedNotification | PackedMessagingMessage;
+
+export default async function(userId: string, type: notificationType, body: notificationBody) {
 	const meta = await fetchMeta();
 
 	if (!meta.enableServiceWorker || meta.swPublicKey == null || meta.swPrivateKey == null) return;
diff --git a/webpack.config.ts b/webpack.config.ts
index 64cf4c858151db69360d90bfcbe9843b14b5d66a..fa364e603af94a9f4fbe404d7c09bd190a70cc08 100644
--- a/webpack.config.ts
+++ b/webpack.config.ts
@@ -34,7 +34,7 @@ const postcss = {
 module.exports = {
 	entry: {
 		app: './src/client/init.ts',
-		sw: './src/client/sw.js'
+		sw: './src/client/sw.ts'
 	},
 	module: {
 		rules: [{
diff --git a/yarn.lock b/yarn.lock
index c10ddb19c2176c85150101fa5fb1808e7140d525..4ea29f672567ee6f431d0bd17f89712ade32c29d 100644
--- a/yarn.lock
+++ b/yarn.lock
@@ -2819,6 +2819,11 @@ decompress-response@^4.2.0:
   dependencies:
     mimic-response "^2.0.0"
 
+deep-entries@3.1.0:
+  version "3.1.0"
+  resolved "https://registry.yarnpkg.com/deep-entries/-/deep-entries-3.1.0.tgz#e456aa791d01b045641c75e41e170c0c95a9d472"
+  integrity sha512-pCpcCqx/hclnT2e4mMlM9geG8XIaxWN+yNKJHHwu1FZyYKErKU/fPztYYSk2HwnqRPf55cDEXraV6MLv8I5FrA==
+
 deep-eql@^3.0.1:
   version "3.0.1"
   resolved "https://registry.yarnpkg.com/deep-eql/-/deep-eql-3.0.1.tgz#dfc9404400ad1c8fe023e7da1df1c147c4b444df"
@@ -4488,6 +4493,11 @@ icss-utils@^4.0.0, icss-utils@^4.1.1:
   dependencies:
     postcss "^7.0.14"
 
+idb-keyval@3.2.0:
+  version "3.2.0"
+  resolved "https://registry.yarnpkg.com/idb-keyval/-/idb-keyval-3.2.0.tgz#cbbf354deb5684b6cdc84376294fc05932845bd6"
+  integrity sha512-slx8Q6oywCCSfKgPgL0sEsXtPVnSbTLWpyiDcu6msHOyKOLari1TD1qocXVCft80umnkk3/Qqh3lwoFt8T/BPQ==
+
 ieee754@1.1.13, ieee754@^1.1.13, ieee754@^1.1.4:
   version "1.1.13"
   resolved "https://registry.yarnpkg.com/ieee754/-/ieee754-1.1.13.tgz#ec168558e95aa181fd87d37f55c32bbcb6708b84"