From 08a2162aa71cf9369d2797c3a19f1ff41b950d32 Mon Sep 17 00:00:00 2001
From: tamaina <tamaina@hotmail.co.jp>
Date: Sat, 23 May 2020 13:19:31 +0900
Subject: [PATCH] =?UTF-8?q?feat(client):=20=E7=BF=BB=E8=A8=B3=E3=82=92Inde?=
 =?UTF-8?q?xedDB=E3=81=AB=E4=BF=9D=E5=AD=98=E3=83=BB=E3=83=97=E3=83=83?=
 =?UTF-8?q?=E3=82=B7=E3=83=A5=E9=80=9A=E7=9F=A5=E3=82=92=E7=BF=BB=E8=A8=B3?=
 =?UTF-8?q?=20(#6396)?=
MIME-Version: 1.0
Content-Type: text/plain; charset=UTF-8
Content-Transfer-Encoding: 8bit

* wip

* tabun ok

* better msg

* oops

* fix lint

* Update gulpfile.ts

Co-authored-by: Acid Chicken (硫酸鶏) <root@acid-chicken.com>

* Update src/client/scripts/set-i18n-contexts.ts

Co-authored-by: Acid Chicken (硫酸鶏) <root@acid-chicken.com>

* refactor

Co-authored-by: acid-chicken <root@acid-chicken.com>

* ✨

* wip

* fix lint

* たぶんおk

* fix flush

* Translate Notification

* remove console.log

* fix

* add notifications

* remove san

* wip

* ok

* :v:

* Update src/prelude/array.ts

Co-authored-by: Acid Chicken (硫酸鶏) <root@acid-chicken.com>

* wip

* i18n refactor

* Update init.ts

* :v:

Co-authored-by: Acid Chicken (硫酸鶏) <root@acid-chicken.com>
Co-authored-by: syuilo <syuilotan@yahoo.co.jp>
---
 gulpfile.ts                                   |  6 +-
 locales/ja-JP.yml                             | 16 +++
 package.json                                  |  2 +
 src/client/app.vue                            |  3 -
 src/client/components/captcha.vue             |  2 -
 src/client/components/cw-button.vue           |  3 -
 src/client/components/date-separated-list.vue |  3 -
 src/client/components/dialog.vue              |  3 -
 src/client/components/drive-window.vue        |  3 -
 src/client/components/drive.file.vue          |  3 -
 src/client/components/drive.folder.vue        |  3 -
 src/client/components/drive.nav-folder.vue    |  3 -
 src/client/components/drive.vue               |  3 -
 src/client/components/emoji-picker.vue        |  3 -
 src/client/components/error.vue               |  2 -
 src/client/components/follow-button.vue       |  3 -
 src/client/components/google.vue              |  2 -
 src/client/components/image-viewer.vue        |  3 -
 src/client/components/instance-stats.vue      |  3 -
 src/client/components/media-banner.vue        |  2 -
 src/client/components/media-image.vue         |  2 -
 src/client/components/media-video.vue         |  2 -
 src/client/components/mention.vue             |  2 -
 src/client/components/note.vue                |  2 -
 src/client/components/notes.vue               |  3 -
 src/client/components/notification.vue        |  6 +-
 src/client/components/notifications.vue       |  3 -
 src/client/components/page/page.post.vue      |  2 -
 src/client/components/page/page.vue           |  3 -
 src/client/components/poll-editor.vue         |  2 -
 src/client/components/poll.vue                |  2 -
 src/client/components/post-form-attaches.vue  |  3 -
 src/client/components/post-form.vue           |  3 -
 src/client/components/reaction-icon.vue       |  2 -
 src/client/components/reaction-picker.vue     |  3 -
 .../components/reactions-viewer.details.vue   |  2 -
 src/client/components/remote-caution.vue      |  2 -
 src/client/components/signin-dialog.vue       |  3 -
 src/client/components/signin.vue              |  3 -
 src/client/components/signup-dialog.vue       |  3 -
 src/client/components/signup.vue              |  3 -
 src/client/components/stream-indicator.vue    |  2 -
 src/client/components/sub-note-content.vue    |  2 -
 src/client/components/time.vue                |  2 -
 src/client/components/uploader.vue            |  2 -
 src/client/components/url-preview-popup.vue   |  3 -
 src/client/components/url-preview.vue         |  3 -
 src/client/components/user-list.vue           |  3 -
 src/client/components/user-menu.vue           |  3 -
 src/client/components/user-preview.vue        |  3 -
 src/client/components/user-select.vue         |  3 -
 src/client/components/users-dialog.vue        |  3 -
 src/client/components/visibility-chooser.vue  |  2 -
 src/client/components/window.vue              |  3 -
 src/client/config.ts                          |  5 +-
 src/client/db.ts                              | 68 +++++++++++++
 src/client/i18n.ts                            | 12 ---
 src/client/init.ts                            | 44 ++++-----
 src/client/pages/about-misskey.vue            |  3 -
 src/client/pages/about.vue                    |  3 -
 src/client/pages/announcements.vue            |  3 -
 src/client/pages/auth.form.vue                |  2 -
 src/client/pages/auth.vue                     |  2 -
 src/client/pages/doc.vue                      |  3 -
 src/client/pages/explore.vue                  |  3 -
 src/client/pages/follow.vue                   |  3 -
 src/client/pages/index.welcome.entrance.vue   |  3 -
 src/client/pages/index.welcome.setup.vue      |  2 -
 src/client/pages/instance/announcements.vue   |  3 -
 .../pages/instance/federation.instance.vue    |  3 -
 src/client/pages/instance/federation.vue      |  3 -
 src/client/pages/instance/index.vue           |  3 -
 src/client/pages/instance/queue.queue.vue     |  3 -
 src/client/pages/instance/queue.vue           |  3 -
 src/client/pages/instance/relays.vue          |  3 -
 src/client/pages/instance/settings.vue        |  3 -
 src/client/pages/instance/users.user.vue      |  3 -
 src/client/pages/messaging/index.vue          |  3 -
 .../pages/messaging/messaging-room.form.vue   |  2 -
 .../messaging/messaging-room.message.vue      |  2 -
 src/client/pages/messaging/messaging-room.vue |  3 -
 src/client/pages/miauth.vue                   |  2 -
 .../pages/my-antennas/index.antenna.vue       |  3 -
 src/client/pages/my-groups/group.vue          |  3 -
 src/client/pages/my-lists/list.vue            |  3 -
 src/client/pages/my-settings/2fa.vue          |  2 -
 src/client/pages/my-settings/api.vue          |  2 -
 src/client/pages/my-settings/drive.vue        |  3 -
 .../pages/my-settings/import-export.vue       |  3 -
 src/client/pages/my-settings/integration.vue  |  3 -
 src/client/pages/my-settings/mute-block.vue   |  3 -
 src/client/pages/my-settings/privacy.vue      |  3 -
 src/client/pages/my-settings/profile.vue      |  3 -
 src/client/pages/my-settings/reaction.vue     |  3 -
 src/client/pages/my-settings/security.vue     |  3 -
 src/client/pages/not-found.vue                |  3 -
 src/client/pages/note.vue                     |  2 -
 .../page-editor/els/page-editor.el.button.vue |  3 -
 .../page-editor/els/page-editor.el.canvas.vue |  3 -
 .../els/page-editor.el.counter.vue            |  3 -
 .../page-editor/els/page-editor.el.if.vue     |  3 -
 .../page-editor/els/page-editor.el.image.vue  |  3 -
 .../els/page-editor.el.number-input.vue       |  3 -
 .../page-editor/els/page-editor.el.post.vue   |  3 -
 .../els/page-editor.el.radio-button.vue       |  2 -
 .../els/page-editor.el.section.vue            |  3 -
 .../page-editor/els/page-editor.el.switch.vue |  3 -
 .../els/page-editor.el.text-input.vue         |  3 -
 .../page-editor/els/page-editor.el.text.vue   |  3 -
 .../els/page-editor.el.textarea-input.vue     |  3 -
 .../els/page-editor.el.textarea.vue           |  3 -
 .../page-editor/page-editor.container.vue     |  3 -
 .../page-editor/page-editor.script-block.vue  |  3 -
 src/client/pages/page-editor/page-editor.vue  |  3 -
 src/client/pages/pages.vue                    |  2 -
 src/client/pages/preferences/index.vue        | 22 ++++-
 src/client/pages/preferences/sidebar.vue      |  3 -
 src/client/pages/preferences/theme.vue        |  3 -
 src/client/pages/room/room.vue                |  3 -
 src/client/pages/scratchpad.vue               |  3 -
 src/client/pages/share.vue                    |  3 -
 src/client/pages/user/follow-list.vue         |  3 -
 src/client/pages/user/index.photos.vue        |  2 -
 src/client/scripts/compose-notification.ts    | 97 +++++++++++++------
 src/client/scripts/set-i18n-contexts.ts       | 18 ++++
 src/client/{sw.js => sw.ts}                   | 13 +--
 src/client/tsconfig.json                      |  5 +
 src/client/widgets/activity.chart.vue         |  2 -
 src/client/widgets/activity.vue               |  2 -
 src/client/widgets/calendar.vue               |  2 -
 src/client/widgets/memo.vue                   |  2 -
 src/client/widgets/notifications.vue          |  2 -
 src/client/widgets/photos.vue                 |  2 -
 src/client/widgets/rss.vue                    |  2 -
 src/client/widgets/timeline.vue               |  2 -
 src/client/widgets/trends.vue                 |  2 -
 src/misc/get-note-summary.ts                  | 14 +--
 src/prelude/array.ts                          | 12 ++-
 src/server/web/index.ts                       |  3 +-
 src/server/web/views/flush.pug                | 32 ++++--
 src/services/push-notification.ts             |  7 +-
 webpack.config.ts                             |  2 +-
 yarn.lock                                     | 10 ++
 143 files changed, 290 insertions(+), 433 deletions(-)
 create mode 100644 src/client/db.ts
 delete mode 100644 src/client/i18n.ts
 create mode 100644 src/client/scripts/set-i18n-contexts.ts
 rename src/client/{sw.js => sw.ts} (88%)

diff --git a/gulpfile.ts b/gulpfile.ts
index 262c0a503..880adb51d 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 ab293eb89..3c7dc6640 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 15c1478db..d34394d13 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 5e7396205..8e192d463 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 6b1ee6f0b..1a894d935 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 4516e5210..07a44d970 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 b80c6494e..a27e9a05a 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 da8e54684..58115b47a 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 d63881c0e..c42cb6661 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 a547abf9a..1b24c61df 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 b778acc77..9e8065319 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 0689faecd..9e805a5e9 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 08c7097a8..65eb1cb81 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 868a6125c..7871b438c 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 dd9de43c1..fea81305e 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 23cb0cd94..7967c0e15 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 01dcf24bf..de96cbd16 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 3359b600d..c78112b98 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 378e9ce39..552e3523f 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 088c11fab..0f746d434 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 6c33b657f..6d1b5345d 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 d9b4415cb..a5e06bfaa 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 06dcf1288..8c939f839 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 fd895ad5a..6e513a4b2 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 0cf4dee2d..515bc58e2 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 d3ebc8f17..de233d14a 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 ecf526898..3ed198a04 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 6f79374f3..da5bc8bfa 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 e3b04d7fd..b3cc01ec2 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 91c7dab59..0687e999b 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 c748b6b09..e0c42cd00 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 d9c065361..2415bf28e 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 05faea514..cdb61f51d 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 3c6d56b80..fe2b52836 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 99b27ad9c..e331410c3 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 ea2523a11..67c8b261b 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 95b37d305..21af9f766 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 a356c3ccd..98b75e627 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 dc73ad8a0..a7653b17b 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 4db79af51..eff1f79c4 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 acb6a745a..ff1932b42 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 dd7a5d07c..ec00f4cbf 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 e60c19744..a14c832ea 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 6d092cf4f..2a871d6d8 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 4ceb5e287..6ebdf123b 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 acd9b1aa9..52731296c 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 c2dd0038b..d77cfafd1 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 bde3af690..7a9cd58a4 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 a2275197d..25937fb3c 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 89150eaac..8c8eee2a3 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 a82626652..9b4a68ddb 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 9d0c5e425..0e0cc36c2 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 dc7b41e28..0f7e37a08 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 0b2ba248b..db1398518 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 0d4a96964..f71647a05 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 000000000..3000a0c96
--- /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 05d319fba..000000000
--- 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 500092061..e2772502f 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 84cd5d5e9..2c4a257b1 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 a3a4b6ac7..25fb0ca13 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 5c6d4f58a..089475ed6 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 e6f61f52f..c5a9b769a 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 e025924fe..5c40842da 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 7c4f7ebcc..e4c4ef5c6 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 7ff4b5ed6..39904846c 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 d76525973..8659763bb 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 a9343e87c..9bb2e85fc 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 6d08f5b5d..9a66a4dff 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 2889cf8cc..0e11e2932 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 08f4d1b4f..6b6352a15 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 5babc6045..77819235d 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 1d90aa553..d21f8d455 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 7f0fc7d2b..1649d1e17 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 c4892e88d..7a2204e51 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 9b523bd0e..dd18867b6 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 afd6d4cc6..0436e8780 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 1fb064f7f..25f026063 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 7a55004cb..7ee782c4a 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 0cd3dfcc8..be47efd2c 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 67756572f..58e1e54ad 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 317ad087f..e97d5532a 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 0e170af11..15cde8bc2 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 2a9aebbcb..6435e4fc9 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 c8170a2a5..0132bc2c3 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 cf85a80cc..7052c5516 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 6ceca21fe..58ba03c41 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 f394c826d..79b459fb5 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 c3d2d1dc2..7612c5011 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 479574118..cc148d48d 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 3dd7783f1..2d6e57e22 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 03cf4aacc..8eb43a6e2 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 7ac9062d8..527ac9ea3 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 b168c89ec..16bba7a27 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 68c481707..ef4f6f672 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 ba670b2f6..dc77ca12c 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 9608e0778..7f4c46c23 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 37c66833e..48629a4eb 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 9ca9fe06f..982120166 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 497731891..a49920780 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 d9a4dddde..f439f3e6f 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 0449b9cf2..53cb9e2ae 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 e22701e5c..dd690da6f 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 76dd25446..62d2e1bf8 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 2c6ce24e9..06dea51c1 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 8d404ec0d..34a9366d6 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 a32cf9c75..e89a8b840 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 8f169c3d2..5055da4f6 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 7c9e3d6a0..bd5fb3761 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 c6722236e..a50b1113b 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 8081e706b..33c49c705 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 d31da5dfa..e2e8848cc 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 5bc974467..3fa09f560 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 9eafd5daa..f3270f02e 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 4437c7716..2beb2df38 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 dd3d09db0..a26178571 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 9f4bb6795..14d22bf02 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 2dced10e7..34c9916cf 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 f35b5d6ed..2111fa224 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 6ede771c5..cf6850526 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 622c40398..81d4e6045 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 566650e30..153de7680 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 3da0b8359..666e2d04f 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 07b4db0a9..83a261840 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 bf3255250..29eb515bf 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 000000000..2eb76047f
--- /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 68e43429a..341198852 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 3ec0271f6..aac0d1bfe 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 0278e02ae..2b7049355 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 6c32642bb..4fdd81ae5 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 c041734a4..328e6bc62 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 3c170adc4..cdc716b9f 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 9c1bddb2e..39fc8a936 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 1deb6de62..6e4e43a56 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 61c1e23b6..4e57281e9 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 55f78f985..633131182 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 690383d1f..61f5bfbd3 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 e3458cb18..c23306ab1 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 f4d684d57..9e1dfead5 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 3da86944d..5bb052a69 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 f279c2360..59fed1f15 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 f0d9c4e22..d0a0c04d6 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 64cf4c858..fa364e603 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 c10ddb19c..4ea29f672 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"