From 25a69ec1b69cb7f380949ef9cf1c3599eaa4face Mon Sep 17 00:00:00 2001
From: syuilo <Syuilotan@yahoo.co.jp>
Date: Fri, 9 Nov 2018 03:44:35 +0900
Subject: [PATCH] Refactoring of i18n (#3165)

Refactoring of i18n
---
 locales/ja-JP.yml                             | 149 +++++-------
 src/client/app/admin/views/announcements.vue  |  22 +-
 src/client/app/admin/views/charts.vue         |  46 ++--
 src/client/app/admin/views/dashboard.vue      |  22 +-
 src/client/app/admin/views/emoji.vue          |  40 ++--
 src/client/app/admin/views/hashtags.vue       |   8 +-
 src/client/app/admin/views/index.vue          |  22 +-
 src/client/app/admin/views/instance.vue       |  84 +++----
 src/client/app/admin/views/users.vue          |  28 +--
 src/client/app/auth/views/form.vue            |  29 +--
 src/client/app/auth/views/index.vue           |  18 +-
 .../common/views/components/api-settings.vue  |  28 +--
 .../app/common/views/components/avatar.vue    |   1 +
 .../connect-failed.troubleshooter.vue         |  22 +-
 .../views/components/connect-failed.vue       |  14 +-
 .../app/common/views/components/cw-button.vue |   4 +-
 .../views/components/drive-settings.vue       |   8 +-
 .../app/common/views/components/error.vue     |  13 +-
 .../components/games/reversi/reversi.game.vue |  28 +--
 .../games/reversi/reversi.gameroom.vue        |   2 +
 .../games/reversi/reversi.index.vue           |  28 +--
 .../components/games/reversi/reversi.room.vue |  40 ++--
 .../components/games/reversi/reversi.vue      |   6 +-
 .../views/components/github-setting.vue       |  10 +-
 .../app/common/views/components/google.vue    |   5 +-
 .../app/common/views/components/index.ts      |   2 -
 .../app/common/views/components/instance.vue  |   6 +-
 .../common/views/components/media-banner.vue  |   6 +-
 .../views/components/messaging-room.form.vue  |  10 +-
 .../components/messaging-room.message.vue     |   8 +-
 .../views/components/messaging-room.vue       |  16 +-
 .../app/common/views/components/messaging.vue |  10 +-
 .../views/components/mute-and-block.vue       |  12 +-
 .../app/common/views/components/nav.vue       |  10 +-
 .../common/views/components/note-header.vue   |   4 +-
 .../app/common/views/components/note-menu.vue |  20 +-
 .../views/components/password-settings.vue    |  14 +-
 .../common/views/components/poll-editor.vue   |  12 +-
 .../app/common/views/components/poll.vue      |  12 +-
 .../views/components/profile-editor.vue       |  40 ++--
 .../common/views/components/reaction-icon.vue |  24 +-
 .../views/components/reaction-picker.vue      |  28 +--
 .../views/components/reactions-viewer.vue     |   1 +
 .../app/common/views/components/signin.vue    |  14 +-
 .../app/common/views/components/signup.vue    |  40 ++--
 .../views/components/special-message.vue      |  42 ----
 .../views/components/stream-indicator.vue     |   8 +-
 .../app/common/views/components/tag-cloud.vue |   6 +-
 .../app/common/views/components/theme.vue     |  84 +++----
 .../app/common/views/components/time.vue      |  22 +-
 .../app/common/views/components/trends.vue    |   8 +-
 .../views/components/twitter-setting.vue      |  10 +-
 .../common/views/components/ui/textarea.vue   |   1 -
 .../app/common/views/components/uploader.vue  |   4 +-
 .../app/common/views/components/url.vue       |   1 +
 .../views/components/visibility-chooser.vue   |  18 +-
 src/client/app/common/views/pages/follow.vue  |  14 +-
 .../app/common/views/widgets/broadcast.vue    |  10 +-
 .../app/common/views/widgets/calendar.vue     |  29 +--
 .../app/common/views/widgets/donation.vue     |   9 +-
 .../app/common/views/widgets/hashtags.vue     |   4 +-
 src/client/app/common/views/widgets/memo.vue  |   8 +-
 .../app/common/views/widgets/photo-stream.vue |  10 +-
 .../common/views/widgets/posts-monitor.vue    |   9 +-
 src/client/app/common/views/widgets/rss.vue   |   5 +-
 .../app/common/views/widgets/server.vue       |   9 +-
 .../app/common/views/widgets/slideshow.vue    |  10 +-
 src/client/app/common/views/widgets/tips.vue  |  48 ++--
 .../views/components/activity.chart.vue       |   4 +-
 .../app/desktop/views/components/activity.vue |   8 +-
 .../app/desktop/views/components/calendar.vue |  24 +-
 .../choose-file-from-drive-window.vue         |  14 +-
 .../choose-folder-from-drive-window.vue       |   8 +-
 .../desktop/views/components/crop-window.vue  |   8 +-
 .../desktop/views/components/drive-window.vue |   6 +-
 .../desktop/views/components/drive.file.vue   |  36 +--
 .../desktop/views/components/drive.folder.vue |  22 +-
 .../views/components/drive.nav-folder.vue     |   4 +-
 .../app/desktop/views/components/drive.vue    |  38 +--
 .../views/components/follow-button.vue        |  12 +-
 .../views/components/friends-maker.vue        |  12 +-
 .../desktop/views/components/game-window.vue  |   4 +-
 .../app/desktop/views/components/home.vue     |  67 +++---
 .../desktop/views/components/input-dialog.vue |   6 +-
 .../desktop/views/components/media-image.vue  |   6 +-
 .../desktop/views/components/media-video.vue  |   6 +-
 .../components/messaging-room-window.vue      |   4 +-
 .../views/components/messaging-window.vue     |   4 +-
 .../desktop/views/components/note-detail.vue  |  18 +-
 .../app/desktop/views/components/note.vue     |  14 +-
 .../app/desktop/views/components/notes.vue    |   6 +-
 .../views/components/notifications.vue        |   8 +-
 .../views/components/post-form-window.vue     |  10 +-
 .../desktop/views/components/post-form.vue    |  66 +++---
 .../views/components/progress-dialog.vue      |   5 +-
 .../received-follow-requests-window.vue       |   7 +-
 .../views/components/renote-form-window.vue   |   4 +-
 .../desktop/views/components/renote-form.vue  |  12 +-
 .../views/components/settings-window.vue      |   4 +-
 .../desktop/views/components/settings.2fa.vue |  32 +--
 .../views/components/settings.apps.vue        |   4 +-
 .../views/components/settings.tags.vue        |  10 +-
 .../app/desktop/views/components/settings.vue | 218 +++++++++---------
 .../views/components/sub-note-content.vue     |  12 +-
 .../views/components/timeline.core.vue        |   4 +-
 .../app/desktop/views/components/timeline.vue |  24 +-
 .../views/components/ui.header.account.vue    |  22 +-
 .../views/components/ui.header.nav.vue        |  14 +-
 .../components/ui.header.notifications.vue    |   4 +-
 .../views/components/ui.header.post.vue       |   4 +-
 .../views/components/ui.header.search.vue     |   4 +-
 .../desktop/views/components/ui.header.vue    |   5 +-
 .../desktop/views/components/ui.sidebar.vue   |   4 +-
 .../views/components/user-lists-window.vue    |   9 +-
 .../desktop/views/components/user-preview.vue |   8 +-
 .../app/desktop/views/components/window.vue   |   6 +-
 .../desktop/views/pages/deck/deck.column.vue  |  20 +-
 .../views/pages/deck/deck.direct-column.vue   |   4 +-
 .../desktop/views/pages/deck/deck.direct.vue  |   2 +-
 .../views/pages/deck/deck.mentions-column.vue |   4 +-
 .../views/pages/deck/deck.mentions.vue        |   2 +-
 .../views/pages/deck/deck.note-column.vue     |   6 +-
 .../desktop/views/pages/deck/deck.notes.vue   |   6 +-
 .../pages/deck/deck.notifications-column.vue  |   4 +-
 .../views/pages/deck/deck.notifications.vue   |   8 +-
 .../views/pages/deck/deck.tl-column.vue       |  16 +-
 .../views/pages/deck/deck.user-column.vue     |  22 +-
 .../app/desktop/views/pages/deck/deck.vue     |  26 ++-
 .../views/pages/deck/deck.widgets-column.vue  |  52 +++--
 src/client/app/desktop/views/pages/drive.vue  |   7 +-
 .../app/desktop/views/pages/favorites.vue     |   4 +-
 .../desktop/views/pages/home-customize.vue    |  10 -
 .../desktop/views/pages/messaging-room.vue    |   2 +
 src/client/app/desktop/views/pages/note.vue   |   6 +-
 src/client/app/desktop/views/pages/search.vue |   6 +-
 .../app/desktop/views/pages/selectdrive.vue   |  10 +-
 src/client/app/desktop/views/pages/share.vue  |   6 +-
 src/client/app/desktop/views/pages/tag.vue    |   4 +-
 .../pages/user-following-or-followers.vue     |  14 +-
 .../pages/user/user.followers-you-know.vue    |   8 +-
 .../desktop/views/pages/user/user.friends.vue |   8 +-
 .../desktop/views/pages/user/user.header.vue  |  12 +-
 .../desktop/views/pages/user/user.photos.vue  |   8 +-
 .../desktop/views/pages/user/user.profile.vue |  22 +-
 .../views/pages/user/user.timeline.vue        |  10 +-
 .../app/desktop/views/pages/user/user.vue     |   7 +-
 .../app/desktop/views/pages/welcome.vue       |  48 ++--
 .../app/desktop/views/widgets/messaging.vue   |   4 +-
 .../desktop/views/widgets/notifications.vue   |   7 +-
 .../app/desktop/views/widgets/polls.vue       |  10 +-
 .../app/desktop/views/widgets/post-form.vue   |  19 +-
 .../app/desktop/views/widgets/profile.vue     |   6 +-
 .../app/desktop/views/widgets/trends.vue      |  10 +-
 .../app/desktop/views/widgets/users.vue       |  10 +-
 src/client/app/dev/views/app.vue              |   4 +-
 src/client/app/dev/views/apps.vue             |  10 +-
 src/client/app/dev/views/index.vue            |   7 +-
 src/client/app/dev/views/new-app.vue          |  42 ++--
 src/client/app/i18n.ts                        |  14 ++
 src/client/app/init.ts                        |  16 +-
 src/client/app/mios.ts                        |   1 +
 .../views/components/drive-file-chooser.vue   |   5 +-
 .../views/components/drive-folder-chooser.vue |   4 +-
 .../views/components/drive.file-detail.vue    |  20 +-
 .../mobile/views/components/drive.file.vue    |   4 +-
 .../app/mobile/views/components/drive.vue     |  36 +--
 .../mobile/views/components/follow-button.vue |  12 +-
 .../mobile/views/components/friends-maker.vue |  12 +-
 .../mobile/views/components/media-image.vue   |   6 +-
 .../mobile/views/components/media-video.vue   |   6 +-
 .../mobile/views/components/note-detail.vue   |  16 +-
 .../app/mobile/views/components/note.vue      |  10 +-
 .../app/mobile/views/components/notes.vue     |  11 +-
 .../mobile/views/components/notifications.vue |   8 +-
 .../app/mobile/views/components/post-form.vue |  32 +--
 .../views/components/sub-note-content.vue     |  12 +-
 .../app/mobile/views/components/ui.header.vue |   5 +-
 .../app/mobile/views/components/ui.nav.vue    |  32 +--
 .../mobile/views/components/user-timeline.vue |   4 +-
 .../mobile/views/components/users-list.vue    |  12 +-
 src/client/app/mobile/views/pages/drive.vue   |   4 +-
 .../app/mobile/views/pages/favorites.vue      |   6 +-
 .../app/mobile/views/pages/followers.vue      |   7 +-
 .../app/mobile/views/pages/following.vue      |   7 +-
 .../app/mobile/views/pages/games/reversi.vue  |   4 +-
 src/client/app/mobile/views/pages/home.vue    |  27 ++-
 .../app/mobile/views/pages/messaging-room.vue |   4 +-
 .../app/mobile/views/pages/messaging.vue      |   6 +-
 src/client/app/mobile/views/pages/note.vue    |   8 +-
 .../app/mobile/views/pages/notifications.vue  |   8 +-
 .../views/pages/received-follow-requests.vue  |   8 +-
 src/client/app/mobile/views/pages/search.vue  |   4 +-
 .../app/mobile/views/pages/selectdrive.vue    |   6 +-
 .../app/mobile/views/pages/settings.vue       | 134 +++++------
 src/client/app/mobile/views/pages/share.vue   |   6 +-
 src/client/app/mobile/views/pages/signup.vue  |   6 +-
 src/client/app/mobile/views/pages/tag.vue     |   4 +-
 .../app/mobile/views/pages/user-lists.vue     |   8 +-
 src/client/app/mobile/views/pages/user.vue    |  24 +-
 .../pages/user/home.followers-you-know.vue    |   6 +-
 .../mobile/views/pages/user/home.friends.vue  |   6 +-
 .../mobile/views/pages/user/home.notes.vue    |   6 +-
 .../mobile/views/pages/user/home.photos.vue   |   6 +-
 .../app/mobile/views/pages/user/home.vue      |  14 +-
 src/client/app/mobile/views/pages/welcome.vue |  30 +--
 src/client/app/mobile/views/pages/widgets.vue |  40 ++--
 .../app/mobile/views/widgets/activity.vue     |   4 +-
 src/misc/i18n.ts                              |  73 ------
 src/server/web/docs.ts                        |   5 +-
 webpack.config.ts                             |  22 --
 webpack/i18n.ts                               |  19 --
 211 files changed, 1825 insertions(+), 1624 deletions(-)
 delete mode 100644 src/client/app/common/views/components/special-message.vue
 create mode 100644 src/client/app/i18n.ts
 delete mode 100644 src/misc/i18n.ts
 delete mode 100644 webpack/i18n.ts

diff --git a/locales/ja-JP.yml b/locales/ja-JP.yml
index 66a9a83353..284342f005 100644
--- a/locales/ja-JP.yml
+++ b/locales/ja-JP.yml
@@ -25,6 +25,7 @@ common:
   application-authorization: "アプリの連携"
   close: "閉じる"
   do-not-copy-paste: "ここにコードを入力したり張り付けたりしないでください。アカウントが不正利用される可能性があります。"
+  load-more: "もっと読み込む"
   BSoD:
     fatal-error: ":( 致命的な問題が発生しました。"
     update-browser-os: "お使いのブラウザ(またはOS)のバージョンを更新すると解決する可能性があります。"
@@ -37,10 +38,7 @@ common:
   got-it: "わかった"
   customization-tips:
     title: "カスタマイズのヒント"
-    paragraph1: "ホームのカスタマイズでは、ウィジェットを追加/削除したり、ドラッグ&ドロップして並べ替えたりすることができます。"
-    paragraph2: "一部のウィジェットは、<strong><strong>右</strong>クリック</strong>することで表示を変更することができます。"
-    paragraph3: "ウィジェットを削除するには、ヘッダーの<strong>「ゴミ箱」</strong>と書かれたエリアにウィジェットをドラッグ&ドロップします。"
-    paragraph4: "カスタマイズを終了するには、右上の「完了」をクリックします。"
+    paragraph: "<p>ホームのカスタマイズでは、ウィジェットを追加/削除したり、ドラッグ&ドロップして並べ替えたりすることができます。</p><p>一部のウィジェットは、<strong><strong>右</strong>クリック</strong>することで表示を変更することができます。</p><p>ウィジェットを削除するには、ヘッダーの<strong>「ゴミ箱」</strong>と書かれたエリアにウィジェットをドラッグ&ドロップします。</p><p>カスタマイズを終了するには、右上の「完了」をクリックします。</p>"
     gotit: "Got it!"
   notification:
     file-uploaded: "ファイルがアップロードされました"
@@ -65,6 +63,7 @@ common:
 
   trash: "ゴミ箱"
   drive: "ドライブ"
+  messaging: "トーク"
 
   weekday-short:
     sunday: "日"
@@ -147,13 +146,13 @@ common:
     drawn: "引き分け"
     my-turn: "あなたのターンです"
     opponent-turn: "相手のターンです"
-    turn-of: "{}のターンです"
-    past-turn-of: "{}のターン"
-    won: "{}の勝ち"
+    turn-of: "{name}のターンです"
+    past-turn-of: "{name}のターン"
+    won: "{name}の勝ち"
     black: "黒"
     white: "白"
     total: "合計"
-    this-turn: "{}ターン目"
+    this-turn: "{count}ターン目"
 
   widgets:
     analog-clock: "アナログ時計"
@@ -173,34 +172,12 @@ common:
     users: "おすすめユーザー"
     polls: "アンケート"
     post-form: "投稿フォーム"
-    messaging: "メッセージ"
     server: "サーバー情報"
     donation: "寄付のお願い"
     nav: "ナビゲーション"
     tips: "ヒント"
     hashtags: "ハッシュタグ"
 
-  deck:
-    widgets: "ウィジェット"
-    home: "ホーム"
-    local: "ローカル"
-    hybrid: "ソーシャル"
-    hashtag: "ハッシュタグ"
-    global: "グローバル"
-    mentions: "あなた宛て"
-    direct: "ダイレクト投稿"
-    notifications: "通知"
-    list: "リスト"
-    swap-left: "左に移動"
-    swap-right: "右に移動"
-    swap-up: "上に移動"
-    swap-down: "下に移動"
-    remove: "カラムを削除"
-    add-column: "カラムを追加"
-    rename: "名前を変更"
-    stack-left: "左に重ねる"
-    pop-right: "右に出す"
-
   dev: "アプリの作成に失敗しました。再度お試しください。"
   ai-chan-kawaii: "藍ちゃかわいい"
 
@@ -354,7 +331,6 @@ common/views/components/messaging.vue:
 
 common/views/components/messaging-room.vue:
   empty: "このユーザーと話したことはありません"
-  more: "もっと読む"
   no-history: "これより過去の履歴はありません"
   resize-form: "ドラッグしてフォームの広さを調整"
   new-message: "新しいメッセージがあります"
@@ -604,13 +580,13 @@ desktop/views/components/activity.vue:
   toggle: "表示を切り替え"
 
 desktop/views/components/calendar.vue:
-  title: "{1}年 {2}月"
+  title: "{year}年 {month}月"
   prev: "前の月"
   next: "次の月"
   go: "クリックして時間遡行"
 
 desktop/views/components/choose-file-from-drive-window.vue:
-  choose-file: "ファイル選択中"
+  chosen-files: "{count}ファイル選択中"
   upload: "PCからドライブにファイルをアップロード"
   cancel: "キャンセル"
   ok: "決定"
@@ -662,7 +638,6 @@ desktop/views/components/drive.folder.vue:
 
 desktop/views/components/drive.vue:
   search: "検索"
-  load-more: "もっと読み込む"
   empty-draghover: "ドロップですか?いいですよ、ボクはカワイイですからね"
   empty-drive: "ドライブには何もありません。"
   empty-drive-description: "右クリックして「ファイルをアップロード」を選んだり、ファイルをドラッグ&ドロップすることでもアップロードできます。"
@@ -734,7 +709,6 @@ desktop/views/components/messaging-window.vue:
   title: "メッセージ"
 
 desktop/views/components/note-detail.vue:
-  more: "会話をもっと読み込む"
   private: "この投稿は非公開です"
   deleted: "この投稿は削除されました"
   reposted-by: "{}がRenote"
@@ -754,10 +728,8 @@ desktop/views/components/note.vue:
 desktop/views/components/notes.vue:
   error: "読み込みに失敗しました。"
   retry: "リトライ"
-  load-more: "もっと読み込む"
 
 desktop/views/components/notifications.vue:
-  more: "もっと見る"
   empty: "ありません!"
 
 desktop/views/components/post-form.vue:
@@ -1019,7 +991,6 @@ desktop/views/components/ui.header.account.vue:
 desktop/views/components/ui.header.nav.vue:
   home: "ホーム"
   deck: "デッキ"
-  messaging: "メッセージ"
   game: "ゲーム"
 
 desktop/views/components/ui.header.notifications.vue:
@@ -1049,7 +1020,6 @@ desktop/views/components/user-preview.vue:
 desktop/views/components/users-list.vue:
   all: "すべて"
   iknow: "知り合い"
-  load-more: "もっと"
   fetching: "読み込んでいます"
 
 desktop/views/components/users-list-item.vue:
@@ -1196,21 +1166,6 @@ admin/views/announcements.vue:
 admin/views/hashtags.vue:
   hided-tags: "Hidden Tags"
 
-desktop/views/pages/deck/deck.tl-column.vue:
-  is-media-only: "メディア投稿のみ"
-  is-media-view: "メディアビュー"
-  edit: "オプション"
-
-desktop/views/pages/deck/deck.user-column.vue:
-  posts: "投稿"
-  following: "フォロー"
-  followers: "フォロワー"
-  images: "画像"
-  activity: "アクティビティ"
-  timeline: "タイムライン"
-  pinned-notes: "ピン留めされた投稿"
-  push-to-a-list: "リストに追加"
-
 desktop/views/pages/welcome.vue:
   about: "詳しく..."
   gotit: "わかった"
@@ -1227,9 +1182,6 @@ desktop/views/pages/welcome.vue:
 desktop/views/pages/drive.vue:
   title: "Misskey Drive"
 
-desktop/views/pages/favorites.vue:
-  more: "さらに読み込む"
-
 desktop/views/pages/home-customize.vue:
   title: "ホームのカスタマイズ"
 
@@ -1245,13 +1197,13 @@ desktop/views/pages/selectdrive.vue:
 
 desktop/views/pages/search.vue:
   not-available: "検索機能はインスタンスの設定で無効になっています。"
-  not-found: "「{}」に関する投稿は見つかりませんでした。"
+  not-found: "「{q}」に関する投稿は見つかりませんでした。"
 
 desktop/views/pages/share.vue:
-  share-with: "{}で共有"
+  share-with: "{name}で共有"
 
 desktop/views/pages/tag.vue:
-  no-posts-found: "ハッシュタグ「{}」が付けられた投稿は見つかりませんでした。"
+  no-posts-found: "ハッシュタグ「{q}」が付けられた投稿は見つかりませんでした。"
 
 desktop/views/pages/user-list.users.vue:
   users: "ユーザー"
@@ -1268,10 +1220,6 @@ desktop/views/pages/user/user.friends.vue:
   loading: "読み込み中"
   no-users: "よく話すユーザーはいません"
 
-desktop/views/pages/user/user.vue:
-  is-suspended: "このユーザーは凍結されています。"
-  last-used-at: "最終アクセス"
-
 desktop/views/pages/user/user.photos.vue:
   title: "フォト"
   loading: "読み込み中"
@@ -1342,7 +1290,6 @@ mobile/views/components/drive.vue:
   folder-count: "フォルダ"
   count-separator: "、"
   file-count: "ファイル"
-  load-more: "もっと読み込む"
   nothing-in-drive: "ドライブには何もありません"
   folder-is-empty: "このフォルダは空です"
   prompt: "何をしますか?(数字を入力してください): <1 → ファイルをアップロード | 2 → ファイルをURLでアップロード | 3 → フォルダ作成 | 4 → このフォルダ名を変更 | 5 → このフォルダを移動 | 6 → このフォルダを削除>"
@@ -1353,9 +1300,6 @@ mobile/views/components/drive.vue:
   url-prompt: "アップロードしたいファイルのURL"
   uploading: "アップロードをリクエストしました。アップロードが完了するまで時間がかかる場合があります。"
 
-mobile/views/components/drive-file-detail.vue:
-  rename: "名前を変更"
-
 mobile/views/components/drive-file-chooser.vue:
   select-file: "ファイルを選択"
 
@@ -1421,12 +1365,7 @@ mobile/views/components/note-sub.vue:
   bot: "bot"
   cat: "cat"
 
-mobile/views/components/notes.vue:
-  failed: "読み込みに失敗しました。"
-  retry: "リトライ"
-
 mobile/views/components/notifications.vue:
-  more: "もっと見る"
   empty: "ありません!"
 
 mobile/views/components/post-form.vue:
@@ -1449,7 +1388,6 @@ mobile/views/components/sub-note-content.vue:
 
 mobile/views/components/timeline.vue:
   empty: "投稿がありません"
-  load-more: "もっと"
 
 mobile/views/components/ui.header.vue:
   welcome-back: "おかえりなさい、"
@@ -1458,7 +1396,6 @@ mobile/views/components/ui.header.vue:
 mobile/views/components/ui.nav.vue:
   timeline: "タイムライン"
   notifications: "通知"
-  messaging: "メッセージ"
   follow-requests: "フォロー申請"
   search: "検索"
   favorites: "お気に入り"
@@ -1473,12 +1410,10 @@ mobile/views/components/ui.nav.vue:
 mobile/views/components/user-timeline.vue:
   no-notes: "このユーザーは投稿していないようです。"
   no-notes-with-media: "メディア付き投稿はありません。"
-  load-more: "もっと"
 
 mobile/views/components/users-list.vue:
   all: "すべて"
   known: "知り合い"
-  load-more: "もっと"
 
 mobile/views/pages/favorites.vue:
   title: "お気に入り"
@@ -1487,17 +1422,14 @@ mobile/views/pages/user-lists.vue:
   title: "リスト"
   enter-list-name: "リスト名を入力してください"
 
-mobile/views/pages/drive.vue:
-  more: "もっと見る"
-
 mobile/views/pages/signup.vue:
   lets-start: "📦 始めましょう"
 
 mobile/views/pages/followers.vue:
-  followers-of: "{}のフォロワー"
+  followers-of: "{name}のフォロワー"
 
 mobile/views/pages/following.vue:
-  following-of: "{}のフォロー"
+  following-of: "{name}のフォロー"
 
 mobile/views/pages/home.vue:
   home: "ホーム"
@@ -1508,7 +1440,7 @@ mobile/views/pages/home.vue:
   messages: "メッセージ"
 
 mobile/views/pages/tag.vue:
-  no-posts-found: "ハッシュタグ「{}」が付けられた投稿は見つかりませんでした。"
+  no-posts-found: "ハッシュタグ「{q}」が付けられた投稿は見つかりませんでした。"
 
 mobile/views/pages/welcome.vue:
   signup: "新規登録"
@@ -1523,13 +1455,7 @@ mobile/views/pages/widgets/activity.vue:
   activity: "アクティビティ"
 
 mobile/views/pages/share.vue:
-  share-with: "{}で共有"
-
-mobile/views/pages/messaging.vue:
-  messaging: "メッセージ"
-
-mobile/views/pages/messaging-room.vue:
-  messaging: "メッセージ"
+  share-with: "{name}で共有"
 
 mobile/views/pages/received-follow-requests.vue:
   title: "フォロー申請"
@@ -1550,8 +1476,7 @@ mobile/views/pages/games/reversi.vue:
 
 mobile/views/pages/search.vue:
   search: "検索"
-  empty: "「{}」に関する投稿は見つかりませんでした。"
-  not-found: "「{}」に関する投稿は見つかりませんでした。"
+  not-found: "「{q}」に関する投稿は見つかりませんでした。"
 
 mobile/views/pages/selectdrive.vue:
   select-file: "ファイルを選択"
@@ -1637,21 +1562,53 @@ mobile/views/pages/user/home.vue:
   last-used-at: "最終ログイン"
 
 mobile/views/pages/user/home.followers-you-know.vue:
-  loading: "読み込み中"
   no-users: "知り合いのユーザーはいません"
 
 mobile/views/pages/user/home.friends.vue:
-  loading: "読み込み中"
   no-users: "よく会話するユーザーはいません"
 
 mobile/views/pages/user/home.notes.vue:
-  loading: "読み込み中"
   no-notes: "投稿はありません"
 
 mobile/views/pages/user/home.photos.vue:
-  loading: "読み込み中"
   no-photos: "写真はありません"
 
+deck:
+  widgets: "ウィジェット"
+  home: "ホーム"
+  local: "ローカル"
+  hybrid: "ソーシャル"
+  hashtag: "ハッシュタグ"
+  global: "グローバル"
+  mentions: "あなた宛て"
+  direct: "ダイレクト投稿"
+  notifications: "通知"
+  list: "リスト"
+  swap-left: "左に移動"
+  swap-right: "右に移動"
+  swap-up: "上に移動"
+  swap-down: "下に移動"
+  remove: "カラムを削除"
+  add-column: "カラムを追加"
+  rename: "名前を変更"
+  stack-left: "左に重ねる"
+  pop-right: "右に出す"
+
+deck/deck.tl-column.vue:
+  is-media-only: "メディア投稿のみ"
+  is-media-view: "メディアビュー"
+  edit: "オプション"
+
+deck/deck.user-column.vue:
+  posts: "投稿"
+  following: "フォロー"
+  followers: "フォロワー"
+  images: "画像"
+  activity: "アクティビティ"
+  timeline: "タイムライン"
+  pinned-notes: "ピン留めされた投稿"
+  push-to-a-list: "リストに追加"
+
 docs:
   edit-this-page-on-github: "間違いや改善点を見つけましたか?"
   edit-this-page-on-github-link: "このページをGitHubで編集"
diff --git a/src/client/app/admin/views/announcements.vue b/src/client/app/admin/views/announcements.vue
index 14177a6211..0c0c6e57cf 100644
--- a/src/client/app/admin/views/announcements.vue
+++ b/src/client/app/admin/views/announcements.vue
@@ -1,30 +1,32 @@
 <template>
 <div class="cdeuzmsthagexbkpofbmatmugjuvogfb">
 	<ui-card>
-		<div slot="title"><fa icon="broadcast-tower"/> %i18n:@announcements%</div>
+		<div slot="title"><fa icon="broadcast-tower"/> {{ $t('announcements') }}</div>
 		<section v-for="(announcement, i) in announcements" class="fit-top">
 			<ui-input v-model="announcement.title" @change="save">
-				<span>%i18n:@title%</span>
+				<span>{{ $t('title') }}</span>
 			</ui-input>
 			<ui-textarea v-model="announcement.text">
-				<span>%i18n:@text%</span>
+				<span>{{ $t('text') }}</span>
 			</ui-textarea>
 			<ui-horizon-group>
-				<ui-button @click="save()"><fa :icon="['far', 'save']"/> %i18n:@save%</ui-button>
-				<ui-button @click="remove(i)"><fa :icon="['far', 'trash-alt']"/> %i18n:@remove%</ui-button>
+				<ui-button @click="save()"><fa :icon="['far', 'save']"/> {{ $t('save') }}</ui-button>
+				<ui-button @click="remove(i)"><fa :icon="['far', 'trash-alt']"/> {{ $t('remove') }}</ui-button>
 			</ui-horizon-group>
 		</section>
 		<section>
-			<ui-button @click="add"><fa icon="plus"/> %i18n:@add%</ui-button>
+			<ui-button @click="add"><fa icon="plus"/> {{ $t('add') }}</ui-button>
 		</section>
 	</ui-card>
 </div>
 </template>
 
 <script lang="ts">
-import Vue from "vue";
+import Vue from 'vue';
+import i18n from '../../i18n';
 
 export default Vue.extend({
+	i18n: i18n('admin/views/announcements.vue'),
 	data() {
 		return {
 			announcements: [],
@@ -48,7 +50,7 @@ export default Vue.extend({
 		remove(i) {
 			this.$swal({
 				type: 'warning',
-				text: '%i18n:@_remove.are-you-sure%'.replace('$1', this.announcements.find((_, j) => j == i).title),
+				text: this.$t('_remove.are-you-sure').replace('$1', this.announcements.find((_, j) => j == i).title),
 				showCancelButton: true
 			}).then(res => {
 				if (!res.value) return;
@@ -56,7 +58,7 @@ export default Vue.extend({
 				this.save(true);
 				this.$swal({
 					type: 'success',
-					text: '%i18n:@_remove.removed%'
+					text: this.$t('_remove.removed')
 				});
 			});
 		},
@@ -68,7 +70,7 @@ export default Vue.extend({
 				if (!silent) {
 					this.$swal({
 						type: 'success',
-						text: '%i18n:@saved%'
+						text: this.$t('saved')
 					});
 				}
 			}).catch(e => {
diff --git a/src/client/app/admin/views/charts.vue b/src/client/app/admin/views/charts.vue
index 59e06d6ff6..fbd52eebea 100644
--- a/src/client/app/admin/views/charts.vue
+++ b/src/client/app/admin/views/charts.vue
@@ -1,36 +1,36 @@
 <template>
 <div class="qvgidhudpqhjttdhxubzuyrhyzgslujw">
 	<header>
-		<b><fa :icon="['far', 'chart-bar']"/> %i18n:@title%:</b>
+		<b><fa :icon="['far', 'chart-bar']"/> {{ $t('title') }}:</b>
 		<select v-model="src">
-			<optgroup label="%i18n:@federation%">
-				<option value="federation-instances">%i18n:@charts.federation-instances%</option>
-				<option value="federation-instances-total">%i18n:@charts.federation-instances-total%</option>
+			<optgroup :label="$t('label')">
+				<option value="federation-instances">{{ $t('charts.federation-instances') }}</option>
+				<option value="federation-instances-total">{{ $t('charts.federation-instances-total') }}</option>
 			</optgroup>
-			<optgroup label="%i18n:@users%">
-				<option value="users">%i18n:@charts.users%</option>
-				<option value="users-total">%i18n:@charts.users-total%</option>
+			<optgroup :label="$t('label')">
+				<option value="users">{{ $t('charts.users') }}</option>
+				<option value="users-total">{{ $t('charts.users-total') }}</option>
 			</optgroup>
-			<optgroup label="%i18n:@notes%">
-				<option value="notes">%i18n:@charts.notes%</option>
-				<option value="local-notes">%i18n:@charts.local-notes%</option>
-				<option value="remote-notes">%i18n:@charts.remote-notes%</option>
-				<option value="notes-total">%i18n:@charts.notes-total%</option>
+			<optgroup :label="$t('label')">
+				<option value="notes">{{ $t('charts.notes') }}</option>
+				<option value="local-notes">{{ $t('charts.local-notes') }}</option>
+				<option value="remote-notes">{{ $t('charts.remote-notes') }}</option>
+				<option value="notes-total">{{ $t('charts.notes-total') }}</option>
 			</optgroup>
-			<optgroup label="%i18n:@drive%">
-				<option value="drive-files">%i18n:@charts.drive-files%</option>
-				<option value="drive-files-total">%i18n:@charts.drive-files-total%</option>
-				<option value="drive">%i18n:@charts.drive%</option>
-				<option value="drive-total">%i18n:@charts.drive-total%</option>
+			<optgroup :label="$t('label')">
+				<option value="drive-files">{{ $t('charts.drive-files') }}</option>
+				<option value="drive-files-total">{{ $t('charts.drive-files-total') }}</option>
+				<option value="drive">{{ $t('charts.drive') }}</option>
+				<option value="drive-total">{{ $t('charts.drive-total') }}</option>
 			</optgroup>
-			<optgroup label="%i18n:@network%">
-				<option value="network-requests">%i18n:@charts.network-requests%</option>
-				<option value="network-time">%i18n:@charts.network-time%</option>
-				<option value="network-usage">%i18n:@charts.network-usage%</option>
+			<optgroup :label="$t('label')">
+				<option value="network-requests">{{ $t('charts.network-requests') }}</option>
+				<option value="network-time">{{ $t('charts.network-time') }}</option>
+				<option value="network-usage">{{ $t('charts.network-usage') }}</option>
 			</optgroup>
 		</select>
 		<div>
-			<span @click="span = 'day'" :class="{ active: span == 'day' }">%i18n:@per-day%</span> | <span @click="span = 'hour'" :class="{ active: span == 'hour' }">%i18n:@per-hour%</span>
+			<span @click="span = 'day'" :class="{ active: span == 'day' }">{{ $t('per-day') }}</span> | <span @click="span = 'hour'" :class="{ active: span == 'hour' }">{{ $t('per-hour') }}</span>
 		</div>
 	</header>
 	<div ref="chart"></div>
@@ -39,6 +39,7 @@
 
 <script lang="ts">
 import Vue from 'vue';
+import i18n from '../../i18n';
 import * as tinycolor from 'tinycolor2';
 import * as ApexCharts from 'apexcharts';
 
@@ -48,6 +49,7 @@ const sum = (...arr) => arr.reduce((r, a) => r.map((b, i) => a[i] + b));
 const negate = arr => arr.map(x => -x);
 
 export default Vue.extend({
+	i18n: i18n('admin/views/charts.vue'),
 	data() {
 		return {
 			chart: null,
diff --git a/src/client/app/admin/views/dashboard.vue b/src/client/app/admin/views/dashboard.vue
index 383b859263..d62feecbd2 100644
--- a/src/client/app/admin/views/dashboard.vue
+++ b/src/client/app/admin/views/dashboard.vue
@@ -5,7 +5,7 @@
 		<p><b>Machine</b><span>{{ meta.machine }}</span></p>
 		<p><b>OS</b><span>{{ meta.os }}</span></p>
 		<p><b>Node</b><span>{{ meta.node }}</span></p>
-		<p>%i18n:common.ai-chan-kawaii%</p>
+		<p>{{ $t('@.ai-chan-kawaii') }}</p>
 	</header>
 
 	<div v-if="stats" class="stats">
@@ -13,12 +13,12 @@
 			<div>
 				<div><fa icon="user"/></div>
 				<div>
-					<span>%i18n:@accounts%</span>
+					<span>{{ $t('accounts') }}</span>
 					<b class="primary">{{ stats.originalUsersCount | number }}</b>
 				</div>
 			</div>
 			<div>
-				<span><fa icon="home"/> %i18n:@this-instance%</span>
+				<span><fa icon="home"/> {{ $t('this-instance') }}</span>
 				<span @click="setChartSrc('users')"><fa :icon="['far', 'chart-bar']"/></span>
 			</div>
 		</div>
@@ -26,12 +26,12 @@
 			<div>
 				<div><fa icon="pencil-alt"/></div>
 				<div>
-					<span>%i18n:@notes%</span>
+					<span>{{ $t('notes') }}</span>
 					<b class="primary">{{ stats.originalNotesCount | number }}</b>
 				</div>
 			</div>
 			<div>
-				<span><fa icon="home"/> %i18n:@this-instance%</span>
+				<span><fa icon="home"/> {{ $t('this-instance') }}</span>
 				<span @click="setChartSrc('notes')"><fa :icon="['far', 'chart-bar']"/></span>
 			</div>
 		</div>
@@ -39,12 +39,12 @@
 			<div>
 				<div><fa icon="database"/></div>
 				<div>
-					<span>%i18n:@drive%</span>
+					<span>{{ $t('drive') }}</span>
 					<b>{{ stats.driveUsageLocal | bytes }}</b>
 				</div>
 			</div>
 			<div>
-				<span><fa icon="home"/> %i18n:@this-instance%</span>
+				<span><fa icon="home"/> {{ $t('this-instance') }}</span>
 				<span @click="setChartSrc('drive')"><fa :icon="['far', 'chart-bar']"/></span>
 			</div>
 		</div>
@@ -52,12 +52,12 @@
 			<div>
 				<div><fa :icon="['far', 'hdd']"/></div>
 				<div>
-					<span>%i18n:@instances%</span>
+					<span>{{ $t('instances') }}</span>
 					<b>{{ stats.instances | number }}</b>
 				</div>
 			</div>
 			<div>
-				<span><fa icon="globe"/> %i18n:@federated%</span>
+				<span><fa icon="globe"/> {{ $t('federated') }}</span>
 				<span @click="setChartSrc('federation-instances-total')"><fa :icon="['far', 'chart-bar']"/></span>
 			</div>
 		</div>
@@ -78,12 +78,14 @@
 </template>
 
 <script lang="ts">
-import Vue from "vue";
+import Vue from 'vue';
+import i18n from '../../i18n';
 import XCpuMemory from "./cpu-memory.vue";
 import XCharts from "./charts.vue";
 import XApLog from "./ap-log.vue";
 
 export default Vue.extend({
+	i18n: i18n('admin/views/dashboard.vue'),
 	components: {
 		XCpuMemory,
 		XCharts,
diff --git a/src/client/app/admin/views/emoji.vue b/src/client/app/admin/views/emoji.vue
index 155fae82a1..fb6e871bb9 100644
--- a/src/client/app/admin/views/emoji.vue
+++ b/src/client/app/admin/views/emoji.vue
@@ -1,46 +1,46 @@
 <template>
 <div class="tumhkfkmgtvzljezfvmgkeurkfncshbe">
 	<ui-card>
-		<div slot="title"><fa icon="plus"/> %i18n:@add-emoji.title%</div>
+		<div slot="title"><fa icon="plus"/> {{ $t('add-emoji.title') }}</div>
 		<section class="fit-top">
 			<ui-horizon-group inputs>
 				<ui-input v-model="name">
-					<span>%i18n:@add-emoji.name%</span>
-					<span slot="desc">%i18n:@add-emoji.name-desc%</span>
+					<span>{{ $t('add-emoji.name') }}</span>
+					<span slot="desc">{{ $t('add-emoji.name-desc') }}</span>
 				</ui-input>
 				<ui-input v-model="aliases">
-					<span>%i18n:@add-emoji.aliases%</span>
-					<span slot="desc">%i18n:@add-emoji.aliases-desc%</span>
+					<span>{{ $t('add-emoji.aliases') }}</span>
+					<span slot="desc">{{ $t('add-emoji.aliases-desc') }}</span>
 				</ui-input>
 			</ui-horizon-group>
 			<ui-input v-model="url">
 				<i slot="icon"><fa icon="link"/></i>
-				<span>%i18n:@add-emoji.url%</span>
+				<span>{{ $t('add-emoji.url') }}</span>
 			</ui-input>
-			<ui-info>%i18n:@add-emoji.info%</ui-info>
-			<ui-button @click="add">%i18n:@add-emoji.add%</ui-button>
+			<ui-info>{{ $t('add-emoji.info') }}</ui-info>
+			<ui-button @click="add">{{ $t('add-emoji.add') }}</ui-button>
 		</section>
 	</ui-card>
 
 	<ui-card>
-		<div slot="title"><fa :icon="['far', 'grin']"/> %i18n:@emojis.title%</div>
+		<div slot="title"><fa :icon="['far', 'grin']"/> {{ $t('emojis.title') }}</div>
 		<section v-for="emoji in emojis">
 			<img :src="emoji.url" :alt="emoji.name" style="width: 64px;"/>
 			<ui-horizon-group inputs>
 				<ui-input v-model="emoji.name">
-					<span>%i18n:@add-emoji.name%</span>
+					<span>{{ $t('add-emoji.name') }}</span>
 				</ui-input>
 				<ui-input v-model="emoji.aliases">
-					<span>%i18n:@add-emoji.aliases%</span>
+					<span>{{ $t('add-emoji.aliases') }}</span>
 				</ui-input>
 			</ui-horizon-group>
 			<ui-input v-model="emoji.url">
 				<i slot="icon"><fa icon="link"/></i>
-				<span>%i18n:@add-emoji.url%</span>
+				<span>{{ $t('add-emoji.url') }}</span>
 			</ui-input>
 			<ui-horizon-group>
-				<ui-button @click="updateEmoji(emoji)"><fa :icon="['far', 'save']"/> %i18n:@emojis.update%</ui-button>
-				<ui-button @click="removeEmoji(emoji)"><fa :icon="['far', 'trash-alt']"/> %i18n:@emojis.remove%</ui-button>
+				<ui-button @click="updateEmoji(emoji)"><fa :icon="['far', 'save']"/> {{ $t('emojis.update') }}</ui-button>
+				<ui-button @click="removeEmoji(emoji)"><fa :icon="['far', 'trash-alt']"/> {{ $t('emojis.remove') }}</ui-button>
 			</ui-horizon-group>
 		</section>
 	</ui-card>
@@ -48,9 +48,11 @@
 </template>
 
 <script lang="ts">
-import Vue from "vue";
+import Vue from 'vue';
+import i18n from '../../i18n';
 
 export default Vue.extend({
+	i18n: i18n('admin/views/emoji.vue'),
 	data() {
 		return {
 			name: '',
@@ -73,7 +75,7 @@ export default Vue.extend({
 			}).then(() => {
 				this.$swal({
 					type: 'success',
-					text: '%i18n:@add-emoji.added%'
+					text: this.$t('add-emoji.added')
 				});
 				this.fetchEmojis();
 			}).catch(e => {
@@ -101,7 +103,7 @@ export default Vue.extend({
 			}).then(() => {
 				this.$swal({
 					type: 'success',
-					text: '%i18n:@updated%'
+					text: this.$t('updated')
 				});
 			}).catch(e => {
 				this.$swal({
@@ -114,7 +116,7 @@ export default Vue.extend({
 		removeEmoji(emoji) {
 			this.$swal({
 				type: 'warning',
-				text: '%i18n:@remove-emoji.are-you-sure%'.replace('$1', emoji.name),
+				text: this.$t('remove-emoji.are-you-sure').replace('$1', emoji.name),
 				showCancelButton: true
 			}).then(res => {
 				if (!res.value) return;
@@ -124,7 +126,7 @@ export default Vue.extend({
 				}).then(() => {
 					this.$swal({
 						type: 'success',
-						text: '%i18n:@remove-emoji.removed%'
+						text: this.$t('remove-emoji.removed')
 					});
 					this.fetchEmojis();
 				}).catch(e => {
diff --git a/src/client/app/admin/views/hashtags.vue b/src/client/app/admin/views/hashtags.vue
index 1635305f17..9ddb378a8d 100644
--- a/src/client/app/admin/views/hashtags.vue
+++ b/src/client/app/admin/views/hashtags.vue
@@ -1,19 +1,21 @@
 <template>
 <div>
 	<ui-card>
-		<div slot="title">%i18n:@hided-tags%</div>
+		<div slot="title">{{ $t('hided-tags') }}</div>
 		<section>
 			<textarea class="jdnqwkzlnxcfftthoybjxrebyolvoucw" v-model="hidedTags"></textarea>
-			<ui-button @click="save">%i18n:@save%</ui-button>
+			<ui-button @click="save">{{ $t('save') }}</ui-button>
 		</section>
 	</ui-card>
 </div>
 </template>
 
 <script lang="ts">
-import Vue from "vue";
+import Vue from 'vue';
+import i18n from '../../i18n';
 
 export default Vue.extend({
+	i18n: i18n('admin/views/hashtags.vue'),
 	data() {
 		return {
 			hidedTags: '',
diff --git a/src/client/app/admin/views/index.vue b/src/client/app/admin/views/index.vue
index 33465b9b63..c812f28855 100644
--- a/src/client/app/admin/views/index.vue
+++ b/src/client/app/admin/views/index.vue
@@ -18,18 +18,18 @@
 			<p class="name">{{ $store.state.i | userName }}</p>
 		</div>
 		<ul>
-			<li @click="nav('dashboard')" :class="{ active: page == 'dashboard' }"><fa icon="home" fixed-width/>%i18n:@dashboard%</li>
-			<li @click="nav('instance')" :class="{ active: page == 'instance' }"><fa icon="cog" fixed-width/>%i18n:@instance%</li>
-			<li @click="nav('users')" :class="{ active: page == 'users' }"><fa icon="users" fixed-width/>%i18n:@users%</li>
-			<li @click="nav('emoji')" :class="{ active: page == 'emoji' }"><fa :icon="['far', 'grin']" fixed-width/>%i18n:@emoji%</li>
-			<li @click="nav('announcements')" :class="{ active: page == 'announcements' }"><fa icon="broadcast-tower" fixed-width/>%i18n:@announcements%</li>
-			<li @click="nav('hashtags')" :class="{ active: page == 'hashtags' }"><fa icon="hashtag" fixed-width/>%i18n:@hashtags%</li>
+			<li @click="nav('dashboard')" :class="{ active: page == 'dashboard' }"><fa icon="home" fixed-width/>{{ $t('dashboard') }}</li>
+			<li @click="nav('instance')" :class="{ active: page == 'instance' }"><fa icon="cog" fixed-width/>{{ $t('instance') }}</li>
+			<li @click="nav('users')" :class="{ active: page == 'users' }"><fa icon="users" fixed-width/>{{ $t('users') }}</li>
+			<li @click="nav('emoji')" :class="{ active: page == 'emoji' }"><fa :icon="['far', 'grin']" fixed-width/>{{ $t('emoji') }}</li>
+			<li @click="nav('announcements')" :class="{ active: page == 'announcements' }"><fa icon="broadcast-tower" fixed-width/>{{ $t('announcements') }}</li>
+			<li @click="nav('hashtags')" :class="{ active: page == 'hashtags' }"><fa icon="hashtag" fixed-width/>{{ $t('hashtags') }}</li>
 
-			<!-- <li @click="nav('drive')" :class="{ active: page == 'drive' }"><fa icon="cloud" fixed-width/>%i18n:common.drive%</li> -->
-			<!-- <li @click="nav('update')" :class="{ active: page == 'update' }">%i18n:@update%</li> -->
+			<!-- <li @click="nav('drive')" :class="{ active: page == 'drive' }"><fa icon="cloud" fixed-width/>{{ $t('@.drive') }}</li> -->
+			<!-- <li @click="nav('update')" :class="{ active: page == 'update' }">{{ $t('update') }}</li> -->
 		</ul>
 		<div class="back-to-misskey">
-			<a href="/"><fa icon="arrow-left"/> %i18n:@back-to-misskey%</a>
+			<a href="/"><fa icon="arrow-left"/> {{ $t('back-to-misskey') }}</a>
 		</div>
 		<div class="version">
 			<small>Misskey {{ version }}</small>
@@ -49,7 +49,8 @@
 </template>
 
 <script lang="ts">
-import Vue from "vue";
+import Vue from 'vue';
+import i18n from '../../i18n';
 import { version } from '../../config';
 import XDashboard from "./dashboard.vue";
 import XInstance from "./instance.vue";
@@ -63,6 +64,7 @@ const ua = navigator.userAgent.toLowerCase();
 const isMobile = /mobile|iphone|ipad|android/.test(ua);
 
 export default Vue.extend({
+	i18n: i18n('admin/views/index.vue'),
 	components: {
 		XDashboard,
 		XInstance,
diff --git a/src/client/app/admin/views/instance.vue b/src/client/app/admin/views/instance.vue
index 73d504ef9b..f5a29e5b52 100644
--- a/src/client/app/admin/views/instance.vue
+++ b/src/client/app/admin/views/instance.vue
@@ -1,87 +1,89 @@
 <template>
 <div class="axbwjelsbymowqjyywpirzhdlszoncqs">
 	<ui-card>
-		<div slot="title"><fa icon="cog"/> %i18n:@instance%</div>
+		<div slot="title"><fa icon="cog"/> {{ $t('instance') }}</div>
 		<section class="fit-top fit-bottom">
-			<ui-input v-model="name">%i18n:@instance-name%</ui-input>
-			<ui-textarea v-model="description">%i18n:@instance-description%</ui-textarea>
-			<ui-input v-model="bannerUrl"><i slot="icon"><fa icon="link"/></i>%i18n:@banner-url%</ui-input>
-			<ui-input v-model="languages"><i slot="icon"><fa icon="language"/></i>%i18n:@languages%<span slot="desc">%i18n:@languages-desc%</span></ui-input>
+			<ui-input v-model="name">{{ $t('instance-name') }}</ui-input>
+			<ui-textarea v-model="description">{{ $t('instance-description') }}</ui-textarea>
+			<ui-input v-model="bannerUrl"><i slot="icon"><fa icon="link"/></i>{{ $t('banner-url') }}</ui-input>
+			<ui-input v-model="languages"><i slot="icon"><fa icon="language"/></i>{{ $t('languages') }}<span slot="desc">{{ $t('languages-desc') }}</span></ui-input>
 		</section>
 		<section class="fit-bottom">
-			<header><fa icon="headset"/> %i18n:@maintainer-config%</header>
-			<ui-input v-model="maintainerName">%i18n:@maintainer-name%</ui-input>
-			<ui-input v-model="maintainerEmail" type="email"><i slot="icon"><fa :icon="['far', 'envelope']"/></i>%i18n:@maintainer-email%</ui-input>
+			<header><fa icon="headset"/> {{ $t('maintainer-config') }}</header>
+			<ui-input v-model="maintainerName">{{ $t('maintainer-name') }}</ui-input>
+			<ui-input v-model="maintainerEmail" type="email"><i slot="icon"><fa :icon="['far', 'envelope']"/></i>{{ $t('maintainer-email') }}</ui-input>
 		</section>
 		<section class="fit-top fit-bottom">
-			<ui-input v-model="maxNoteTextLength">%i18n:@max-note-text-length%</ui-input>
+			<ui-input v-model="maxNoteTextLength">{{ $t('max-note-text-length') }}</ui-input>
 		</section>
 		<section class="fit-bottom">
-			<header><fa icon="cloud"/> %i18n:@drive-config%</header>
-			<ui-switch v-model="cacheRemoteFiles">%i18n:@cache-remote-files%<span slot="desc">%i18n:@cache-remote-files-desc%</span></ui-switch>
-			<ui-input v-model="localDriveCapacityMb" type="number">%i18n:@local-drive-capacity-mb%<span slot="suffix">MB</span><span slot="desc">%i18n:@mb%</span></ui-input>
-			<ui-input v-model="remoteDriveCapacityMb" type="number" :disabled="!cacheRemoteFiles">%i18n:@remote-drive-capacity-mb%<span slot="suffix">MB</span><span slot="desc">%i18n:@mb%</span></ui-input>
+			<header><fa icon="cloud"/> {{ $t('drive-config') }}</header>
+			<ui-switch v-model="cacheRemoteFiles">{{ $t('cache-remote-files') }}<span slot="desc">{{ $t('cache-remote-files-desc') }}</span></ui-switch>
+			<ui-input v-model="localDriveCapacityMb" type="number">{{ $t('local-drive-capacity-mb') }}<span slot="suffix">MB</span><span slot="desc">{{ $t('mb') }}</span></ui-input>
+			<ui-input v-model="remoteDriveCapacityMb" type="number" :disabled="!cacheRemoteFiles">{{ $t('remote-drive-capacity-mb') }}<span slot="suffix">MB</span><span slot="desc">{{ $t('mb') }}</span></ui-input>
 		</section>
 		<section class="fit-bottom">
-			<header><fa icon="shield-alt"/> %i18n:@recaptcha-config%</header>
-			<ui-switch v-model="enableRecaptcha">%i18n:@enable-recaptcha%</ui-switch>
-			<ui-info>%i18n:@recaptcha-info%</ui-info>
-			<ui-input v-model="recaptchaSiteKey" :disabled="!enableRecaptcha"><i slot="icon"><fa icon="key"/></i>%i18n:@recaptcha-site-key%</ui-input>
-			<ui-input v-model="recaptchaSecretKey" :disabled="!enableRecaptcha"><i slot="icon"><fa icon="key"/></i>%i18n:@recaptcha-secret-key%</ui-input>
+			<header><fa icon="shield-alt"/> {{ $t('recaptcha-config') }}</header>
+			<ui-switch v-model="enableRecaptcha">{{ $t('enable-recaptcha') }}</ui-switch>
+			<ui-info>{{ $t('recaptcha-info') }}</ui-info>
+			<ui-input v-model="recaptchaSiteKey" :disabled="!enableRecaptcha"><i slot="icon"><fa icon="key"/></i>{{ $t('recaptcha-site-key') }}</ui-input>
+			<ui-input v-model="recaptchaSecretKey" :disabled="!enableRecaptcha"><i slot="icon"><fa icon="key"/></i>{{ $t('recaptcha-secret-key') }}</ui-input>
 		</section>
 		<section>
-			<header><fa icon="ghost"/> %i18n:@proxy-account-config%</header>
-			<ui-info>%i18n:@proxy-account-info%</ui-info>
-			<ui-input v-model="proxyAccount"><span slot="prefix">@</span>%i18n:@proxy-account-username%<span slot="desc">%i18n:@proxy-account-username-desc%</span></ui-input>
-			<ui-info warn>%i18n:@proxy-account-warn%</ui-info>
+			<header><fa icon="ghost"/> {{ $t('proxy-account-config') }}</header>
+			<ui-info>{{ $t('proxy-account-info') }}</ui-info>
+			<ui-input v-model="proxyAccount"><span slot="prefix">@</span>{{ $t('proxy-account-username') }}<span slot="desc">{{ $t('proxy-account-username-desc') }}</span></ui-input>
+			<ui-info warn>{{ $t('proxy-account-warn') }}</ui-info>
 		</section>
 		<section>
-			<ui-switch v-model="disableRegistration">%i18n:@disable-registration%</ui-switch>
+			<ui-switch v-model="disableRegistration">{{ $t('disable-registration') }}</ui-switch>
 		</section>
 		<section>
-			<ui-switch v-model="disableLocalTimeline">%i18n:@disable-local-timeline%</ui-switch>
+			<ui-switch v-model="disableLocalTimeline">{{ $t('disable-local-timeline') }}</ui-switch>
 		</section>
 		<section>
-			<ui-button @click="updateMeta">%i18n:@save%</ui-button>
+			<ui-button @click="updateMeta">{{ $t('save') }}</ui-button>
 		</section>
 	</ui-card>
 
 	<ui-card>
-		<div slot="title">%i18n:@invite%</div>
+		<div slot="title">{{ $t('invite') }}</div>
 		<section>
-			<ui-button @click="invite">%i18n:@invite%</ui-button>
+			<ui-button @click="invite">{{ $t('invite') }}</ui-button>
 			<p v-if="inviteCode">Code: <code>{{ inviteCode }}</code></p>
 		</section>
 	</ui-card>
 
 	<ui-card>
-		<div slot="title"><fa :icon="['fab', 'twitter']"/> %i18n:@twitter-integration-config%</div>
+		<div slot="title"><fa :icon="['fab', 'twitter']"/> {{ $t('twitter-integration-config') }}</div>
 		<section>
-			<ui-switch v-model="enableTwitterIntegration">%i18n:@enable-twitter-integration%</ui-switch>
-			<ui-info>%i18n:@twitter-integration-info%</ui-info>
-			<ui-input v-model="twitterConsumerKey" :disabled="!enableTwitterIntegration"><i slot="icon"><fa icon="key"/></i>%i18n:@twitter-integration-consumer-key%</ui-input>
-			<ui-input v-model="twitterConsumerSecret" :disabled="!enableTwitterIntegration"><i slot="icon"><fa icon="key"/></i>%i18n:@twitter-integration-consumer-secret%</ui-input>
-			<ui-button @click="updateMeta">%i18n:@save%</ui-button>
+			<ui-switch v-model="enableTwitterIntegration">{{ $t('enable-twitter-integration') }}</ui-switch>
+			<ui-info>{{ $t('twitter-integration-info') }}</ui-info>
+			<ui-input v-model="twitterConsumerKey" :disabled="!enableTwitterIntegration"><i slot="icon"><fa icon="key"/></i>{{ $t('twitter-integration-consumer-key') }}</ui-input>
+			<ui-input v-model="twitterConsumerSecret" :disabled="!enableTwitterIntegration"><i slot="icon"><fa icon="key"/></i>{{ $t('twitter-integration-consumer-secret') }}</ui-input>
+			<ui-button @click="updateMeta">{{ $t('save') }}</ui-button>
 		</section>
 	</ui-card>
 
 	<ui-card>
-		<div slot="title"><fa :icon="['fab', 'github']"/> %i18n:@github-integration-config%</div>
+		<div slot="title"><fa :icon="['fab', 'github']"/> {{ $t('github-integration-config') }}</div>
 		<section>
-			<ui-switch v-model="enableGithubIntegration">%i18n:@enable-github-integration%</ui-switch>
-			<ui-info>%i18n:@github-integration-info%</ui-info>
-			<ui-input v-model="githubClientId" :disabled="!enableGithubIntegration"><i slot="icon"><fa icon="key"/></i>%i18n:@github-integration-client-id%</ui-input>
-			<ui-input v-model="githubClientSecret" :disabled="!enableGithubIntegration"><i slot="icon"><fa icon="key"/></i>%i18n:@github-integration-client-secret%</ui-input>
-			<ui-button @click="updateMeta">%i18n:@save%</ui-button>
+			<ui-switch v-model="enableGithubIntegration">{{ $t('enable-github-integration') }}</ui-switch>
+			<ui-info>{{ $t('github-integration-info') }}</ui-info>
+			<ui-input v-model="githubClientId" :disabled="!enableGithubIntegration"><i slot="icon"><fa icon="key"/></i>{{ $t('github-integration-client-id') }}</ui-input>
+			<ui-input v-model="githubClientSecret" :disabled="!enableGithubIntegration"><i slot="icon"><fa icon="key"/></i>{{ $t('github-integration-client-secret') }}</ui-input>
+			<ui-button @click="updateMeta">{{ $t('save') }}</ui-button>
 		</section>
 	</ui-card>
 </div>
 </template>
 
 <script lang="ts">
-import Vue from "vue";
+import Vue from 'vue';
+import i18n from '../../i18n';
 
 export default Vue.extend({
+	i18n: i18n('admin/views/instance.vue'),
 	data() {
 		return {
 			maintainerName: null,
@@ -174,7 +176,7 @@ export default Vue.extend({
 			}).then(() => {
 				this.$swal({
 					type: 'success',
-					text: '%i18n:@saved%'
+					text: this.$t('saved')
 				});
 			}).catch(e => {
 				this.$swal({
diff --git a/src/client/app/admin/views/users.vue b/src/client/app/admin/views/users.vue
index ed9f372f09..6cc0a420e9 100644
--- a/src/client/app/admin/views/users.vue
+++ b/src/client/app/admin/views/users.vue
@@ -1,52 +1,54 @@
 <template>
 <div class="ucnffhbtogqgscfmqcymwmmupoknpfsw">
 	<ui-card>
-		<div slot="title">%i18n:@verify-user%</div>
+		<div slot="title">{{ $t('verify-user') }}</div>
 		<section class="fit-top">
 			<ui-input v-model="verifyUsername" type="text">
 				<span slot="prefix">@</span>
 			</ui-input>
-			<ui-button @click="verifyUser" :disabled="verifying">%i18n:@verify%</ui-button>
+			<ui-button @click="verifyUser" :disabled="verifying">{{ $t('verify') }}</ui-button>
 		</section>
 	</ui-card>
 
 	<ui-card>
-		<div slot="title">%i18n:@unverify-user%</div>
+		<div slot="title">{{ $t('unverify-user') }}</div>
 		<section class="fit-top">
 			<ui-input v-model="unverifyUsername" type="text">
 				<span slot="prefix">@</span>
 			</ui-input>
-			<ui-button @click="unverifyUser" :disabled="unverifying">%i18n:@unverify%</ui-button>
+			<ui-button @click="unverifyUser" :disabled="unverifying">{{ $t('unverify') }}</ui-button>
 		</section>
 	</ui-card>
 
 	<ui-card>
-		<div slot="title">%i18n:@suspend-user%</div>
+		<div slot="title">{{ $t('suspend-user') }}</div>
 		<section class="fit-top">
 			<ui-input v-model="suspendUsername" type="text">
 				<span slot="prefix">@</span>
 			</ui-input>
-			<ui-button @click="suspendUser" :disabled="suspending">%i18n:@suspend%</ui-button>
+			<ui-button @click="suspendUser" :disabled="suspending">{{ $t('suspend') }}</ui-button>
 		</section>
 	</ui-card>
 
 	<ui-card>
-		<div slot="title">%i18n:@unsuspend-user%</div>
+		<div slot="title">{{ $t('unsuspend-user') }}</div>
 		<section class="fit-top">
 			<ui-input v-model="unsuspendUsername" type="text">
 				<span slot="prefix">@</span>
 			</ui-input>
-			<ui-button @click="unsuspendUser" :disabled="unsuspending">%i18n:@unsuspend%</ui-button>
+			<ui-button @click="unsuspendUser" :disabled="unsuspending">{{ $t('unsuspend') }}</ui-button>
 		</section>
 	</ui-card>
 </div>
 </template>
 
 <script lang="ts">
-import Vue from "vue";
+import Vue from 'vue';
+import i18n from '../../i18n';
 import parseAcct from "../../../../misc/acct/parse";
 
 export default Vue.extend({
+	i18n: i18n('admin/views/users.vue'),
 	data() {
 		return {
 			verifyUsername: null,
@@ -67,7 +69,7 @@ export default Vue.extend({
 			const process = async () => {
 				const user = await (this as any).os.api('users/show', parseAcct(this.verifyUsername));
 				await (this as any).os.api('admin/verify-user', { userId: user.id });
-				//(this as any).os.apis.dialog({ text: '%i18n:@verified%' });
+				//(this as any).os.apis.dialog({ text: this.$t('verified') });
 			};
 
 			await process().catch(e => {
@@ -83,7 +85,7 @@ export default Vue.extend({
 			const process = async () => {
 				const user = await (this as any).os.api('users/show', parseAcct(this.unverifyUsername));
 				await (this as any).os.api('admin/unverify-user', { userId: user.id });
-				//(this as any).os.apis.dialog({ text: '%i18n:@unverified%' });
+				//(this as any).os.apis.dialog({ text: this.$t('unverified') });
 			};
 
 			await process().catch(e => {
@@ -99,7 +101,7 @@ export default Vue.extend({
 			const process = async () => {
 				const user = await (this as any).os.api('users/show', parseAcct(this.suspendUsername));
 				await (this as any).os.api('admin/suspend-user', { userId: user.id });
-				//(this as any).os.apis.dialog({ text: '%i18n:@suspended%' });
+				//(this as any).os.apis.dialog({ text: this.$t('suspended') });
 			};
 
 			await process().catch(e => {
@@ -115,7 +117,7 @@ export default Vue.extend({
 			const process = async () => {
 				const user = await (this as any).os.api('users/show', parseAcct(this.unsuspendUsername));
 				await (this as any).os.api('admin/unsuspend-user', { userId: user.id });
-				//(this as any).os.apis.dialog({ text: '%i18n:@unsuspended%' });
+				//(this as any).os.apis.dialog({ text: this.$t('unsuspended') });
 			};
 
 			await process().catch(e => {
diff --git a/src/client/app/auth/views/form.vue b/src/client/app/auth/views/form.vue
index d18a4f7eb4..6bd0acb359 100644
--- a/src/client/app/auth/views/form.vue
+++ b/src/client/app/auth/views/form.vue
@@ -1,7 +1,7 @@
 <template>
 <div class="form">
 	<header>
-		<h1>%i18n:@share-access%</h1>
+		<h1>{{ $t('share-access') }}</h1>
 		<img :src="app.iconUrl"/>
 	</header>
 	<div class="app">
@@ -11,32 +11,35 @@
 			<p class="description">{{ app.description }}</p>
 		</section>
 		<section>
-			<h2>%i18n:@permission-ask%</h2>
+			<h2>{{ $t('permission-ask') }}</h2>
 			<ul>
 				<template v-for="p in app.permission">
-					<li v-if="p == 'account-read'">%i18n:@account-read%</li>
-					<li v-if="p == 'account-write'">%i18n:@account-write%</li>
-					<li v-if="p == 'note-write'">%i18n:@note-write%</li>
-					<li v-if="p == 'like-write'">%i18n:@like-write%</li>
-					<li v-if="p == 'following-write'">%i18n:@following-write%</li>
-					<li v-if="p == 'drive-read'">%i18n:@drive-read%</li>
-					<li v-if="p == 'drive-write'">%i18n:@drive-write%</li>
-					<li v-if="p == 'notification-read'">%i18n:@notification-read%</li>
-					<li v-if="p == 'notification-write'">%i18n:@notification-write%</li>
+					<li v-if="p == 'account-read'">{{ $t('account-read') }}</li>
+					<li v-if="p == 'account-write'">{{ $t('account-write') }}</li>
+					<li v-if="p == 'note-write'">{{ $t('note-write') }}</li>
+					<li v-if="p == 'like-write'">{{ $t('like-write') }}</li>
+					<li v-if="p == 'following-write'">{{ $t('following-write') }}</li>
+					<li v-if="p == 'drive-read'">{{ $t('drive-read') }}</li>
+					<li v-if="p == 'drive-write'">{{ $t('drive-write') }}</li>
+					<li v-if="p == 'notification-read'">{{ $t('notification-read') }}</li>
+					<li v-if="p == 'notification-write'">{{ $t('notification-write') }}</li>
 				</template>
 			</ul>
 		</section>
 	</div>
 	<div class="action">
-		<button @click="cancel">%i18n:@cancel%</button>
-		<button @click="accept">%i18n:@accept%</button>
+		<button @click="cancel">{{ $t('cancel') }}</button>
+		<button @click="accept">{{ $t('accept') }}</button>
 	</div>
 </div>
 </template>
 
 <script lang="ts">
 import Vue from 'vue';
+import i18n from '../../i18n';
+
 export default Vue.extend({
+	i18n: i18n('auth/views/form.vue'),
 	props: ['session'],
 	computed: {
 		app(): any {
diff --git a/src/client/app/auth/views/index.vue b/src/client/app/auth/views/index.vue
index ba7df911e5..9158ade721 100644
--- a/src/client/app/auth/views/index.vue
+++ b/src/client/app/auth/views/index.vue
@@ -1,7 +1,7 @@
 <template>
 <div class="index">
 	<main v-if="$store.getters.isSignedIn">
-		<p class="fetching" v-if="fetching">%i18n:@loading%<mk-ellipsis/></p>
+		<p class="fetching" v-if="fetching">{{ $t('loading') }}<mk-ellipsis/></p>
 		<x-form
 			class="form"
 			ref="form"
@@ -11,20 +11,20 @@
 			@accepted="accepted"
 		/>
 		<div class="denied" v-if="state == 'denied'">
-			<h1>%i18n:@denied%</h1>
-			<p>%i18n:@denied-paragraph%</p>
+			<h1>{{ $t('denied') }}</h1>
+			<p>{{ $t('denied-paragraph') }}</p>
 		</div>
 		<div class="accepted" v-if="state == 'accepted'">
-			<h1>{{ session.app.isAuthorized ? '%i18n:@already-authorized%' : '%i18n:@allowed%' }}</h1>
-			<p v-if="session.app.callbackUrl">%i18n:@callback-url%<mk-ellipsis/></p>
-			<p v-if="!session.app.callbackUrl">%i18n:@please-go-back%</p>
+			<h1>{{ session.app.isAuthorized ? this.$t('already-authorized') : this.$t('allowed') }}</h1>
+			<p v-if="session.app.callbackUrl">{{ $t('callback-url') }}<mk-ellipsis/></p>
+			<p v-if="!session.app.callbackUrl">{{ $t('please-go-back') }}</p>
 		</div>
 		<div class="error" v-if="state == 'fetch-session-error'">
-			<p>%i18n:@error%</p>
+			<p>{{ $t('error') }}</p>
 		</div>
 	</main>
 	<main class="signin" v-if="!$store.getters.isSignedIn">
-		<h1>%i18n:@sign-in%</h1>
+		<h1>{{ $t('sign-in') }}</h1>
 		<mk-signin/>
 	</main>
 	<footer><img src="/assets/auth/icon.svg" alt="Misskey"/></footer>
@@ -33,9 +33,11 @@
 
 <script lang="ts">
 import Vue from 'vue';
+import i18n from '../../i18n';
 import XForm from './form.vue';
 
 export default Vue.extend({
+	i18n: i18n('auth/views/index.vue'),
 	components: {
 		XForm
 	},
diff --git a/src/client/app/common/views/components/api-settings.vue b/src/client/app/common/views/components/api-settings.vue
index 3a9cd06c80..52750ad5c6 100644
--- a/src/client/app/common/views/components/api-settings.vue
+++ b/src/client/app/common/views/components/api-settings.vue
@@ -4,29 +4,29 @@
 
 	<section class="fit-top">
 		<ui-input :value="$store.state.i.token" readonly>
-			<span>%i18n:@token%</span>
+			<span>{{ $t('token') }}</span>
 		</ui-input>
-		<p>%i18n:@intro%</p>
-		<ui-info warn>%i18n:@caution%</ui-info>
-		<p>%i18n:@regeneration-of-token%</p>
-		<ui-button @click="regenerateToken"><fa icon="sync-alt"/> %i18n:@regenerate-token%</ui-button>
+		<p>{{ $t('intro') }}</p>
+		<ui-info warn>{{ $t('caution') }}</ui-info>
+		<p>{{ $t('regeneration-of-token') }}</p>
+		<ui-button @click="regenerateToken"><fa icon="sync-alt"/> {{ $t('regenerate-token') }}</ui-button>
 	</section>
 
 	<section>
-		<header><fa icon="terminal"/> %i18n:@console.title%</header>
+		<header><fa icon="terminal"/> {{ $t('console.title') }}</header>
 		<ui-input v-model="endpoint">
-			<span>%i18n:@console.endpoint%</span>
+			<span>{{ $t('console.endpoint') }}</span>
 		</ui-input>
 		<ui-textarea v-model="body">
-			<span>%i18n:@console.parameter% (JSON or JSON5)</span>
-			<span slot="desc">%i18n:@console.credential-info%</span>
+			<span>{{ $t('console.parameter') }} (JSON or JSON5)</span>
+			<span slot="desc">{{ $t('console.credential-info') }}</span>
 		</ui-textarea>
 		<ui-button @click="send" :disabled="sending">
-			<template v-if="sending">%i18n:@console.sending%</template>
-			<template v-else><fa icon="paper-plane"/> %i18n:@console.send%</template>
+			<template v-if="sending">{{ $t('console.sending') }}</template>
+			<template v-else><fa icon="paper-plane"/> {{ $t('console.send') }}</template>
 		</ui-button>
 		<ui-textarea v-if="res" v-model="res" readonly tall>
-			<span>%i18n:@console.response%</span>
+			<span>{{ $t('console.response') }}</span>
 		</ui-textarea>
 	</section>
 </ui-card>
@@ -34,9 +34,11 @@
 
 <script lang="ts">
 import Vue from 'vue';
+import i18n from '../../../i18n';
 import * as JSON5 from 'json5';
 
 export default Vue.extend({
+	i18n: i18n('common/views/components/api-settings.vue'),
 	data() {
 		return {
 			endpoint: '',
@@ -49,7 +51,7 @@ export default Vue.extend({
 	methods: {
 		regenerateToken() {
 			(this as any).apis.input({
-				title: '%i18n:@enter-password%',
+				title: this.$t('enter-password'),
 				type: 'password'
 			}).then(password => {
 				(this as any).api('i/regenerate_token', {
diff --git a/src/client/app/common/views/components/avatar.vue b/src/client/app/common/views/components/avatar.vue
index ac018abcfc..698873833d 100644
--- a/src/client/app/common/views/components/avatar.vue
+++ b/src/client/app/common/views/components/avatar.vue
@@ -15,6 +15,7 @@
 
 <script lang="ts">
 import Vue from 'vue';
+
 export default Vue.extend({
 	props: {
 		user: {
diff --git a/src/client/app/common/views/components/connect-failed.troubleshooter.vue b/src/client/app/common/views/components/connect-failed.troubleshooter.vue
index 544bb7eaa2..19b8c3e974 100644
--- a/src/client/app/common/views/components/connect-failed.troubleshooter.vue
+++ b/src/client/app/common/views/components/connect-failed.troubleshooter.vue
@@ -1,47 +1,49 @@
 <template>
 <div class="troubleshooter">
 	<div class="body">
-		<h1><fa icon="wrench"/>%i18n:@title%</h1>
+		<h1><fa icon="wrench"/>{{ $t('title') }}</h1>
 		<div>
 			<p :data-wip="network == null">
 				<template v-if="network != null">
 					<template v-if="network"><fa icon="check"/></template>
 					<template v-if="!network"><fa icon="times"/></template>
 				</template>
-				{{ network == null ? '%i18n:@checking-network%' : '%i18n:@network%' }}<mk-ellipsis v-if="network == null"/>
+				{{ network == null ? this.$t('checking-network') : this.$t('network') }}<mk-ellipsis v-if="network == null"/>
 			</p>
 			<p v-if="network == true" :data-wip="internet == null">
 				<template v-if="internet != null">
 					<template v-if="internet"><fa icon="check"/></template>
 					<template v-if="!internet"><fa icon="times"/></template>
 				</template>
-				{{ internet == null ? '%i18n:@checking-internet%' : '%i18n:@internet%' }}<mk-ellipsis v-if="internet == null"/>
+				{{ internet == null ? this.$t('checking-internet') : this.$t('internet') }}<mk-ellipsis v-if="internet == null"/>
 			</p>
 			<p v-if="internet == true" :data-wip="server == null">
 				<template v-if="server != null">
 					<template v-if="server"><fa icon="check"/></template>
 					<template v-if="!server"><fa icon="times"/></template>
 				</template>
-				{{ server == null ? '%i18n:@checking-server%' : '%i18n:@server%' }}<mk-ellipsis v-if="server == null"/>
+				{{ server == null ? this.$t('checking-server') : this.$t('server') }}<mk-ellipsis v-if="server == null"/>
 			</p>
 		</div>
-		<p v-if="!end">%i18n:@finding%<mk-ellipsis/></p>
-		<p v-if="network === false"><b><fa icon="exclamation-triangle"/>%i18n:@no-network%</b><br>%i18n:@no-network-desc%</p>
-		<p v-if="internet === false"><b><fa icon="exclamation-triangle"/>%i18n:@no-internet%</b><br>%i18n:@no-internet-desc%</p>
-		<p v-if="server === false"><b><fa icon="exclamation-triangle"/>%i18n:@no-server%</b><br>%i18n:@no-server-desc%</p>
-		<p v-if="server === true" class="success"><b><fa icon="info-circle"/>%i18n:@success%</b><br>%i18n:@success-desc%</p>
+		<p v-if="!end">{{ $t('finding') }}<mk-ellipsis/></p>
+		<p v-if="network === false"><b><fa icon="exclamation-triangle"/>{{ $t('no-network') }}</b><br>{{ $t('no-network-desc') }}</p>
+		<p v-if="internet === false"><b><fa icon="exclamation-triangle"/>{{ $t('no-internet') }}</b><br>{{ $t('no-internet-desc') }}</p>
+		<p v-if="server === false"><b><fa icon="exclamation-triangle"/>{{ $t('no-server') }}</b><br>{{ $t('no-server-desc') }}</p>
+		<p v-if="server === true" class="success"><b><fa icon="info-circle"/>{{ $t('success') }}</b><br>{{ $t('success-desc') }}</p>
 	</div>
 	<footer>
-		<a href="/assets/flush.html">%i18n:@flush%</a> | <a href="/assets/version.html">%i18n:@set-version%</a>
+		<a href="/assets/flush.html">{{ $t('flush') }}</a> | <a href="/assets/version.html">{{ $t('set-version') }}</a>
 	</footer>
 </div>
 </template>
 
 <script lang="ts">
 import Vue from 'vue';
+import i18n from '../../../i18n';
 import { apiUrl } from '../../../config';
 
 export default Vue.extend({
+	i18n: i18n('common/views/components/connect-failed.troubleshooter.vue'),
 	data() {
 		return {
 			network: navigator.onLine,
diff --git a/src/client/app/common/views/components/connect-failed.vue b/src/client/app/common/views/components/connect-failed.vue
index 8567f9a15c..b9f8181dae 100644
--- a/src/client/app/common/views/components/connect-failed.vue
+++ b/src/client/app/common/views/components/connect-failed.vue
@@ -1,23 +1,25 @@
 <template>
 <div class="mk-connect-failed">
 	<img src="https://raw.githubusercontent.com/syuilo/misskey/develop/src/client/assets/error.jpg" alt=""/>
-	<h1>%i18n:@title%</h1>
+	<h1>{{ $t('title') }}</h1>
 	<p class="text">
-		<span>{{ '%i18n:@description%'.substr(0, '%i18n:@description%'.indexOf('{')) }}</span>
-		<a @click="reload">{{ '%i18n:@description%'.match(/\{(.+?)\}/)[1] }}</a>
-		<span>{{ '%i18n:@description%'.substr('%i18n:@description%'.indexOf('}') + 1) }}</span>
+		<span>{{ this.$t('description').substr(0, this.$t('description').indexOf('{')) }}</span>
+		<a @click="reload">{{ this.$t('description').match(/\{(.+?)\}/)[1] }}</a>
+		<span>{{ this.$t('description').substr(this.$t('description').indexOf('}') + 1) }}</span>
 	</p>
-	<button v-if="!troubleshooting" @click="troubleshooting = true">%i18n:@troubleshoot%</button>
+	<button v-if="!troubleshooting" @click="troubleshooting = true">{{ $t('troubleshoot') }}</button>
 	<x-troubleshooter v-if="troubleshooting"/>
-	<p class="thanks">%i18n:@thanks%</p>
+	<p class="thanks">{{ $t('thanks') }}</p>
 </div>
 </template>
 
 <script lang="ts">
 import Vue from 'vue';
+import i18n from '../../../i18n';
 import XTroubleshooter from './connect-failed.troubleshooter.vue';
 
 export default Vue.extend({
+	i18n: i18n('common/views/components/connect-failed.vue'),
 	components: {
 		XTroubleshooter
 	},
diff --git a/src/client/app/common/views/components/cw-button.vue b/src/client/app/common/views/components/cw-button.vue
index 79917f82ab..bda39f2d48 100644
--- a/src/client/app/common/views/components/cw-button.vue
+++ b/src/client/app/common/views/components/cw-button.vue
@@ -1,11 +1,13 @@
 <template>
-<button class="nrvgflfuaxwgkxoynpnumyookecqrrvh" @click="toggle">{{ value ? '%i18n:@hide%' : '%i18n:@show%' }}</button>
+<button class="nrvgflfuaxwgkxoynpnumyookecqrrvh" @click="toggle">{{ value ? this.$t('hide') : this.$t('show') }}</button>
 </template>
 
 <script lang="ts">
 import Vue from 'vue';
+import i18n from '../../../i18n';
 
 export default Vue.extend({
+	i18n: i18n('common/views/components/cw-button.vue'),
 	props: {
 		value: {
 			type: Boolean,
diff --git a/src/client/app/common/views/components/drive-settings.vue b/src/client/app/common/views/components/drive-settings.vue
index 1d0da3a53f..fbf70f71f1 100644
--- a/src/client/app/common/views/components/drive-settings.vue
+++ b/src/client/app/common/views/components/drive-settings.vue
@@ -1,14 +1,14 @@
 <template>
 <ui-card>
-	<div slot="title"><fa icon="cloud"/> %i18n:common.drive%</div>
+	<div slot="title"><fa icon="cloud"/> {{ $t('@.drive') }}</div>
 
 	<section v-if="!fetching" class="juakhbxthdewydyreaphkepoxgxvfogn">
 		<div class="meter"><div :style="meterStyle"></div></div>
-		<p>%i18n:@max%: <b>{{ capacity | bytes }}</b> %i18n:@in-use%: <b>{{ usage | bytes }}</b></p>
+		<p>{{ $t('max') }}: <b>{{ capacity | bytes }}</b> {{ $t('in-use') }}: <b>{{ usage | bytes }}</b></p>
 	</section>
 
 	<section>
-		<header>%i18n:@stats%</header>
+		<header>{{ $t('stats') }}</header>
 		<div ref="chart" style="margin-bottom: -16px; color: #000;"></div>
 	</section>
 </ui-card>
@@ -16,10 +16,12 @@
 
 <script lang="ts">
 import Vue from 'vue';
+import i18n from '../../../i18n';
 import * as tinycolor from 'tinycolor2';
 import * as ApexCharts from 'apexcharts';
 
 export default Vue.extend({
+	i18n: i18n('common/views/components/drive-settings.vue'),
 	data() {
 		return {
 			fetching: true,
diff --git a/src/client/app/common/views/components/error.vue b/src/client/app/common/views/components/error.vue
index 902e4f31d4..0462a6efda 100644
--- a/src/client/app/common/views/components/error.vue
+++ b/src/client/app/common/views/components/error.vue
@@ -1,10 +1,19 @@
 <template>
 <div class="wjqjnyhzogztorhrdgcpqlkxhkmuetgj">
-	<p><fa icon="exclamation-triangle"/> %i18n:common.error.title%</p>
-	<ui-button @click="() => $emit('retry')">%i18n:common.error.retry%</ui-button>
+	<p><fa icon="exclamation-triangle"/> {{ $t('@.error.title') }}</p>
+	<ui-button @click="() => $emit('retry')">{{ $t('@.error.retry') }}</ui-button>
 </div>
 </template>
 
+<script lang="ts">
+import Vue from 'vue';
+import i18n from '../../../i18n';
+
+export default Vue.extend({
+	i18n: i18n()
+});
+</script>
+
 <style lang="stylus" scoped>
 .wjqjnyhzogztorhrdgcpqlkxhkmuetgj
 	max-width 350px
diff --git a/src/client/app/common/views/components/games/reversi/reversi.game.vue b/src/client/app/common/views/components/games/reversi/reversi.game.vue
index b2cf8b4d9d..08c69f3076 100644
--- a/src/client/app/common/views/components/games/reversi/reversi.game.vue
+++ b/src/client/app/common/views/components/games/reversi/reversi.game.vue
@@ -1,19 +1,19 @@
 <template>
 <div class="xqnhankfuuilcwvhgsopeqncafzsquya">
 	<button class="go-index" v-if="selfNav" @click="goIndex"><fa icon="arrow-left"/></button>
-	<header><b><router-link :to="blackUser | userPage">{{ blackUser | userName }}</router-link></b>(%i18n:common.reversi.black%) vs <b><router-link :to="whiteUser | userPage">{{ whiteUser | userName }}</router-link></b>(%i18n:common.reversi.white%)</header>
+	<header><b><router-link :to="blackUser | userPage">{{ blackUser | userName }}</router-link></b>({{ $t('@.reversi.black') }}) vs <b><router-link :to="whiteUser | userPage">{{ whiteUser | userName }}</router-link></b>({{ $t('@.reversi.white') }})</header>
 
 	<div style="overflow: hidden; line-height: 28px;">
-		<p class="turn" v-if="!iAmPlayer && !game.isEnded">{{ '%i18n:common.reversi.turn-of%'.replace('{}', $options.filters.userName(turnUser)) }}<mk-ellipsis/></p>
-		<p class="turn" v-if="logPos != logs.length">{{ '%i18n:common.reversi.past-turn-of%'.replace('{}', $options.filters.userName(turnUser)) }}</p>
-		<p class="turn1" v-if="iAmPlayer && !game.isEnded && !isMyTurn">%i18n:common.reversi.opponent-turn%<mk-ellipsis/></p>
-		<p class="turn2" v-if="iAmPlayer && !game.isEnded && isMyTurn" v-animate-css="{ classes: 'tada', iteration: 'infinite' }">%i18n:common.reversi.my-turn%</p>
+		<p class="turn" v-if="!iAmPlayer && !game.isEnded">{{ $t('@.reversi.turn-of', { name: $options.filters.userName(turnUser) }) }}<mk-ellipsis/></p>
+		<p class="turn" v-if="logPos != logs.length">{{ $t('@.reversi.past-turn-of', { name: $options.filters.userName(turnUser) }) }}</p>
+		<p class="turn1" v-if="iAmPlayer && !game.isEnded && !isMyTurn">{{ $t('@.reversi.opponent-turn') }}<mk-ellipsis/></p>
+		<p class="turn2" v-if="iAmPlayer && !game.isEnded && isMyTurn" v-animate-css="{ classes: 'tada', iteration: 'infinite' }">{{ $t('@.reversi.my-turn') }}</p>
 		<p class="result" v-if="game.isEnded && logPos == logs.length">
 			<template v-if="game.winner">
-				<span>{{ '%i18n:common.reversi.won%'.replace('{}', $options.filters.userName(game.winner)) }}</span>
-				<span v-if="game.surrendered != null"> (%i18n:@surrendered%)</span>
+				<span>{{ $t('@.reversi.won', { name: $options.filters.userName(game.winner) }) }}</span>
+				<span v-if="game.surrendered != null"> ({{ $t('surrendered') }})</span>
 			</template>
-			<template v-else>%i18n:common.reversi.drawn%</template>
+			<template v-else>{{ $t('@.reversi.drawn') }}</template>
 		</p>
 	</div>
 
@@ -43,10 +43,10 @@
 		</div>
 	</div>
 
-	<p class="status"><b>{{ '%i18n:common.reversi.this-turn%'.split('{}')[0] }}{{ logPos }}{{ '%i18n:common.reversi.this-turn%'.split('{}')[1] }}</b> %i18n:common.reversi.black%:{{ o.blackCount }} %i18n:common.reversi.white%:{{ o.whiteCount }} %i18n:common.reversi.total%:{{ o.blackCount + o.whiteCount }}</p>
+	<p class="status"><b>{{ $t('@.reversi.this-turn', { count: logPos }) }}</b> {{ $t('@.reversi.black') }}:{{ o.blackCount }} {{ $t('@.reversi.white') }}:{{ o.whiteCount }} {{ $t('@.reversi.total') }}:{{ o.blackCount + o.whiteCount }}</p>
 
 	<div class="actions" v-if="!game.isEnded && iAmPlayer">
-		<form-button @click="surrender">%i18n:@surrender%</form-button>
+		<form-button @click="surrender">{{ $t('surrender') }}</form-button>
 	</div>
 
 	<div class="player" v-if="game.isEnded">
@@ -62,20 +62,22 @@
 	</div>
 
 	<div class="info">
-		<p v-if="game.settings.isLlotheo">%i18n:@is-llotheo%</p>
-		<p v-if="game.settings.loopedBoard">%i18n:@looped-map%</p>
-		<p v-if="game.settings.canPutEverywhere">%i18n:@can-put-everywhere%</p>
+		<p v-if="game.settings.isLlotheo">{{ $t('is-llotheo') }}</p>
+		<p v-if="game.settings.loopedBoard">{{ $t('looped-map') }}</p>
+		<p v-if="game.settings.canPutEverywhere">{{ $t('can-put-everywhere') }}</p>
 	</div>
 </div>
 </template>
 
 <script lang="ts">
 import Vue from 'vue';
+import i18n from '../../../../../i18n';
 import * as CRC32 from 'crc-32';
 import Reversi, { Color } from '../../../../../../../games/reversi/core';
 import { url } from '../../../../../config';
 
 export default Vue.extend({
+	i18n: i18n('common/views/components/games/reversi/reversi.game.vue'),
 	props: {
 		initGame: {
 			type: Object,
diff --git a/src/client/app/common/views/components/games/reversi/reversi.gameroom.vue b/src/client/app/common/views/components/games/reversi/reversi.gameroom.vue
index 0a18e0b19a..471decc240 100644
--- a/src/client/app/common/views/components/games/reversi/reversi.gameroom.vue
+++ b/src/client/app/common/views/components/games/reversi/reversi.gameroom.vue
@@ -7,10 +7,12 @@
 
 <script lang="ts">
 import Vue from 'vue';
+import i18n from '../../../../../i18n';
 import XGame from './reversi.game.vue';
 import XRoom from './reversi.room.vue';
 
 export default Vue.extend({
+	i18n: i18n('common/views/components/games/reversi/reversi.gameroom.vue'),
 	components: {
 		XGame,
 		XRoom
diff --git a/src/client/app/common/views/components/games/reversi/reversi.index.vue b/src/client/app/common/views/components/games/reversi/reversi.index.vue
index a040162802..5711f0cf2b 100644
--- a/src/client/app/common/views/components/games/reversi/reversi.index.vue
+++ b/src/client/app/common/views/components/games/reversi/reversi.index.vue
@@ -1,22 +1,22 @@
 <template>
 <div class="phgnkghfpyvkrvwiajkiuoxyrdaqpzcx">
-	<h1>%i18n:@title%</h1>
-	<p>%i18n:@sub-title%</p>
+	<h1>{{ $t('title') }}</h1>
+	<p>{{ $t('sub-title') }}</p>
 	<div class="play">
-		<form-button primary round @click="match">%i18n:@invite%</form-button>
+		<form-button primary round @click="match">{{ $t('invite') }}</form-button>
 		<details>
-			<summary>%i18n:@rule%</summary>
+			<summary>{{ $t('rule') }}</summary>
 			<div>
-				<p>%i18n:@rule-desc%</p>
+				<p>{{ $t('rule-desc') }}</p>
 				<dl>
-					<dt><b>%i18n:@mode-invite%</b></dt>
-					<dd>%i18n:@mode-invite-desc%</dd>
+					<dt><b>{{ $t('mode-invite') }}</b></dt>
+					<dd>{{ $t('mode-invite-desc') }}</dd>
 				</dl>
 			</div>
 		</details>
 	</div>
 	<section v-if="invitations.length > 0">
-		<h2>%i18n:@invitations%</h2>
+		<h2>{{ $t('invitations') }}</h2>
 		<div class="invitation" v-for="i in invitations" tabindex="-1" @click="accept(i)">
 			<mk-avatar class="avatar" :user="i.parent"/>
 			<span class="name"><b>{{ i.parent | userName }}</b></span>
@@ -25,22 +25,22 @@
 		</div>
 	</section>
 	<section v-if="myGames.length > 0">
-		<h2>%i18n:@my-games%</h2>
+		<h2>{{ $t('my-games') }}</h2>
 		<a class="game" v-for="g in myGames" tabindex="-1" @click.prevent="go(g)" :href="`/reversi/${g.id}`">
 			<mk-avatar class="avatar" :user="g.user1"/>
 			<mk-avatar class="avatar" :user="g.user2"/>
 			<span><b>{{ g.user1 | userName }}</b> vs <b>{{ g.user2 | userName }}</b></span>
-			<span class="state">{{ g.isEnded ? '%i18n:@game-state.ended%' : '%i18n:@game-state.playing%' }}</span>
+			<span class="state">{{ g.isEnded ? this.$t('game-state.ended') : this.$t('game-state.playing') }}</span>
 			<mk-time :time="g.createdAt" />
 		</a>
 	</section>
 	<section v-if="games.length > 0">
-		<h2>%i18n:@all-games%</h2>
+		<h2>{{ $t('all-games') }}</h2>
 		<a class="game" v-for="g in games" tabindex="-1" @click.prevent="go(g)" :href="`/reversi/${g.id}`">
 			<mk-avatar class="avatar" :user="g.user1"/>
 			<mk-avatar class="avatar" :user="g.user2"/>
 			<span><b>{{ g.user1 | userName }}</b> vs <b>{{ g.user2 | userName }}</b></span>
-			<span class="state">{{ g.isEnded ? '%i18n:@game-state.ended%' : '%i18n:@game-state.playing%' }}</span>
+			<span class="state">{{ g.isEnded ? this.$t('game-state.ended') : this.$t('game-state.playing') }}</span>
 			<mk-time :time="g.createdAt" />
 		</a>
 	</section>
@@ -49,8 +49,10 @@
 
 <script lang="ts">
 import Vue from 'vue';
+import i18n from '../../../../../i18n';
 
 export default Vue.extend({
+	i18n: i18n('common/views/components/games/reversi/reversi.index.vue'),
 	data() {
 		return {
 			games: [],
@@ -99,7 +101,7 @@ export default Vue.extend({
 
 		match() {
 			(this as any).apis.input({
-				title: '%i18n:@enter-username%'
+				title: this.$t('enter-username')
 			}).then(username => {
 				(this as any).api('users/show', {
 					username
diff --git a/src/client/app/common/views/components/games/reversi/reversi.room.vue b/src/client/app/common/views/components/games/reversi/reversi.room.vue
index 7571d69af9..693d2de95a 100644
--- a/src/client/app/common/views/components/games/reversi/reversi.room.vue
+++ b/src/client/app/common/views/components/games/reversi/reversi.room.vue
@@ -3,13 +3,13 @@
 	<header><b>{{ game.user1 | userName }}</b> vs <b>{{ game.user2 | userName }}</b></header>
 
 	<div>
-		<p>%i18n:@settings-of-the-game%</p>
+		<p>{{ $t('settings-of-the-game') }}</p>
 
 		<div class="card map">
 			<header>
-				<select v-model="mapName" placeholder="%i18n:@choose-map%" @change="onMapChange">
+				<select v-model="mapName" :placeholder="$t('placeholder')" @change="onMapChange">
 					<option label="-Custom-" :value="mapName" v-if="mapName == '-Custom-'"/>
-					<option label="%i18n:@random%" :value="null"/>
+					<option :label="$t('label')" :value="null"/>
 					<optgroup v-for="c in mapCategories" :key="c" :label="c">
 						<option v-for="m in maps" v-if="m.category == c" :key="m.name" :label="m.name" :value="m.name">{{ m.name }}</option>
 					</optgroup>
@@ -31,31 +31,31 @@
 
 		<div class="card">
 			<header>
-				<span>%i18n:@black-or-white%</span>
+				<span>{{ $t('black-or-white') }}</span>
 			</header>
 
 			<div>
-				<form-radio v-model="game.settings.bw" value="random" @change="updateSettings">%i18n:@random%</form-radio>
-				<form-radio v-model="game.settings.bw" :value="1" @change="updateSettings">{{ '%i18n:@black-is%'.split('{}')[0] }}<b>{{ game.user1 | userName }}</b>{{ '%i18n:@black-is%'.split('{}')[1] }}</form-radio>
-				<form-radio v-model="game.settings.bw" :value="2" @change="updateSettings">{{ '%i18n:@black-is%'.split('{}')[0] }}<b>{{ game.user2 | userName }}</b>{{ '%i18n:@black-is%'.split('{}')[1] }}</form-radio>
+				<form-radio v-model="game.settings.bw" value="random" @change="updateSettings">{{ $t('random') }}</form-radio>
+				<form-radio v-model="game.settings.bw" :value="1" @change="updateSettings">{{ this.$t('black-is').split('{}')[0] }}<b>{{ game.user1 | userName }}</b>{{ this.$t('black-is').split('{}')[1] }}</form-radio>
+				<form-radio v-model="game.settings.bw" :value="2" @change="updateSettings">{{ this.$t('black-is').split('{}')[0] }}<b>{{ game.user2 | userName }}</b>{{ this.$t('black-is').split('{}')[1] }}</form-radio>
 			</div>
 		</div>
 
 		<div class="card">
 			<header>
-				<span>%i18n:@rules%</span>
+				<span>{{ $t('rules') }}</span>
 			</header>
 
 			<div>
-				<ui-switch v-model="game.settings.isLlotheo" @change="updateSettings">%i18n:@is-llotheo%</ui-switch>
-				<ui-switch v-model="game.settings.loopedBoard" @change="updateSettings">%i18n:@looped-map%</ui-switch>
-				<ui-switch v-model="game.settings.canPutEverywhere" @change="updateSettings">%i18n:@can-put-everywhere%</ui-switch>
+				<ui-switch v-model="game.settings.isLlotheo" @change="updateSettings">{{ $t('is-llotheo') }}</ui-switch>
+				<ui-switch v-model="game.settings.loopedBoard" @change="updateSettings">{{ $t('looped-map') }}</ui-switch>
+				<ui-switch v-model="game.settings.canPutEverywhere" @change="updateSettings">{{ $t('can-put-everywhere') }}</ui-switch>
 			</div>
 		</div>
 
 		<div class="card form" v-if="form">
 			<header>
-				<span>%i18n:@settings-of-the-bot%</span>
+				<span>{{ $t('settings-of-the-bot') }}</span>
 			</header>
 
 			<div>
@@ -98,16 +98,16 @@
 
 	<footer>
 		<p class="status">
-			<template v-if="isAccepted && isOpAccepted">%i18n:@this-game-is-started-soon%<mk-ellipsis/></template>
-			<template v-if="isAccepted && !isOpAccepted">%i18n:@waiting-for-other%<mk-ellipsis/></template>
-			<template v-if="!isAccepted && isOpAccepted">%i18n:@waiting-for-me%</template>
-			<template v-if="!isAccepted && !isOpAccepted">%i18n:@waiting-for-both%<mk-ellipsis/></template>
+			<template v-if="isAccepted && isOpAccepted">{{ $t('this-game-is-started-soon') }}<mk-ellipsis/></template>
+			<template v-if="isAccepted && !isOpAccepted">{{ $t('waiting-for-other') }}<mk-ellipsis/></template>
+			<template v-if="!isAccepted && isOpAccepted">{{ $t('waiting-for-me') }}</template>
+			<template v-if="!isAccepted && !isOpAccepted">{{ $t('waiting-for-both') }}<mk-ellipsis/></template>
 		</p>
 
 		<div class="actions">
-			<form-button @click="exit">%i18n:@cancel%</form-button>
-			<form-button primary @click="accept" v-if="!isAccepted">%i18n:@ready%</form-button>
-			<form-button primary @click="cancel" v-if="isAccepted">%i18n:@cancel-ready%</form-button>
+			<form-button @click="exit">{{ $t('cancel') }}</form-button>
+			<form-button primary @click="accept" v-if="!isAccepted">{{ $t('ready') }}</form-button>
+			<form-button primary @click="cancel" v-if="isAccepted">{{ $t('cancel-ready') }}</form-button>
 		</div>
 	</footer>
 </div>
@@ -115,9 +115,11 @@
 
 <script lang="ts">
 import Vue from 'vue';
+import i18n from '../../../../../i18n';
 import * as maps from '../../../../../../../games/reversi/maps';
 
 export default Vue.extend({
+	i18n: i18n('common/views/components/games/reversi/reversi.room.vue'),
 	props: ['game', 'connection'],
 
 	data() {
diff --git a/src/client/app/common/views/components/games/reversi/reversi.vue b/src/client/app/common/views/components/games/reversi/reversi.vue
index 7c9e1b6a5e..ab47acbfe3 100644
--- a/src/client/app/common/views/components/games/reversi/reversi.vue
+++ b/src/client/app/common/views/components/games/reversi/reversi.vue
@@ -4,9 +4,9 @@
 		<x-gameroom :game="game" :self-nav="selfNav" @go-index="goIndex"/>
 	</div>
 	<div class="matching" v-else-if="matching">
-		<h1>{{ '%i18n:@matching.waiting-for%'.split('{}')[0] }}<b>{{ matching | userName }}</b>{{ '%i18n:@matching.waiting-for%'.split('{}')[1] }}<mk-ellipsis/></h1>
+		<h1>{{ this.$t('matching.waiting-for').split('{}')[0] }}<b>{{ matching | userName }}</b>{{ this.$t('matching.waiting-for').split('{}')[1] }}<mk-ellipsis/></h1>
 		<div class="cancel">
-			<form-button round @click="cancel">%i18n:@matching.cancel%</form-button>
+			<form-button round @click="cancel">{{ $t('matching.cancel') }}</form-button>
 		</div>
 	</div>
 	<div v-else-if="gameId">
@@ -20,11 +20,13 @@
 
 <script lang="ts">
 import Vue from 'vue';
+import i18n from '../../../../../i18n';
 import XGameroom from './reversi.gameroom.vue';
 import XIndex from './reversi.index.vue';
 import Progress from '../../../../scripts/loading';
 
 export default Vue.extend({
+	i18n: i18n('common/views/components/games/reversi/reversi.vue'),
 	components: {
 		XGameroom,
 		XIndex
diff --git a/src/client/app/common/views/components/github-setting.vue b/src/client/app/common/views/components/github-setting.vue
index f79a700a92..f9bb0e69d2 100644
--- a/src/client/app/common/views/components/github-setting.vue
+++ b/src/client/app/common/views/components/github-setting.vue
@@ -1,11 +1,11 @@
 <template>
 <div class="mk-github-setting">
-	<p>%i18n:@description%<a :href="`${docsUrl}/link-to-github`" target="_blank">%i18n:@detail%</a></p>
-	<p class="account" v-if="$store.state.i.github" :title="`GitHub ID: ${$store.state.i.github.id}`">%i18n:@connected-to%: <a :href="`https://github.com/${$store.state.i.github.login}`" target="_blank">@{{ $store.state.i.github.login }}</a></p>
+	<p>{{ $t('description') }}<a :href="`${docsUrl}/link-to-github`" target="_blank">{{ $t('detail') }}</a></p>
+	<p class="account" v-if="$store.state.i.github" :title="`GitHub ID: ${$store.state.i.github.id}`">{{ $t('connected-to') }}: <a :href="`https://github.com/${$store.state.i.github.login}`" target="_blank">@{{ $store.state.i.github.login }}</a></p>
 	<p>
-		<a :href="`${apiUrl}/connect/github`" target="_blank" @click.prevent="connect">{{ $store.state.i.github ? '%i18n:@reconnect%' : '%i18n:@connect%' }}</a>
+		<a :href="`${apiUrl}/connect/github`" target="_blank" @click.prevent="connect">{{ $store.state.i.github ? this.$t('reconnect') : this.$t('connect') }}</a>
 		<span v-if="$store.state.i.github"> or </span>
-		<a :href="`${apiUrl}/disconnect/github`" target="_blank" v-if="$store.state.i.github" @click.prevent="disconnect">%i18n:@disconnect%</a>
+		<a :href="`${apiUrl}/disconnect/github`" target="_blank" v-if="$store.state.i.github" @click.prevent="disconnect">{{ $t('disconnect') }}</a>
 	</p>
 	<p class="id" v-if="$store.state.i.github">GitHub ID: {{ $store.state.i.github.id }}</p>
 </div>
@@ -13,9 +13,11 @@
 
 <script lang="ts">
 import Vue from 'vue';
+import i18n from '../../../i18n';
 import { apiUrl, docsUrl } from '../../../config';
 
 export default Vue.extend({
+	i18n: i18n('common/views/components/github-setting.vue'),
 	data() {
 		return {
 			form: null,
diff --git a/src/client/app/common/views/components/google.vue b/src/client/app/common/views/components/google.vue
index 1d710bc3c9..1d852cf25a 100644
--- a/src/client/app/common/views/components/google.vue
+++ b/src/client/app/common/views/components/google.vue
@@ -1,13 +1,16 @@
 <template>
 <div class="mk-google">
 	<input type="search" v-model="query" :placeholder="q">
-	<button @click="search"><fa icon="search"/> %i18n:common.search%</button>
+	<button @click="search"><fa icon="search"/> {{ $t('@.search') }}</button>
 </div>
 </template>
 
 <script lang="ts">
 import Vue from 'vue';
+import i18n from '../../../i18n';
+
 export default Vue.extend({
+	i18n: i18n(),
 	props: ['q'],
 	data() {
 		return {
diff --git a/src/client/app/common/views/components/index.ts b/src/client/app/common/views/components/index.ts
index 914285ac46..62184c3d13 100644
--- a/src/client/app/common/views/components/index.ts
+++ b/src/client/app/common/views/components/index.ts
@@ -30,7 +30,6 @@ import time from './time.vue';
 import timer from './timer.vue';
 import mediaList from './media-list.vue';
 import uploader from './uploader.vue';
-import specialMessage from './special-message.vue';
 import streamIndicator from './stream-indicator.vue';
 import ellipsis from './ellipsis.vue';
 import messaging from './messaging.vue';
@@ -84,7 +83,6 @@ Vue.component('mk-time', time);
 Vue.component('mk-timer', timer);
 Vue.component('mk-media-list', mediaList);
 Vue.component('mk-uploader', uploader);
-Vue.component('mk-special-message', specialMessage);
 Vue.component('mk-stream-indicator', streamIndicator);
 Vue.component('mk-ellipsis', ellipsis);
 Vue.component('mk-messaging', messaging);
diff --git a/src/client/app/common/views/components/instance.vue b/src/client/app/common/views/components/instance.vue
index c3935cce0e..0dd1510e08 100644
--- a/src/client/app/common/views/components/instance.vue
+++ b/src/client/app/common/views/components/instance.vue
@@ -3,15 +3,17 @@
 	<div class="banner" :style="{ backgroundImage: meta.bannerUrl ? `url(${meta.bannerUrl})` : null }"></div>
 
 	<h1>{{ meta.name }}</h1>
-	<p v-html="meta.description || '%i18n:common.about%'"></p>
-	<router-link to="/">%i18n:@start%</router-link>
+	<p v-html="meta.description || this.$t('@.about')"></p>
+	<router-link to="/">{{ $t('start') }}</router-link>
 </div>
 </template>
 
 <script lang="ts">
 import Vue from 'vue';
+import i18n from '../../../i18n';
 
 export default Vue.extend({
+	i18n: i18n('common/views/components/instance.vue'),
 	data() {
 		return {
 			meta: null
diff --git a/src/client/app/common/views/components/media-banner.vue b/src/client/app/common/views/components/media-banner.vue
index 00a7f22e45..17e4dd6041 100644
--- a/src/client/app/common/views/components/media-banner.vue
+++ b/src/client/app/common/views/components/media-banner.vue
@@ -2,8 +2,8 @@
 <div class="mk-media-banner">
 	<div class="sensitive" v-if="media.isSensitive && hide" @click="hide = false">
 		<span class="icon"><fa icon="exclamation-triangle"/></span>
-		<b>%i18n:@sensitive%</b>
-		<span>%i18n:@click-to-show%</span>
+		<b>{{ $t('sensitive') }}</b>
+		<span>{{ $t('click-to-show') }}</span>
 	</div>
 	<div class="audio" v-else-if="media.type.startsWith('audio')">
 		<audio class="audio"
@@ -26,8 +26,10 @@
 
 <script lang="ts">
 import Vue from 'vue';
+import i18n from '../../../i18n';
 
 export default Vue.extend({
+	i18n: i18n('common/views/components/media-banner.vue'),
 	props: {
 		media: {
 			type: Object,
diff --git a/src/client/app/common/views/components/messaging-room.form.vue b/src/client/app/common/views/components/messaging-room.form.vue
index 3bd04bdbce..ef160424eb 100644
--- a/src/client/app/common/views/components/messaging-room.form.vue
+++ b/src/client/app/common/views/components/messaging-room.form.vue
@@ -8,18 +8,18 @@
 		ref="textarea"
 		@keypress="onKeypress"
 		@paste="onPaste"
-		placeholder="%i18n:@input-message-here%"
+		:placeholder="$t('placeholder')"
 		v-autocomplete="'text'"
 	></textarea>
 	<div class="file" @click="file = null" v-if="file">{{ file.name }}</div>
 	<mk-uploader ref="uploader" @uploaded="onUploaded"/>
-	<button class="send" @click="send" :disabled="!canSend || sending" title="%i18n:@send%">
+	<button class="send" @click="send" :disabled="!canSend || sending" :title="$t('send')">
 		<template v-if="!sending"><fa icon="paper-plane"/></template><template v-if="sending"><fa icon="spinner .spin"/></template>
 	</button>
-	<button class="attach-from-local" @click="chooseFile" title="%i18n:@attach-from-local%">
+	<button class="attach-from-local" @click="chooseFile" :title="$t('attach-from-local')">
 		<fa icon="upload"/>
 	</button>
-	<button class="attach-from-drive" @click="chooseFileFromDrive" title="%i18n:@attach-from-drive%">
+	<button class="attach-from-drive" @click="chooseFileFromDrive" :title="$t('attach-from-drive')">
 		<fa :icon="['far', 'folder-open']"/>
 	</button>
 	<input ref="file" type="file" @change="onChangeFile"/>
@@ -28,9 +28,11 @@
 
 <script lang="ts">
 import Vue from 'vue';
+import i18n from '../../../i18n';
 import * as autosize from 'autosize';
 
 export default Vue.extend({
+	i18n: i18n('common/views/components/messaging-room.form.vue'),
 	props: ['user'],
 	data() {
 		return {
diff --git a/src/client/app/common/views/components/messaging-room.message.vue b/src/client/app/common/views/components/messaging-room.message.vue
index 3060ff87ac..0dce57a06a 100644
--- a/src/client/app/common/views/components/messaging-room.message.vue
+++ b/src/client/app/common/views/components/messaging-room.message.vue
@@ -3,7 +3,7 @@
 	<mk-avatar class="avatar" :user="message.user" target="_blank"/>
 	<div class="content">
 		<div class="balloon" :data-no-text="message.text == null">
-			<!-- <button class="delete-button" v-if="isMe" title="%i18n:common.delete%">
+			<!-- <button class="delete-button" v-if="isMe" :title="$t('@.delete')">
 				<img src="/assets/desktop/messaging/delete.png" alt="Delete"/>
 			</button> -->
 			<div class="content" v-if="!message.isDeleted">
@@ -16,13 +16,13 @@
 				</div>
 			</div>
 			<div class="content" v-if="message.isDeleted">
-				<p class="is-deleted">%i18n:@deleted%</p>
+				<p class="is-deleted">{{ $t('deleted') }}</p>
 			</div>
 		</div>
 		<div></div>
 		<mk-url-preview v-for="url in urls" :url="url" :key="url"/>
 		<footer>
-			<span class="read" v-if="isMe && message.isRead">%i18n:@is-read%</span>
+			<span class="read" v-if="isMe && message.isRead">{{ $t('is-read') }}</span>
 			<mk-time :time="message.createdAt"/>
 			<template v-if="message.is_edited"><fa icon="pencil-alt"/></template>
 		</footer>
@@ -32,9 +32,11 @@
 
 <script lang="ts">
 import Vue from 'vue';
+import i18n from '../../../i18n';
 import parse from '../../../../../mfm/parse';
 
 export default Vue.extend({
+	i18n: i18n('common/views/components/messaging-room.message.vue'),
 	props: {
 		message: {
 			required: true
diff --git a/src/client/app/common/views/components/messaging-room.vue b/src/client/app/common/views/components/messaging-room.vue
index 577617c49a..5b5de4058a 100644
--- a/src/client/app/common/views/components/messaging-room.vue
+++ b/src/client/app/common/views/components/messaging-room.vue
@@ -4,11 +4,11 @@
 	@drop.prevent.stop="onDrop"
 >
 	<div class="body">
-		<p class="init" v-if="init"><fa icon="spinner .spin"/>%i18n:common.loading%</p>
-		<p class="empty" v-if="!init && messages.length == 0"><fa icon="info-circle"/>%i18n:@empty%</p>
-		<p class="no-history" v-if="!init && messages.length > 0 && !existMoreMessages"><fa icon="flag"/>%i18n:@no-history%</p>
+		<p class="init" v-if="init"><fa icon="spinner .spin"/>{{ $t('@.loading') }}</p>
+		<p class="empty" v-if="!init && messages.length == 0"><fa icon="info-circle"/>{{ $t('empty') }}</p>
+		<p class="no-history" v-if="!init && messages.length > 0 && !existMoreMessages"><fa icon="flag"/>{{ $t('no-history') }}</p>
 		<button class="more" :class="{ fetching: fetchingMoreMessages }" v-if="existMoreMessages" @click="fetchMoreMessages" :disabled="fetchingMoreMessages">
-			<template v-if="fetchingMoreMessages"><fa icon="spinner .pulse" fixed-width/></template>{{ fetchingMoreMessages ? '%i18n:common.loading%' : '%i18n:@more%' }}
+			<template v-if="fetchingMoreMessages"><fa icon="spinner .pulse" fixed-width/></template>{{ fetchingMoreMessages ? $t('@.loading') : $t('@.load-more') }}
 		</button>
 		<template v-for="(message, i) in _messages">
 			<x-message :message="message" :key="message.id"/>
@@ -20,7 +20,7 @@
 	<footer>
 		<transition name="fade">
 			<div class="new-message" v-show="showIndicator">
-				<button @click="onIndicatorClick"><i><fa icon="arrow-circle-down"/></i>%i18n:@new-message%</button>
+				<button @click="onIndicatorClick"><i><fa icon="arrow-circle-down"/></i>{{ $t('new-message') }}</button>
 			</div>
 		</transition>
 		<x-form :user="user" ref="form"/>
@@ -30,11 +30,13 @@
 
 <script lang="ts">
 import Vue from 'vue';
+import i18n from '../../../i18n';
 import XMessage from './messaging-room.message.vue';
 import XForm from './messaging-room.form.vue';
 import { url } from '../../../config';
 
 export default Vue.extend({
+	i18n: i18n('common/views/components/messaging-room.vue'),
 	components: {
 		XMessage,
 		XForm
@@ -60,7 +62,7 @@ export default Vue.extend({
 				const date = new Date(message.createdAt).getDate();
 				const month = new Date(message.createdAt).getMonth() + 1;
 				message._date = date;
-				message._datetext = '%i18n:common.month-and-day%'.replace('{month}', month.toString()).replace('{day}', date.toString());
+				message._datetext = this.$t('@.month-and-day').replace('{month}', month.toString()).replace('{day}', date.toString());
 				return message;
 			});
 		},
@@ -120,7 +122,7 @@ export default Vue.extend({
 				this.form.upload(e.dataTransfer.files[0]);
 				return;
 			} else if (e.dataTransfer.files.length > 1) {
-				alert('%i18n:@only-one-file-attached%');
+				alert(this.$t('only-one-file-attached'));
 				return;
 			}
 
diff --git a/src/client/app/common/views/components/messaging.vue b/src/client/app/common/views/components/messaging.vue
index fe6c001c87..b818f599a2 100644
--- a/src/client/app/common/views/components/messaging.vue
+++ b/src/client/app/common/views/components/messaging.vue
@@ -3,7 +3,7 @@
 	<div class="search" v-if="!compact" :style="{ top: headerTop + 'px' }">
 		<div class="form">
 			<label for="search-input"><i><fa icon="search"/></i></label>
-			<input v-model="q" type="search" @input="search" @keydown="onSearchKeydown" placeholder="%i18n:@search-user%"/>
+			<input v-model="q" type="search" @input="search" @keydown="onSearchKeydown" :placeholder="$t('placeholder')"/>
 		</div>
 		<div class="result">
 			<ol class="users" v-if="result.length > 0" ref="searchResult">
@@ -38,22 +38,24 @@
 						<mk-time :time="message.createdAt"/>
 					</header>
 					<div class="body">
-						<p class="text"><span class="me" v-if="isMe(message)">%i18n:@you%:</span>{{ message.text }}</p>
+						<p class="text"><span class="me" v-if="isMe(message)">{{ $t('you') }}:</span>{{ message.text }}</p>
 					</div>
 				</div>
 			</a>
 		</template>
 	</div>
-	<p class="no-history" v-if="!fetching && messages.length == 0">%i18n:@no-history%</p>
-	<p class="fetching" v-if="fetching"><fa icon="spinner .pulse" fixed-width/>%i18n:common.loading%<mk-ellipsis/></p>
+	<p class="no-history" v-if="!fetching && messages.length == 0">{{ $t('no-history') }}</p>
+	<p class="fetching" v-if="fetching"><fa icon="spinner .pulse" fixed-width/>{{ $t('@.loading') }}<mk-ellipsis/></p>
 </div>
 </template>
 
 <script lang="ts">
 import Vue from 'vue';
+import i18n from '../../../i18n';
 import getAcct from '../../../../../misc/acct/render';
 
 export default Vue.extend({
+	i18n: i18n('common/views/components/messaging.vue'),
 	props: {
 		compact: {
 			type: Boolean,
diff --git a/src/client/app/common/views/components/mute-and-block.vue b/src/client/app/common/views/components/mute-and-block.vue
index e4627726f2..f8e7183e24 100644
--- a/src/client/app/common/views/components/mute-and-block.vue
+++ b/src/client/app/common/views/components/mute-and-block.vue
@@ -1,10 +1,10 @@
 <template>
 <ui-card>
-	<div slot="title"><fa icon="ban"/> %i18n:@mute-and-block%</div>
+	<div slot="title"><fa icon="ban"/> {{ $t('mute-and-block') }}</div>
 
 	<section>
-		<header>%i18n:@mute%</header>
-		<ui-info v-if="!muteFetching && mute.length == 0">%i18n:@no-muted-users%</ui-info>
+		<header>{{ $t('mute') }}</header>
+		<ui-info v-if="!muteFetching && mute.length == 0">{{ $t('no-muted-users') }}</ui-info>
 		<div class="users" v-if="mute.length != 0">
 			<div v-for="user in mute" :key="user.id">
 				<p><b>{{ user | userName }}</b> @{{ user | acct }}</p>
@@ -13,8 +13,8 @@
 	</section>
 
 	<section>
-		<header>%i18n:@block%</header>
-		<ui-info v-if="!blockFetching && block.length == 0">%i18n:@no-blocked-users%</ui-info>
+		<header>{{ $t('block') }}</header>
+		<ui-info v-if="!blockFetching && block.length == 0">{{ $t('no-blocked-users') }}</ui-info>
 		<div class="users" v-if="block.length != 0">
 			<div v-for="user in block" :key="user.id">
 				<p><b>{{ user | userName }}</b> @{{ user | acct }}</p>
@@ -26,8 +26,10 @@
 
 <script lang="ts">
 import Vue from 'vue';
+import i18n from '../../../i18n';
 
 export default Vue.extend({
+	i18n: i18n('common/views/components/mute-and-block.vue'),
 	data() {
 		return {
 			muteFetching: true,
diff --git a/src/client/app/common/views/components/nav.vue b/src/client/app/common/views/components/nav.vue
index b844848f28..c3daf9a88b 100644
--- a/src/client/app/common/views/components/nav.vue
+++ b/src/client/app/common/views/components/nav.vue
@@ -1,20 +1,22 @@
 <template>
 <span class="mk-nav">
-	<a :href="aboutUrl">%i18n:@about%</a>
+	<a :href="aboutUrl">{{ $t('about') }}</a>
 	<i>・</i>
-	<a :href="repositoryUrl">%i18n:@repository%</a>
+	<a :href="repositoryUrl">{{ $t('repository') }}</a>
 	<i>・</i>
-	<a :href="feedbackUrl" target="_blank">%i18n:@feedback%</a>
+	<a :href="feedbackUrl" target="_blank">{{ $t('feedback') }}</a>
 	<i>・</i>
-	<a href="/dev">%i18n:@develop%</a>
+	<a href="/dev">{{ $t('develop') }}</a>
 </span>
 </template>
 
 <script lang="ts">
 import Vue from 'vue';
+import i18n from '../../../i18n';
 import { lang } from '../../../config';
 
 export default Vue.extend({
+	i18n: i18n('common/views/components/nav.vue'),
 	data() {
 		return {
 			aboutUrl: `/docs/${lang}/about`,
diff --git a/src/client/app/common/views/components/note-header.vue b/src/client/app/common/views/components/note-header.vue
index 4a94e5b6e9..8910b43d32 100644
--- a/src/client/app/common/views/components/note-header.vue
+++ b/src/client/app/common/views/components/note-header.vue
@@ -6,7 +6,7 @@
 	<span class="is-bot" v-if="note.user.isBot">bot</span>
 	<span class="is-cat" v-if="note.user.isCat">cat</span>
 	<span class="username"><mk-acct :user="note.user"/></span>
-	<span class="is-verified" v-if="note.user.isVerified" title="%i18n:common.verified-user%"><fa icon="star"/></span>
+	<span class="is-verified" v-if="note.user.isVerified" :title="$t('@.verified-user')"><fa icon="star"/></span>
 	<div class="info">
 		<span class="app" v-if="note.app && !mini">via <b>{{ note.app.name }}</b></span>
 		<span class="mobile" v-if="note.viaMobile"><fa icon="mobile-alt"/></span>
@@ -25,8 +25,10 @@
 
 <script lang="ts">
 import Vue from 'vue';
+import i18n from '../../../i18n';
 
 export default Vue.extend({
+	i18n: i18n(),
 	props: {
 		note: {
 			type: Object,
diff --git a/src/client/app/common/views/components/note-menu.vue b/src/client/app/common/views/components/note-menu.vue
index 524a3d8fec..f17f038696 100644
--- a/src/client/app/common/views/components/note-menu.vue
+++ b/src/client/app/common/views/components/note-menu.vue
@@ -6,28 +6,30 @@
 
 <script lang="ts">
 import Vue from 'vue';
+import i18n from '../../../i18n';
 import { url } from '../../../config';
 import copyToClipboard from '../../../common/scripts/copy-to-clipboard';
 import Ok from './ok.vue';
 
 export default Vue.extend({
+	i18n: i18n('common/views/components/note-menu.vue'),
 	props: ['note', 'source', 'compact'],
 	computed: {
 		items() {
 			const items = [{
 				icon: 'info-circle',
-				text: '%i18n:@detail%',
+				text: this.$t('detail'),
 				action: this.detail
 			}, {
 				icon: 'link',
-				text: '%i18n:@copy-link%',
+				text: this.$t('copy-link'),
 				action: this.copyLink
 			}];
 
 			if (this.note.uri) {
 				items.push({
 					icon: 'external-link-square-alt',
-					text: '%i18n:@remote%',
+					text: this.$t('remote'),
 					action: () => {
 						window.open(this.note.uri, '_blank');
 					}
@@ -39,13 +41,13 @@ export default Vue.extend({
 			if (this.note.isFavorited) {
 				items.push({
 					icon: 'star',
-					text: '%i18n:@unfavorite%',
+					text: this.$t('unfavorite'),
 					action: this.unfavorite
 				});
 			} else {
 				items.push({
 					icon: 'star',
-					text: '%i18n:@favorite%',
+					text: this.$t('favorite'),
 					action: this.favorite
 				});
 			}
@@ -54,13 +56,13 @@ export default Vue.extend({
 				if ((this.$store.state.i.pinnedNoteIds || []).includes(this.note.id)) {
 					items.push({
 						icon: 'thumbtack',
-						text: '%i18n:@unpin%',
+						text: this.$t('unpin'),
 						action: this.unpin
 					});
 				} else {
 					items.push({
 						icon: 'thumbtack',
-						text: '%i18n:@pin%',
+						text: this.$t('pin'),
 						action: this.pin
 					});
 				}
@@ -70,7 +72,7 @@ export default Vue.extend({
 				items.push(null);
 				items.push({
 					icon: ['far', 'trash-alt'],
-					text: '%i18n:@delete%',
+					text: this.$t('delete'),
 					action: this.del
 				});
 			}
@@ -106,7 +108,7 @@ export default Vue.extend({
 		},
 
 		del() {
-			if (!window.confirm('%i18n:@delete-confirm%')) return;
+			if (!window.confirm(this.$t('delete-confirm'))) return;
 			(this as any).api('notes/delete', {
 				noteId: this.note.id
 			}).then(() => {
diff --git a/src/client/app/common/views/components/password-settings.vue b/src/client/app/common/views/components/password-settings.vue
index 82b163f1fa..da95a6e768 100644
--- a/src/client/app/common/views/components/password-settings.vue
+++ b/src/client/app/common/views/components/password-settings.vue
@@ -1,31 +1,33 @@
 <template>
 <div>
-	<ui-button @click="reset">%i18n:@reset%</ui-button>
+	<ui-button @click="reset">{{ $t('reset') }}</ui-button>
 </div>
 </template>
 
 <script lang="ts">
 import Vue from 'vue';
+import i18n from '../../../i18n';
 
 export default Vue.extend({
+	i18n: i18n('common/views/components/password-settings.vue'),
 	methods: {
 		reset() {
 			(this as any).apis.input({
-				title: '%i18n:@enter-current-password%',
+				title: this.$t('enter-current-password'),
 				type: 'password'
 			}).then(currentPassword => {
 				(this as any).apis.input({
-					title: '%i18n:@enter-new-password%',
+					title: this.$t('enter-new-password'),
 					type: 'password'
 				}).then(newPassword => {
 					(this as any).apis.input({
-						title: '%i18n:@enter-new-password-again%',
+						title: this.$t('enter-new-password-again'),
 						type: 'password'
 					}).then(newPassword2 => {
 						if (newPassword !== newPassword2) {
 							(this as any).apis.dialog({
 								title: null,
-								text: '%i18n:@not-match%',
+								text: this.$t('not-match'),
 								actions: [{
 									text: 'OK'
 								}]
@@ -36,7 +38,7 @@ export default Vue.extend({
 							currentPasword: currentPassword,
 							newPassword: newPassword
 						}).then(() => {
-							(this as any).apis.notify('%i18n:@changed%');
+							(this as any).apis.notify(this.$t('changed'));
 						});
 					});
 				});
diff --git a/src/client/app/common/views/components/poll-editor.vue b/src/client/app/common/views/components/poll-editor.vue
index 2df84a2f88..bd4647cde9 100644
--- a/src/client/app/common/views/components/poll-editor.vue
+++ b/src/client/app/common/views/components/poll-editor.vue
@@ -1,18 +1,18 @@
 <template>
 <div class="mk-poll-editor">
 	<p class="caution" v-if="choices.length < 2">
-		<fa icon="exclamation-triangle"/>%i18n:@no-only-one-choice%
+		<fa icon="exclamation-triangle"/>{{ $t('no-only-one-choice') }}
 	</p>
 	<ul ref="choices">
 		<li v-for="(choice, i) in choices">
-			<input :value="choice" @input="onInput(i, $event)" :placeholder="'%i18n:@choice-n%'.replace('{}', i + 1)">
-			<button @click="remove(i)" title="%i18n:@remove%">
+			<input :value="choice" @input="onInput(i, $event)" :placeholder="this.$t('choice-n').replace('{}', i + 1)">
+			<button @click="remove(i)" :title="$t('remove')">
 				<fa icon="times"/>
 			</button>
 		</li>
 	</ul>
-	<button class="add" v-if="choices.length < 10" @click="add">%i18n:@add%</button>
-	<button class="destroy" @click="destroy" title="%i18n:@destroy%">
+	<button class="add" v-if="choices.length < 10" @click="add">{{ $t('add') }}</button>
+	<button class="destroy" @click="destroy" :title="$t('destroy')">
 		<fa icon="times"/>
 	</button>
 </div>
@@ -20,8 +20,10 @@
 
 <script lang="ts">
 import Vue from 'vue';
+import i18n from '../../../i18n';
 import { erase } from '../../../../../prelude/array';
 export default Vue.extend({
+	i18n: i18n('common/views/components/poll-editor.vue'),
 	data() {
 		return {
 			choices: ['', '']
diff --git a/src/client/app/common/views/components/poll.vue b/src/client/app/common/views/components/poll.vue
index 0550fd3761..23245985c7 100644
--- a/src/client/app/common/views/components/poll.vue
+++ b/src/client/app/common/views/components/poll.vue
@@ -1,28 +1,30 @@
 <template>
 <div class="mk-poll" :data-is-voted="isVoted">
 	<ul>
-		<li v-for="choice in poll.choices" :key="choice.id" @click="vote(choice.id)" :class="{ voted: choice.voted }" :title="!isVoted ? '%i18n:@vote-to%'.replace('{}', choice.text) : ''">
+		<li v-for="choice in poll.choices" :key="choice.id" @click="vote(choice.id)" :class="{ voted: choice.voted }" :title="!isVoted ? $t('vote-to').replace('{}', choice.text) : ''">
 			<div class="backdrop" :style="{ 'width': (showResult ? (choice.votes / total * 100) : 0) + '%' }"></div>
 			<span>
 				<template v-if="choice.isVoted"><fa icon="check"/></template>
 				<span>{{ choice.text }}</span>
-				<span class="votes" v-if="showResult">({{ '%i18n:@vote-count%'.replace('{}', choice.votes) }})</span>
+				<span class="votes" v-if="showResult">({{ $t('vote-count').replace('{}', choice.votes) }})</span>
 			</span>
 		</li>
 	</ul>
 	<p v-if="total > 0">
-		<span>{{ '%i18n:@total-users%'.replace('{}', total) }}</span>
+		<span>{{ $t('total-users').replace('{}', total) }}</span>
 		<span>・</span>
-		<a v-if="!isVoted" @click="toggleShowResult">{{ showResult ? '%i18n:@vote%' : '%i18n:@show-result%' }}</a>
-		<span v-if="isVoted">%i18n:@voted%</span>
+		<a v-if="!isVoted" @click="toggleShowResult">{{ showResult ? $t('vote') : $t('show-result') }}</a>
+		<span v-if="isVoted">{{ $t('voted') }}</span>
 	</p>
 </div>
 </template>
 
 <script lang="ts">
 import Vue from 'vue';
+import i18n from '../../../i18n';
 import { sum } from '../../../../../prelude/array';
 export default Vue.extend({
+	i18n: i18n('common/views/components/poll.vue'),
 	props: ['note'],
 	data() {
 		return {
diff --git a/src/client/app/common/views/components/profile-editor.vue b/src/client/app/common/views/components/profile-editor.vue
index 90e0f139e3..0d2b3e9c0d 100644
--- a/src/client/app/common/views/components/profile-editor.vue
+++ b/src/client/app/common/views/components/profile-editor.vue
@@ -1,65 +1,65 @@
 <template>
 <ui-card>
-	<div slot="title"><fa icon="user"/> %i18n:@title%</div>
+	<div slot="title"><fa icon="user"/> {{ $t('title') }}</div>
 
 	<section class="fit-top">
 		<ui-form :disabled="saving">
 			<ui-input v-model="name" :max="30">
-				<span>%i18n:@name%</span>
+				<span>{{ $t('name') }}</span>
 			</ui-input>
 
 			<ui-input v-model="username" readonly>
-				<span>%i18n:@account%</span>
+				<span>{{ $t('account') }}</span>
 				<span slot="prefix">@</span>
 				<span slot="suffix">@{{ host }}</span>
 			</ui-input>
 
 			<ui-input v-model="location">
-				<span>%i18n:@location%</span>
+				<span>{{ $t('location') }}</span>
 				<span slot="prefix"><fa icon="map-marker-alt"/></span>
 			</ui-input>
 
 			<ui-input v-model="birthday" type="date">
-				<span>%i18n:@birthday%</span>
+				<span>{{ $t('birthday') }}</span>
 				<span slot="prefix"><fa icon="birthday-cake"/></span>
 			</ui-input>
 
 			<ui-textarea v-model="description" :max="500">
-				<span>%i18n:@description%</span>
+				<span>{{ $t('description') }}</span>
 			</ui-textarea>
 
 			<ui-input type="file" @change="onAvatarChange">
-				<span>%i18n:@avatar%</span>
+				<span>{{ $t('avatar') }}</span>
 				<span slot="icon"><fa icon="image"/></span>
-				<span slot="desc" v-if="avatarUploading">%i18n:@uploading%<mk-ellipsis/></span>
+				<span slot="desc" v-if="avatarUploading">{{ $t('uploading') }}<mk-ellipsis/></span>
 			</ui-input>
 
 			<ui-input type="file" @change="onBannerChange">
-				<span>%i18n:@banner%</span>
+				<span>{{ $t('banner') }}</span>
 				<span slot="icon"><fa icon="image"/></span>
-				<span slot="desc" v-if="bannerUploading">%i18n:@uploading%<mk-ellipsis/></span>
+				<span slot="desc" v-if="bannerUploading">{{ $t('uploading') }}<mk-ellipsis/></span>
 			</ui-input>
 
-			<ui-button @click="save(true)">%i18n:@save%</ui-button>
+			<ui-button @click="save(true)">{{ $t('save') }}</ui-button>
 		</ui-form>
 	</section>
 
 	<section>
-		<header>%i18n:@advanced%</header>
+		<header>{{ $t('advanced') }}</header>
 
 		<div>
-			<ui-switch v-model="isCat" @change="save(false)">%i18n:@is-cat%</ui-switch>
-			<ui-switch v-model="isBot" @change="save(false)">%i18n:@is-bot%</ui-switch>
-			<ui-switch v-model="alwaysMarkNsfw">%i18n:common.always-mark-nsfw%</ui-switch>
+			<ui-switch v-model="isCat" @change="save(false)">{{ $t('is-cat') }}</ui-switch>
+			<ui-switch v-model="isBot" @change="save(false)">{{ $t('is-bot') }}</ui-switch>
+			<ui-switch v-model="alwaysMarkNsfw">{{ $t('@.always-mark-nsfw') }}</ui-switch>
 		</div>
 	</section>
 
 	<section>
-		<header>%i18n:@privacy%</header>
+		<header>{{ $t('privacy') }}</header>
 
 		<div>
-			<ui-switch v-model="isLocked" @change="save(false)">%i18n:@is-locked%</ui-switch>
-			<ui-switch v-model="carefulBot" @change="save(false)">%i18n:@careful-bot%</ui-switch>
+			<ui-switch v-model="isLocked" @change="save(false)">{{ $t('is-locked') }}</ui-switch>
+			<ui-switch v-model="carefulBot" @change="save(false)">{{ $t('careful-bot') }}</ui-switch>
 		</div>
 	</section>
 </ui-card>
@@ -67,9 +67,11 @@
 
 <script lang="ts">
 import Vue from 'vue';
+import i18n from '../../../i18n';
 import { apiUrl, host } from '../../../config';
 
 export default Vue.extend({
+	i18n: i18n('common/views/components/profile-editor.vue'),
 	data() {
 		return {
 			host,
@@ -180,7 +182,7 @@ export default Vue.extend({
 				if (notify) {
 					this.$swal({
 						type: 'success',
-						text: '%i18n:@saved%'
+						text: this.$t('saved')
 					});
 				}
 			});
diff --git a/src/client/app/common/views/components/reaction-icon.vue b/src/client/app/common/views/components/reaction-icon.vue
index c668efac6b..d413bece6b 100644
--- a/src/client/app/common/views/components/reaction-icon.vue
+++ b/src/client/app/common/views/components/reaction-icon.vue
@@ -1,24 +1,26 @@
 <template>
 <span class="mk-reaction-icon">
-	<img v-if="reaction == 'like'" src="https://twemoji.maxcdn.com/2/svg/1f44d.svg" alt="%i18n:common.reactions.like%">
-	<img v-if="reaction == 'love'" src="https://twemoji.maxcdn.com/2/svg/2764.svg" alt="%i18n:common.reactions.love%">
-	<img v-if="reaction == 'laugh'" src="https://twemoji.maxcdn.com/2/svg/1f606.svg" alt="%i18n:common.reactions.laugh%">
-	<img v-if="reaction == 'hmm'" src="https://twemoji.maxcdn.com/2/svg/1f914.svg" alt="%i18n:common.reactions.hmm%">
-	<img v-if="reaction == 'surprise'" src="https://twemoji.maxcdn.com/2/svg/1f62e.svg" alt="%i18n:common.reactions.surprise%">
-	<img v-if="reaction == 'congrats'" src="https://twemoji.maxcdn.com/2/svg/1f389.svg" alt="%i18n:common.reactions.congrats%">
-	<img v-if="reaction == 'angry'" src="https://twemoji.maxcdn.com/2/svg/1f4a2.svg" alt="%i18n:common.reactions.angry%">
-	<img v-if="reaction == 'confused'" src="https://twemoji.maxcdn.com/2/svg/1f625.svg" alt="%i18n:common.reactions.confused%">
-	<img v-if="reaction == 'rip'" src="https://twemoji.maxcdn.com/2/svg/1f607.svg" alt="%i18n:common.reactions.rip%">
+	<img v-if="reaction == 'like'" src="https://twemoji.maxcdn.com/2/svg/1f44d.svg" :alt="$t('@.reactions.like')">
+	<img v-if="reaction == 'love'" src="https://twemoji.maxcdn.com/2/svg/2764.svg" :alt="$t('@.reactions.love')">
+	<img v-if="reaction == 'laugh'" src="https://twemoji.maxcdn.com/2/svg/1f606.svg" :alt="$t('@.reactions.laugh')">
+	<img v-if="reaction == 'hmm'" src="https://twemoji.maxcdn.com/2/svg/1f914.svg" :alt="$t('@.reactions.hmm')">
+	<img v-if="reaction == 'surprise'" src="https://twemoji.maxcdn.com/2/svg/1f62e.svg" :alt="$t('@.reactions.surprise')">
+	<img v-if="reaction == 'congrats'" src="https://twemoji.maxcdn.com/2/svg/1f389.svg" :alt="$t('@.reactions.congrats')">
+	<img v-if="reaction == 'angry'" src="https://twemoji.maxcdn.com/2/svg/1f4a2.svg" :alt="$t('@.reactions.angry')">
+	<img v-if="reaction == 'confused'" src="https://twemoji.maxcdn.com/2/svg/1f625.svg" :alt="$t('@.reactions.confused')">
+	<img v-if="reaction == 'rip'" src="https://twemoji.maxcdn.com/2/svg/1f607.svg" :alt="$t('@.reactions.rip')">
 	<template v-if="reaction == 'pudding'">
-		<img v-if="$store.getters.isSignedIn && $store.state.settings.iLikeSushi" src="https://twemoji.maxcdn.com/2/svg/1f363.svg" alt="%i18n:common.reactions.pudding%">
-		<img v-else src="https://twemoji.maxcdn.com/2/svg/1f36e.svg" alt="%i18n:common.reactions.pudding%">
+		<img v-if="$store.getters.isSignedIn && $store.state.settings.iLikeSushi" src="https://twemoji.maxcdn.com/2/svg/1f363.svg" :alt="$t('@.reactions.pudding')">
+		<img v-else src="https://twemoji.maxcdn.com/2/svg/1f36e.svg" :alt="$t('@.reactions.pudding')">
 	</template>
 </span>
 </template>
 
 <script lang="ts">
 import Vue from 'vue';
+import i18n from '../../../i18n';
 export default Vue.extend({
+	i18n: i18n(),
 	props: ['reaction']
 });
 </script>
diff --git a/src/client/app/common/views/components/reaction-picker.vue b/src/client/app/common/views/components/reaction-picker.vue
index 13e8cf1f07..a38089409c 100644
--- a/src/client/app/common/views/components/reaction-picker.vue
+++ b/src/client/app/common/views/components/reaction-picker.vue
@@ -4,16 +4,16 @@
 	<div class="popover" :class="{ compact, big }" ref="popover">
 		<p v-if="!compact">{{ title }}</p>
 		<div ref="buttons" :class="{ showFocus }">
-			<button @click="react('like')" @mouseover="onMouseover" @mouseout="onMouseout" tabindex="1" title="%i18n:common.reactions.like%"><mk-reaction-icon reaction='like'/></button>
-			<button @click="react('love')" @mouseover="onMouseover" @mouseout="onMouseout" tabindex="2" title="%i18n:common.reactions.love%"><mk-reaction-icon reaction='love'/></button>
-			<button @click="react('laugh')" @mouseover="onMouseover" @mouseout="onMouseout" tabindex="3" title="%i18n:common.reactions.laugh%"><mk-reaction-icon reaction='laugh'/></button>
-			<button @click="react('hmm')" @mouseover="onMouseover" @mouseout="onMouseout" tabindex="4" title="%i18n:common.reactions.hmm%"><mk-reaction-icon reaction='hmm'/></button>
-			<button @click="react('surprise')" @mouseover="onMouseover" @mouseout="onMouseout" tabindex="5" title="%i18n:common.reactions.surprise%"><mk-reaction-icon reaction='surprise'/></button>
-			<button @click="react('congrats')" @mouseover="onMouseover" @mouseout="onMouseout" tabindex="6" title="%i18n:common.reactions.congrats%"><mk-reaction-icon reaction='congrats'/></button>
-			<button @click="react('angry')" @mouseover="onMouseover" @mouseout="onMouseout" tabindex="7" title="%i18n:common.reactions.angry%"><mk-reaction-icon reaction='angry'/></button>
-			<button @click="react('confused')" @mouseover="onMouseover" @mouseout="onMouseout" tabindex="8" title="%i18n:common.reactions.confused%"><mk-reaction-icon reaction='confused'/></button>
-			<button @click="react('rip')" @mouseover="onMouseover" @mouseout="onMouseout" tabindex="9" title="%i18n:common.reactions.rip%"><mk-reaction-icon reaction='rip'/></button>
-			<button @click="react('pudding')" @mouseover="onMouseover" @mouseout="onMouseout" tabindex="10" title="%i18n:common.reactions.pudding%"><mk-reaction-icon reaction='pudding'/></button>
+			<button @click="react('like')" @mouseover="onMouseover" @mouseout="onMouseout" tabindex="1" :title="$t('@.reactions.like')"><mk-reaction-icon reaction='like'/></button>
+			<button @click="react('love')" @mouseover="onMouseover" @mouseout="onMouseout" tabindex="2" :title="$t('@.reactions.love')"><mk-reaction-icon reaction='love'/></button>
+			<button @click="react('laugh')" @mouseover="onMouseover" @mouseout="onMouseout" tabindex="3" :title="$t('@.reactions.laugh')"><mk-reaction-icon reaction='laugh'/></button>
+			<button @click="react('hmm')" @mouseover="onMouseover" @mouseout="onMouseout" tabindex="4" :title="$t('@.reactions.hmm')"><mk-reaction-icon reaction='hmm'/></button>
+			<button @click="react('surprise')" @mouseover="onMouseover" @mouseout="onMouseout" tabindex="5" :title="$t('@.reactions.surprise')"><mk-reaction-icon reaction='surprise'/></button>
+			<button @click="react('congrats')" @mouseover="onMouseover" @mouseout="onMouseout" tabindex="6" :title="$t('@.reactions.congrats')"><mk-reaction-icon reaction='congrats'/></button>
+			<button @click="react('angry')" @mouseover="onMouseover" @mouseout="onMouseout" tabindex="7" :title="$t('@.reactions.angry')"><mk-reaction-icon reaction='angry'/></button>
+			<button @click="react('confused')" @mouseover="onMouseover" @mouseout="onMouseout" tabindex="8" :title="$t('@.reactions.confused')"><mk-reaction-icon reaction='confused'/></button>
+			<button @click="react('rip')" @mouseover="onMouseover" @mouseout="onMouseout" tabindex="9" :title="$t('@.reactions.rip')"><mk-reaction-icon reaction='rip'/></button>
+			<button @click="react('pudding')" @mouseover="onMouseover" @mouseout="onMouseout" tabindex="10" :title="$t('@.reactions.pudding')"><mk-reaction-icon reaction='pudding'/></button>
 		</div>
 	</div>
 </div>
@@ -21,11 +21,11 @@
 
 <script lang="ts">
 import Vue from 'vue';
+import i18n from '../../../i18n';
 import * as anime from 'animejs';
 
-const placeholder = '%i18n:@choose-reaction%';
-
 export default Vue.extend({
+	i18n: i18n('common/views/components/reaction-picker.vue'),
 	props: {
 		note: {
 			type: Object,
@@ -67,7 +67,7 @@ export default Vue.extend({
 
 	data() {
 		return {
-			title: placeholder,
+			title: this.$t('choose-reaction'),
 			focus: null
 		};
 	},
@@ -160,7 +160,7 @@ export default Vue.extend({
 		},
 
 		onMouseout(e) {
-			this.title = placeholder;
+			this.title = this.$t('choose-reaction');
 		},
 
 		close() {
diff --git a/src/client/app/common/views/components/reactions-viewer.vue b/src/client/app/common/views/components/reactions-viewer.vue
index 15989ff4d8..674ea5c6f5 100644
--- a/src/client/app/common/views/components/reactions-viewer.vue
+++ b/src/client/app/common/views/components/reactions-viewer.vue
@@ -17,6 +17,7 @@
 
 <script lang="ts">
 import Vue from 'vue';
+
 export default Vue.extend({
 	props: ['note'],
 	computed: {
diff --git a/src/client/app/common/views/components/signin.vue b/src/client/app/common/views/components/signin.vue
index cd24cdd8d0..04bffb4967 100644
--- a/src/client/app/common/views/components/signin.vue
+++ b/src/client/app/common/views/components/signin.vue
@@ -2,26 +2,28 @@
 <form class="mk-signin" :class="{ signing }" @submit.prevent="onSubmit">
 	<div class="avatar" :style="{ backgroundImage: user ? `url('${ user.avatarUrl }')` : null }" v-show="withAvatar"></div>
 	<ui-input v-model="username" type="text" pattern="^[a-zA-Z0-9_]+$" spellcheck="false" autofocus required @input="onUsernameChange" styl="fill">
-		<span>%i18n:@username%</span>
+		<span>{{ $t('username') }}</span>
 		<span slot="prefix">@</span>
 		<span slot="suffix">@{{ host }}</span>
 	</ui-input>
 	<ui-input v-model="password" type="password" required styl="fill">
-		<span>%i18n:@password%</span>
+		<span>{{ $t('password') }}</span>
 		<span slot="prefix"><fa icon="lock"/></span>
 	</ui-input>
 	<ui-input v-if="user && user.twoFactorEnabled" v-model="token" type="number" required styl="fill"/>
-	<ui-button type="submit" :disabled="signing">{{ signing ? '%i18n:@signing-in%' : '%i18n:@signin%' }}</ui-button>
-	<p style="margin: 8px 0;">%i18n:@or% <a :href="`${apiUrl}/signin/twitter`">%i18n:@signin-with-twitter%</a></p>
-	<p style="margin: 8px 0;">%i18n:@or% <a :href="`${apiUrl}/signin/github`">%i18n:@signin-with-github%</a></p>
+	<ui-button type="submit" :disabled="signing">{{ signing ? $t('signing-in') : $t('signin') }}</ui-button>
+	<p style="margin: 8px 0;"><a :href="`${apiUrl}/signin/twitter`">{{ $t('signin-with-twitter') }}</a></p>
+	<p style="margin: 8px 0;"><a :href="`${apiUrl}/signin/github`">{{ $t('signin-with-github') }}</a></p>
 </form>
 </template>
 
 <script lang="ts">
 import Vue from 'vue';
+import i18n from '../../../i18n';
 import { apiUrl, host } from '../../../config';
 
 export default Vue.extend({
+	i18n: i18n('common/views/components/signin.vue'),
 	props: {
 		withAvatar: {
 			type: Boolean,
@@ -60,7 +62,7 @@ export default Vue.extend({
 			}, true).then(() => {
 				location.reload();
 			}).catch(() => {
-				alert('%i18n:@login-failed%');
+				alert(this.$t('login-failed'));
 				this.signing = false;
 			});
 		}
diff --git a/src/client/app/common/views/components/signup.vue b/src/client/app/common/views/components/signup.vue
index 42d6d23fb4..26f7da19de 100644
--- a/src/client/app/common/views/components/signup.vue
+++ b/src/client/app/common/views/components/signup.vue
@@ -2,51 +2,53 @@
 <form class="mk-signup" @submit.prevent="onSubmit" :autocomplete="Math.random()">
 	<template v-if="meta">
 		<ui-input v-if="meta.disableRegistration" v-model="invitationCode" type="text" :autocomplete="Math.random()" spellcheck="false" required styl="fill">
-			<span>%i18n:@invitation-code%</span>
+			<span>{{ $t('invitation-code') }}</span>
 			<span slot="prefix"><fa icon="id-card-alt"/></span>
-			<p slot="desc" v-html="'%i18n:@invitation-info%'.replace('{}', 'mailto:' + meta.maintainer.email)"></p>
+			<p slot="desc" v-html="this.$t('invitation-info').replace('{}', 'mailto:' + meta.maintainer.email)"></p>
 		</ui-input>
 		<ui-input v-model="username" type="text" pattern="^[a-zA-Z0-9_]{1,20}$" :autocomplete="Math.random()" spellcheck="false" required @input="onChangeUsername" styl="fill">
-			<span>%i18n:@username%</span>
+			<span>{{ $t('username') }}</span>
 			<span slot="prefix">@</span>
 			<span slot="suffix">@{{ host }}</span>
-			<p slot="desc" v-if="usernameState == 'wait'" style="color:#999"><fa icon="spinner .pulse" fixed-width/> %i18n:@checking%</p>
-			<p slot="desc" v-if="usernameState == 'ok'" style="color:#3CB7B5"><fa icon="check" fixed-width/> %i18n:@available%</p>
-			<p slot="desc" v-if="usernameState == 'unavailable'" style="color:#FF1161"><fa icon="exclamation-triangle" fixed-width/> %i18n:@unavailable%</p>
-			<p slot="desc" v-if="usernameState == 'error'" style="color:#FF1161"><fa icon="exclamation-triangle" fixed-width/> %i18n:@error%</p>
-			<p slot="desc" v-if="usernameState == 'invalid-format'" style="color:#FF1161"><fa icon="exclamation-triangle" fixed-width/> %i18n:@invalid-format%</p>
-			<p slot="desc" v-if="usernameState == 'min-range'" style="color:#FF1161"><fa icon="exclamation-triangle" fixed-width/> %i18n:@too-short%</p>
-			<p slot="desc" v-if="usernameState == 'max-range'" style="color:#FF1161"><fa icon="exclamation-triangle" fixed-width/> %i18n:@too-long%</p>
+			<p slot="desc" v-if="usernameState == 'wait'" style="color:#999"><fa icon="spinner .pulse" fixed-width/> {{ $t('checking') }}</p>
+			<p slot="desc" v-if="usernameState == 'ok'" style="color:#3CB7B5"><fa icon="check" fixed-width/> {{ $t('available') }}</p>
+			<p slot="desc" v-if="usernameState == 'unavailable'" style="color:#FF1161"><fa icon="exclamation-triangle" fixed-width/> {{ $t('unavailable') }}</p>
+			<p slot="desc" v-if="usernameState == 'error'" style="color:#FF1161"><fa icon="exclamation-triangle" fixed-width/> {{ $t('error') }}</p>
+			<p slot="desc" v-if="usernameState == 'invalid-format'" style="color:#FF1161"><fa icon="exclamation-triangle" fixed-width/> {{ $t('invalid-format') }}</p>
+			<p slot="desc" v-if="usernameState == 'min-range'" style="color:#FF1161"><fa icon="exclamation-triangle" fixed-width/> {{ $t('too-short') }}</p>
+			<p slot="desc" v-if="usernameState == 'max-range'" style="color:#FF1161"><fa icon="exclamation-triangle" fixed-width/> {{ $t('too-long') }}</p>
 		</ui-input>
 		<ui-input v-model="password" type="password" :autocomplete="Math.random()" required @input="onChangePassword" :with-password-meter="true" styl="fill">
-			<span>%i18n:@password%</span>
+			<span>{{ $t('password') }}</span>
 			<span slot="prefix"><fa icon="lock"/></span>
 			<div slot="desc">
-				<p v-if="passwordStrength == 'low'" style="color:#FF1161"><fa icon="exclamation-triangle" fixed-width/> %i18n:@weak-password%</p>
-				<p v-if="passwordStrength == 'medium'" style="color:#3CB7B5"><fa icon="check" fixed-width/> %i18n:@normal-password%</p>
-				<p v-if="passwordStrength == 'high'" style="color:#3CB7B5"><fa icon="check" fixed-width/> %i18n:@strong-password%</p>
+				<p v-if="passwordStrength == 'low'" style="color:#FF1161"><fa icon="exclamation-triangle" fixed-width/> {{ $t('weak-password') }}</p>
+				<p v-if="passwordStrength == 'medium'" style="color:#3CB7B5"><fa icon="check" fixed-width/> {{ $t('normal-password') }}</p>
+				<p v-if="passwordStrength == 'high'" style="color:#3CB7B5"><fa icon="check" fixed-width/> {{ $t('strong-password') }}</p>
 			</div>
 		</ui-input>
 		<ui-input v-model="retypedPassword" type="password" :autocomplete="Math.random()" required @input="onChangePasswordRetype" styl="fill">
-			<span>%i18n:@password% (%i18n:@retype%)</span>
+			<span>{{ $t('password') }} ({{ $t('retype') }})</span>
 			<span slot="prefix"><fa icon="lock"/></span>
 			<div slot="desc">
-				<p v-if="passwordRetypeState == 'match'" style="color:#3CB7B5"><fa icon="check" fixed-width/> %i18n:@password-matched%</p>
-				<p v-if="passwordRetypeState == 'not-match'" style="color:#FF1161"><fa icon="exclamation-triangle" fixed-width/> %i18n:@password-not-matched%</p>
+				<p v-if="passwordRetypeState == 'match'" style="color:#3CB7B5"><fa icon="check" fixed-width/> {{ $t('password-matched') }}</p>
+				<p v-if="passwordRetypeState == 'not-match'" style="color:#FF1161"><fa icon="exclamation-triangle" fixed-width/> {{ $t('password-not-matched') }}</p>
 			</div>
 		</ui-input>
 		<div v-if="meta.enableRecaptcha" class="g-recaptcha" :data-sitekey="meta.recaptchaSiteKey" style="margin: 16px 0;"></div>
-		<ui-button type="submit">%i18n:@create%</ui-button>
+		<ui-button type="submit">{{ $t('create') }}</ui-button>
 	</template>
 </form>
 </template>
 
 <script lang="ts">
 import Vue from 'vue';
+import i18n from '../../../i18n';
 const getPasswordStrength = require('syuilo-password-strength');
 import { host, url } from '../../../config';
 
 export default Vue.extend({
+	i18n: i18n('common/views/components/signup.vue'),
 	data() {
 		return {
 			host,
@@ -139,7 +141,7 @@ export default Vue.extend({
 					location.href = '/';
 				});
 			}).catch(() => {
-				alert('%i18n:@some-error%');
+				alert(this.$t('some-error'));
 
 				if (this.meta.enableRecaptcha) {
 					(window as any).grecaptcha.reset();
diff --git a/src/client/app/common/views/components/special-message.vue b/src/client/app/common/views/components/special-message.vue
deleted file mode 100644
index deac757c5b..0000000000
--- a/src/client/app/common/views/components/special-message.vue
+++ /dev/null
@@ -1,42 +0,0 @@
-<template>
-<div class="mk-special-message">
-	<p v-if="m == 1 && d == 1">%i18n:@new-year%</p>
-	<p v-if="m == 12 && d == 25">%i18n:@christmas%</p>
-</div>
-</template>
-
-<script lang="ts">
-import Vue from 'vue';
-export default Vue.extend({
-	data() {
-		return {
-			now: new Date()
-		};
-	},
-	computed: {
-		d(): number {
-			return this.now.getDate();
-		},
-		m(): number {
-			return this.now.getMonth() + 1;
-		}
-	}
-});
-</script>
-
-<style lang="stylus" scoped>
-.mk-special-message
-	&:empty
-		display none
-
-	> p
-		margin 0
-		padding 4px
-		text-align center
-		font-size 14px
-		font-weight bold
-		text-transform uppercase
-		color #fff
-		background #ff1036
-
-</style>
diff --git a/src/client/app/common/views/components/stream-indicator.vue b/src/client/app/common/views/components/stream-indicator.vue
index d639261fdc..3955c2711d 100644
--- a/src/client/app/common/views/components/stream-indicator.vue
+++ b/src/client/app/common/views/components/stream-indicator.vue
@@ -2,24 +2,26 @@
 <div class="mk-stream-indicator">
 	<p v-if="stream.state == 'initializing'">
 		<fa icon="spinner .pulse"/>
-		<span>%i18n:@connecting%<mk-ellipsis/></span>
+		<span>{{ $t('connecting') }}<mk-ellipsis/></span>
 	</p>
 	<p v-if="stream.state == 'reconnecting'">
 		<fa icon="spinner .pulse"/>
-		<span>%i18n:@reconnecting%<mk-ellipsis/></span>
+		<span>{{ $t('reconnecting') }}<mk-ellipsis/></span>
 	</p>
 	<p v-if="stream.state == 'connected'">
 		<fa icon="check"/>
-		<span>%i18n:@connected%</span>
+		<span>{{ $t('connected') }}</span>
 	</p>
 </div>
 </template>
 
 <script lang="ts">
 import Vue from 'vue';
+import i18n from '../../../i18n';
 import * as anime from 'animejs';
 
 export default Vue.extend({
+	i18n: i18n('common/views/components/stream-indicator.vue'),
 	computed: {
 		stream() {
 			return (this as any).os.stream;
diff --git a/src/client/app/common/views/components/tag-cloud.vue b/src/client/app/common/views/components/tag-cloud.vue
index 5a1768828c..8bca98bf75 100644
--- a/src/client/app/common/views/components/tag-cloud.vue
+++ b/src/client/app/common/views/components/tag-cloud.vue
@@ -1,7 +1,7 @@
 <template>
 <div class="jtivnzhfwquxpsfidertopbmwmchmnmo">
-	<p class="fetching" v-if="fetching"><fa icon="spinner .pulse" fixed-width/>%i18n:common.loading%<mk-ellipsis/></p>
-	<p class="empty" v-else-if="tags.length == 0"><fa icon="exclamation-circle"/>%i18n:@empty%</p>
+	<p class="fetching" v-if="fetching"><fa icon="spinner .pulse" fixed-width/>{{ $t('@.loading') }}<mk-ellipsis/></p>
+	<p class="empty" v-else-if="tags.length == 0"><fa icon="exclamation-circle"/>{{ $t('empty') }}</p>
 	<div v-else>
 		<vue-word-cloud
 				:words="tags.slice(0, 20).map(x => [x.name, x.count])"
@@ -19,9 +19,11 @@
 
 <script lang="ts">
 import Vue from 'vue';
+import i18n from '../../../i18n';
 import * as VueWordCloud from 'vuewordcloud';
 
 export default Vue.extend({
+	i18n: i18n('common/views/components/tag-cloud.vue'),
 	components: {
 		[VueWordCloud.name]: VueWordCloud
 	},
diff --git a/src/client/app/common/views/components/theme.vue b/src/client/app/common/views/components/theme.vue
index d4e8d155e9..31abef1d20 100644
--- a/src/client/app/common/views/components/theme.vue
+++ b/src/client/app/common/views/components/theme.vue
@@ -1,96 +1,96 @@
 <template>
 <div class="nicnklzforebnpfgasiypmpdaaglujqm">
 	<label>
-		<span>%i18n:@light-theme%</span>
-		<ui-select v-model="light" placeholder="%i18n:@light-theme%">
-			<optgroup label="%i18n:@light-themes%">
+		<span>{{ $t('light-theme') }}</span>
+		<ui-select v-model="light" :placeholder="$t('placeholder')">
+			<optgroup :label="$t('label')">
 				<option v-for="x in lightThemes" :value="x.id" :key="x.id">{{ x.name }}</option>
 			</optgroup>
-			<optgroup label="%i18n:@dark-themes%">
+			<optgroup :label="$t('label')">
 				<option v-for="x in darkThemes" :value="x.id" :key="x.id">{{ x.name }}</option>
 			</optgroup>
 		</ui-select>
 	</label>
 
 	<label>
-		<span>%i18n:@dark-theme%</span>
-		<ui-select v-model="dark" placeholder="%i18n:@dark-theme%">
-			<optgroup label="%i18n:@dark-themes%">
+		<span>{{ $t('dark-theme') }}</span>
+		<ui-select v-model="dark" :placeholder="$t('placeholder')">
+			<optgroup :label="$t('label')">
 				<option v-for="x in darkThemes" :value="x.id" :key="x.id">{{ x.name }}</option>
 			</optgroup>
-			<optgroup label="%i18n:@light-themes%">
+			<optgroup :label="$t('label')">
 				<option v-for="x in lightThemes" :value="x.id" :key="x.id">{{ x.name }}</option>
 			</optgroup>
 		</ui-select>
 	</label>
 
 	<details class="creator">
-		<summary><fa icon="palette"/> %i18n:@create-a-theme%</summary>
+		<summary><fa icon="palette"/> {{ $t('create-a-theme') }}</summary>
 		<div>
-			<span>%i18n:@base-theme%:</span>
-			<ui-radio v-model="myThemeBase" value="light">%i18n:@base-theme-light%</ui-radio>
-			<ui-radio v-model="myThemeBase" value="dark">%i18n:@base-theme-dark%</ui-radio>
+			<span>{{ $t('base-theme') }}:</span>
+			<ui-radio v-model="myThemeBase" value="light">{{ $t('base-theme-light') }}</ui-radio>
+			<ui-radio v-model="myThemeBase" value="dark">{{ $t('base-theme-dark') }}</ui-radio>
 		</div>
 		<div>
 			<ui-input v-model="myThemeName">
-				<span>%i18n:@theme-name%</span>
+				<span>{{ $t('theme-name') }}</span>
 			</ui-input>
 			<ui-textarea v-model="myThemeDesc">
-				<span>%i18n:@desc%</span>
+				<span>{{ $t('desc') }}</span>
 			</ui-textarea>
 		</div>
 		<div>
-			<div style="padding-bottom:8px;">%i18n:@primary-color%:</div>
+			<div style="padding-bottom:8px;">{{ $t('primary-color') }}:</div>
 			<color-picker v-model="myThemePrimary"/>
 		</div>
 		<div>
-			<div style="padding-bottom:8px;">%i18n:@secondary-color%:</div>
+			<div style="padding-bottom:8px;">{{ $t('secondary-color') }}:</div>
 			<color-picker v-model="myThemeSecondary"/>
 		</div>
 		<div>
-			<div style="padding-bottom:8px;">%i18n:@text-color%:</div>
+			<div style="padding-bottom:8px;">{{ $t('text-color') }}:</div>
 			<color-picker v-model="myThemeText"/>
 		</div>
-		<ui-button @click="preview()"><fa icon="eye"/> %i18n:@preview-created-theme%</ui-button>
-		<ui-button primary @click="gen()"><fa :icon="['far', 'save']"/> %i18n:@save-created-theme%</ui-button>
+		<ui-button @click="preview()"><fa icon="eye"/> {{ $t('preview-created-theme') }}</ui-button>
+		<ui-button primary @click="gen()"><fa :icon="['far', 'save']"/> {{ $t('save-created-theme') }}</ui-button>
 	</details>
 
 	<details>
-		<summary><fa icon="download"/> %i18n:@install-a-theme%</summary>
-		<ui-button @click="import_()"><fa icon="file-import"/> %i18n:@import%</ui-button>
+		<summary><fa icon="download"/> {{ $t('install-a-theme') }}</summary>
+		<ui-button @click="import_()"><fa icon="file-import"/> {{ $t('import') }}</ui-button>
 		<input ref="file" type="file" accept=".misskeytheme" style="display:none;" @change="onUpdateImportFile"/>
-		<p>%i18n:@import-by-code%:</p>
+		<p>{{ $t('import-by-code') }}:</p>
 		<ui-textarea v-model="installThemeCode">
-			<span>%i18n:@theme-code%</span>
+			<span>{{ $t('theme-code') }}</span>
 		</ui-textarea>
-		<ui-button @click="() => install(this.installThemeCode)"><fa icon="check"/> %i18n:@install%</ui-button>
+		<ui-button @click="() => install(this.installThemeCode)"><fa icon="check"/> {{ $t('install') }}</ui-button>
 	</details>
 
 	<details>
-		<summary><fa icon="folder-open"/> %i18n:@manage-themes%</summary>
-		<ui-select v-model="selectedThemeId" placeholder="%i18n:@select-theme%">
-			<optgroup label="%i18n:@builtin-themes%">
+		<summary><fa icon="folder-open"/> {{ $t('manage-themes') }}</summary>
+		<ui-select v-model="selectedThemeId" :placeholder="$t('placeholder')">
+			<optgroup :label="$t('label')">
 				<option v-for="x in builtinThemes" :value="x.id" :key="x.id">{{ x.name }}</option>
 			</optgroup>
-			<optgroup label="%i18n:@my-themes%">
+			<optgroup :label="$t('label')">
 				<option v-for="x in installedThemes.filter(t => t.author == this.$store.state.i.username)" :value="x.id" :key="x.id">{{ x.name }}</option>
 			</optgroup>
-			<optgroup label="%i18n:@installed-themes%">
+			<optgroup :label="$t('label')">
 				<option v-for="x in installedThemes.filter(t => t.author != this.$store.state.i.username)" :value="x.id" :key="x.id">{{ x.name }}</option>
 			</optgroup>
 		</ui-select>
 		<template v-if="selectedTheme">
 			<ui-input readonly :value="selectedTheme.author">
-				<span>%i18n:@author%</span>
+				<span>{{ $t('author') }}</span>
 			</ui-input>
 			<ui-textarea v-if="selectedTheme.desc" readonly :value="selectedTheme.desc">
-				<span>%i18n:@desc%</span>
+				<span>{{ $t('desc') }}</span>
 			</ui-textarea>
 			<ui-textarea readonly :value="selectedThemeCode">
-				<span>%i18n:@theme-code%</span>
+				<span>{{ $t('theme-code') }}</span>
 			</ui-textarea>
-			<ui-button @click="export_()" link :download="`${selectedTheme.name}.misskeytheme`" ref="export"><fa icon="box"/> %i18n:@export%</ui-button>
-			<ui-button @click="uninstall()" v-if="!builtinThemes.some(t => t.id == selectedTheme.id)"><fa :icon="['far', 'trash-alt']"/> %i18n:@uninstall%</ui-button>
+			<ui-button @click="export_()" link :download="`${selectedTheme.name}.misskeytheme`" ref="export"><fa icon="box"/> {{ $t('export') }}</ui-button>
+			<ui-button @click="uninstall()" v-if="!builtinThemes.some(t => t.id == selectedTheme.id)"><fa :icon="['far', 'trash-alt']"/> {{ $t('uninstall') }}</ui-button>
 		</template>
 	</details>
 </div>
@@ -98,6 +98,7 @@
 
 <script lang="ts">
 import Vue from 'vue';
+import i18n from '../../../i18n';
 import { lightTheme, darkTheme, builtinThemes, applyTheme, Theme } from '../../../theme';
 import { Chrome } from 'vue-color';
 import * as uuid from 'uuid';
@@ -119,6 +120,7 @@ function convertOldThemedefinition(t) {
 }
 
 export default Vue.extend({
+	i18n: i18n('common/views/components/theme.vue'),
 	components: {
 		ColorPicker: Chrome
 	},
@@ -221,7 +223,7 @@ export default Vue.extend({
 			} catch (e) {
 				this.$swal({
 					type: 'error',
-					text: '%i18n:@invalid-theme%'
+					text: this.$t('invalid-theme')
 				});
 				return;
 			}
@@ -234,7 +236,7 @@ export default Vue.extend({
 			if (theme.id == null) {
 				this.$swal({
 					type: 'error',
-					text: '%i18n:@invalid-theme%'
+					text: this.$t('invalid-theme')
 				});
 				return;
 			}
@@ -242,7 +244,7 @@ export default Vue.extend({
 			if (this.$store.state.device.themes.some(t => t.id == theme.id)) {
 				this.$swal({
 					type: 'info',
-					text: '%i18n:@already-installed%'
+					text: this.$t('already-installed')
 				});
 				return;
 			}
@@ -254,7 +256,7 @@ export default Vue.extend({
 
 			this.$swal({
 				type: 'success',
-				text: '%i18n:@installed%'.replace('{}', theme.name)
+				text: this.$t('installed').replace('{}', theme.name)
 			});
 		},
 
@@ -267,7 +269,7 @@ export default Vue.extend({
 
 			this.$swal({
 				type: 'info',
-				text: '%i18n:@uninstalled%'.replace('{}', theme.name)
+				text: this.$t('uninstalled').replace('{}', theme.name)
 			});
 		},
 
@@ -304,7 +306,7 @@ export default Vue.extend({
 			if (theme.name == null || theme.name.trim() == '') {
 				this.$swal({
 					type: 'warning',
-					text: '%i18n:@theme-name-required%'
+					text: this.$t('theme-name-required')
 				});
 				return;
 			}
@@ -318,7 +320,7 @@ export default Vue.extend({
 
 			this.$swal({
 				type: 'success',
-				text: '%i18n:@saved%'
+				text: this.$t('saved')
 			});
 		}
 	}
diff --git a/src/client/app/common/views/components/time.vue b/src/client/app/common/views/components/time.vue
index 700643ff05..994f593f95 100644
--- a/src/client/app/common/views/components/time.vue
+++ b/src/client/app/common/views/components/time.vue
@@ -8,8 +8,10 @@
 
 <script lang="ts">
 import Vue from 'vue';
+import i18n from '../../../i18n';
 
 export default Vue.extend({
+	i18n: i18n(),
 	props: {
 		time: {
 			type: [Date, String],
@@ -44,16 +46,16 @@ export default Vue.extend({
 			const time = this._time;
 			const ago = (this.now.getTime() - time.getTime()) / 1000/*ms*/;
 			return (
-				ago >= 31536000 ? '%i18n:common.time.years_ago%'  .replace('{}', (~~(ago / 31536000)).toString()) :
-				ago >= 2592000  ? '%i18n:common.time.months_ago%' .replace('{}', (~~(ago / 2592000)).toString()) :
-				ago >= 604800   ? '%i18n:common.time.weeks_ago%'  .replace('{}', (~~(ago / 604800)).toString()) :
-				ago >= 86400    ? '%i18n:common.time.days_ago%'   .replace('{}', (~~(ago / 86400)).toString()) :
-				ago >= 3600     ? '%i18n:common.time.hours_ago%'  .replace('{}', (~~(ago / 3600)).toString()) :
-				ago >= 60       ? '%i18n:common.time.minutes_ago%'.replace('{}', (~~(ago / 60)).toString()) :
-				ago >= 10       ? '%i18n:common.time.seconds_ago%'.replace('{}', (~~(ago % 60)).toString()) :
-				ago >= 0        ? '%i18n:common.time.just_now%' :
-				ago <  0        ? '%i18n:common.time.future%' :
-				'%i18n:common.time.unknown%');
+				ago >= 31536000 ? this.$t('@.time.years_ago')  .replace('{}', (~~(ago / 31536000)).toString()) :
+				ago >= 2592000  ? this.$t('@.time.months_ago') .replace('{}', (~~(ago / 2592000)).toString()) :
+				ago >= 604800   ? this.$t('@.time.weeks_ago')  .replace('{}', (~~(ago / 604800)).toString()) :
+				ago >= 86400    ? this.$t('@.time.days_ago')   .replace('{}', (~~(ago / 86400)).toString()) :
+				ago >= 3600     ? this.$t('@.time.hours_ago')  .replace('{}', (~~(ago / 3600)).toString()) :
+				ago >= 60       ? this.$t('@.time.minutes_ago').replace('{}', (~~(ago / 60)).toString()) :
+				ago >= 10       ? this.$t('@.time.seconds_ago').replace('{}', (~~(ago % 60)).toString()) :
+				ago >= 0        ? this.$t('@.time.just_now') :
+				ago <  0        ? this.$t('@.time.future') :
+				this.$t('@.time.unknown'));
 		}
 	},
 	created() {
diff --git a/src/client/app/common/views/components/trends.vue b/src/client/app/common/views/components/trends.vue
index ea3f854c6e..b39f711b9f 100644
--- a/src/client/app/common/views/components/trends.vue
+++ b/src/client/app/common/views/components/trends.vue
@@ -1,13 +1,13 @@
 <template>
 <div class="csqvmxybqbycalfhkxvyfrgbrdalkaoc">
-	<p class="fetching" v-if="fetching"><fa icon="spinner .pulse" fixed-width/>%i18n:common.loading%<mk-ellipsis/></p>
-	<p class="empty" v-else-if="stats.length == 0"><fa icon="exclamation-circle"/>%i18n:@empty%</p>
+	<p class="fetching" v-if="fetching"><fa icon="spinner .pulse" fixed-width/>{{ $t('@.loading') }}<mk-ellipsis/></p>
+	<p class="empty" v-else-if="stats.length == 0"><fa icon="exclamation-circle"/>{{ $t('empty') }}</p>
 	<!-- トランジションを有効にするとなぜかメモリリークする -->
 	<transition-group v-else tag="div" name="chart">
 		<div v-for="stat in stats" :key="stat.tag">
 			<div class="tag">
 				<router-link :to="`/tags/${ encodeURIComponent(stat.tag) }`" :title="stat.tag">#{{ stat.tag }}</router-link>
-				<p>{{ '%i18n:@count%'.replace('{}', stat.usersCount) }}</p>
+				<p>{{ this.$t('count').replace('{}', stat.usersCount) }}</p>
 			</div>
 			<x-chart class="chart" :src="stat.chart"/>
 		</div>
@@ -17,9 +17,11 @@
 
 <script lang="ts">
 import Vue from 'vue';
+import i18n from '../../../i18n';
 import XChart from './trends.chart.vue';
 
 export default Vue.extend({
+	i18n: i18n('common/views/components/trends.vue'),
 	components: {
 		XChart
 	},
diff --git a/src/client/app/common/views/components/twitter-setting.vue b/src/client/app/common/views/components/twitter-setting.vue
index d1cb78c544..a584cd4044 100644
--- a/src/client/app/common/views/components/twitter-setting.vue
+++ b/src/client/app/common/views/components/twitter-setting.vue
@@ -1,11 +1,11 @@
 <template>
 <div class="mk-twitter-setting">
-	<p>%i18n:@description%<a :href="`${docsUrl}/link-to-twitter`" target="_blank">%i18n:@detail%</a></p>
-	<p class="account" v-if="$store.state.i.twitter" :title="`Twitter ID: ${$store.state.i.twitter.userId}`">%i18n:@connected-to%: <a :href="`https://twitter.com/${$store.state.i.twitter.screenName}`" target="_blank">@{{ $store.state.i.twitter.screenName }}</a></p>
+	<p>{{ $t('description') }}<a :href="`${docsUrl}/link-to-twitter`" target="_blank">{{ $t('detail') }}</a></p>
+	<p class="account" v-if="$store.state.i.twitter" :title="`Twitter ID: ${$store.state.i.twitter.userId}`">{{ $t('connected-to') }}: <a :href="`https://twitter.com/${$store.state.i.twitter.screenName}`" target="_blank">@{{ $store.state.i.twitter.screenName }}</a></p>
 	<p>
-		<a :href="`${apiUrl}/connect/twitter`" target="_blank" @click.prevent="connect">{{ $store.state.i.twitter ? '%i18n:@reconnect%' : '%i18n:@connect%' }}</a>
+		<a :href="`${apiUrl}/connect/twitter`" target="_blank" @click.prevent="connect">{{ $store.state.i.twitter ? this.$t('reconnect') : this.$t('connect') }}</a>
 		<span v-if="$store.state.i.twitter"> or </span>
-		<a :href="`${apiUrl}/disconnect/twitter`" target="_blank" v-if="$store.state.i.twitter" @click.prevent="disconnect">%i18n:@disconnect%</a>
+		<a :href="`${apiUrl}/disconnect/twitter`" target="_blank" v-if="$store.state.i.twitter" @click.prevent="disconnect">{{ $t('disconnect') }}</a>
 	</p>
 	<p class="id" v-if="$store.state.i.twitter">Twitter ID: {{ $store.state.i.twitter.userId }}</p>
 </div>
@@ -13,9 +13,11 @@
 
 <script lang="ts">
 import Vue from 'vue';
+import i18n from '../../../i18n';
 import { apiUrl, docsUrl } from '../../../config';
 
 export default Vue.extend({
+	i18n: i18n('common/views/components/twitter-setting.vue'),
 	data() {
 		return {
 			form: null,
diff --git a/src/client/app/common/views/components/ui/textarea.vue b/src/client/app/common/views/components/ui/textarea.vue
index 947dfd9c69..00b8b0f043 100644
--- a/src/client/app/common/views/components/ui/textarea.vue
+++ b/src/client/app/common/views/components/ui/textarea.vue
@@ -19,7 +19,6 @@
 
 <script lang="ts">
 import Vue from 'vue';
-const getPasswordStrength = require('syuilo-password-strength');
 
 export default Vue.extend({
 	props: {
diff --git a/src/client/app/common/views/components/uploader.vue b/src/client/app/common/views/components/uploader.vue
index 67ad7ecc99..d516227ad3 100644
--- a/src/client/app/common/views/components/uploader.vue
+++ b/src/client/app/common/views/components/uploader.vue
@@ -5,7 +5,7 @@
 			<div class="img" :style="{ backgroundImage: `url(${ ctx.img })` }"></div>
 			<p class="name"><fa icon="spinner .pulse"/>{{ ctx.name }}</p>
 			<p class="status">
-				<span class="initing" v-if="ctx.progress == undefined">%i18n:@waiting%<mk-ellipsis/></span>
+				<span class="initing" v-if="ctx.progress == undefined">{{ $t('waiting') }}<mk-ellipsis/></span>
 				<span class="kb" v-if="ctx.progress != undefined">{{ String(Math.floor(ctx.progress.value / 1024)).replace(/(\d)(?=(\d\d\d)+(?!\d))/g, '$1,') }}<i>KB</i> / {{ String(Math.floor(ctx.progress.max / 1024)).replace(/(\d)(?=(\d\d\d)+(?!\d))/g, '$1,') }}<i>KB</i></span>
 				<span class="percentage" v-if="ctx.progress != undefined">{{ Math.floor((ctx.progress.value / ctx.progress.max) * 100) }}</span>
 			</p>
@@ -19,10 +19,12 @@
 
 <script lang="ts">
 import Vue from 'vue';
+import i18n from '../../../i18n';
 import { apiUrl } from '../../../config';
 import getMD5 from '../../scripts/get-md5';
 
 export default Vue.extend({
+	i18n: i18n('common/views/components/uploader.vue'),
 	data() {
 		return {
 			uploads: []
diff --git a/src/client/app/common/views/components/url.vue b/src/client/app/common/views/components/url.vue
index 8698cb4a06..2daf79cba6 100644
--- a/src/client/app/common/views/components/url.vue
+++ b/src/client/app/common/views/components/url.vue
@@ -13,6 +13,7 @@
 <script lang="ts">
 import Vue from 'vue';
 import { toUnicode as decodePunycode } from 'punycode';
+
 export default Vue.extend({
 	props: ['url', 'target'],
 	data() {
diff --git a/src/client/app/common/views/components/visibility-chooser.vue b/src/client/app/common/views/components/visibility-chooser.vue
index 62ec9c35e8..896be039b3 100644
--- a/src/client/app/common/views/components/visibility-chooser.vue
+++ b/src/client/app/common/views/components/visibility-chooser.vue
@@ -5,34 +5,34 @@
 		<div @click="choose('public')" :class="{ active: v == 'public' }">
 			<div><fa icon="globe"/></div>
 			<div>
-				<span>%i18n:@public%</span>
+				<span>{{ $t('public') }}</span>
 			</div>
 		</div>
 		<div @click="choose('home')" :class="{ active: v == 'home' }">
 			<div><fa icon="home"/></div>
 			<div>
-				<span>%i18n:@home%</span>
-				<span>%i18n:@home-desc%</span>
+				<span>{{ $t('home') }}</span>
+				<span>{{ $t('home-desc') }}</span>
 			</div>
 		</div>
 		<div @click="choose('followers')" :class="{ active: v == 'followers' }">
 			<div><fa icon="unlock"/></div>
 			<div>
-				<span>%i18n:@followers%</span>
-				<span>%i18n:@followers-desc%</span>
+				<span>{{ $t('followers') }}</span>
+				<span>{{ $t('followers-desc') }}</span>
 			</div>
 		</div>
 		<div @click="choose('specified')" :class="{ active: v == 'specified' }">
 			<div><fa icon="envelope"/></div>
 			<div>
-				<span>%i18n:@specified%</span>
-				<span>%i18n:@specified-desc%</span>
+				<span>{{ $t('specified') }}</span>
+				<span>{{ $t('specified-desc') }}</span>
 			</div>
 		</div>
 		<div @click="choose('private')" :class="{ active: v == 'private' }">
 			<div><fa icon="lock"/></div>
 			<div>
-				<span>%i18n:@private%</span>
+				<span>{{ $t('private') }}</span>
 			</div>
 		</div>
 	</div>
@@ -41,9 +41,11 @@
 
 <script lang="ts">
 import Vue from 'vue';
+import i18n from '../../../i18n';
 import * as anime from 'animejs';
 
 export default Vue.extend({
+	i18n: i18n('common/views/components/visibility-chooser.vue'),
 	props: ['source', 'compact'],
 	data() {
 		return {
diff --git a/src/client/app/common/views/pages/follow.vue b/src/client/app/common/views/pages/follow.vue
index b800d65d14..7b396fa38f 100644
--- a/src/client/app/common/views/pages/follow.vue
+++ b/src/client/app/common/views/pages/follow.vue
@@ -1,6 +1,6 @@
 <template>
 <div class="syxhndwprovvuqhmyvveewmbqayniwkv" v-if="!fetching">
-	<div class="signed-in-as" v-html="'%i18n:@signed-in-as%'.replace('{}', `<b>${myName}`)"></div>
+	<div class="signed-in-as" v-html="this.$t('signed-in-as').replace('{}', `<b>${myName}`)"></div>
 
 	<main>
 		<div class="banner" :style="bannerStyle"></div>
@@ -19,11 +19,11 @@
 			@click="onClick"
 			:disabled="followWait">
 		<template v-if="!followWait">
-			<template v-if="user.hasPendingFollowRequestFromYou && user.isLocked"><fa icon="hourglass-half"/> %i18n:@request-pending%</template>
-			<template v-else-if="user.hasPendingFollowRequestFromYou && !user.isLocked"><fa icon="hourglass-start"/> %i18n:@follow-processing%</template>
-			<template v-else-if="user.isFollowing"><fa icon="minus"/> %i18n:@following%</template>
-			<template v-else-if="!user.isFollowing && user.isLocked"><fa icon="plus"/> %i18n:@follow-request%</template>
-			<template v-else-if="!user.isFollowing && !user.isLocked"><fa icon="plus"/> %i18n:@follow%</template>
+			<template v-if="user.hasPendingFollowRequestFromYou && user.isLocked"><fa icon="hourglass-half"/> {{ $t('request-pending') }}</template>
+			<template v-else-if="user.hasPendingFollowRequestFromYou && !user.isLocked"><fa icon="hourglass-start"/> {{ $t('follow-processing') }}</template>
+			<template v-else-if="user.isFollowing"><fa icon="minus"/> {{ $t('following') }}</template>
+			<template v-else-if="!user.isFollowing && user.isLocked"><fa icon="plus"/> {{ $t('follow-request') }}</template>
+			<template v-else-if="!user.isFollowing && !user.isLocked"><fa icon="plus"/> {{ $t('follow') }}</template>
 		</template>
 		<template v-else><fa icon="spinner .pulse" fixed-width/></template>
 	</button>
@@ -32,10 +32,12 @@
 
 <script lang="ts">
 import Vue from 'vue';
+import i18n from '../../../i18n';
 import parseAcct from '../../../../../misc/acct/parse';
 import Progress from '../../../common/scripts/loading';
 
 export default Vue.extend({
+	i18n: i18n('common/views/pages/follow.vue'),
 	data() {
 		return {
 			fetching: true,
diff --git a/src/client/app/common/views/widgets/broadcast.vue b/src/client/app/common/views/widgets/broadcast.vue
index 620b09ff0e..f5486936bf 100644
--- a/src/client/app/common/views/widgets/broadcast.vue
+++ b/src/client/app/common/views/widgets/broadcast.vue
@@ -15,13 +15,13 @@
 					<path class="wave d" d="M29.18,1.06c-0.479-0.502-1.273-0.522-1.775-0.044c-0.016,0.015-0.029,0.029-0.045,0.044c-0.5,0.52-0.5,1.36,0,1.88 c1.361,1.4,2.041,3.24,2.041,5.08s-0.68,3.66-2.041,5.08c-0.5,0.52-0.5,1.36,0,1.88c0.509,0.508,1.332,0.508,1.841,0 c1.86-1.92,2.8-4.44,2.8-6.96C31.99,5.424,30.98,2.931,29.18,1.06z"></path>
 				</svg>
 			</div>
-			<p class="fetching" v-if="fetching">%i18n:@fetching%<mk-ellipsis/></p>
-			<h1 v-if="!fetching">{{ announcements.length == 0 ? '%i18n:@no-broadcasts%' : announcements[i].title }}</h1>
+			<p class="fetching" v-if="fetching">{{ $t('fetching') }}<mk-ellipsis/></p>
+			<h1 v-if="!fetching">{{ announcements.length == 0 ? this.$t('no-broadcasts') : announcements[i].title }}</h1>
 			<p v-if="!fetching">
 				<span v-if="announcements.length != 0" v-html="announcements[i].text"></span>
-				<template v-if="announcements.length == 0">%i18n:@have-a-nice-day%</template>
+				<template v-if="announcements.length == 0">{{ $t('have-a-nice-day') }}</template>
 			</p>
-			<a v-if="announcements.length > 1" @click="next">%i18n:@next% &gt;&gt;</a>
+			<a v-if="announcements.length > 1" @click="next">{{ $t('next') }} &gt;&gt;</a>
 		</div>
 	</mk-widget-container>
 </div>
@@ -29,6 +29,7 @@
 
 <script lang="ts">
 import define from '../../../common/define-widget';
+import i18n from '../../../i18n';
 
 export default define({
 	name: 'broadcast',
@@ -36,6 +37,7 @@ export default define({
 		design: 0
 	})
 }).extend({
+	i18n: i18n('common/views/widgets/broadcast.vue'),
 	data() {
 		return {
 			i: 0,
diff --git a/src/client/app/common/views/widgets/calendar.vue b/src/client/app/common/views/widgets/calendar.vue
index 308d73bad8..f476968348 100644
--- a/src/client/app/common/views/widgets/calendar.vue
+++ b/src/client/app/common/views/widgets/calendar.vue
@@ -4,27 +4,27 @@
 		<div class="mkw-calendar--body">
 			<div class="calendar" :data-is-holiday="isHoliday">
 				<p class="month-and-year">
-					<span class="year">{{ '%i18n:@year%'.split('{}')[0] }}{{ year }}{{ '%i18n:@year%'.split('{}')[1] }}</span>
-					<span class="month">{{ '%i18n:@month%'.split('{}')[0] }}{{ month }}{{ '%i18n:@month%'.split('{}')[1] }}</span>
+					<span class="year">{{ this.$t('year').split('{}')[0] }}{{ year }}{{ this.$t('year').split('{}')[1] }}</span>
+					<span class="month">{{ this.$t('month').split('{}')[0] }}{{ month }}{{ this.$t('month').split('{}')[1] }}</span>
 				</p>
-				<p class="day">{{ '%i18n:@day%'.split('{}')[0] }}{{ day }}{{ '%i18n:@day%'.split('{}')[1] }}</p>
+				<p class="day">{{ this.$t('day').split('{}')[0] }}{{ day }}{{ this.$t('day').split('{}')[1] }}</p>
 				<p class="week-day">{{ weekDay }}</p>
 			</div>
 			<div class="info">
 				<div>
-					<p>%i18n:@today%<b>{{ dayP.toFixed(1) }}%</b></p>
+					<p>{{ $t('today') }}<b>{{ dayP.toFixed(1) }}%</b></p>
 					<div class="meter">
 						<div class="val" :style="{ width: `${dayP}%` }"></div>
 					</div>
 				</div>
 				<div>
-					<p>%i18n:@this-month%<b>{{ monthP.toFixed(1) }}%</b></p>
+					<p>{{ $t('this-month') }}<b>{{ monthP.toFixed(1) }}%</b></p>
 					<div class="meter">
 						<div class="val" :style="{ width: `${monthP}%` }"></div>
 					</div>
 				</div>
 				<div>
-					<p>%i18n:@this-year%<b>{{ yearP.toFixed(1) }}%</b></p>
+					<p>{{ $t('this-year') }}<b>{{ yearP.toFixed(1) }}%</b></p>
 					<div class="meter">
 						<div class="val" :style="{ width: `${yearP}%` }"></div>
 					</div>
@@ -37,12 +37,15 @@
 
 <script lang="ts">
 import define from '../../../common/define-widget';
+import i18n from '../../../i18n';
+
 export default define({
 	name: 'calendar',
 	props: () => ({
 		design: 0
 	})
 }).extend({
+	i18n: i18n('common/views/widgets/calendar.vue'),
 	data() {
 		return {
 			now: new Date(),
@@ -85,13 +88,13 @@ export default define({
 			this.month = nm + 1;
 			this.day = nd;
 			this.weekDay = [
-				'%i18n:common.weekday.sunday%',
-				'%i18n:common.weekday.monday%',
-				'%i18n:common.weekday.tuesday%',
-				'%i18n:common.weekday.wednesday%',
-				'%i18n:common.weekday.thursday%',
-				'%i18n:common.weekday.friday%',
-				'%i18n:common.weekday.saturday%'
+				this.$t('@.weekday.sunday'),
+				this.$t('@.weekday.monday'),
+				this.$t('@.weekday.tuesday'),
+				this.$t('@.weekday.wednesday'),
+				this.$t('@.weekday.thursday'),
+				this.$t('@.weekday.friday'),
+				this.$t('@.weekday.saturday')
 			][now.getDay()];
 
 			const dayNumer   = now.getTime() - new Date(ny, nm, nd).getTime();
diff --git a/src/client/app/common/views/widgets/donation.vue b/src/client/app/common/views/widgets/donation.vue
index 3321b768e1..785c9bb391 100644
--- a/src/client/app/common/views/widgets/donation.vue
+++ b/src/client/app/common/views/widgets/donation.vue
@@ -2,11 +2,11 @@
 <div>
 	<mk-widget-container :show-header="false">
 		<article class="dolfvtibguprpxxhfndqaosjitixjohx">
-			<h1><fa icon="heart"/>%i18n:@title%</h1>
+			<h1><fa icon="heart"/>{{ $t('title') }}</h1>
 			<p v-if="meta">
-				{{ '%i18n:@text%'.substr(0, '%i18n:@text%'.indexOf('{')) }}
+				{{ this.$t('text').substr(0, this.$t('text').indexOf('{')) }}
 				<a :href="'mailto:' + meta.maintainer.email">{{ meta.maintainer.name }}</a>
-				{{ '%i18n:@text%'.substr('%i18n:@text%'.indexOf('}') + 1) }}
+				{{ this.$t('text').substr(this.$t('text').indexOf('}') + 1) }}
 			</p>
 		</article>
 	</mk-widget-container>
@@ -15,9 +15,12 @@
 
 <script lang="ts">
 import define from '../../../common/define-widget';
+import i18n from '../../../i18n';
+
 export default define({
 	name: 'donation'
 }).extend({
+	i18n: i18n('common/views/widgets/donation.vue'),
 	data() {
 		return {
 			meta: null
diff --git a/src/client/app/common/views/widgets/hashtags.vue b/src/client/app/common/views/widgets/hashtags.vue
index 656c683171..dd3933465c 100644
--- a/src/client/app/common/views/widgets/hashtags.vue
+++ b/src/client/app/common/views/widgets/hashtags.vue
@@ -1,7 +1,7 @@
 <template>
 <div class="mkw-hashtags">
 	<mk-widget-container :show-header="!props.compact">
-		<template slot="header"><fa icon="hashtag"/>%i18n:@title%</template>
+		<template slot="header"><fa icon="hashtag"/>{{ $t('title') }}</template>
 
 		<div class="mkw-hashtags--body" :data-mobile="platform == 'mobile'">
 			<mk-trends/>
@@ -12,6 +12,7 @@
 
 <script lang="ts">
 import define from '../../../common/define-widget';
+import i18n from '../../../i18n';
 
 export default define({
 	name: 'hashtags',
@@ -19,6 +20,7 @@ export default define({
 		compact: false
 	})
 }).extend({
+	i18n: i18n('common/views/widgets/hashtags.vue'),
 	methods: {
 		func() {
 			this.props.compact = !this.props.compact;
diff --git a/src/client/app/common/views/widgets/memo.vue b/src/client/app/common/views/widgets/memo.vue
index 07d0c58345..d47257d22d 100644
--- a/src/client/app/common/views/widgets/memo.vue
+++ b/src/client/app/common/views/widgets/memo.vue
@@ -1,11 +1,11 @@
 <template>
 <div class="mkw-memo">
 	<mk-widget-container :show-header="!props.compact">
-		<template slot="header"><fa :icon="['far', 'sticky-note']"/>%i18n:@title%</template>
+		<template slot="header"><fa :icon="['far', 'sticky-note']"/>{{ $t('title') }}</template>
 
 		<div class="mkw-memo--body">
-			<textarea v-model="text" placeholder="%i18n:@memo%" @input="onChange"></textarea>
-			<button @click="saveMemo" :disabled="!changed">%i18n:@save%</button>
+			<textarea v-model="text" :placeholder="$t('placeholder')" @input="onChange"></textarea>
+			<button @click="saveMemo" :disabled="!changed">{{ $t('save') }}</button>
 		</div>
 	</mk-widget-container>
 </div>
@@ -13,6 +13,7 @@
 
 <script lang="ts">
 import define from '../../define-widget';
+import i18n from '../../../i18n';
 
 export default define({
 	name: 'memo',
@@ -20,6 +21,7 @@ export default define({
 		compact: false
 	})
 }).extend({
+	i18n: i18n('common/views/widgets/memo.vue'),
 	data() {
 		return {
 			text: null,
diff --git a/src/client/app/common/views/widgets/photo-stream.vue b/src/client/app/common/views/widgets/photo-stream.vue
index 9c6ce129b7..812ef3417b 100644
--- a/src/client/app/common/views/widgets/photo-stream.vue
+++ b/src/client/app/common/views/widgets/photo-stream.vue
@@ -1,25 +1,29 @@
 <template>
 <div class="mkw-photo-stream" :class="$style.root" :data-melt="props.design == 2">
 	<mk-widget-container :show-header="props.design == 0" :naked="props.design == 2">
-		<template slot="header"><fa icon="camera"/>%i18n:@title%</template>
+		<template slot="header"><fa icon="camera"/>{{ $t('title') }}</template>
 
-		<p :class="$style.fetching" v-if="fetching"><fa icon="spinner .pulse" fixed-width/>%i18n:common.loading%<mk-ellipsis/></p>
+		<p :class="$style.fetching" v-if="fetching"><fa icon="spinner .pulse" fixed-width/>{{ $t('@.loading') }}<mk-ellipsis/></p>
 		<div :class="$style.stream" v-if="!fetching && images.length > 0">
 			<div v-for="image in images" :class="$style.img" :style="`background-image: url(${image.thumbnailUrl || image.url})`"></div>
 		</div>
-		<p :class="$style.empty" v-if="!fetching && images.length == 0">%i18n:@no-photos%</p>
+		<p :class="$style.empty" v-if="!fetching && images.length == 0">{{ $t('no-photos') }}</p>
 	</mk-widget-container>
 </div>
 </template>
 
 <script lang="ts">
 import define from '../../../common/define-widget';
+import i18n from '../../../i18n';
+
 export default define({
 	name: 'photo-stream',
 	props: () => ({
 		design: 0
 	})
 }).extend({
+	i18n: i18n('common/views/widgets/photo-stream.vue'),
+
 	data() {
 		return {
 			images: [],
diff --git a/src/client/app/common/views/widgets/posts-monitor.vue b/src/client/app/common/views/widgets/posts-monitor.vue
index 4527489f52..75d1c95b24 100644
--- a/src/client/app/common/views/widgets/posts-monitor.vue
+++ b/src/client/app/common/views/widgets/posts-monitor.vue
@@ -1,8 +1,8 @@
 <template>
 <div class="mkw-posts-monitor">
 	<mk-widget-container :show-header="props.design == 0" :naked="props.design == 2">
-		<template slot="header"><fa icon="chart-line"/>%i18n:@title%</template>
-		<button slot="func" @click="toggle" title="%i18n:@toggle%"><fa icon="sort"/></button>
+		<template slot="header"><fa icon="chart-line"/>{{ $t('title') }}</template>
+		<button slot="func" @click="toggle" :title="$t('toggle')"><fa icon="sort"/></button>
 
 		<div class="qpdmibaztplkylerhdbllwcokyrfxeyj" :class="{ dual: props.view == 0 }">
 			<svg :viewBox="`0 0 ${ viewBoxX } ${ viewBoxY }`" v-show="props.view != 2">
@@ -70,15 +70,18 @@
 
 <script lang="ts">
 import define from '../../../common/define-widget';
+import i18n from '../../../i18n';
 import * as uuid from 'uuid';
 
 export default define({
-	name: 'server',
+	name: 'posts-monitor',
 	props: () => ({
 		design: 0,
 		view: 0
 	})
 }).extend({
+	i18n: i18n('common/views/widgets/posts-monitor.vue'),
+
 	data() {
 		return {
 			connection: null,
diff --git a/src/client/app/common/views/widgets/rss.vue b/src/client/app/common/views/widgets/rss.vue
index 2fc97c8043..6e3819da15 100644
--- a/src/client/app/common/views/widgets/rss.vue
+++ b/src/client/app/common/views/widgets/rss.vue
@@ -5,7 +5,7 @@
 		<button slot="func" title="設定" @click="setting"><fa icon="cog"/></button>
 
 		<div class="mkw-rss--body" :data-mobile="platform == 'mobile'">
-			<p class="fetching" v-if="fetching"><fa icon="spinner .pulse" fixed-width/>%i18n:common.loading%<mk-ellipsis/></p>
+			<p class="fetching" v-if="fetching"><fa icon="spinner .pulse" fixed-width/>{{ $t('@.loading') }}<mk-ellipsis/></p>
 			<div class="feed" v-else>
 				<a v-for="item in items" :href="item.link" target="_blank">{{ item.title }}</a>
 			</div>
@@ -16,6 +16,8 @@
 
 <script lang="ts">
 import define from '../../../common/define-widget';
+import i18n from '../../../i18n';
+
 export default define({
 	name: 'rss',
 	props: () => ({
@@ -23,6 +25,7 @@ export default define({
 		url: 'http://news.yahoo.co.jp/pickup/rss.xml'
 	})
 }).extend({
+	i18n: i18n(),
 	data() {
 		return {
 			items: [],
diff --git a/src/client/app/common/views/widgets/server.vue b/src/client/app/common/views/widgets/server.vue
index 18c2af1e4b..50cc1e4967 100644
--- a/src/client/app/common/views/widgets/server.vue
+++ b/src/client/app/common/views/widgets/server.vue
@@ -1,10 +1,10 @@
 <template>
 <div class="mkw-server">
 	<mk-widget-container :show-header="props.design == 0" :naked="props.design == 2">
-		<template slot="header"><fa icon="server"/>%i18n:@title%</template>
-		<button slot="func" @click="toggle" title="%i18n:@toggle%"><fa icon="sort"/></button>
+		<template slot="header"><fa icon="server"/>{{ $t('title') }}</template>
+		<button slot="func" @click="toggle" :title="$t('toggle')"><fa icon="sort"/></button>
 
-		<p :class="$style.fetching" v-if="fetching"><fa icon="spinner .pulse" fixed-width/>%i18n:common.loading%<mk-ellipsis/></p>
+		<p :class="$style.fetching" v-if="fetching"><fa icon="spinner .pulse" fixed-width/>{{ $t('@.loading') }}<mk-ellipsis/></p>
 		<template v-if="!fetching">
 			<x-cpu-memory v-show="props.view == 0" :connection="connection"/>
 			<x-cpu v-show="props.view == 1" :connection="connection" :meta="meta"/>
@@ -19,6 +19,7 @@
 
 <script lang="ts">
 import define from '../../../common/define-widget';
+import i18n from '../../../i18n';
 import XCpuMemory from './server.cpu-memory.vue';
 import XCpu from './server.cpu.vue';
 import XMemory from './server.memory.vue';
@@ -33,6 +34,8 @@ export default define({
 		view: 0
 	})
 }).extend({
+	i18n: i18n('common/views/widgets/server.vue'),
+
 	components: {
 		XCpuMemory,
 		XCpu,
diff --git a/src/client/app/common/views/widgets/slideshow.vue b/src/client/app/common/views/widgets/slideshow.vue
index 4f7f7e313f..abe56cd6f3 100644
--- a/src/client/app/common/views/widgets/slideshow.vue
+++ b/src/client/app/common/views/widgets/slideshow.vue
@@ -2,10 +2,10 @@
 <div class="mkw-slideshow" :data-mobile="platform == 'mobile'">
 	<div @click="choose">
 		<p v-if="props.folder === undefined">
-			<template v-if="isCustomizeMode">%i18n:@folder-customize-mode%</template>
-			<template v-else>%i18n:@folder%</template>
+			<template v-if="isCustomizeMode">{{ $t('folder-customize-mode') }}</template>
+			<template v-else>{{ $t('folder') }}</template>
 		</p>
-		<p v-if="props.folder !== undefined && images.length == 0 && !fetching">%i18n:@no-image%</p>
+		<p v-if="props.folder !== undefined && images.length == 0 && !fetching">{{ $t('no-image') }}</p>
 		<div ref="slideA" class="slide a"></div>
 		<div ref="slideB" class="slide b"></div>
 	</div>
@@ -15,6 +15,8 @@
 <script lang="ts">
 import * as anime from 'animejs';
 import define from '../../../common/define-widget';
+import i18n from '../../../i18n';
+
 export default define({
 	name: 'slideshow',
 	props: () => ({
@@ -22,6 +24,8 @@ export default define({
 		size: 0
 	})
 }).extend({
+	i18n: i18n('common/views/widgets/slideshow.vue'),
+
 	data() {
 		return {
 			images: [],
diff --git a/src/client/app/common/views/widgets/tips.vue b/src/client/app/common/views/widgets/tips.vue
index 3ed6548801..58b16f7d42 100644
--- a/src/client/app/common/views/widgets/tips.vue
+++ b/src/client/app/common/views/widgets/tips.vue
@@ -7,35 +7,37 @@
 <script lang="ts">
 import * as anime from 'animejs';
 import define from '../../../common/define-widget';
-
-const tips = [
-	'%i18n:@tips-line1%',
-	'%i18n:@tips-line2%',
-	'%i18n:@tips-line3%',
-	'%i18n:@tips-line4%',
-	'%i18n:@tips-line5%',
-	'%i18n:@tips-line6%',
-	'%i18n:@tips-line7%',
-	'%i18n:@tips-line8%',
-	'%i18n:@tips-line9%',
-	'%i18n:@tips-line10%',
-	'%i18n:@tips-line11%',
-	'%i18n:@tips-line13%',
-	'%i18n:@tips-line14%',
-	'%i18n:@tips-line17%',
-	'%i18n:@tips-line19%',
-	'%i18n:@tips-line20%',
-	'%i18n:@tips-line21%',
-	'%i18n:@tips-line23%',
-	'%i18n:@tips-line24%',
-	'%i18n:@tips-line25%'
-]
+import i18n from '../../../i18n';
 
 export default define({
 	name: 'tips'
 }).extend({
+	i18n: i18n('common/views/widgets/tips.vue'),
+
 	data() {
 		return {
+			tips: [
+				this.$t('tips-line1'),
+				this.$t('tips-line2'),
+				this.$t('tips-line3'),
+				this.$t('tips-line4'),
+				this.$t('tips-line5'),
+				this.$t('tips-line6'),
+				this.$t('tips-line7'),
+				this.$t('tips-line8'),
+				this.$t('tips-line9'),
+				this.$t('tips-line10'),
+				this.$t('tips-line11'),
+				this.$t('tips-line13'),
+				this.$t('tips-line14'),
+				this.$t('tips-line17'),
+				this.$t('tips-line19'),
+				this.$t('tips-line20'),
+				this.$t('tips-line21'),
+				this.$t('tips-line23'),
+				this.$t('tips-line24'),
+				this.$t('tips-line25')
+			],
 			tip: null,
 			clock: null
 		};
diff --git a/src/client/app/desktop/views/components/activity.chart.vue b/src/client/app/desktop/views/components/activity.chart.vue
index 202eabe90d..a6122ce8e8 100644
--- a/src/client/app/desktop/views/components/activity.chart.vue
+++ b/src/client/app/desktop/views/components/activity.chart.vue
@@ -1,6 +1,6 @@
 <template>
 <svg :viewBox="`0 0 ${ viewBoxX } ${ viewBoxY }`" @mousedown.prevent="onMousedown">
-	<title>%i18n:@total%<br/>%i18n:@notes%<br/>%i18n:@replies%<br/>%i18n:@renotes%</title>
+	<title>{{ $t('total') }}<br/>{{ $t('notes') }}<br/>{{ $t('replies') }}<br/>{{ $t('renotes') }}</title>
 	<polyline
 		:points="pointsNote"
 		fill="none"
@@ -27,6 +27,7 @@
 
 <script lang="ts">
 import Vue from 'vue';
+import i18n from '../../../i18n';
 
 function dragListen(fn) {
 	window.addEventListener('mousemove',  fn);
@@ -41,6 +42,7 @@ function dragClear(fn) {
 }
 
 export default Vue.extend({
+	i18n: i18n('desktop/views/components/activity.chart.vue'),
 	props: ['data'],
 	data() {
 		return {
diff --git a/src/client/app/desktop/views/components/activity.vue b/src/client/app/desktop/views/components/activity.vue
index 941fb59164..56dfaa8f2f 100644
--- a/src/client/app/desktop/views/components/activity.vue
+++ b/src/client/app/desktop/views/components/activity.vue
@@ -1,10 +1,10 @@
 <template>
 <div class="mk-activity">
 	<mk-widget-container :show-header="design == 0" :naked="design == 2">
-		<template slot="header"><fa icon="chart-bar"/>%i18n:@title%</template>
-		<button slot="func" title="%i18n:@toggle%" @click="toggle"><fa icon="sort"/></button>
+		<template slot="header"><fa icon="chart-bar"/>{{ $t('title') }}</template>
+		<button slot="func" :title="$t('toggle')" @click="toggle"><fa icon="sort"/></button>
 
-		<p :class="$style.fetching" v-if="fetching"><fa icon="spinner .pulse" fixed-width/>%i18n:common.loading%<mk-ellipsis/></p>
+		<p :class="$style.fetching" v-if="fetching"><fa icon="spinner .pulse" fixed-width/>{{ $t('@.loading') }}<mk-ellipsis/></p>
 		<template v-else>
 			<x-calendar v-show="view == 0" :data="[].concat(activity)"/>
 			<x-chart v-show="view == 1" :data="[].concat(activity)"/>
@@ -15,10 +15,12 @@
 
 <script lang="ts">
 import Vue from 'vue';
+import i18n from '../../../i18n';
 import XCalendar from './activity.calendar.vue';
 import XChart from './activity.chart.vue';
 
 export default Vue.extend({
+	i18n: i18n('desktop/views/components/activity.vue'),
 	components: {
 		XCalendar,
 		XChart
diff --git a/src/client/app/desktop/views/components/calendar.vue b/src/client/app/desktop/views/components/calendar.vue
index 7eb5e5ad12..fda984bd25 100644
--- a/src/client/app/desktop/views/components/calendar.vue
+++ b/src/client/app/desktop/views/components/calendar.vue
@@ -1,9 +1,9 @@
 <template>
 <div class="mk-calendar" :data-melt="design == 4 || design == 5">
 	<template v-if="design == 0 || design == 1">
-		<button @click="prev" title="%i18n:@prev%"><fa icon="chevron-circle-left"/></button>
-		<p class="title">{{ '%i18n:@title%'.replace('{1}', year).replace('{2}', month) }}</p>
-		<button @click="next" title="%i18n:@next%"><fa icon="chevron-circle-right"/></button>
+		<button @click="prev" :title="$t('prev')"><fa icon="chevron-circle-left"/></button>
+		<p class="title">{{ $t('title', { year, month }) }}</p>
+		<button @click="next" :title="$t('next')"><fa icon="chevron-circle-right"/></button>
 	</template>
 
 	<div class="calendar">
@@ -21,7 +21,7 @@
 			:data-is-out-of-range="isOutOfRange(i + 1)"
 			:data-is-donichi="isDonichi(i + 1)"
 			@click="go(i + 1)"
-			:title="isOutOfRange(i + 1) ? null : '%i18n:@go%'"
+			:title="isOutOfRange(i + 1) ? null : $t('go')"
 		>
 			<div>{{ i + 1 }}</div>
 		</div>
@@ -31,6 +31,7 @@
 
 <script lang="ts">
 import Vue from 'vue';
+import i18n from '../../../i18n';
 
 const eachMonthDays = [31, 28, 31, 30, 31, 30, 31, 31, 30, 31, 30, 31];
 
@@ -39,6 +40,7 @@ function isLeapYear(year) {
 }
 
 export default Vue.extend({
+	i18n: i18n('desktop/views/components/calendar.vue'),
 	props: {
 		design: {
 			default: 0
@@ -55,13 +57,13 @@ export default Vue.extend({
 			month: new Date().getMonth() + 1,
 			selected: new Date(),
 			weekdayText: [
-				'%i18n:common.weekday-short.sunday%',
-				'%i18n:common.weekday-short.monday%',
-				'%i18n:common.weekday-short.tuesday%',
-				'%i18n:common.weekday-short.wednesday%',
-				'%i18n:common.weekday-short.thursday%',
-				'%i18n:common.weekday-short.friday%',
-				'%i18n:common.weekday-short.saturday%'
+				this.$t('@.weekday-short.sunday'),
+				this.$t('@.weekday-short.monday'),
+				this.$t('@.weekday-short.tuesday'),
+				this.$t('@.weekday-short.wednesday'),
+				this.$t('@.weekday-short.thursday'),
+				this.$t('@.weekday-short.friday'),
+				this.$t('@.weekday-short.saturday')
 			]
 		};
 	},
diff --git a/src/client/app/desktop/views/components/choose-file-from-drive-window.vue b/src/client/app/desktop/views/components/choose-file-from-drive-window.vue
index 268ace74db..3b1df38c99 100644
--- a/src/client/app/desktop/views/components/choose-file-from-drive-window.vue
+++ b/src/client/app/desktop/views/components/choose-file-from-drive-window.vue
@@ -2,7 +2,7 @@
 <mk-window ref="window" is-modal width="800px" height="500px" @closed="destroyDom">
 	<span slot="header">
 		<span v-html="title" :class="$style.title"></span>
-		<span :class="$style.count" v-if="multiple && files.length > 0">({{ files.length }}%i18n:@choose-file%)</span>
+		<span :class="$style.count" v-if="multiple && files.length > 0">({{ $t('chosen-files', { count: files.length }) }})</span>
 	</span>
 
 	<mk-drive
@@ -13,22 +13,24 @@
 		@change-selection="onChangeSelection"
 	/>
 	<div :class="$style.footer">
-		<button :class="$style.upload" title="%i18n:@upload%" @click="upload"><fa icon="upload"/></button>
-		<button :class="$style.cancel" @click="cancel">%i18n:@cancel%</button>
-		<button :class="$style.ok" :disabled="multiple && files.length == 0" @click="ok">%i18n:@ok%</button>
+		<button :class="$style.upload" :title="$t('title')" @click="upload"><fa icon="upload"/></button>
+		<button :class="$style.cancel" @click="cancel">{{ $t('cancel') }}</button>
+		<button :class="$style.ok" :disabled="multiple && files.length == 0" @click="ok">{{ $t('ok') }}</button>
 	</div>
 </mk-window>
 </template>
 
 <script lang="ts">
 import Vue from 'vue';
+import i18n from '../../../i18n';
 export default Vue.extend({
+	i18n: i18n('desktop/views/components/choose-file-from-drive-window.vue'),
 	props: {
 		multiple: {
 			default: false
 		},
 		title: {
-			default: '<fa :icon="['far', 'file']"/>%i18n:@choose-prompt%'
+			default: () => this.$t('choose-prompt')
 		}
 	},
 	data() {
@@ -59,8 +61,6 @@ export default Vue.extend({
 </script>
 
 <style lang="stylus" module>
-
-
 .title
 	> [data-icon]
 		margin-right 4px
diff --git a/src/client/app/desktop/views/components/choose-folder-from-drive-window.vue b/src/client/app/desktop/views/components/choose-folder-from-drive-window.vue
index eeeaffe7e6..9cc7feee83 100644
--- a/src/client/app/desktop/views/components/choose-folder-from-drive-window.vue
+++ b/src/client/app/desktop/views/components/choose-folder-from-drive-window.vue
@@ -10,18 +10,20 @@
 		:multiple="false"
 	/>
 	<div :class="$style.footer">
-		<button :class="$style.cancel" @click="cancel">%i18n:@cancel%</button>
-		<button :class="$style.ok" @click="ok">%i18n:@ok%</button>
+		<button :class="$style.cancel" @click="cancel">{{ $t('cancel') }}</button>
+		<button :class="$style.ok" @click="ok">{{ $t('ok') }}</button>
 	</div>
 </mk-window>
 </template>
 
 <script lang="ts">
 import Vue from 'vue';
+import i18n from '../../../i18n';
 export default Vue.extend({
+	i18n: i18n('desktop/views/components/choose-folder-from-drive-window.vue'),
 	props: {
 		title: {
-			default: '<fa :icon="['far', 'folder']"/>%i18n:@choose-prompt%'
+			default: () => this.$t('choose-prompt')
 		}
 	},
 	methods: {
diff --git a/src/client/app/desktop/views/components/crop-window.vue b/src/client/app/desktop/views/components/crop-window.vue
index 8aeb90b539..68a8f4b017 100644
--- a/src/client/app/desktop/views/components/crop-window.vue
+++ b/src/client/app/desktop/views/components/crop-window.vue
@@ -10,18 +10,20 @@
 			/>
 		</div>
 		<div :class="$style.actions">
-			<button :class="$style.skip" @click="skip">%i18n:@skip%</button>
-			<button :class="$style.cancel" @click="cancel">%i18n:@cancel%</button>
-			<button :class="$style.ok" @click="ok">%i18n:@ok%</button>
+			<button :class="$style.skip" @click="skip">{{ $t('skip') }}</button>
+			<button :class="$style.cancel" @click="cancel">{{ $t('cancel') }}</button>
+			<button :class="$style.ok" @click="ok">{{ $t('ok') }}</button>
 		</div>
 	</mk-window>
 </template>
 
 <script lang="ts">
 import Vue from 'vue';
+import i18n from '../../../i18n';
 import VueCropper from 'vue-cropperjs';
 
 export default Vue.extend({
+	i18n: i18n('desktop/views/components/crop-window.vue'),
 	components: {
 		VueCropper
 	},
diff --git a/src/client/app/desktop/views/components/drive-window.vue b/src/client/app/desktop/views/components/drive-window.vue
index 5f9818969f..0ac18bd344 100644
--- a/src/client/app/desktop/views/components/drive-window.vue
+++ b/src/client/app/desktop/views/components/drive-window.vue
@@ -1,8 +1,8 @@
 <template>
 <mk-window ref="window" @closed="destroyDom" width="800px" height="500px" :popout-url="popout">
 	<template slot="header">
-		<p v-if="usage" :class="$style.info"><b>{{ usage.toFixed(1) }}%</b> %i18n:@used%</p>
-		<span :class="$style.title"><fa icon="cloud"/>%i18n:common.drive%</span>
+		<p v-if="usage" :class="$style.info"><b>{{ usage.toFixed(1) }}%</b> {{ $t('used') }}</p>
+		<span :class="$style.title"><fa icon="cloud"/>{{ $t('@.drive') }}</span>
 	</template>
 	<mk-drive :class="$style.browser" multiple :init-folder="folder" ref="browser"/>
 </mk-window>
@@ -10,9 +10,11 @@
 
 <script lang="ts">
 import Vue from 'vue';
+import i18n from '../../../i18n';
 import { url } from '../../../config';
 
 export default Vue.extend({
+	i18n: i18n('desktop/views/components/drive-window.vue'),
 	props: ['folder'],
 	data() {
 		return {
diff --git a/src/client/app/desktop/views/components/drive.file.vue b/src/client/app/desktop/views/components/drive.file.vue
index 90a57b148d..376b2e32dc 100644
--- a/src/client/app/desktop/views/components/drive.file.vue
+++ b/src/client/app/desktop/views/components/drive.file.vue
@@ -11,15 +11,15 @@
 >
 	<div class="label" v-if="$store.state.i.avatarId == file.id">
 		<img src="/assets/label.svg"/>
-		<p>%i18n:@avatar%</p>
+		<p>{{ $t('avatar') }}</p>
 	</div>
 	<div class="label" v-if="$store.state.i.bannerId == file.id">
 		<img src="/assets/label.svg"/>
-		<p>%i18n:@banner%</p>
+		<p>{{ $t('banner') }}</p>
 	</div>
 	<div class="label red" v-if="file.isSensitive">
 		<img src="/assets/label-red.svg"/>
-		<p>%i18n:@nsfw%</p>
+		<p>{{ $t('nsfw') }}</p>
 	</div>
 	<div class="thumbnail" ref="thumbnail" :style="`background-color: ${ background }`">
 		<img :src="file.thumbnailUrl" alt="" @load="onThumbnailLoaded"/>
@@ -33,11 +33,13 @@
 
 <script lang="ts">
 import Vue from 'vue';
+import i18n from '../../../i18n';
 import * as anime from 'animejs';
 import contextmenu from '../../api/contextmenu';
 import copyToClipboard from '../../../common/scripts/copy-to-clipboard';
 
 export default Vue.extend({
+	i18n: i18n('desktop/views/components/drive.file.vue'),
 	props: ['file'],
 	data() {
 		return {
@@ -70,44 +72,44 @@ export default Vue.extend({
 			this.isContextmenuShowing = true;
 			contextmenu((this as any).os)(e, [{
 				type: 'item',
-				text: '%i18n:@contextmenu.rename%',
+				text: this.$t('contextmenu.rename'),
 				icon: 'i-cursor',
 				action: this.rename
 			}, {
 				type: 'item',
-				text: this.file.isSensitive ? '%i18n:@contextmenu.unmark-as-sensitive%' : '%i18n:@contextmenu.mark-as-sensitive%',
+				text: this.file.isSensitive ? this.$t('contextmenu.unmark-as-sensitive') : this.$t('contextmenu.mark-as-sensitive'),
 				icon: this.file.isSensitive ? ['far', 'eye'] : ['far', 'eye-slash'],
 				action: this.toggleSensitive
 			}, null, {
 				type: 'item',
-				text: '%i18n:@contextmenu.copy-url%',
+				text: this.$t('contextmenu.copy-url'),
 				icon: 'link',
 				action: this.copyUrl
 			}, {
 				type: 'link',
 				href: `${this.file.url}?download`,
-				text: '%i18n:@contextmenu.download%',
+				text: this.$t('contextmenu.download'),
 				icon: 'download',
 			}, null, {
 				type: 'item',
-				text: '%i18n:common.delete%',
+				text: this.$t('@.delete'),
 				icon: ['far', 'trash-alt'],
 				action: this.deleteFile
 			}, null, {
 				type: 'nest',
-				text: '%i18n:@contextmenu.else-files%',
+				text: this.$t('contextmenu.else-files'),
 				menu: [{
 					type: 'item',
-					text: '%i18n:@contextmenu.set-as-avatar%',
+					text: this.$t('contextmenu.set-as-avatar'),
 					action: this.setAsAvatar
 				}, {
 					type: 'item',
-					text: '%i18n:@contextmenu.set-as-banner%',
+					text: this.$t('contextmenu.set-as-banner'),
 					action: this.setAsBanner
 				}]
 			}, /*{
 				type: 'nest',
-				text: '%i18n:@contextmenu.open-in-app%',
+				text: this.$t('contextmenu.open-in-app'),
 				menu: [{
 					type: 'item',
 					text: '%i18n:@contextmenu.add-app%...',
@@ -148,8 +150,8 @@ export default Vue.extend({
 
 		rename() {
 			(this as any).apis.input({
-				title: '%i18n:@contextmenu.rename-file%',
-				placeholder: '%i18n:@contextmenu.input-new-file-name%',
+				title: this.$t('contextmenu.rename-file'),
+				placeholder: this.$t('contextmenu.input-new-file-name'),
 				default: this.file.name,
 				allowEmpty: false
 			}).then(name => {
@@ -170,10 +172,10 @@ export default Vue.extend({
 		copyUrl() {
 			copyToClipboard(this.file.url);
 			(this as any).apis.dialog({
-				title: '<fa icon="check"/>%i18n:@contextmenu.copied%',
-				text: '%i18n:@contextmenu.copied-url-to-clipboard%',
+				title: this.$t('contextmenu.copied'),
+				text: this.$t('contextmenu.copied-url-to-clipboard'),
 				actions: [{
-					text: '%i18n:common.ok%'
+					text: this.$t('@.ok')
 				}]
 			});
 		},
diff --git a/src/client/app/desktop/views/components/drive.folder.vue b/src/client/app/desktop/views/components/drive.folder.vue
index 6862e151d3..ff87a12692 100644
--- a/src/client/app/desktop/views/components/drive.folder.vue
+++ b/src/client/app/desktop/views/components/drive.folder.vue
@@ -25,9 +25,11 @@
 
 <script lang="ts">
 import Vue from 'vue';
+import i18n from '../../../i18n';
 import contextmenu from '../../api/contextmenu';
 
 export default Vue.extend({
+	i18n: i18n('desktop/views/components/drive.folder.vue'),
 	props: ['folder'],
 	data() {
 		return {
@@ -54,22 +56,22 @@ export default Vue.extend({
 			this.isContextmenuShowing = true;
 			contextmenu((this as any).os)(e, [{
 				type: 'item',
-				text: '%i18n:@contextmenu.move-to-this-folder%',
+				text: this.$t('contextmenu.move-to-this-folder'),
 				icon: 'arrow-right',
 				action: this.go
 			}, {
 				type: 'item',
-				text: '%i18n:@contextmenu.show-in-new-window%',
+				text: this.$t('contextmenu.show-in-new-window'),
 				icon: ['far', 'window-restore'],
 				action: this.newWindow
 			}, null, {
 				type: 'item',
-				text: '%i18n:@contextmenu.rename%',
+				text: this.$t('contextmenu.rename'),
 				icon: 'i-cursor',
 				action: this.rename
 			}, null, {
 				type: 'item',
-				text: '%i18n:common.delete%',
+				text: this.$t('@.delete'),
 				icon: ['far', 'trash-alt'],
 				action: this.deleteFolder
 			}], {
@@ -155,15 +157,15 @@ export default Vue.extend({
 					switch (err) {
 						case 'detected-circular-definition':
 							(this as any).apis.dialog({
-								title: '<fa icon="exclamation-triangle"/>%i18n:@unable-to-process%',
-								text: '%i18n:@circular-reference-detected%',
+								title: this.$t('unable-to-process'),
+								text: this.$t('circular-reference-detected'),
 								actions: [{
-									text: '%i18n:common.ok%'
+									text: this.$t('@.ok')
 								}]
 							});
 							break;
 						default:
-							alert(`%i18n:@unhandled-error% ${err}`);
+							alert(this.$t('unhandled-error'));
 					}
 				});
 			}
@@ -195,8 +197,8 @@ export default Vue.extend({
 
 		rename() {
 			(this as any).apis.input({
-				title: '%i18n:@contextmenu.rename-folder%',
-				placeholder: '%i18n:@contextmenu.input-new-folder-name%',
+				title: this.$t('contextmenu.rename-folder'),
+				placeholder: this.$t('contextmenu.input-new-folder-name'),
 				default: this.folder.name
 			}).then(name => {
 				(this as any).api('drive/folders/update', {
diff --git a/src/client/app/desktop/views/components/drive.nav-folder.vue b/src/client/app/desktop/views/components/drive.nav-folder.vue
index b750a6e5e8..fb46d8ede2 100644
--- a/src/client/app/desktop/views/components/drive.nav-folder.vue
+++ b/src/client/app/desktop/views/components/drive.nav-folder.vue
@@ -8,13 +8,15 @@
 	@drop.stop="onDrop"
 >
 	<i v-if="folder == null" class="cloud"><fa icon="cloud"/></i>
-	<span>{{ folder == null ? '%i18n:common.drive%' : folder.name }}</span>
+	<span>{{ folder == null ? $t('@.drive') : folder.name }}</span>
 </div>
 </template>
 
 <script lang="ts">
 import Vue from 'vue';
+import i18n from '../../../i18n';
 export default Vue.extend({
+	i18n: i18n(),
 	props: ['folder'],
 	data() {
 		return {
diff --git a/src/client/app/desktop/views/components/drive.vue b/src/client/app/desktop/views/components/drive.vue
index 725db77679..5c5b898e39 100644
--- a/src/client/app/desktop/views/components/drive.vue
+++ b/src/client/app/desktop/views/components/drive.vue
@@ -30,18 +30,18 @@
 				<x-folder v-for="folder in folders" :key="folder.id" class="folder" :folder="folder"/>
 				<!-- SEE: https://stackoverflow.com/questions/18744164/flex-box-align-last-row-to-grid -->
 				<div class="padding" v-for="n in 16"></div>
-				<button v-if="moreFolders">%i18n:@load-more%</button>
+				<button v-if="moreFolders">{{ $t('@.load-more') }}</button>
 			</div>
 			<div class="files" ref="filesContainer" v-if="files.length > 0">
 				<x-file v-for="file in files" :key="file.id" class="file" :file="file"/>
 				<!-- SEE: https://stackoverflow.com/questions/18744164/flex-box-align-last-row-to-grid -->
 				<div class="padding" v-for="n in 16"></div>
-				<button v-if="moreFiles" @click="fetchMoreFiles">%i18n:@load-more%</button>
+				<button v-if="moreFiles" @click="fetchMoreFiles">{{ $t('@.load-more') }}</button>
 			</div>
 			<div class="empty" v-if="files.length == 0 && folders.length == 0 && !fetching">
-				<p v-if="draghover">%i18n:@empty-draghover%</p>
-				<p v-if="!draghover && folder == null"><strong>%i18n:@empty-drive%</strong><br/>%i18n:@empty-drive-description%</p>
-				<p v-if="!draghover && folder != null">%i18n:@empty-folder%</p>
+				<p v-if="draghover">{{ $t('empty-draghover') }}</p>
+				<p v-if="!draghover && folder == null"><strong>{{ $t('empty-drive') }}</strong><br/>{{ $t('empty-drive-description') }}</p>
+				<p v-if="!draghover && folder != null">{{ $t('empty-folder') }}</p>
 			</div>
 		</div>
 		<div class="fetching" v-if="fetching">
@@ -59,6 +59,7 @@
 
 <script lang="ts">
 import Vue from 'vue';
+import i18n from '../../../i18n';
 import MkDriveWindow from './drive-window.vue';
 import XNavFolder from './drive.nav-folder.vue';
 import XFolder from './drive.folder.vue';
@@ -68,6 +69,7 @@ import contextmenu from '../../api/contextmenu';
 import { url } from '../../../config';
 
 export default Vue.extend({
+	i18n: i18n('desktop/views/components/drive.vue'),
 	components: {
 		XNavFolder,
 		XFolder,
@@ -137,17 +139,17 @@ export default Vue.extend({
 		onContextmenu(e) {
 			contextmenu((this as any).os)(e, [{
 				type: 'item',
-				text: '%i18n:@contextmenu.create-folder%',
+				text: this.$t('contextmenu.create-folder'),
 				icon: ['far', 'folder'],
 				action: this.createFolder
 			}, {
 				type: 'item',
-				text: '%i18n:@contextmenu.upload%',
+				text: this.$t('contextmenu.upload'),
 				icon: 'upload',
 				action: this.selectLocalFile
 			}, {
 				type: 'item',
-				text: '%i18n:@contextmenu.url-upload%',
+				text: this.$t('contextmenu.url-upload'),
 				icon: 'cloud-upload-alt',
 				action: this.urlUpload
 			}]);
@@ -313,10 +315,10 @@ export default Vue.extend({
 					switch (err) {
 						case 'detected-circular-definition':
 							(this as any).apis.dialog({
-								title: '<fa icon="exclamation-triangle"/>%i18n:@unable-to-process%',
-								text: '%i18n:@circular-reference-detected%',
+								title: this.$t('unable-to-process'),
+								text: this.$t('circular-reference-detected'),
 								actions: [{
-									text: '%i18n:common.ok%'
+									text: this.$t('@.ok')
 								}]
 							});
 							break;
@@ -334,8 +336,8 @@ export default Vue.extend({
 
 		urlUpload() {
 			(this as any).apis.input({
-				title: '%i18n:@url-upload%',
-				placeholder: '%i18n:@url-of-file%'
+				title: this.$t('url-upload'),
+				placeholder: this.$t('url-of-file')
 			}).then(url => {
 				(this as any).api('drive/files/upload_from_url', {
 					url: url,
@@ -343,10 +345,10 @@ export default Vue.extend({
 				});
 
 				(this as any).apis.dialog({
-					title: '<fa icon="check"/>%i18n:@url-upload-requested%',
-					text: '%i18n:@may-take-time%',
+					title: this.$t('url-upload-requested'),
+					text: this.$t('may-take-time'),
 					actions: [{
-						text: '%i18n:common.ok%'
+						text: this.$t('@.ok')
 					}]
 				});
 			});
@@ -354,8 +356,8 @@ export default Vue.extend({
 
 		createFolder() {
 			(this as any).apis.input({
-				title: '%i18n:@create-folder%',
-				placeholder: '%i18n:@folder-name%'
+				title: this.$t('create-folder'),
+				placeholder: this.$t('folder-name')
 			}).then(name => {
 				(this as any).api('drive/folders/create', {
 					name: name,
diff --git a/src/client/app/desktop/views/components/follow-button.vue b/src/client/app/desktop/views/components/follow-button.vue
index 7adda36501..caaa6df7f9 100644
--- a/src/client/app/desktop/views/components/follow-button.vue
+++ b/src/client/app/desktop/views/components/follow-button.vue
@@ -5,11 +5,11 @@
 	:disabled="wait"
 >
 	<template v-if="!wait">
-		<template v-if="u.hasPendingFollowRequestFromYou && u.isLocked"><fa icon="hourglass-half"/><template v-if="size == 'big'"> %i18n:@request-pending%</template></template>
-		<template v-else-if="u.hasPendingFollowRequestFromYou && !u.isLocked"><fa icon="hourglass-start"/><template v-if="size == 'big'"> %i18n:@follow-processing%</template></template>
-		<template v-else-if="u.isFollowing"><fa icon="minus"/><template v-if="size == 'big'"> %i18n:@following%</template></template>
-		<template v-else-if="!u.isFollowing && u.isLocked"><fa icon="plus"/><template v-if="size == 'big'"> %i18n:@follow-request%</template></template>
-		<template v-else-if="!u.isFollowing && !u.isLocked"><fa icon="plus"/><template v-if="size == 'big'"> %i18n:@follow%</template></template>
+		<template v-if="u.hasPendingFollowRequestFromYou && u.isLocked"><fa icon="hourglass-half"/><template v-if="size == 'big'"> {{ $t('request-pending') }}</template></template>
+		<template v-else-if="u.hasPendingFollowRequestFromYou && !u.isLocked"><fa icon="hourglass-start"/><template v-if="size == 'big'"> {{ $t('follow-processing') }}</template></template>
+		<template v-else-if="u.isFollowing"><fa icon="minus"/><template v-if="size == 'big'"> {{ $t('following') }}</template></template>
+		<template v-else-if="!u.isFollowing && u.isLocked"><fa icon="plus"/><template v-if="size == 'big'"> {{ $t('follow-request') }}</template></template>
+		<template v-else-if="!u.isFollowing && !u.isLocked"><fa icon="plus"/><template v-if="size == 'big'"> {{ $t('follow') }}</template></template>
 	</template>
 	<template v-else><fa icon="spinner .pulse" fixed-width/></template>
 </button>
@@ -17,8 +17,10 @@
 
 <script lang="ts">
 import Vue from 'vue';
+import i18n from '../../../i18n';
 
 export default Vue.extend({
+	i18n: i18n('desktop/views/components/follow-button.vue'),
 	props: {
 		user: {
 			type: Object,
diff --git a/src/client/app/desktop/views/components/friends-maker.vue b/src/client/app/desktop/views/components/friends-maker.vue
index 8b4344ad60..ffd6cf421b 100644
--- a/src/client/app/desktop/views/components/friends-maker.vue
+++ b/src/client/app/desktop/views/components/friends-maker.vue
@@ -1,6 +1,6 @@
 <template>
 <div class="mk-friends-maker">
-	<p class="title">%i18n:@title%</p>
+	<p class="title">{{ $t('title') }}</p>
 	<div class="users" v-if="!fetching && users.length > 0">
 		<div class="user" v-for="user in users" :key="user.id">
 			<mk-avatar class="avatar" :user="user" target="_blank"/>
@@ -10,17 +10,19 @@
 			</div>
 		</div>
 	</div>
-	<p class="empty" v-if="!fetching && users.length == 0">%i18n:@empty%</p>
-	<p class="fetching" v-if="fetching"><fa icon="spinner .pulse" fixed-width/>%i18n:@fetching%<mk-ellipsis/></p>
-	<a class="refresh" @click="refresh">%i18n:@refresh%</a>
-	<button class="close" @click="destroyDom()" title="%i18n:@close%"><fa icon="times"/></button>
+	<p class="empty" v-if="!fetching && users.length == 0">{{ $t('empty') }}</p>
+	<p class="fetching" v-if="fetching"><fa icon="spinner .pulse" fixed-width/>{{ $t('fetching') }}<mk-ellipsis/></p>
+	<a class="refresh" @click="refresh">{{ $t('refresh') }}</a>
+	<button class="close" @click="destroyDom()" :title="$t('title')"><fa icon="times"/></button>
 </div>
 </template>
 
 <script lang="ts">
 import Vue from 'vue';
+import i18n from '../../../i18n';
 
 export default Vue.extend({
+	i18n: i18n('desktop/views/components/friends-maker.vue'),
 	data() {
 		return {
 			users: [],
diff --git a/src/client/app/desktop/views/components/game-window.vue b/src/client/app/desktop/views/components/game-window.vue
index adf61bb161..2ffe8d2f94 100644
--- a/src/client/app/desktop/views/components/game-window.vue
+++ b/src/client/app/desktop/views/components/game-window.vue
@@ -1,15 +1,17 @@
 <template>
 <mk-window ref="window" width="500px" height="560px" :popout-url="popout" @closed="destroyDom">
-	<span slot="header" :class="$style.header"><fa icon="gamepad"/>%i18n:@game%</span>
+	<span slot="header" :class="$style.header"><fa icon="gamepad"/>{{ $t('game') }}</span>
 	<x-reversi :class="$style.content" @gamed="g => game = g"/>
 </mk-window>
 </template>
 
 <script lang="ts">
 import Vue from 'vue';
+import i18n from '../../../i18n';
 import { url } from '../../../config';
 
 export default Vue.extend({
+	i18n: i18n('desktop/views/components/game-window.vue'),
 	components: {
 		XReversi: () => import('../../../common/views/components/games/reversi/reversi.vue')
 	},
diff --git a/src/client/app/desktop/views/components/home.vue b/src/client/app/desktop/views/components/home.vue
index b3bebd2623..cf09d73ffc 100644
--- a/src/client/app/desktop/views/components/home.vue
+++ b/src/client/app/desktop/views/components/home.vue
@@ -1,40 +1,40 @@
 <template>
 <div class="mk-home" :data-customize="customize">
 	<div class="customize" v-if="customize">
-		<router-link to="/"><fa icon="check"/>%i18n:@done%</router-link>
+		<router-link to="/"><fa icon="check"/>{{ $t('done') }}</router-link>
 		<div>
 			<div class="adder">
-				<p>%i18n:@add-widget%</p>
+				<p>{{ $t('add-widget') }}</p>
 				<select v-model="widgetAdderSelected">
-					<option value="profile">%i18n:common.widgets.profile%</option>
-					<option value="analog-clock">%i18n:common.widgets.analog-clock%</option>
-					<option value="calendar">%i18n:common.widgets.calendar%</option>
-					<option value="timemachine">%i18n:common.widgets.timemachine%</option>
-					<option value="activity">%i18n:common.widgets.activity%</option>
-					<option value="rss">%i18n:common.widgets.rss%</option>
-					<option value="trends">%i18n:common.widgets.trends%</option>
-					<option value="photo-stream">%i18n:common.widgets.photo-stream%</option>
-					<option value="slideshow">%i18n:common.widgets.slideshow%</option>
-					<option value="version">%i18n:common.widgets.version%</option>
-					<option value="broadcast">%i18n:common.widgets.broadcast%</option>
-					<option value="notifications">%i18n:common.widgets.notifications%</option>
-					<option value="users">%i18n:common.widgets.users%</option>
-					<option value="polls">%i18n:common.widgets.polls%</option>
-					<option value="post-form">%i18n:common.widgets.post-form%</option>
-					<option value="messaging">%i18n:common.widgets.messaging%</option>
-					<option value="memo">%i18n:common.widgets.memo%</option>
-					<option value="hashtags">%i18n:common.widgets.hashtags%</option>
-					<option value="posts-monitor">%i18n:common.widgets.posts-monitor%</option>
-					<option value="server">%i18n:common.widgets.server%</option>
-					<option value="donation">%i18n:common.widgets.donation%</option>
-					<option value="nav">%i18n:common.widgets.nav%</option>
-					<option value="tips">%i18n:common.widgets.tips%</option>
+					<option value="profile">{{ $t('@.widgets.profile') }}</option>
+					<option value="analog-clock">{{ $t('@.widgets.analog-clock') }}</option>
+					<option value="calendar">{{ $t('@.widgets.calendar') }}</option>
+					<option value="timemachine">{{ $t('@.widgets.timemachine') }}</option>
+					<option value="activity">{{ $t('@.widgets.activity') }}</option>
+					<option value="rss">{{ $t('@.widgets.rss') }}</option>
+					<option value="trends">{{ $t('@.widgets.trends') }}</option>
+					<option value="photo-stream">{{ $t('@.widgets.photo-stream') }}</option>
+					<option value="slideshow">{{ $t('@.widgets.slideshow') }}</option>
+					<option value="version">{{ $t('@.widgets.version') }}</option>
+					<option value="broadcast">{{ $t('@.widgets.broadcast') }}</option>
+					<option value="notifications">{{ $t('@.widgets.notifications') }}</option>
+					<option value="users">{{ $t('@.widgets.users') }}</option>
+					<option value="polls">{{ $t('@.widgets.polls') }}</option>
+					<option value="post-form">{{ $t('@.widgets.post-form') }}</option>
+					<option value="messaging">{{ $t('@.widgets.messaging') }}</option>
+					<option value="memo">{{ $t('@.widgets.memo') }}</option>
+					<option value="hashtags">{{ $t('@.widgets.hashtags') }}</option>
+					<option value="posts-monitor">{{ $t('@.widgets.posts-monitor') }}</option>
+					<option value="server">{{ $t('@.widgets.server') }}</option>
+					<option value="donation">{{ $t('@.widgets.donation') }}</option>
+					<option value="nav">{{ $t('@.widgets.nav') }}</option>
+					<option value="tips">{{ $t('@.widgets.tips') }}</option>
 				</select>
-				<button @click="addWidget">%i18n:@add%</button>
+				<button @click="addWidget">{{ $t('add') }}</button>
 			</div>
 			<div class="trash">
 				<x-draggable v-model="trash" :options="{ group: 'x' }" @add="onTrash"></x-draggable>
-				<p>%i18n:common.trash%</p>
+				<p>{{ $t('@.trash') }}</p>
 			</div>
 		</div>
 	</div>
@@ -53,7 +53,7 @@
 				</div>
 			</x-draggable>
 			<div class="main">
-				<a @click="hint">%i18n:common.customization-tips.title%</a>
+				<a @click="hint">{{ $t('@.customization-tips.title') }}</a>
 				<div>
 					<mk-post-form v-if="$store.state.settings.showPostFormOnTopOfTl"/>
 					<mk-timeline ref="tl" @loaded="onTlLoaded"/>
@@ -75,6 +75,7 @@
 
 <script lang="ts">
 import Vue from 'vue';
+import i18n from '../../../i18n';
 import * as XDraggable from 'vuedraggable';
 import * as uuid from 'uuid';
 
@@ -123,6 +124,7 @@ defaultDesktopHomeWidgets.right.forEach(widget => {
 //#endregion
 
 export default Vue.extend({
+	i18n: i18n('desktop/views/components/home.vue'),
 	components: {
 		XDraggable
 	},
@@ -185,13 +187,10 @@ export default Vue.extend({
 	methods: {
 		hint() {
 			(this as any).apis.dialog({
-				title: '<fa icon="info-circle"/>%i18n:common.customization-tips.title%',
-				text: '<p>%i18n:common.customization-tips.paragraph1%</p>' +
-					'<p>%i18n:common.customization-tips.paragraph2%</p>' +
-					'<p>%i18n:common.customization-tips.paragraph3%</p>' +
-					'<p>%i18n:common.customization-tips.paragraph4%</p>',
+				title: this.$t('@.customization-tips.title'),
+				text: this.$t('@.customization-tips.paragraph'),
 				actions: [{
-					text: '%i18n:common.customization-tips.gotit%'
+					text: this.$t('@.customization-tips.gotit')
 				}]
 			});
 		},
diff --git a/src/client/app/desktop/views/components/input-dialog.vue b/src/client/app/desktop/views/components/input-dialog.vue
index 60f672c93d..d08410e36d 100644
--- a/src/client/app/desktop/views/components/input-dialog.vue
+++ b/src/client/app/desktop/views/components/input-dialog.vue
@@ -8,15 +8,17 @@
 		<input ref="text" v-model="text" :type="type" @keydown="onKeydown" :placeholder="placeholder"/>
 	</div>
 	<div :class="$style.actions">
-		<button :class="$style.cancel" @click="cancel">%i18n:@cancel%</button>
-		<button :class="$style.ok" :disabled="!allowEmpty && text.length == 0" @click="ok">%i18n:@ok%</button>
+		<button :class="$style.cancel" @click="cancel">{{ $t('cancel') }}</button>
+		<button :class="$style.ok" :disabled="!allowEmpty && text.length == 0" @click="ok">{{ $t('ok') }}</button>
 	</div>
 </mk-window>
 </template>
 
 <script lang="ts">
 import Vue from 'vue';
+import i18n from '../../../i18n';
 export default Vue.extend({
+	i18n: i18n('desktop/views/input-dialog.vue'),
 	props: {
 		title: {
 			type: String
diff --git a/src/client/app/desktop/views/components/media-image.vue b/src/client/app/desktop/views/components/media-image.vue
index 510fdbc4ee..f420266aea 100644
--- a/src/client/app/desktop/views/components/media-image.vue
+++ b/src/client/app/desktop/views/components/media-image.vue
@@ -1,8 +1,8 @@
 <template>
 <div class="ldwbgwstjsdgcjruamauqdrffetqudry" v-if="image.isSensitive && hide && !$store.state.device.alwaysShowNsfw" @click="hide = false">
 	<div>
-		<b><fa icon="exclamation-triangle"/> %i18n:@sensitive%</b>
-		<span>%i18n:@click-to-show%</span>
+		<b><fa icon="exclamation-triangle"/> {{ $t('sensitive') }}</b>
+		<span>{{ $t('click-to-show') }}</span>
 	</div>
 </div>
 <a class="lcjomzwbohoelkxsnuqjiaccdbdfiazy" v-else
@@ -17,9 +17,11 @@
 
 <script lang="ts">
 import Vue from 'vue';
+import i18n from '../../../i18n';
 import ImageViewer from '../../../common/views/components/image-viewer.vue';
 
 export default Vue.extend({
+	i18n: i18n('desktop/views/components/media-image.vue'),
 	props: {
 		image: {
 			type: Object,
diff --git a/src/client/app/desktop/views/components/media-video.vue b/src/client/app/desktop/views/components/media-video.vue
index 9565621487..560fe9bc5a 100644
--- a/src/client/app/desktop/views/components/media-video.vue
+++ b/src/client/app/desktop/views/components/media-video.vue
@@ -1,8 +1,8 @@
 <template>
 <div class="uofhebxjdgksfmltszlxurtjnjjsvioh" v-if="video.isSensitive && hide" @click="hide = false">
 	<div>
-		<b><fa icon="exclamation-triangle"/> %i18n:@sensitive%</b>
-		<span>%i18n:@click-to-show%</span>
+		<b><fa icon="exclamation-triangle"/> {{ $t('sensitive') }}</b>
+		<span>{{ $t('click-to-show') }}</span>
 	</div>
 </div>
 <div class="vwxdhznewyashiknzolsoihtlpicqepe" v-else>
@@ -19,9 +19,11 @@
 
 <script lang="ts">
 import Vue from 'vue';
+import i18n from '../../../i18n';
 import MkMediaVideoDialog from './media-video-dialog.vue';
 
 export default Vue.extend({
+	i18n: i18n('desktop/views/components/media-video.vue'),
 	props: {
 		video: {
 			type: Object,
diff --git a/src/client/app/desktop/views/components/messaging-room-window.vue b/src/client/app/desktop/views/components/messaging-room-window.vue
index 7d7adc89d4..90d67414ba 100644
--- a/src/client/app/desktop/views/components/messaging-room-window.vue
+++ b/src/client/app/desktop/views/components/messaging-room-window.vue
@@ -1,16 +1,18 @@
 <template>
 <mk-window ref="window" width="500px" height="560px" :popout-url="popout" @closed="destroyDom">
-	<span slot="header" :class="$style.header"><fa icon="comments"/>%i18n:@title% {{ user | userName }}</span>
+	<span slot="header" :class="$style.header"><fa icon="comments"/>{{ $t('title') }} {{ user | userName }}</span>
 	<mk-messaging-room :user="user" :class="$style.content"/>
 </mk-window>
 </template>
 
 <script lang="ts">
 import Vue from 'vue';
+import i18n from '../../../i18n';
 import { url } from '../../../config';
 import getAcct from '../../../../../misc/acct/render';
 
 export default Vue.extend({
+	i18n: i18n('desktop/views/components/messaging-room-window.vue'),
 	props: ['user'],
 	computed: {
 		popout(): string {
diff --git a/src/client/app/desktop/views/components/messaging-window.vue b/src/client/app/desktop/views/components/messaging-window.vue
index 3ec07f2bcc..ddfaf6f817 100644
--- a/src/client/app/desktop/views/components/messaging-window.vue
+++ b/src/client/app/desktop/views/components/messaging-window.vue
@@ -1,15 +1,17 @@
 <template>
 <mk-window ref="window" width="500px" height="560px" @closed="destroyDom">
-	<span slot="header" :class="$style.header"><fa icon="comments"/>%i18n:@title%</span>
+	<span slot="header" :class="$style.header"><fa icon="comments"/>{{ $t('title') }}</span>
 	<mk-messaging :class="$style.content" @navigate="navigate"/>
 </mk-window>
 </template>
 
 <script lang="ts">
 import Vue from 'vue';
+import i18n from '../../../i18n';
 import MkMessagingRoomWindow from './messaging-room-window.vue';
 
 export default Vue.extend({
+	i18n: i18n('desktop/views/components/messaging-window.vue'),
 	methods: {
 		navigate(user) {
 			(this as any).os.new(MkMessagingRoomWindow, {
diff --git a/src/client/app/desktop/views/components/note-detail.vue b/src/client/app/desktop/views/components/note-detail.vue
index ae9cee5d0b..a442f83586 100644
--- a/src/client/app/desktop/views/components/note-detail.vue
+++ b/src/client/app/desktop/views/components/note-detail.vue
@@ -3,7 +3,7 @@
 	<button
 		class="read-more"
 		v-if="p.reply && p.reply.replyId && conversation.length == 0"
-		title="%i18n:@more%"
+		:title="$t('title')"
 		@click="fetchConversation"
 		:disabled="conversationFetching"
 	>
@@ -21,9 +21,9 @@
 			<mk-avatar class="avatar" :user="note.user"/>
 			<fa icon="retweet"/>
 			<router-link class="name" :href="note.user | userPage">{{ note.user | userName }}</router-link>
-			<span>{{ '%i18n:@reposted-by%'.substr(0, '%i18n:@reposted-by%'.indexOf('{')) }}</span>
+			<span>{{ this.$t('reposted-by').substr(0, this.$t('reposted-by').indexOf('{')) }}</span>
 			<a class="name" :href="note.user | userPage" v-user-preview="note.userId">{{ note.user | userName }}</a>
-			<span>{{ '%i18n:@reposted-by%'.substr('%i18n:@reposted-by%'.indexOf('}') + 1) }}</span>
+			<span>{{ this.$t('reposted-by').substr(this.$t('reposted-by').indexOf('}') + 1) }}</span>
 			<mk-time :time="note.createdAt"/>
 		</p>
 	</div>
@@ -43,8 +43,8 @@
 			</p>
 			<div class="content" v-show="p.cw == null || showContent">
 				<div class="text">
-					<span v-if="p.isHidden" style="opacity: 0.5">%i18n:@private%</span>
-					<span v-if="p.deletedAt" style="opacity: 0.5">%i18n:@deleted%</span>
+					<span v-if="p.isHidden" style="opacity: 0.5">{{ $t('private') }}</span>
+					<span v-if="p.deletedAt" style="opacity: 0.5">{{ $t('deleted') }}</span>
 					<misskey-flavored-markdown v-if="p.text" :text="p.text" :i="$store.state.i" :customEmojis="p.emojis" />
 				</div>
 				<div class="files" v-if="p.files.length > 0">
@@ -52,7 +52,7 @@
 				</div>
 				<mk-poll v-if="p.poll" :note="p"/>
 				<mk-url-preview v-for="url in urls" :url="url" :key="url" :detail="true"/>
-				<a class="location" v-if="p.geo" :href="`https://maps.google.com/maps?q=${p.geo.coordinates[1]},${p.geo.coordinates[0]}`" target="_blank"><fa icon="map-marker-alt"/> %i18n:@location%</a>
+				<a class="location" v-if="p.geo" :href="`https://maps.google.com/maps?q=${p.geo.coordinates[1]},${p.geo.coordinates[0]}`" target="_blank"><fa icon="map-marker-alt"/> {{ $t('location') }}</a>
 				<div class="map" v-if="p.geo" ref="map"></div>
 				<div class="renote" v-if="p.renote">
 					<mk-note-preview :note="p.renote"/>
@@ -67,10 +67,10 @@
 				<template v-else><fa icon="reply"/></template>
 				<p class="count" v-if="p.repliesCount > 0">{{ p.repliesCount }}</p>
 			</button>
-			<button class="renoteButton" @click="renote" title="%i18n:@renote%">
+			<button class="renoteButton" @click="renote" :title="$t('title')">
 				<fa icon="retweet"/><p class="count" v-if="p.renoteCount > 0">{{ p.renoteCount }}</p>
 			</button>
-			<button class="reactionButton" :class="{ reacted: p.myReaction != null }" @click="react" ref="reactButton" title="%i18n:@add-reaction%">
+			<button class="reactionButton" :class="{ reacted: p.myReaction != null }" @click="react" ref="reactButton" :title="$t('title')">
 				<fa icon="plus"/><p class="count" v-if="p.reactions_count > 0">{{ p.reactions_count }}</p>
 			</button>
 			<button @click="menu" ref="menuButton">
@@ -86,6 +86,7 @@
 
 <script lang="ts">
 import Vue from 'vue';
+import i18n from '../../../i18n';
 import parse from '../../../../../mfm/parse';
 
 import MkPostFormWindow from './post-form-window.vue';
@@ -97,6 +98,7 @@ import { sum } from '../../../../../prelude/array';
 import noteSubscriber from '../../../common/scripts/note-subscriber';
 
 export default Vue.extend({
+	i18n: i18n('desktop/views/components/note-detail.vue'),
 	components: {
 		XSub
 	},
diff --git a/src/client/app/desktop/views/components/note.vue b/src/client/app/desktop/views/components/note.vue
index f267ebad10..8b237be8c5 100644
--- a/src/client/app/desktop/views/components/note.vue
+++ b/src/client/app/desktop/views/components/note.vue
@@ -16,9 +16,9 @@
 	<div class="renote" v-if="isRenote">
 		<mk-avatar class="avatar" :user="note.user"/>
 		<fa icon="retweet"/>
-		<span>{{ '%i18n:@reposted-by%'.substr(0, '%i18n:@reposted-by%'.indexOf('{')) }}</span>
+		<span>{{ this.$t('reposted-by').substr(0, this.$t('reposted-by').indexOf('{')) }}</span>
 		<router-link class="name" :to="note.user | userPage" v-user-preview="note.userId">{{ note.user | userName }}</router-link>
-		<span>{{ '%i18n:@reposted-by%'.substr('%i18n:@reposted-by%'.indexOf('}') + 1) }}</span>
+		<span>{{ this.$t('reposted-by').substr(this.$t('reposted-by').indexOf('}') + 1) }}</span>
 		<mk-time :time="note.createdAt"/>
 	</div>
 	<article>
@@ -32,7 +32,7 @@
 				</p>
 				<div class="content" v-show="appearNote.cw == null || showContent">
 					<div class="text">
-						<span v-if="appearNote.isHidden" style="opacity: 0.5">%i18n:@private%</span>
+						<span v-if="appearNote.isHidden" style="opacity: 0.5">{{ $t('private') }}</span>
 						<a class="reply" v-if="appearNote.reply"><fa icon="reply"/></a>
 						<misskey-flavored-markdown v-if="appearNote.text" :text="appearNote.text" :i="$store.state.i" :class="$style.text" :customEmojis="appearNote.emojis"/>
 						<a class="rp" v-if="appearNote.renote">RN:</a>
@@ -49,15 +49,15 @@
 			<footer>
 				<span class="app" v-if="appearNote.app && mini">via <b>{{ appearNote.app.name }}</b></span>
 				<mk-reactions-viewer :note="appearNote" ref="reactionsViewer"/>
-				<button class="replyButton" @click="reply()" title="%i18n:@reply%">
+				<button class="replyButton" @click="reply()" :title="$t('reply')">
 					<template v-if="appearNote.reply"><fa icon="reply-all"/></template>
 					<template v-else><fa icon="reply"/></template>
 					<p class="count" v-if="appearNote.repliesCount > 0">{{ appearNote.repliesCount }}</p>
 				</button>
-				<button class="renoteButton" @click="renote()" title="%i18n:@renote%">
+				<button class="renoteButton" @click="renote()" :title="$t('renote')">
 					<fa icon="retweet"/><p class="count" v-if="appearNote.renoteCount > 0">{{ appearNote.renoteCount }}</p>
 				</button>
-				<button class="reactionButton" :class="{ reacted: appearNote.myReaction != null }" @click="react()" ref="reactButton" title="%i18n:@add-reaction%">
+				<button class="reactionButton" :class="{ reacted: appearNote.myReaction != null }" @click="react()" ref="reactButton" :title="$t('add-reaction')">
 					<fa icon="plus"/><p class="count" v-if="appearNote.reactions_count > 0">{{ appearNote.reactions_count }}</p>
 				</button>
 				<button @click="menu()" ref="menuButton">
@@ -74,12 +74,14 @@
 
 <script lang="ts">
 import Vue from 'vue';
+import i18n from '../../../i18n';
 
 import XSub from './note.sub.vue';
 import noteMixin from '../../../common/scripts/note-mixin';
 import noteSubscriber from '../../../common/scripts/note-subscriber';
 
 export default Vue.extend({
+	i18n: i18n('desktop/views/components/note.vue'),
 	components: {
 		XSub
 	},
diff --git a/src/client/app/desktop/views/components/notes.vue b/src/client/app/desktop/views/components/notes.vue
index 7808ed7427..47f7919d72 100644
--- a/src/client/app/desktop/views/components/notes.vue
+++ b/src/client/app/desktop/views/components/notes.vue
@@ -25,7 +25,7 @@
 
 	<footer v-if="more">
 		<button @click="loadMore" :disabled="moreFetching" :style="{ cursor: moreFetching ? 'wait' : 'pointer' }">
-			<template v-if="!moreFetching">%i18n:@load-more%</template>
+			<template v-if="!moreFetching">{{ $t('@.load-more') }}</template>
 			<template v-if="moreFetching"><fa icon="spinner .pulse" fixed-width/></template>
 		</button>
 	</footer>
@@ -34,6 +34,7 @@
 
 <script lang="ts">
 import Vue from 'vue';
+import i18n from '../../../i18n';
 import * as config from '../../../config';
 
 import XNote from './note.vue';
@@ -41,6 +42,7 @@ import XNote from './note.vue';
 const displayLimit = 30;
 
 export default Vue.extend({
+	i18n: i18n(),
 	components: {
 		XNote
 	},
@@ -68,7 +70,7 @@ export default Vue.extend({
 				const date = new Date(note.createdAt).getDate();
 				const month = new Date(note.createdAt).getMonth() + 1;
 				note._date = date;
-				note._datetext = '%i18n:common.month-and-day%'.replace('{month}', month.toString()).replace('{day}', date.toString());
+				note._datetext = this.$t('@.month-and-day').replace('{month}', month.toString()).replace('{day}', date.toString());
 				return note;
 			});
 		}
diff --git a/src/client/app/desktop/views/components/notifications.vue b/src/client/app/desktop/views/components/notifications.vue
index a8a7d2be58..918cb821bb 100644
--- a/src/client/app/desktop/views/components/notifications.vue
+++ b/src/client/app/desktop/views/components/notifications.vue
@@ -105,17 +105,19 @@
 		</component>
 	</div>
 	<button class="more" :class="{ fetching: fetchingMoreNotifications }" v-if="moreNotifications" @click="fetchMoreNotifications" :disabled="fetchingMoreNotifications">
-		<template v-if="fetchingMoreNotifications"><fa icon="spinner .pulse" fixed-width/></template>{{ fetchingMoreNotifications ? '%i18n:common.loading%' : '%i18n:@more%' }}
+		<template v-if="fetchingMoreNotifications"><fa icon="spinner .pulse" fixed-width/></template>{{ fetchingMoreNotifications ? $t('@.loading') : $t('@.load-more') }}
 	</button>
-	<p class="empty" v-if="notifications.length == 0 && !fetching">%i18n:@empty%</p>
+	<p class="empty" v-if="notifications.length == 0 && !fetching">{{ $t('empty') }}</p>
 </div>
 </template>
 
 <script lang="ts">
 import Vue from 'vue';
+import i18n from '../../../i18n';
 import getNoteSummary from '../../../../../misc/get-note-summary';
 
 export default Vue.extend({
+	i18n: i18n(),
 	data() {
 		return {
 			fetching: true,
@@ -133,7 +135,7 @@ export default Vue.extend({
 				const date = new Date(notification.createdAt).getDate();
 				const month = new Date(notification.createdAt).getMonth() + 1;
 				notification._date = date;
-				notification._datetext = '%i18n:common.month-and-day%'.replace('{month}', month.toString()).replace('{day}', date.toString());
+				notification._datetext = this.$t('@.month-and-day').replace('{month}', month.toString()).replace('{day}', date.toString());
 				return notification;
 			});
 		}
diff --git a/src/client/app/desktop/views/components/post-form-window.vue b/src/client/app/desktop/views/components/post-form-window.vue
index 347304539f..cebf480bf1 100644
--- a/src/client/app/desktop/views/components/post-form-window.vue
+++ b/src/client/app/desktop/views/components/post-form-window.vue
@@ -2,10 +2,10 @@
 <mk-window class="mk-post-form-window" ref="window" is-modal @closed="onWindowClosed" :animation="animation">
 	<span slot="header" class="mk-post-form-window--header">
 		<span class="icon" v-if="geo"><fa icon="map-marker-alt"/></span>
-		<span v-if="!reply">%i18n:@note%</span>
-		<span v-if="reply">%i18n:@reply%</span>
-		<span class="count" v-if="files.length != 0">{{ '%i18n:@attaches%'.replace('{}', files.length) }}</span>
-		<span class="count" v-if="uploadings.length != 0">{{ '%i18n:@uploading-media%'.replace('{}', uploadings.length) }}<mk-ellipsis/></span>
+		<span v-if="!reply">{{ $t('note') }}</span>
+		<span v-if="reply">{{ $t('reply') }}</span>
+		<span class="count" v-if="files.length != 0">{{ this.$t('attaches').replace('{}', files.length) }}</span>
+		<span class="count" v-if="uploadings.length != 0">{{ this.$t('uploading-media').replace('{}', uploadings.length) }}<mk-ellipsis/></span>
 	</span>
 
 	<div class="mk-post-form-window--body">
@@ -23,8 +23,10 @@
 
 <script lang="ts">
 import Vue from 'vue';
+import i18n from '../../../i18n';
 
 export default Vue.extend({
+	i18n: i18n('desktop/views/components/post-form-window.vue'),
 	props: {
 		reply: {
 			type: Object,
diff --git a/src/client/app/desktop/views/components/post-form.vue b/src/client/app/desktop/views/components/post-form.vue
index e6d3fd6937..fb6d966b69 100644
--- a/src/client/app/desktop/views/components/post-form.vue
+++ b/src/client/app/desktop/views/components/post-form.vue
@@ -8,13 +8,13 @@
 	<div class="content">
 		<div v-if="visibility == 'specified'" class="visibleUsers">
 			<span v-for="u in visibleUsers">{{ u | userName }}<a @click="removeVisibleUser(u)">[x]</a></span>
-			<a @click="addVisibleUser">%i18n:@add-visible-user%</a>
+			<a @click="addVisibleUser">{{ $t('add-visible-user') }}</a>
 		</div>
 		<div class="hashtags" v-if="recentHashtags.length > 0 && $store.state.settings.suggestRecentHashtags">
-			<b>%i18n:@recent-tags%:</b>
-			<a v-for="tag in recentHashtags.slice(0, 5)" @click="addTag(tag)" title="%i18n:@click-to-tagging%">#{{ tag }}</a>
+			<b>{{ $t('recent-tags') }}:</b>
+			<a v-for="tag in recentHashtags.slice(0, 5)" @click="addTag(tag)" :title="$t('click-to-tagging')">#{{ tag }}</a>
 		</div>
-		<input v-show="useCw" v-model="cw" placeholder="%i18n:@annotations%">
+		<input v-show="useCw" v-model="cw" :placeholder="$t('placeholder')">
 		<textarea :class="{ with: (files.length != 0 || poll) }"
 			ref="text" v-model="text" :disabled="posting"
 			@keydown="onKeydown" @paste="onPaste" :placeholder="placeholder"
@@ -24,7 +24,7 @@
 			<x-draggable :list="files" :options="{ animation: 150 }">
 				<div v-for="file in files" :key="file.id">
 					<div class="img" :style="{ backgroundImage: `url(${file.thumbnailUrl})` }" :title="file.name"></div>
-					<img class="remove" @click="detachMedia(file.id)" src="/assets/desktop/remove.png" title="%i18n:@attach-cancel%" alt=""/>
+					<img class="remove" @click="detachMedia(file.id)" src="/assets/desktop/remove.png" :title="$t('attach-cancel')" alt=""/>
 				</div>
 			</x-draggable>
 			<p class="remain">{{ 4 - files.length }}/4</p>
@@ -32,13 +32,13 @@
 		<mk-poll-editor v-if="poll" ref="poll" @destroyed="poll = false" @updated="saveDraft()"/>
 	</div>
 	<mk-uploader ref="uploader" @uploaded="attachMedia" @change="onChangeUploadings"/>
-	<button class="upload" title="%i18n:@attach-media-from-local%" @click="chooseFile"><fa icon="upload"/></button>
-	<button class="drive" title="%i18n:@attach-media-from-drive%" @click="chooseFileFromDrive"><fa icon="cloud"/></button>
-	<button class="kao" title="%i18n:@insert-a-kao%" @click="kao"><fa :icon="['far', 'smile']"/></button>
-	<button class="poll" title="%i18n:@create-poll%" @click="poll = !poll"><fa icon="chart-pie"/></button>
-	<button class="poll" title="%i18n:@hide-contents%" @click="useCw = !useCw"><fa icon="eye-slash"/></button>
-	<button class="geo" title="%i18n:@attach-location-information%" @click="geo ? removeGeo() : setGeo()"><fa icon="map-marker-alt"/></button>
-	<button class="visibility" title="%i18n:@visibility%" @click="setVisibility" ref="visibilityButton">
+	<button class="upload" :title="$t('attach-media-from-local')" @click="chooseFile"><fa icon="upload"/></button>
+	<button class="drive" :title="$t('attach-media-from-drive')" @click="chooseFileFromDrive"><fa icon="cloud"/></button>
+	<button class="kao" :title="$t('insert-a-kao')" @click="kao"><fa :icon="['far', 'smile']"/></button>
+	<button class="poll" :title="$t('create-poll')" @click="poll = !poll"><fa icon="chart-pie"/></button>
+	<button class="cw%" :title="$t('hide-contents%')" @click="useCw = !useCw"><fa icon="eye-slash"/></button>
+	<button class="geo" :title="$t('attach-location-information')" @click="geo ? removeGeo() : setGeo()"><fa icon="map-marker-alt"/></button>
+	<button class="visibility" :title="$t('visibility')" @click="setVisibility" ref="visibilityButton">
 		<span v-if="visibility === 'public'"><fa icon="globe"/></span>
 		<span v-if="visibility === 'home'"><fa icon="home"/></span>
 		<span v-if="visibility === 'followers'"><fa icon="unlock"/></span>
@@ -47,7 +47,7 @@
 	</button>
 	<p class="text-count" :class="{ over: this.trimmedLength(text) > this.maxNoteTextLength }">{{ this.maxNoteTextLength - this.trimmedLength(text) }}</p>
 	<button :class="{ posting }" class="submit" :disabled="!canPost" @click="post">
-		{{ posting ? '%i18n:@posting%' : submitText }}<mk-ellipsis v-if="posting"/>
+		{{ posting ? this.$t('posting') : submitText }}<mk-ellipsis v-if="posting"/>
 	</button>
 	<input ref="file" type="file" multiple="multiple" tabindex="-1" @change="onChangeFile"/>
 	<div class="dropzone" v-if="draghover"></div>
@@ -56,6 +56,7 @@
 
 <script lang="ts">
 import Vue from 'vue';
+import i18n from '../../../i18n';
 import insertTextAtCursor from 'insert-text-at-cursor';
 import * as XDraggable from 'vuedraggable';
 import getFace from '../../../common/scripts/get-face';
@@ -68,6 +69,7 @@ import parseAcct from '../../../../../misc/acct/parse';
 import { toASCII } from 'punycode';
 
 export default Vue.extend({
+	i18n: i18n('desktop/views/components/post-form.vue'),
 	components: {
 		XDraggable,
 		MkVisibilityChooser
@@ -129,28 +131,28 @@ export default Vue.extend({
 
 		placeholder(): string {
 			const xs = [
-				'%i18n:common.note-placeholders.a%',
-				'%i18n:common.note-placeholders.b%',
-				'%i18n:common.note-placeholders.c%',
-				'%i18n:common.note-placeholders.d%',
-				'%i18n:common.note-placeholders.e%',
-				'%i18n:common.note-placeholders.f%'
+				this.$t('@.note-placeholders.a'),
+				this.$t('@.note-placeholders.b'),
+				this.$t('@.note-placeholders.c'),
+				this.$t('@.note-placeholders.d'),
+				this.$t('@.note-placeholders.e'),
+				this.$t('@.note-placeholders.f')
 			];
 			const x = xs[Math.floor(Math.random() * xs.length)];
 
 			return this.renote
-				? '%i18n:@quote-placeholder%'
+				? this.$t('quote-placeholder')
 				: this.reply
-					? '%i18n:@reply-placeholder%'
+					? this.$t('reply-placeholder')
 					: x;
 		},
 
 		submitText(): string {
 			return this.renote
-				? '%i18n:@renote%'
+				? this.$t('renote')
 				: this.reply
-					? '%i18n:@reply%'
-					: '%i18n:@submit%';
+					? this.$t('reply')
+					: this.$t('submit');
 		},
 
 		canPost(): boolean {
@@ -332,7 +334,7 @@ export default Vue.extend({
 
 		setGeo() {
 			if (navigator.geolocation == null) {
-				alert('%i18n:@geolocation-alert%');
+				alert(this.$t('geolocation-alert'));
 				return;
 			}
 
@@ -362,7 +364,7 @@ export default Vue.extend({
 
 		addVisibleUser() {
 			(this as any).apis.input({
-				title: '%i18n:@enter-username%'
+				title: this.$t('enter-username')
 			}).then(acct => {
 				if (acct.startsWith('@')) acct = acct.substr(1);
 				(this as any).api('users/show', parseAcct(acct)).then(user => {
@@ -400,16 +402,16 @@ export default Vue.extend({
 				this.deleteDraft();
 				this.$emit('posted');
 				(this as any).apis.notify(this.renote
-					? '%i18n:@reposted%'
+					? this.$t('reposted')
 					: this.reply
-						? '%i18n:@replied%'
-						: '%i18n:@posted%');
+						? this.$t('replied')
+						: this.$t('posted'));
 			}).catch(err => {
 				(this as any).apis.notify(this.renote
-					? '%i18n:@renote-failed%'
+					? this.$t('renote-failed')
 					: this.reply
-						? '%i18n:@reply-failed%'
-						: '%i18n:@note-failed%');
+						? this.$t('reply-failed')
+						: this.$t('note-failed'));
 			}).then(() => {
 				this.posting = false;
 			});
diff --git a/src/client/app/desktop/views/components/progress-dialog.vue b/src/client/app/desktop/views/components/progress-dialog.vue
index feda6050bc..0bf4a09898 100644
--- a/src/client/app/desktop/views/components/progress-dialog.vue
+++ b/src/client/app/desktop/views/components/progress-dialog.vue
@@ -2,7 +2,7 @@
 <mk-window ref="window" :is-modal="false" :can-close="false" width="500px" @closed="destroyDom">
 	<span slot="header">{{ title }}<mk-ellipsis/></span>
 	<div :class="$style.body">
-		<p :class="$style.init" v-if="isNaN(value)">%i18n:@waiting%<mk-ellipsis/></p>
+		<p :class="$style.init" v-if="isNaN(value)">{{ $t('waiting') }}<mk-ellipsis/></p>
 		<p :class="$style.percentage" v-if="!isNaN(value)">{{ Math.floor((value / max) * 100) }}</p>
 		<progress :class="$style.progress"
 			v-if="!isNaN(value) && value < max"
@@ -16,7 +16,10 @@
 
 <script lang="ts">
 import Vue from 'vue';
+import i18n from '../../../i18n';
+
 export default Vue.extend({
+	i18n: i18n('desktop/views/components/progress-dialog.vue'),
 	props: ['title', 'initValue', 'initMax'],
 	data() {
 		return {
diff --git a/src/client/app/desktop/views/components/received-follow-requests-window.vue b/src/client/app/desktop/views/components/received-follow-requests-window.vue
index 5ebe0cc35a..c0acca4a40 100644
--- a/src/client/app/desktop/views/components/received-follow-requests-window.vue
+++ b/src/client/app/desktop/views/components/received-follow-requests-window.vue
@@ -1,12 +1,12 @@
 <template>
 <mk-window ref="window" is-modal width="450px" height="500px" @closed="destroyDom">
-	<span slot="header"><fa :icon="['far', 'envelope']"/> %i18n:@title%</span>
+	<span slot="header"><fa :icon="['far', 'envelope']"/> {{ $t('title') }}</span>
 
 	<div class="slpqaxdoxhvglersgjukmvizkqbmbokc">
 		<div v-for="req in requests">
 			<router-link :key="req.id" :to="req.follower | userPage">{{ req.follower | userName }}</router-link>
 			<span>
-				<a @click="accept(req.follower)">%i18n:@accept%</a>|<a @click="reject(req.follower)">%i18n:@reject%</a>
+				<a @click="accept(req.follower)">{{ $t('accept') }}</a>|<a @click="reject(req.follower)">{{ $t('reject') }}</a>
 			</span>
 		</div>
 	</div>
@@ -15,7 +15,10 @@
 
 <script lang="ts">
 import Vue from 'vue';
+import i18n from '../../../i18n';
+
 export default Vue.extend({
+	i18n: i18n('desktop/views/components/received-follow-requests-window.vue'),
 	data() {
 		return {
 			fetching: true,
diff --git a/src/client/app/desktop/views/components/renote-form-window.vue b/src/client/app/desktop/views/components/renote-form-window.vue
index 933f10475e..b2bc9e4bf8 100644
--- a/src/client/app/desktop/views/components/renote-form-window.vue
+++ b/src/client/app/desktop/views/components/renote-form-window.vue
@@ -1,14 +1,16 @@
 <template>
 <mk-window ref="window" is-modal @closed="onWindowClosed" :animation="animation">
-	<span slot="header" :class="$style.header"><fa icon="retweet"/>%i18n:@title%</span>
+	<span slot="header" :class="$style.header"><fa icon="retweet"/>{{ $t('title') }}</span>
 	<mk-renote-form ref="form" :note="note" @posted="onPosted" @canceled="onCanceled" v-hotkey.global="keymap"/>
 </mk-window>
 </template>
 
 <script lang="ts">
 import Vue from 'vue';
+import i18n from '../../../i18n';
 
 export default Vue.extend({
+	i18n: i18n('desktop/views/components/renote-form-window.vue'),
 	props: {
 		note: {
 			type: Object,
diff --git a/src/client/app/desktop/views/components/renote-form.vue b/src/client/app/desktop/views/components/renote-form.vue
index 68d485bada..2877c222b9 100644
--- a/src/client/app/desktop/views/components/renote-form.vue
+++ b/src/client/app/desktop/views/components/renote-form.vue
@@ -3,9 +3,9 @@
 	<mk-note-preview class="preview" :note="note"/>
 	<template v-if="!quote">
 		<footer>
-			<a class="quote" v-if="!quote" @click="onQuote">%i18n:@quote%</a>
-			<ui-button class="button cancel" inline @click="cancel">%i18n:@cancel%</ui-button>
-			<ui-button class="button ok" inline primary @click="ok" :disabled="wait">{{ wait ? '%i18n:@reposting%' : '%i18n:@renote%' }}</ui-button>
+			<a class="quote" v-if="!quote" @click="onQuote">{{ $t('quote') }}</a>
+			<ui-button class="button cancel" inline @click="cancel">{{ $t('cancel') }}</ui-button>
+			<ui-button class="button ok" inline primary @click="ok" :disabled="wait">{{ wait ? this.$t('reposting') : this.$t('renote') }}</ui-button>
 		</footer>
 	</template>
 	<template v-if="quote">
@@ -16,8 +16,10 @@
 
 <script lang="ts">
 import Vue from 'vue';
+import i18n from '../../../i18n';
 
 export default Vue.extend({
+	i18n: i18n('desktop/views/components/renote-form.vue'),
 	props: ['note'],
 	data() {
 		return {
@@ -32,9 +34,9 @@ export default Vue.extend({
 				renoteId: this.note.id
 			}).then(data => {
 				this.$emit('posted');
-				(this as any).apis.notify('%i18n:@success%');
+				(this as any).apis.notify(this.$t('success'));
 			}).catch(err => {
-				(this as any).apis.notify('%i18n:@failure%');
+				(this as any).apis.notify(this.$t('failure'));
 			}).then(() => {
 				this.wait = false;
 			});
diff --git a/src/client/app/desktop/views/components/settings-window.vue b/src/client/app/desktop/views/components/settings-window.vue
index b69c40e0e0..7ef5ac0c6e 100644
--- a/src/client/app/desktop/views/components/settings-window.vue
+++ b/src/client/app/desktop/views/components/settings-window.vue
@@ -1,13 +1,15 @@
 <template>
 <mk-window ref="window" is-modal width="700px" height="550px" @closed="destroyDom">
-	<span slot="header" :class="$style.header"><fa icon="cog"/>%i18n:@settings%</span>
+	<span slot="header" :class="$style.header"><fa icon="cog"/>{{ $t('settings') }}</span>
 	<mk-settings :initial-page="initialPage" @done="close"/>
 </mk-window>
 </template>
 
 <script lang="ts">
 import Vue from 'vue';
+import i18n from '../../../i18n';
 export default Vue.extend({
+	i18n: i18n('desktop/views/components/settings-window.vue'),
 	props: {
 		initialPage: {
 			type: String,
diff --git a/src/client/app/desktop/views/components/settings.2fa.vue b/src/client/app/desktop/views/components/settings.2fa.vue
index 3d349947e3..9050f8c9b1 100644
--- a/src/client/app/desktop/views/components/settings.2fa.vue
+++ b/src/client/app/desktop/views/components/settings.2fa.vue
@@ -1,30 +1,32 @@
 <template>
 <div class="2fa">
-	<p style="margin-top:0;">%i18n:@intro%<a href="%i18n:@url%" target="_blank">%i18n:@detail%</a></p>
-	<ui-info warn>%i18n:@caution%</ui-info>
-	<p v-if="!data && !$store.state.i.twoFactorEnabled"><ui-button @click="register">%i18n:@register%</ui-button></p>
+	<p style="margin-top:0;">{{ $t('intro') }}<a :href="$t('href')" target="_blank">{{ $t('detail') }}</a></p>
+	<ui-info warn>{{ $t('caution') }}</ui-info>
+	<p v-if="!data && !$store.state.i.twoFactorEnabled"><ui-button @click="register">{{ $t('register') }}</ui-button></p>
 	<template v-if="$store.state.i.twoFactorEnabled">
-		<p>%i18n:@already-registered%</p>
-		<ui-button @click="unregister">%i18n:@unregister%</ui-button>
+		<p>{{ $t('already-registered') }}</p>
+		<ui-button @click="unregister">{{ $t('unregister') }}</ui-button>
 	</template>
 	<div v-if="data">
 		<ol>
-			<li>%i18n:@authenticator% <a href="https://support.google.com/accounts/answer/1066447" target="_blank">%i18n:@howtoinstall%</a></li>
-			<li>%i18n:@scan%<br><img :src="data.qr"></li>
-			<li>%i18n:@done%<br>
+			<li>{{ $t('authenticator% <a href="https://support.google.com/accounts/answer/1066447" target="_blank">%i18n:@howtoinstall') }}</a></li>
+			<li>{{ $t('scan') }}<br><img :src="data.qr"></li>
+			<li>{{ $t('done') }}<br>
 				<input type="number" v-model="token" class="ui">
-				<ui-button primary @click="submit">%i18n:@submit%</ui-button>
+				<ui-button primary @click="submit">{{ $t('submit') }}</ui-button>
 			</li>
 		</ol>
-		<div class="ui info"><p><fa icon="info-circle"/>%i18n:@info%</p></div>
+		<div class="ui info"><p><fa icon="info-circle"/>{{ $t('info') }}</p></div>
 	</div>
 </div>
 </template>
 
 <script lang="ts">
 import Vue from 'vue';
+import i18n from '../../../i18n';
 
 export default Vue.extend({
+	i18n: i18n('desktop/views/components/settings.2fa.vue'),
 	data() {
 		return {
 			data: null,
@@ -34,7 +36,7 @@ export default Vue.extend({
 	methods: {
 		register() {
 			(this as any).apis.input({
-				title: '%i18n:@enter-password%',
+				title: this.$t('enter-password'),
 				type: 'password'
 			}).then(password => {
 				(this as any).api('i/2fa/register', {
@@ -47,13 +49,13 @@ export default Vue.extend({
 
 		unregister() {
 			(this as any).apis.input({
-				title: '%i18n:@enter-password%',
+				title: this.$t('enter-password'),
 				type: 'password'
 			}).then(password => {
 				(this as any).api('i/2fa/unregister', {
 					password: password
 				}).then(() => {
-					(this as any).apis.notify('%i18n:@unregistered%');
+					(this as any).apis.notify(this.$t('unregistered'));
 					this.$store.state.i.twoFactorEnabled = false;
 				});
 			});
@@ -63,10 +65,10 @@ export default Vue.extend({
 			(this as any).api('i/2fa/done', {
 				token: this.token
 			}).then(() => {
-				(this as any).apis.notify('%i18n:@success%');
+				(this as any).apis.notify(this.$t('success'));
 				this.$store.state.i.twoFactorEnabled = true;
 			}).catch(() => {
-				(this as any).apis.notify('%i18n:@failed%');
+				(this as any).apis.notify(this.$t('failed'));
 			});
 		}
 	}
diff --git a/src/client/app/desktop/views/components/settings.apps.vue b/src/client/app/desktop/views/components/settings.apps.vue
index bf8faf250e..de88923262 100644
--- a/src/client/app/desktop/views/components/settings.apps.vue
+++ b/src/client/app/desktop/views/components/settings.apps.vue
@@ -1,6 +1,6 @@
 <template>
 <div class="root">
-	<ui-info v-if="!fetching && apps.length == 0">%i18n:@no-apps%</ui-info>
+	<ui-info v-if="!fetching && apps.length == 0">{{ $t('no-apps') }}</ui-info>
 	<div class="apps" v-if="apps.length != 0">
 		<div v-for="app in apps">
 			<p><b>{{ app.name }}</b></p>
@@ -12,7 +12,9 @@
 
 <script lang="ts">
 import Vue from 'vue';
+import i18n from '../../../i18n';
 export default Vue.extend({
+	i18n: i18n('desktop/views/components/settings.apps.vue'),
 	data() {
 		return {
 			fetching: true,
diff --git a/src/client/app/desktop/views/components/settings.tags.vue b/src/client/app/desktop/views/components/settings.tags.vue
index dfc69a387e..3edd5fdd99 100644
--- a/src/client/app/desktop/views/components/settings.tags.vue
+++ b/src/client/app/desktop/views/components/settings.tags.vue
@@ -2,22 +2,24 @@
 <div class="vfcitkilproprqtbnpoertpsziierwzi">
 	<div v-for="timeline in timelines" class="timeline">
 		<ui-input v-model="timeline.title" @change="save">
-			<span>%i18n:@title%</span>
+			<span>{{ $t('title') }}</span>
 		</ui-input>
 		<ui-textarea :value="timeline.query ? timeline.query.map(tags => tags.join(' ')).join('\n') : ''" @input="onQueryChange(timeline, $event)">
-			<span>%i18n:@query%</span>
+			<span>{{ $t('query') }}</span>
 		</ui-textarea>
-		<ui-button class="save" @click="save">%i18n:@save%</ui-button>
+		<ui-button class="save" @click="save">{{ $t('save') }}</ui-button>
 	</div>
-	<ui-button class="add" @click="add">%i18n:@add%</ui-button>
+	<ui-button class="add" @click="add">{{ $t('add') }}</ui-button>
 </div>
 </template>
 
 <script lang="ts">
 import Vue from 'vue';
+import i18n from '../../../i18n';
 import * as uuid from 'uuid';
 
 export default Vue.extend({
+	i18n: i18n('desktop/views/components/settings.tags.vue'),
 	data() {
 		return {
 			timelines: this.$store.state.settings.tagTimelines
diff --git a/src/client/app/desktop/views/components/settings.vue b/src/client/app/desktop/views/components/settings.vue
index 95965e245c..65296b0768 100644
--- a/src/client/app/desktop/views/components/settings.vue
+++ b/src/client/app/desktop/views/components/settings.vue
@@ -1,31 +1,31 @@
 <template>
 <div class="mk-settings">
 	<div class="nav">
-		<p :class="{ active: page == 'profile' }" @mousedown="page = 'profile'"><fa icon="user" fixed-width/>%i18n:@profile%</p>
-		<p :class="{ active: page == 'theme' }" @mousedown="page = 'theme'"><fa icon="palette" fixed-width/>%i18n:@theme%</p>
+		<p :class="{ active: page == 'profile' }" @mousedown="page = 'profile'"><fa icon="user" fixed-width/>{{ $t('profile') }}</p>
+		<p :class="{ active: page == 'theme' }" @mousedown="page = 'theme'"><fa icon="palette" fixed-width/>{{ $t('theme') }}</p>
 		<p :class="{ active: page == 'web' }" @mousedown="page = 'web'"><fa icon="desktop" fixed-width/>Web</p>
-		<p :class="{ active: page == 'notification' }" @mousedown="page = 'notification'"><fa :icon="['far', 'bell']" fixed-width/>%i18n:@notification%</p>
-		<p :class="{ active: page == 'drive' }" @mousedown="page = 'drive'"><fa icon="cloud" fixed-width/>%i18n:common.drive%</p>
-		<p :class="{ active: page == 'hashtags' }" @mousedown="page = 'hashtags'"><fa icon="hashtag" fixed-width/>%i18n:@tags%</p>
-		<p :class="{ active: page == 'muteAndBlock' }" @mousedown="page = 'muteAndBlock'"><fa icon="ban" fixed-width/>%i18n:@mute-and-block%</p>
-		<p :class="{ active: page == 'apps' }" @mousedown="page = 'apps'"><fa icon="puzzle-piece" fixed-width/>%i18n:@apps%</p>
-		<p :class="{ active: page == 'security' }" @mousedown="page = 'security'"><fa icon="unlock-alt" fixed-width/>%i18n:@security%</p>
+		<p :class="{ active: page == 'notification' }" @mousedown="page = 'notification'"><fa :icon="['far', 'bell']" fixed-width/>{{ $t('notification') }}</p>
+		<p :class="{ active: page == 'drive' }" @mousedown="page = 'drive'"><fa icon="cloud" fixed-width/>{{ $t('@.drive') }}</p>
+		<p :class="{ active: page == 'hashtags' }" @mousedown="page = 'hashtags'"><fa icon="hashtag" fixed-width/>{{ $t('tags') }}</p>
+		<p :class="{ active: page == 'muteAndBlock' }" @mousedown="page = 'muteAndBlock'"><fa icon="ban" fixed-width/>{{ $t('mute-and-block') }}</p>
+		<p :class="{ active: page == 'apps' }" @mousedown="page = 'apps'"><fa icon="puzzle-piece" fixed-width/>{{ $t('apps') }}</p>
+		<p :class="{ active: page == 'security' }" @mousedown="page = 'security'"><fa icon="unlock-alt" fixed-width/>{{ $t('security') }}</p>
 		<p :class="{ active: page == 'api' }" @mousedown="page = 'api'"><fa icon="key" fixed-width/>API</p>
-		<p :class="{ active: page == 'other' }" @mousedown="page = 'other'"><fa icon="cogs" fixed-width/>%i18n:@other%</p>
+		<p :class="{ active: page == 'other' }" @mousedown="page = 'other'"><fa icon="cogs" fixed-width/>{{ $t('other') }}</p>
 	</div>
 	<div class="pages">
 		<div class="profile" v-show="page == 'profile'">
 			<mk-profile-editor/>
 
 			<ui-card>
-				<div slot="title"><fa :icon="['fab', 'twitter']"/> %i18n:@twitter%</div>
+				<div slot="title"><fa :icon="['fab', 'twitter']"/> {{ $t('twitter') }}</div>
 				<section>
 					<mk-twitter-setting/>
 				</section>
 			</ui-card>
 
 			<ui-card>
-				<div slot="title"><fa :icon="['fab', 'github']"/> %i18n:@github%</div>
+				<div slot="title"><fa :icon="['fab', 'github']"/> {{ $t('github') }}</div>
 				<section>
 					<mk-github-setting/>
 				</section>
@@ -33,7 +33,7 @@
 		</div>
 
 		<ui-card class="theme" v-show="page == 'theme'">
-			<div slot="title"><fa icon="palette"/> %i18n:@theme%</div>
+			<div slot="title"><fa icon="palette"/> {{ $t('theme') }}</div>
 
 			<section>
 				<mk-theme/>
@@ -41,158 +41,153 @@
 		</ui-card>
 
 		<ui-card class="web" v-show="page == 'web'">
-			<div slot="title"><fa icon="sliders-h"/> %i18n:@behaviour%</div>
+			<div slot="title"><fa icon="sliders-h"/> {{ $t('behaviour') }}</div>
 
 			<section>
-				<ui-switch v-model="fetchOnScroll">
-					%i18n:@fetch-on-scroll%
-					<span slot="desc">%i18n:@fetch-on-scroll-desc%</span>
+				<ui-switch v-model="fetchOnScroll">{{ $t('fetch-on-scroll') }}
+					<span slot="desc">{{ $t('fetch-on-scroll-desc') }}</span>
 				</ui-switch>
-				<ui-switch v-model="autoPopout">
-					%i18n:@auto-popout%
-					<span slot="desc">%i18n:@auto-popout-desc%</span>
+				<ui-switch v-model="autoPopout">{{ $t('auto-popout') }}
+					<span slot="desc">{{ $t('auto-popout-desc') }}</span>
 				</ui-switch>
-				<ui-switch v-model="deckNav">%i18n:@deck-nav%<span slot="desc">%i18n:@deck-nav-desc%</span></ui-switch>
+				<ui-switch v-model="deckNav">{{ $t('deck-nav') }}<span slot="desc">{{ $t('deck-nav-desc') }}</span></ui-switch>
 
 				<details>
-					<summary>%i18n:@advanced%</summary>
-					<ui-switch v-model="apiViaStream">
-						%i18n:@api-via-stream%
-						<span slot="desc">%i18n:@api-via-stream-desc%</span>
+					<summary>{{ $t('advanced') }}</summary>
+					<ui-switch v-model="apiViaStream">{{ $t('api-via-stream') }}
+						<span slot="desc">{{ $t('api-via-stream-desc') }}</span>
 					</ui-switch>
 				</details>
 			</section>
 
 			<section>
-				<header>%i18n:@timeline%</header>
-				<ui-switch v-model="showMyRenotes">%i18n:@show-my-renotes%</ui-switch>
-				<ui-switch v-model="showRenotedMyNotes">%i18n:@show-renoted-my-notes%</ui-switch>
-				<ui-switch v-model="showLocalRenotes">%i18n:@show-local-renotes%</ui-switch>
+				<header>{{ $t('timeline') }}</header>
+				<ui-switch v-model="showMyRenotes">{{ $t('show-my-renotes') }}</ui-switch>
+				<ui-switch v-model="showRenotedMyNotes">{{ $t('show-renoted-my-notes') }}</ui-switch>
+				<ui-switch v-model="showLocalRenotes">{{ $t('show-local-renotes') }}</ui-switch>
 			</section>
 
 			<section>
-				<header>%i18n:@note-visibility%</header>
-				<ui-switch v-model="rememberNoteVisibility">%i18n:@remember-note-visibility%</ui-switch>
+				<header>{{ $t('note-visibility') }}</header>
+				<ui-switch v-model="rememberNoteVisibility">{{ $t('remember-note-visibility') }}</ui-switch>
 				<section>
-					<header>%i18n:@default-note-visibility%</header>
+					<header>{{ $t('default-note-visibility') }}</header>
 					<ui-select v-model="defaultNoteVisibility">
-						<option value="public">%i18n:common.note-visibility.public%</option>
-						<option value="home">%i18n:common.note-visibility.home%</option>
-						<option value="followers">%i18n:common.note-visibility.followers%</option>
-						<option value="specified">%i18n:common.note-visibility.specified%</option>
-						<option value="private">%i18n:common.note-visibility.private%</option>
+						<option value="public">{{ $t('@.note-visibility.public') }}</option>
+						<option value="home">{{ $t('@.note-visibility.home') }}</option>
+						<option value="followers">{{ $t('@.note-visibility.followers') }}</option>
+						<option value="specified">{{ $t('@.note-visibility.specified') }}</option>
+						<option value="private">{{ $t('@.note-visibility.private') }}</option>
 					</ui-select>
 				</section>
 			</section>
 		</ui-card>
 
 		<ui-card class="web" v-show="page == 'web'">
-			<div slot="title"><fa icon="desktop"/> %i18n:@display%</div>
+			<div slot="title"><fa icon="desktop"/> {{ $t('display') }}</div>
 
 			<section>
-				<ui-switch v-model="showPostFormOnTopOfTl">%i18n:@post-form-on-timeline%</ui-switch>
-				<ui-button @click="customizeHome">%i18n:@customize%</ui-button>
+				<ui-switch v-model="showPostFormOnTopOfTl">{{ $t('post-form-on-timeline') }}</ui-switch>
+				<ui-button @click="customizeHome">{{ $t('customize') }}</ui-button>
 			</section>
 			<section>
-				<header>%i18n:@wallpaper%</header>
-				<ui-button @click="updateWallpaper">%i18n:@choose-wallpaper%</ui-button>
-				<ui-button @click="deleteWallpaper">%i18n:@delete-wallpaper%</ui-button>
+				<header>{{ $t('wallpaper') }}</header>
+				<ui-button @click="updateWallpaper">{{ $t('choose-wallpaper') }}</ui-button>
+				<ui-button @click="deleteWallpaper">{{ $t('delete-wallpaper') }}</ui-button>
 			</section>
 			<section>
-				<header>%i18n:@navbar-position%</header>
-				<ui-radio v-model="navbar" value="top">%i18n:@navbar-position-top%</ui-radio>
-				<ui-radio v-model="navbar" value="left">%i18n:@navbar-position-left%</ui-radio>
-				<ui-radio v-model="navbar" value="right">%i18n:@navbar-position-right%</ui-radio>
+				<header>{{ $t('navbar-position') }}</header>
+				<ui-radio v-model="navbar" value="top">{{ $t('navbar-position-top') }}</ui-radio>
+				<ui-radio v-model="navbar" value="left">{{ $t('navbar-position-left') }}</ui-radio>
+				<ui-radio v-model="navbar" value="right">{{ $t('navbar-position-right') }}</ui-radio>
 			</section>
 			<section>
-				<ui-switch v-model="deckDefault">%i18n:@deck-default%</ui-switch>
+				<ui-switch v-model="deckDefault">{{ $t('deck-default') }}</ui-switch>
 			</section>
 			<section>
-				<ui-switch v-model="darkmode">%i18n:@dark-mode%</ui-switch>
-				<ui-switch v-model="useShadow">%i18n:@use-shadow%</ui-switch>
-				<ui-switch v-model="roundedCorners">%i18n:@rounded-corners%</ui-switch>
-				<ui-switch v-model="circleIcons">%i18n:@circle-icons%</ui-switch>
-				<ui-switch v-model="reduceMotion">%i18n:common.reduce-motion%</ui-switch>
-				<ui-switch v-model="contrastedAcct">%i18n:@contrasted-acct%</ui-switch>
-				<ui-switch v-model="showFullAcct">%i18n:common.show-full-acct%</ui-switch>
-				<ui-switch v-model="useOsDefaultEmojis">%i18n:common.use-os-default-emojis%</ui-switch>
-				<ui-switch v-model="iLikeSushi">%i18n:common.i-like-sushi%</ui-switch>
+				<ui-switch v-model="darkmode">{{ $t('dark-mode') }}</ui-switch>
+				<ui-switch v-model="useShadow">{{ $t('use-shadow') }}</ui-switch>
+				<ui-switch v-model="roundedCorners">{{ $t('rounded-corners') }}</ui-switch>
+				<ui-switch v-model="circleIcons">{{ $t('circle-icons') }}</ui-switch>
+				<ui-switch v-model="reduceMotion">{{ $t('@.reduce-motion') }}</ui-switch>
+				<ui-switch v-model="contrastedAcct">{{ $t('contrasted-acct') }}</ui-switch>
+				<ui-switch v-model="showFullAcct">{{ $t('@.show-full-acct') }}</ui-switch>
+				<ui-switch v-model="useOsDefaultEmojis">{{ $t('@.use-os-default-emojis') }}</ui-switch>
+				<ui-switch v-model="iLikeSushi">{{ $t('@.i-like-sushi') }}</ui-switch>
 			</section>
 			<section>
-				<ui-switch v-model="suggestRecentHashtags">%i18n:@suggest-recent-hashtags%</ui-switch>
-				<ui-switch v-model="showClockOnHeader">%i18n:@show-clock-on-header%</ui-switch>
-				<ui-switch v-model="alwaysShowNsfw">%i18n:common.always-show-nsfw%</ui-switch>
-				<ui-switch v-model="showReplyTarget">%i18n:@show-reply-target%</ui-switch>
-				<ui-switch v-model="showMaps">%i18n:@show-maps%</ui-switch>
-				<ui-switch v-model="disableAnimatedMfm">%i18n:common.disable-animated-mfm%</ui-switch>
+				<ui-switch v-model="suggestRecentHashtags">{{ $t('suggest-recent-hashtags') }}</ui-switch>
+				<ui-switch v-model="showClockOnHeader">{{ $t('show-clock-on-header') }}</ui-switch>
+				<ui-switch v-model="alwaysShowNsfw">{{ $t('@.always-show-nsfw') }}</ui-switch>
+				<ui-switch v-model="showReplyTarget">{{ $t('show-reply-target') }}</ui-switch>
+				<ui-switch v-model="showMaps">{{ $t('show-maps') }}</ui-switch>
+				<ui-switch v-model="disableAnimatedMfm">{{ $t('@.disable-animated-mfm') }}</ui-switch>
 			</section>
 			<section>
-				<header>%i18n:@deck-column-align%</header>
-				<ui-radio v-model="deckColumnAlign" value="center">%i18n:@deck-column-align-center%</ui-radio>
-				<ui-radio v-model="deckColumnAlign" value="left">%i18n:@deck-column-align-left%</ui-radio>
+				<header>{{ $t('deck-column-align') }}</header>
+				<ui-radio v-model="deckColumnAlign" value="center">{{ $t('deck-column-align-center') }}</ui-radio>
+				<ui-radio v-model="deckColumnAlign" value="left">{{ $t('deck-column-align-left') }}</ui-radio>
 			</section>
 			<section>
-				<ui-switch v-model="games_reversi_showBoardLabels">%i18n:common.show-reversi-board-labels%</ui-switch>
-				<ui-switch v-model="games_reversi_useContrastStones">%i18n:common.use-contrast-reversi-stones%</ui-switch>
+				<ui-switch v-model="games_reversi_showBoardLabels">{{ $t('@.show-reversi-board-labels') }}</ui-switch>
+				<ui-switch v-model="games_reversi_useContrastStones">{{ $t('@.use-contrast-reversi-stones') }}</ui-switch>
 			</section>
 		</ui-card>
 
 		<ui-card class="web" v-show="page == 'web'">
-			<div slot="title"><fa icon="volume-up"/> %i18n:@sound%</div>
+			<div slot="title"><fa icon="volume-up"/> {{ $t('sound') }}</div>
 
 			<section>
-				<ui-switch v-model="enableSounds">
-					%i18n:@enable-sounds%
-					<span slot="desc">%i18n:@enable-sounds-desc%</span>
+				<ui-switch v-model="enableSounds">{{ $t('enable-sounds') }}
+					<span slot="desc">{{ $t('enable-sounds-desc') }}</span>
 				</ui-switch>
-				<label>%i18n:@volume%</label>
+				<label>{{ $t('volume') }}</label>
 				<input type="range"
 					v-model="soundVolume"
 					:disabled="!enableSounds"
 					max="1"
 					step="0.1"
 				/>
-				<ui-button @click="soundTest"><fa icon="volume-up"/> %i18n:@test%</ui-button>
+				<ui-button @click="soundTest"><fa icon="volume-up"/> {{ $t('test') }}</ui-button>
 			</section>
 		</ui-card>
 
 		<ui-card class="web" v-show="page == 'web'">
-			<div slot="title"><fa icon="language"/> %i18n:@language%</div>
+			<div slot="title"><fa icon="language"/> {{ $t('language') }}</div>
 			<section class="fit-top">
-				<ui-select v-model="lang" placeholder="%i18n:@pick-language%">
-					<optgroup label="%i18n:@recommended%">
-						<option value="">%i18n:@auto%</option>
+				<ui-select v-model="lang" :placeholder="$t('placeholder')">
+					<optgroup :label="$t('label')">
+						<option value="">{{ $t('auto') }}</option>
 					</optgroup>
 
-					<optgroup label="%i18n:@specify-language%">
+					<optgroup :label="$t('label')">
 						<option v-for="x in langs" :value="x[0]" :key="x[0]">{{ x[1] }}</option>
 					</optgroup>
 				</ui-select>
 				<div class="none ui info">
-					<p><fa icon="info-circle"/>%i18n:@language-desc%</p>
+					<p><fa icon="info-circle"/>{{ $t('language-desc') }}</p>
 				</div>
 			</section>
 		</ui-card>
 
 		<ui-card class="web" v-show="page == 'web'">
-			<div slot="title"><fa :icon="['far', 'trash-alt']"/> %i18n:@cache%</div>
+			<div slot="title"><fa :icon="['far', 'trash-alt']"/> {{ $t('cache') }}</div>
 			<section>
-				<ui-button @click="clean">%i18n:@clean-cache%</ui-button>
+				<ui-button @click="clean">{{ $t('clean-cache') }}</ui-button>
 				<div class="none ui info warn">
-					<p><fa icon="exclamation-triangle"/>%i18n:@cache-warn%</p>
+					<p><fa icon="exclamation-triangle"/>{{ $t('cache-warn') }}</p>
 				</div>
 			</section>
 		</ui-card>
 
 		<ui-card class="notification" v-show="page == 'notification'">
-			<div slot="title"><fa :icon="['far', 'bell']"/> %i18n:@notification%</div>
+			<div slot="title"><fa :icon="['far', 'bell']"/> {{ $t('notification') }}</div>
 			<section>
 				<ui-switch v-model="$store.state.i.settings.autoWatch" @change="onChangeAutoWatch">
-					%i18n:@auto-watch%
-					<span slot="desc">%i18n:@auto-watch-desc%</span>
+					{{ $t('auto-watch') }}<span slot="desc">{{ $t('auto-watch-desc') }}</span>
 				</ui-switch>
 				<section>
-					<ui-button @click="readAllUnreadNotes">%i18n:@mark-as-read-all-unread-notes%</ui-button>
+					<ui-button @click="readAllUnreadNotes">{{ $t('mark-as-read-all-unread-notes') }}</ui-button>
 				</section>
 			</section>
 		</ui-card>
@@ -202,7 +197,7 @@
 		</div>
 
 		<ui-card class="hashtags" v-show="page == 'hashtags'">
-			<div slot="title"><fa icon="hashtag"/> %i18n:@tags%</div>
+			<div slot="title"><fa icon="hashtag"/> {{ $t('tags') }}</div>
 			<section>
 				<x-tags/>
 			</section>
@@ -213,28 +208,28 @@
 		</div>
 
 		<ui-card class="apps" v-show="page == 'apps'">
-			<div slot="title"><fa icon="puzzle-piece"/> %i18n:@apps%</div>
+			<div slot="title"><fa icon="puzzle-piece"/> {{ $t('apps') }}</div>
 			<section>
 				<x-apps/>
 			</section>
 		</ui-card>
 
 		<ui-card class="password" v-show="page == 'security'">
-			<div slot="title"><fa icon="unlock-alt"/> %i18n:@password%</div>
+			<div slot="title"><fa icon="unlock-alt"/> {{ $t('password') }}</div>
 			<section>
 				<mk-password-settings/>
 			</section>
 		</ui-card>
 
 		<ui-card class="2fa" v-show="page == 'security'">
-			<div slot="title"><fa icon="mobile-alt"/> %i18n:@2fa%</div>
+			<div slot="title"><fa icon="mobile-alt"/> {{ $t('2fa') }}</div>
 			<section>
 				<x-2fa/>
 			</section>
 		</ui-card>
 
 		<ui-card class="signin" v-show="page == 'security'">
-			<div slot="title"><fa icon="sign-in-alt"/> %i18n:@signin%</div>
+			<div slot="title"><fa icon="sign-in-alt"/> {{ $t('signin') }}</div>
 			<section>
 				<x-signins/>
 			</section>
@@ -245,46 +240,43 @@
 		</div>
 
 		<ui-card class="other" v-show="page == 'other'">
-			<div slot="title"><fa icon="info-circle"/> %i18n:@about%</div>
+			<div slot="title"><fa icon="info-circle"/> {{ $t('about') }}</div>
 			<section>
-				<p v-if="meta">%i18n:@operator%: <i><a :href="'mailto:' + meta.maintainer.email" target="_blank">{{ meta.maintainer.name }}</a></i></p>
+				<p v-if="meta">{{ $t('operator') }}: <i><a :href="'mailto:' + meta.maintainer.email" target="_blank">{{ meta.maintainer.name }}</a></i></p>
 			</section>
 		</ui-card>
 
 		<ui-card class="other" v-show="page == 'other'">
-			<div slot="title"><fa icon="sync-alt"/> %i18n:@update%</div>
+			<div slot="title"><fa icon="sync-alt"/> {{ $t('update') }}</div>
 			<section>
 				<p>
-					<span>%i18n:@version% <i>{{ version }}</i></span>
+					<span>{{ $t('version') }} <i>{{ version }}</i></span>
 					<template v-if="latestVersion !== undefined">
 						<br>
-						<span>%i18n:@latest-version% <i>{{ latestVersion ? latestVersion : version }}</i></span>
+						<span>{{ $t('latest-version') }} <i>{{ latestVersion ? latestVersion : version }}</i></span>
 					</template>
 				</p>
 				<button class="ui button block" @click="checkForUpdate" :disabled="checkingForUpdate">
-					<template v-if="checkingForUpdate">%i18n:@update-checking%<mk-ellipsis/></template>
-					<template v-else>%i18n:@do-update%</template>
+					<template v-if="checkingForUpdate">{{ $t('update-checking') }}<mk-ellipsis/></template>
+					<template v-else>{{ $t('do-update') }}</template>
 				</button>
 				<details>
-					<summary>%i18n:@update-settings%</summary>
+					<summary>{{ $t('update-settings') }}</summary>
 					<ui-switch v-model="preventUpdate">
-						%i18n:@prevent-update%
-						<span slot="desc">%i18n:@prevent-update-desc%</span>
+						{{ $t('prevent-update') }}<span slot="desc">{{ $t('prevent-update-desc') }}</span>
 					</ui-switch>
 				</details>
 			</section>
 		</ui-card>
 
 		<ui-card class="other" v-show="page == 'other'">
-			<div slot="title"><fa icon="cogs"/> %i18n:@advanced-settings%</div>
+			<div slot="title"><fa icon="cogs"/> {{ $t('advanced-settings') }}</div>
 			<section>
 				<ui-switch v-model="debug">
-					%i18n:@debug-mode%
-					<span slot="desc">%i18n:@debug-mode-desc%</span>
+					{{ $t('debug-mode') }}<span slot="desc">{{ $t('debug-mode-desc') }}</span>
 				</ui-switch>
 				<ui-switch v-model="enableExperimentalFeatures">
-					%i18n:@experimental%
-					<span slot="desc">%i18n:@experimental-desc%</span>
+					{{ $t('experimental') }}<span slot="desc">{{ $t('experimental-desc') }}</span>
 				</ui-switch>
 			</section>
 		</ui-card>
@@ -294,6 +286,7 @@
 
 <script lang="ts">
 import Vue from 'vue';
+import i18n from '../../../i18n';
 import X2fa from './settings.2fa.vue';
 import XApps from './settings.apps.vue';
 import XSignins from './settings.signins.vue';
@@ -302,6 +295,7 @@ import { url, langs, clientVersion as version } from '../../../config';
 import checkForUpdate from '../../../common/scripts/check-for-update';
 
 export default Vue.extend({
+	i18n: i18n('desktop/views/components/settings.vue'),
 	components: {
 		X2fa,
 		XApps,
@@ -544,13 +538,13 @@ export default Vue.extend({
 				this.latestVersion = newer;
 				if (newer == null) {
 					(this as any).apis.dialog({
-						title: '%i18n:@no-updates%',
-						text: '%i18n:@no-updates-desc%'
+						title: this.$t('no-updates'),
+						text: this.$t('no-updates-desc')
 					});
 				} else {
 					(this as any).apis.dialog({
-						title: '%i18n:@update-available%',
-						text: '%i18n:@update-available-desc%'
+						title: this.$t('update-available'),
+						text: this.$t('update-available-desc')
 					});
 				}
 			});
@@ -558,8 +552,8 @@ export default Vue.extend({
 		clean() {
 			localStorage.clear();
 			(this as any).apis.dialog({
-				title: '%i18n:@cache-cleared%',
-				text: '%i18n:@cache-cleared-desc%'
+				title: this.$t('cache-cleared'),
+				text: this.$t('cache-cleared-desc')
 			});
 		},
 		soundTest() {
diff --git a/src/client/app/desktop/views/components/sub-note-content.vue b/src/client/app/desktop/views/components/sub-note-content.vue
index c17b5ccead..0007520e99 100644
--- a/src/client/app/desktop/views/components/sub-note-content.vue
+++ b/src/client/app/desktop/views/components/sub-note-content.vue
@@ -1,18 +1,18 @@
 <template>
 <div class="mk-sub-note-content">
 	<div class="body">
-		<span v-if="note.isHidden" style="opacity: 0.5">%i18n:@private%</span>
-		<span v-if="note.deletedAt" style="opacity: 0.5">%i18n:@deleted%</span>
+		<span v-if="note.isHidden" style="opacity: 0.5">{{ $t('private') }}</span>
+		<span v-if="note.deletedAt" style="opacity: 0.5">{{ $t('deleted') }}</span>
 		<a class="reply" v-if="note.replyId"><fa icon="reply"/></a>
-		<misskey-flavored-markdown v-if="note.text" :text="note.text" :i="$store.state.i" :customEmojis="note.emojis"/>
+		<misskey-flavored-markdown v-if="note.text" :text="note.text" :i="$store.state.i" :custom-emojis="note.emojis"/>
 		<a class="rp" v-if="note.renoteId" :href="`/notes/${note.renoteId}`">RN: ...</a>
 	</div>
 	<details v-if="note.files.length > 0">
-		<summary>({{ '%i18n:@media-count%'.replace('{}', note.files.length) }})</summary>
+		<summary>({{ this.$t('media-count').replace('{}', note.files.length) }})</summary>
 		<mk-media-list :media-list="note.files"/>
 	</details>
 	<details v-if="note.poll">
-		<summary>%i18n:@poll%</summary>
+		<summary>{{ $t('poll') }}</summary>
 		<mk-poll :note="note"/>
 	</details>
 </div>
@@ -20,8 +20,10 @@
 
 <script lang="ts">
 import Vue from 'vue';
+import i18n from '../../../i18n';
 
 export default Vue.extend({
+	i18n: i18n('desktop/views/components/sub-note-content.vue'),
 	props: ['note']
 });
 </script>
diff --git a/src/client/app/desktop/views/components/timeline.core.vue b/src/client/app/desktop/views/components/timeline.core.vue
index 91b3cd5f2e..94126dcf17 100644
--- a/src/client/app/desktop/views/components/timeline.core.vue
+++ b/src/client/app/desktop/views/components/timeline.core.vue
@@ -4,7 +4,7 @@
 
 	<mk-notes ref="timeline" :more="existMore ? more : null">
 		<p :class="$style.empty" slot="empty">
-			<fa :icon="['far', 'comments']"/>%i18n:@empty%
+			<fa :icon="['far', 'comments']"/>{{ $t('empty') }}
 		</p>
 	</mk-notes>
 </div>
@@ -12,10 +12,12 @@
 
 <script lang="ts">
 import Vue from 'vue';
+import i18n from '../../../i18n';
 
 const fetchLimit = 10;
 
 export default Vue.extend({
+	i18n: i18n('desktop/views/components/timeline.core.vue'),
 	props: {
 		src: {
 			type: String,
diff --git a/src/client/app/desktop/views/components/timeline.vue b/src/client/app/desktop/views/components/timeline.vue
index 336033b601..e6f4219b59 100644
--- a/src/client/app/desktop/views/components/timeline.vue
+++ b/src/client/app/desktop/views/components/timeline.vue
@@ -1,17 +1,17 @@
 <template>
 <div class="mk-timeline">
 	<header>
-		<span :data-active="src == 'home'" @click="src = 'home'"><fa icon="home"/> %i18n:@home%</span>
-		<span :data-active="src == 'local'" @click="src = 'local'" v-if="enableLocalTimeline"><fa :icon="['far', 'comments']"/> %i18n:@local%</span>
-		<span :data-active="src == 'hybrid'" @click="src = 'hybrid'" v-if="enableLocalTimeline"><fa icon="share-alt"/> %i18n:@hybrid%</span>
-		<span :data-active="src == 'global'" @click="src = 'global'"><fa icon="globe"/> %i18n:@global%</span>
+		<span :data-active="src == 'home'" @click="src = 'home'"><fa icon="home"/> {{ $t('home') }}</span>
+		<span :data-active="src == 'local'" @click="src = 'local'" v-if="enableLocalTimeline"><fa :icon="['far', 'comments']"/> {{ $t('local') }}</span>
+		<span :data-active="src == 'hybrid'" @click="src = 'hybrid'" v-if="enableLocalTimeline"><fa icon="share-alt"/> {{ $t('hybrid') }}</span>
+		<span :data-active="src == 'global'" @click="src = 'global'"><fa icon="globe"/> {{ $t('global') }}</span>
 		<span :data-active="src == 'tag'" @click="src = 'tag'" v-if="tagTl"><fa icon="hashtag"/> {{ tagTl.title }}</span>
 		<span :data-active="src == 'list'" @click="src = 'list'" v-if="list"><fa icon="list"/> {{ list.title }}</span>
 		<div class="buttons">
-			<button :data-active="src == 'mentions'" @click="src = 'mentions'" title="%i18n:@mentions%"><fa icon="at"/><i class="badge" v-if="$store.state.i.hasUnreadMentions"><fa icon="circle"/></i></button>
-			<button :data-active="src == 'messages'" @click="src = 'messages'" title="%i18n:@messages%"><fa :icon="['far', 'envelope']"/><i class="badge" v-if="$store.state.i.hasUnreadSpecifiedNotes"><fa icon="circle"/></i></button>
-			<button @click="chooseTag" title="%i18n:@hashtag%" ref="tagButton"><fa icon="hashtag"/></button>
-			<button @click="chooseList" title="%i18n:@list%" ref="listButton"><fa icon="list"/></button>
+			<button :data-active="src == 'mentions'" @click="src = 'mentions'" :title="$t('mentions')"><fa icon="at"/><i class="badge" v-if="$store.state.i.hasUnreadMentions"><fa icon="circle"/></i></button>
+			<button :data-active="src == 'messages'" @click="src = 'messages'" :title="$t('messages')"><fa :icon="['far', 'envelope']"/><i class="badge" v-if="$store.state.i.hasUnreadSpecifiedNotes"><fa icon="circle"/></i></button>
+			<button @click="chooseTag" :title="$t('hashtag')" ref="tagButton"><fa icon="hashtag"/></button>
+			<button @click="chooseList" :title="$t('list')" ref="listButton"><fa icon="list"/></button>
 		</div>
 	</header>
 	<x-core v-if="src == 'home'" ref="tl" key="home" src="home"/>
@@ -27,11 +27,13 @@
 
 <script lang="ts">
 import Vue from 'vue';
+import i18n from '../../../i18n';
 import XCore from './timeline.core.vue';
 import Menu from '../../../common/views/components/menu.vue';
 import MkSettingsWindow from './settings-window.vue';
 
 export default Vue.extend({
+	i18n: i18n('desktop/views/components/timeline.vue'),
 	components: {
 		XCore
 	},
@@ -105,10 +107,10 @@ export default Vue.extend({
 
 			let menu = [{
 				icon: 'plus',
-				text: '%i18n:@add-list%',
+				text: this.$t('add-list'),
 				action: () => {
 					(this as any).apis.input({
-						title: '%i18n:@list-name%',
+						title: this.$t('list-name'),
 					}).then(async title => {
 						const list = await (this as any).api('users/lists/create', {
 							title
@@ -143,7 +145,7 @@ export default Vue.extend({
 		chooseTag() {
 			let menu = [{
 				icon: 'plus',
-				text: '%i18n:@add-tag-timeline%',
+				text: this.$t('add-tag-timeline'),
 				action: () => {
 					(this as any).os.new(MkSettingsWindow, {
 						initialPage: 'hashtags'
diff --git a/src/client/app/desktop/views/components/ui.header.account.vue b/src/client/app/desktop/views/components/ui.header.account.vue
index 31118b014e..2a0424b2a5 100644
--- a/src/client/app/desktop/views/components/ui.header.account.vue
+++ b/src/client/app/desktop/views/components/ui.header.account.vue
@@ -10,35 +10,35 @@
 				<li>
 					<router-link :to="`/@${ $store.state.i.username }`">
 						<i><fa icon="user"/></i>
-						<span>%i18n:@profile%</span>
+						<span>{{ $t('profile') }}</span>
 						<i><fa icon="angle-right"/></i>
 					</router-link>
 				</li>
 				<li @click="drive">
 					<p>
 						<i><fa icon="cloud"/></i>
-						<span>%i18n:common.drive%</span>
+						<span>{{ $t('@.drive') }}</span>
 						<i><fa icon="angle-right"/></i>
 					</p>
 				</li>
 				<li>
 					<router-link to="/i/favorites">
 						<i><fa icon="star"/></i>
-						<span>%i18n:@favorites%</span>
+						<span>{{ $t('favorites') }}</span>
 						<i><fa icon="angle-right"/></i>
 					</router-link>
 				</li>
 				<li @click="list">
 					<p>
 						<i><fa icon="list"/></i>
-						<span>%i18n:@lists%</span>
+						<span>{{ $t('lists') }}</span>
 						<i><fa icon="angle-right"/></i>
 					</p>
 				</li>
 				<li @click="followRequests" v-if="($store.state.i.isLocked || $store.state.i.carefulBot)">
 					<p>
 						<i><fa :icon="['far', 'envelope']"/></i>
-						<span>%i18n:@follow-requests%<i v-if="$store.state.i.pendingReceivedFollowRequestsCount">{{ $store.state.i.pendingReceivedFollowRequestsCount }}</i></span>
+						<span>{{ $t('follow-requests') }}<i v-if="$store.state.i.pendingReceivedFollowRequestsCount">{{ $store.state.i.pendingReceivedFollowRequestsCount }}</i></span>
 						<i><fa icon="angle-right"/></i>
 					</p>
 				</li>
@@ -47,21 +47,21 @@
 				<li>
 					<router-link to="/i/customize-home">
 						<i><fa icon="wrench"/></i>
-						<span>%i18n:@customize%</span>
+						<span>{{ $t('customize') }}</span>
 						<i><fa icon="angle-right"/></i>
 					</router-link>
 				</li>
 				<li @click="settings">
 					<p>
 						<i><fa icon="cog"/></i>
-						<span>%i18n:@settings%</span>
+						<span>{{ $t('settings') }}</span>
 						<i><fa icon="angle-right"/></i>
 					</p>
 				</li>
 				<li v-if="$store.state.i.isAdmin">
 					<a href="/admin">
 						<i><fa icon="terminal"/></i>
-						<span>%i18n:@admin%</span>
+						<span>{{ $t('admin') }}</span>
 						<i><fa icon="angle-right"/></i>
 					</a>
 				</li>
@@ -69,7 +69,7 @@
 			<ul>
 				<li @click="dark">
 					<p>
-						<span>%i18n:@dark%</span>
+						<span>{{ $t('dark') }}</span>
 						<template v-if="$store.state.device.darkmode"><i><fa icon="moon"/></i></template>
 						<template v-else><i><fa :icon="['far', 'moon']"/></i></template>
 					</p>
@@ -79,7 +79,7 @@
 				<li @click="signout">
 					<p class="signout">
 						<i><fa icon="power-off"/></i>
-						<span>%i18n:@signout%</span>
+						<span>{{ $t('signout') }}</span>
 					</p>
 				</li>
 			</ul>
@@ -90,6 +90,7 @@
 
 <script lang="ts">
 import Vue from 'vue';
+import i18n from '../../../i18n';
 import MkUserListsWindow from './user-lists-window.vue';
 import MkFollowRequestsWindow from './received-follow-requests-window.vue';
 import MkSettingsWindow from './settings-window.vue';
@@ -97,6 +98,7 @@ import MkDriveWindow from './drive-window.vue';
 import contains from '../../../common/scripts/contains';
 
 export default Vue.extend({
+	i18n: i18n('desktop/views/components/ui.header.account.vue'),
 	data() {
 		return {
 			isOpen: false
diff --git a/src/client/app/desktop/views/components/ui.header.nav.vue b/src/client/app/desktop/views/components/ui.header.nav.vue
index 585f0a9436..5f98d21bd5 100644
--- a/src/client/app/desktop/views/components/ui.header.nav.vue
+++ b/src/client/app/desktop/views/components/ui.header.nav.vue
@@ -4,31 +4,31 @@
 		<template v-if="$store.getters.isSignedIn">
 			<template v-if="$store.state.device.deckDefault">
 				<li class="deck" :class="{ active: $route.name == 'deck' || $route.name == 'index' }" @click="goToTop">
-					<router-link to="/"><fa icon="columns"/><p>%i18n:@deck%</p></router-link>
+					<router-link to="/"><fa icon="columns"/><p>{{ $t('deck') }}</p></router-link>
 				</li>
 				<li class="home" :class="{ active: $route.name == 'home' }" @click="goToTop">
-					<router-link to="/home"><fa icon="home"/><p>%i18n:@home%</p></router-link>
+					<router-link to="/home"><fa icon="home"/><p>{{ $t('home') }}</p></router-link>
 				</li>
 			</template>
 			<template v-else>
 				<li class="home" :class="{ active: $route.name == 'home' || $route.name == 'index' }" @click="goToTop">
-					<router-link to="/"><fa icon="home"/><p>%i18n:@home%</p></router-link>
+					<router-link to="/"><fa icon="home"/><p>{{ $t('home') }}</p></router-link>
 				</li>
 				<li class="deck" :class="{ active: $route.name == 'deck' }" @click="goToTop">
-					<router-link to="/deck"><fa icon="columns"/><p>%i18n:@deck%</p></router-link>
+					<router-link to="/deck"><fa icon="columns"/><p>{{ $t('deck') }}</p></router-link>
 				</li>
 			</template>
 			<li class="messaging">
 				<a @click="messaging">
 					<fa icon="comments"/>
-					<p>%i18n:@messaging%</p>
+					<p>{{ $t('@.messaging') }}</p>
 					<template v-if="hasUnreadMessagingMessage"><fa icon="circle"/></template>
 				</a>
 			</li>
 			<li class="game">
 				<a @click="game">
 					<fa icon="gamepad"/>
-					<p>%i18n:@game%</p>
+					<p>{{ $t('game') }}</p>
 					<template v-if="hasGameInvitations"><fa icon="circle"/></template>
 				</a>
 			</li>
@@ -39,10 +39,12 @@
 
 <script lang="ts">
 import Vue from 'vue';
+import i18n from '../../../i18n';
 import MkMessagingWindow from './messaging-window.vue';
 import MkGameWindow from './game-window.vue';
 
 export default Vue.extend({
+	i18n: i18n('desktop/views/components/ui.header.nav.vue'),
 	data() {
 		return {
 			hasGameInvitations: false,
diff --git a/src/client/app/desktop/views/components/ui.header.notifications.vue b/src/client/app/desktop/views/components/ui.header.notifications.vue
index fa3be79096..b83a5da04c 100644
--- a/src/client/app/desktop/views/components/ui.header.notifications.vue
+++ b/src/client/app/desktop/views/components/ui.header.notifications.vue
@@ -1,6 +1,6 @@
 <template>
 <div class="notifications" v-hotkey.global="keymap">
-	<button :data-active="isOpen" @click="toggle" title="%i18n:@title%">
+	<button :data-active="isOpen" @click="toggle" :title="$t('title')">
 		<i class="bell"><fa :icon="['far', 'bell']"/></i>
 		<i class="circle" v-if="hasUnreadNotification"><fa icon="circle"/></i>
 	</button>
@@ -12,9 +12,11 @@
 
 <script lang="ts">
 import Vue from 'vue';
+import i18n from '../../../i18n';
 import contains from '../../../common/scripts/contains';
 
 export default Vue.extend({
+	i18n: i18n('desktop/views/components/ui.header.notifications.vue'),
 	data() {
 		return {
 			isOpen: false
diff --git a/src/client/app/desktop/views/components/ui.header.post.vue b/src/client/app/desktop/views/components/ui.header.post.vue
index 2955b33487..85f202f7a9 100644
--- a/src/client/app/desktop/views/components/ui.header.post.vue
+++ b/src/client/app/desktop/views/components/ui.header.post.vue
@@ -1,13 +1,15 @@
 <template>
 <div class="note">
-	<button @click="post" title="%i18n:@post%"><fa icon="pencil-alt"/></button>
+	<button @click="post" :title="$t('post')"><fa icon="pencil-alt"/></button>
 </div>
 </template>
 
 <script lang="ts">
 import Vue from 'vue';
+import i18n from '../../../i18n';
 
 export default Vue.extend({
+	i18n: i18n('desktop/views/components/ui.header.post.vue'),
 	methods: {
 		post() {
 			(this as any).apis.post();
diff --git a/src/client/app/desktop/views/components/ui.header.search.vue b/src/client/app/desktop/views/components/ui.header.search.vue
index eb08751eb2..ede9f9da0f 100644
--- a/src/client/app/desktop/views/components/ui.header.search.vue
+++ b/src/client/app/desktop/views/components/ui.header.search.vue
@@ -1,15 +1,17 @@
 <template>
 <form class="search" @submit.prevent="onSubmit">
 	<i><fa icon="search"/></i>
-	<input v-model="q" type="search" placeholder="%i18n:@placeholder%"/>
+	<input v-model="q" type="search" :placeholder="$t('placeholder')"/>
 	<div class="result"></div>
 </form>
 </template>
 
 <script lang="ts">
 import Vue from 'vue';
+import i18n from '../../../i18n';
 
 export default Vue.extend({
+	i18n: i18n('desktop/views/components/ui.header.search.vue'),
 	data() {
 		return {
 			q: ''
diff --git a/src/client/app/desktop/views/components/ui.header.vue b/src/client/app/desktop/views/components/ui.header.vue
index 4cfcda0f1a..1c89a3ea81 100644
--- a/src/client/app/desktop/views/components/ui.header.vue
+++ b/src/client/app/desktop/views/components/ui.header.vue
@@ -1,7 +1,6 @@
 <template>
 <div class="header" :style="style">
-	<p class="warn" v-if="env != 'production'">%i18n:common.do-not-use-in-production%</p>
-	<mk-special-message/>
+	<p class="warn" v-if="env != 'production'">{{ $t('@.do-not-use-in-production') }}</p>
 	<div class="main" ref="main">
 		<div class="backdrop"></div>
 		<div class="main">
@@ -29,6 +28,7 @@
 
 <script lang="ts">
 import Vue from 'vue';
+import i18n from '../../../i18n';
 import * as anime from 'animejs';
 import { env } from '../../../config';
 
@@ -40,6 +40,7 @@ import XPost from './ui.header.post.vue';
 import XClock from './ui.header.clock.vue';
 
 export default Vue.extend({
+	i18n: i18n(),
 	components: {
 		XNav,
 		XSearch,
diff --git a/src/client/app/desktop/views/components/ui.sidebar.vue b/src/client/app/desktop/views/components/ui.sidebar.vue
index 7c68074beb..7834892271 100644
--- a/src/client/app/desktop/views/components/ui.sidebar.vue
+++ b/src/client/app/desktop/views/components/ui.sidebar.vue
@@ -2,7 +2,7 @@
 <div class="header" :class="navbar">
 	<div class="body">
 		<div class="post">
-			<button @click="post" title="%i18n:@post%"><fa icon="pencil-alt"/></button>
+			<button @click="post" :title="$t('title')"><fa icon="pencil-alt"/></button>
 		</div>
 
 		<div class="nav" v-if="$store.getters.isSignedIn">
@@ -77,6 +77,7 @@
 
 <script lang="ts">
 import Vue from 'vue';
+import i18n from '../../../i18n';
 import MkUserListsWindow from './user-lists-window.vue';
 import MkFollowRequestsWindow from './received-follow-requests-window.vue';
 import MkSettingsWindow from './settings-window.vue';
@@ -86,6 +87,7 @@ import MkGameWindow from './game-window.vue';
 import contains from '../../../common/scripts/contains';
 
 export default Vue.extend({
+	i18n: i18n('desktop/views/components/ui.sidebar.vue'),
 	data() {
 		return {
 			hasGameInvitations: false,
diff --git a/src/client/app/desktop/views/components/user-lists-window.vue b/src/client/app/desktop/views/components/user-lists-window.vue
index 5943379200..6f7d07a5f4 100644
--- a/src/client/app/desktop/views/components/user-lists-window.vue
+++ b/src/client/app/desktop/views/components/user-lists-window.vue
@@ -1,9 +1,9 @@
 <template>
 <mk-window ref="window" is-modal width="450px" height="500px" @closed="destroyDom">
-	<span slot="header"><fa icon="list"/> %i18n:@title%</span>
+	<span slot="header"><fa icon="list"/> {{ $t('title') }}</span>
 
 	<div class="xkxvokkjlptzyewouewmceqcxhpgzprp">
-		<button class="ui" @click="add">%i18n:@create-list%</button>
+		<button class="ui" @click="add">{{ $t('create-list') }}</button>
 		<a v-for="list in lists" :key="list.id" @click="choice(list)">{{ list.title }}</a>
 	</div>
 </mk-window>
@@ -11,7 +11,10 @@
 
 <script lang="ts">
 import Vue from 'vue';
+import i18n from '../../../i18n';
+
 export default Vue.extend({
+	i18n: i18n('desktop/views/components/user-lists-window.vue'),
 	data() {
 		return {
 			fetching: true,
@@ -27,7 +30,7 @@ export default Vue.extend({
 	methods: {
 		add() {
 			(this as any).apis.input({
-				title: '%i18n:@list-name%',
+				title: this.$t('list-name'),
 			}).then(async title => {
 				const list = await (this as any).api('users/lists/create', {
 					title
diff --git a/src/client/app/desktop/views/components/user-preview.vue b/src/client/app/desktop/views/components/user-preview.vue
index 7f5e79eae1..1ce5a06f28 100644
--- a/src/client/app/desktop/views/components/user-preview.vue
+++ b/src/client/app/desktop/views/components/user-preview.vue
@@ -10,13 +10,13 @@
 		<div class="description">{{ u.description }}</div>
 		<div class="status">
 			<div>
-				<p>%i18n:@notes%</p><span>{{ u.notesCount }}</span>
+				<p>{{ $t('notes') }}</p><span>{{ u.notesCount }}</span>
 			</div>
 			<div>
-				<p>%i18n:@following%</p><span>{{ u.followingCount }}</span>
+				<p>{{ $t('following') }}</p><span>{{ u.followingCount }}</span>
 			</div>
 			<div>
-				<p>%i18n:@followers%</p><span>{{ u.followersCount }}</span>
+				<p>{{ $t('followers') }}</p><span>{{ u.followersCount }}</span>
 			</div>
 		</div>
 		<mk-follow-button v-if="$store.getters.isSignedIn && u.id != $store.state.i.id" :user="u"/>
@@ -26,10 +26,12 @@
 
 <script lang="ts">
 import Vue from 'vue';
+import i18n from '../../../i18n';
 import * as anime from 'animejs';
 import parseAcct from '../../../../../misc/acct/parse';
 
 export default Vue.extend({
+	i18n: i18n('desktop/views/components/user-preview.vue'),
 	props: {
 		user: {
 			type: [Object, String],
diff --git a/src/client/app/desktop/views/components/window.vue b/src/client/app/desktop/views/components/window.vue
index 6668e0025f..75335f540b 100644
--- a/src/client/app/desktop/views/components/window.vue
+++ b/src/client/app/desktop/views/components/window.vue
@@ -8,10 +8,10 @@
 			>
 				<h1><slot name="header"></slot></h1>
 				<div>
-					<button class="popout" v-if="popoutUrl" @mousedown.stop="() => {}" @click="popout" title="%i18n:@popout%">
+					<button class="popout" v-if="popoutUrl" @mousedown.stop="() => {}" @click="popout" :title="$t('title')">
 						<i><fa :icon="['far', 'window-restore']"/></i>
 					</button>
-					<button class="close" v-if="canClose" @mousedown.stop="() => {}" @click="close" title="%i18n:@close%">
+					<button class="close" v-if="canClose" @mousedown.stop="() => {}" @click="close" :title="$t('title')">
 						<i><fa icon="times"/></i>
 					</button>
 				</div>
@@ -36,6 +36,7 @@
 
 <script lang="ts">
 import Vue from 'vue';
+import i18n from '../../../i18n';
 import * as anime from 'animejs';
 import contains from '../../../common/scripts/contains';
 
@@ -55,6 +56,7 @@ function dragClear(fn) {
 }
 
 export default Vue.extend({
+	i18n: i18n('desktop/views/components/window.vue'),
 	props: {
 		isModal: {
 			type: Boolean,
diff --git a/src/client/app/desktop/views/pages/deck/deck.column.vue b/src/client/app/desktop/views/pages/deck/deck.column.vue
index ac811ce7cc..27a1f7e970 100644
--- a/src/client/app/desktop/views/pages/deck/deck.column.vue
+++ b/src/client/app/desktop/views/pages/deck/deck.column.vue
@@ -27,11 +27,13 @@
 
 <script lang="ts">
 import Vue from 'vue';
+import i18n from '../../../../i18n';
 import Menu from '../../../../common/views/components/menu.vue';
 import contextmenu from '../../../api/contextmenu';
 import { countIf } from '../../../../../../prelude/array';
 
 export default Vue.extend({
+	i18n: i18n('deck'),
 	props: {
 		column: {
 			type: Object,
@@ -164,10 +166,10 @@ export default Vue.extend({
 		getMenu() {
 			const items = [{
 				icon: 'pencil-alt',
-				text: '%i18n:common.deck.rename%',
+				text: this.$t('rename'),
 				action: () => {
 					(this as any).apis.input({
-						title: '%i18n:common.deck.rename%',
+						title: this.$t('rename'),
 						default: this.name,
 						allowEmpty: false
 					}).then(name => {
@@ -176,43 +178,43 @@ export default Vue.extend({
 				}
 			}, null, {
 				icon: 'arrow-left',
-				text: '%i18n:common.deck.swap-left%',
+				text: this.$t('swap-left'),
 				action: () => {
 					this.$store.dispatch('settings/swapLeftDeckColumn', this.column.id);
 				}
 			}, {
 				icon: 'arrow-right',
-				text: '%i18n:common.deck.swap-right%',
+				text: this.$t('swap-right'),
 				action: () => {
 					this.$store.dispatch('settings/swapRightDeckColumn', this.column.id);
 				}
 			}, this.isStacked ? {
 				icon: 'arrow-up',
-				text: '%i18n:common.deck.swap-up%',
+				text: this.$t('swap-up'),
 				action: () => {
 					this.$store.dispatch('settings/swapUpDeckColumn', this.column.id);
 				}
 			} : undefined, this.isStacked ? {
 				icon: 'arrow-down',
-				text: '%i18n:common.deck.swap-down%',
+				text: this.$t('swap-down'),
 				action: () => {
 					this.$store.dispatch('settings/swapDownDeckColumn', this.column.id);
 				}
 			} : undefined, null, {
 				icon: ['far', 'window-restore'],
-				text: '%i18n:common.deck.stack-left%',
+				text: this.$t('stack-left'),
 				action: () => {
 					this.$store.dispatch('settings/stackLeftDeckColumn', this.column.id);
 				}
 			}, this.isStacked ? {
 				icon: ['far', 'window-maximize'],
-				text: '%i18n:common.deck.pop-right%',
+				text: this.$t('pop-right'),
 				action: () => {
 					this.$store.dispatch('settings/popRightDeckColumn', this.column.id);
 				}
 			} : undefined, null, {
 				icon: ['far', 'trash-alt'],
-				text: '%i18n:common.deck.remove%',
+				text: this.$t('remove'),
 				action: () => {
 					this.$store.dispatch('settings/removeDeckColumn', this.column.id);
 				}
diff --git a/src/client/app/desktop/views/pages/deck/deck.direct-column.vue b/src/client/app/desktop/views/pages/deck/deck.direct-column.vue
index baee7ff5e5..cdeba95f5f 100644
--- a/src/client/app/desktop/views/pages/deck/deck.direct-column.vue
+++ b/src/client/app/desktop/views/pages/deck/deck.direct-column.vue
@@ -8,10 +8,12 @@
 
 <script lang="ts">
 import Vue from 'vue';
+import i18n from '../../../../i18n';
 import XColumn from './deck.column.vue';
 import XDirect from './deck.direct.vue';
 
 export default Vue.extend({
+	i18n: i18n(),
 	components: {
 		XColumn,
 		XDirect
@@ -31,7 +33,7 @@ export default Vue.extend({
 	computed: {
 		name(): string {
 			if (this.column.name) return this.column.name;
-			return '%i18n:common.deck.direct%';
+			return this.$t('@deck.direct');
 		}
 	},
 
diff --git a/src/client/app/desktop/views/pages/deck/deck.direct.vue b/src/client/app/desktop/views/pages/deck/deck.direct.vue
index 47fb15370b..c85d9c1bb9 100644
--- a/src/client/app/desktop/views/pages/deck/deck.direct.vue
+++ b/src/client/app/desktop/views/pages/deck/deck.direct.vue
@@ -1,5 +1,5 @@
 <template>
-	<x-notes ref="timeline" :more="existMore ? more : null"/>
+<x-notes ref="timeline" :more="existMore ? more : null"/>
 </template>
 
 <script lang="ts">
diff --git a/src/client/app/desktop/views/pages/deck/deck.mentions-column.vue b/src/client/app/desktop/views/pages/deck/deck.mentions-column.vue
index 19537a2db0..011a321c3a 100644
--- a/src/client/app/desktop/views/pages/deck/deck.mentions-column.vue
+++ b/src/client/app/desktop/views/pages/deck/deck.mentions-column.vue
@@ -8,10 +8,12 @@
 
 <script lang="ts">
 import Vue from 'vue';
+import i18n from '../../../../i18n';
 import XColumn from './deck.column.vue';
 import XMentions from './deck.mentions.vue';
 
 export default Vue.extend({
+	i18n: i18n(),
 	components: {
 		XColumn,
 		XMentions
@@ -31,7 +33,7 @@ export default Vue.extend({
 	computed: {
 		name(): string {
 			if (this.column.name) return this.column.name;
-			return '%i18n:common.deck.mentions%';
+			return this.$t('@deck.mentions');
 		}
 	},
 
diff --git a/src/client/app/desktop/views/pages/deck/deck.mentions.vue b/src/client/app/desktop/views/pages/deck/deck.mentions.vue
index 7890e68409..1935d26722 100644
--- a/src/client/app/desktop/views/pages/deck/deck.mentions.vue
+++ b/src/client/app/desktop/views/pages/deck/deck.mentions.vue
@@ -1,5 +1,5 @@
 <template>
-	<x-notes ref="timeline" :more="existMore ? more : null"/>
+<x-notes ref="timeline" :more="existMore ? more : null"/>
 </template>
 
 <script lang="ts">
diff --git a/src/client/app/desktop/views/pages/deck/deck.note-column.vue b/src/client/app/desktop/views/pages/deck/deck.note-column.vue
index dae6debe7e..c90b5f61b7 100644
--- a/src/client/app/desktop/views/pages/deck/deck.note-column.vue
+++ b/src/client/app/desktop/views/pages/deck/deck.note-column.vue
@@ -7,8 +7,8 @@
 	<div class="rvtscbadixhhbsczoorqoaygovdeecsx" v-if="note">
 		<div class="is-remote" v-if="note.user.host != null">
 			<details>
-				<summary><fa icon="exclamation-triangle"/> %i18n:common.is-remote-post%</summary>
-				<a :href="note.url || note.uri" target="_blank">%i18n:common.view-on-remote%</a>
+				<summary><fa icon="exclamation-triangle"/> {{ $t('@.is-remote-post') }}</summary>
+				<a :href="note.url || note.uri" target="_blank">{{ $t('@.view-on-remote') }}</a>
 			</details>
 		</div>
 		<x-note :note="note" :detail="true" :mini="true"/>
@@ -18,11 +18,13 @@
 
 <script lang="ts">
 import Vue from 'vue';
+import i18n from '../../../../i18n';
 import XColumn from './deck.column.vue';
 import XNotes from './deck.notes.vue';
 import XNote from '../../components/note.vue';
 
 export default Vue.extend({
+	i18n: i18n(),
 	components: {
 		XColumn,
 		XNotes,
diff --git a/src/client/app/desktop/views/pages/deck/deck.notes.vue b/src/client/app/desktop/views/pages/deck/deck.notes.vue
index b7a47ddb27..611b28ffc0 100644
--- a/src/client/app/desktop/views/pages/deck/deck.notes.vue
+++ b/src/client/app/desktop/views/pages/deck/deck.notes.vue
@@ -30,7 +30,7 @@
 
 	<footer v-if="more">
 		<button @click="loadMore" :disabled="moreFetching" :style="{ cursor: moreFetching ? 'wait' : 'pointer' }">
-			<template v-if="!moreFetching">%i18n:@load-more%</template>
+			<template v-if="!moreFetching">{{ $t('@.load-more') }}</template>
 			<template v-if="moreFetching"><fa icon="spinner .pulse" fixed-width/></template>
 		</button>
 	</footer>
@@ -39,12 +39,14 @@
 
 <script lang="ts">
 import Vue from 'vue';
+import i18n from '../../../../i18n';
 
 import XNote from '../../components/note.vue';
 
 const displayLimit = 20;
 
 export default Vue.extend({
+	i18n: i18n(),
 	components: {
 		XNote
 	},
@@ -80,7 +82,7 @@ export default Vue.extend({
 				const date = new Date(note.createdAt).getDate();
 				const month = new Date(note.createdAt).getMonth() + 1;
 				note._date = date;
-				note._datetext = '%i18n:common.month-and-day%'.replace('{month}', month.toString()).replace('{day}', date.toString());
+				note._datetext = this.$t('@.month-and-day').replace('{month}', month.toString()).replace('{day}', date.toString());
 				return note;
 			});
 		}
diff --git a/src/client/app/desktop/views/pages/deck/deck.notifications-column.vue b/src/client/app/desktop/views/pages/deck/deck.notifications-column.vue
index 5ecc41d690..69da7919b6 100644
--- a/src/client/app/desktop/views/pages/deck/deck.notifications-column.vue
+++ b/src/client/app/desktop/views/pages/deck/deck.notifications-column.vue
@@ -8,10 +8,12 @@
 
 <script lang="ts">
 import Vue from 'vue';
+import i18n from '../../../../i18n';
 import XColumn from './deck.column.vue';
 import XNotifications from './deck.notifications.vue';
 
 export default Vue.extend({
+	i18n: i18n(),
 	components: {
 		XColumn,
 		XNotifications
@@ -31,7 +33,7 @@ export default Vue.extend({
 	computed: {
 		name(): string {
 			if (this.column.name) return this.column.name;
-			return '%i18n:common.deck.notifications%';
+			return this.$t('@deck.notifications');
 		}
 	},
 });
diff --git a/src/client/app/desktop/views/pages/deck/deck.notifications.vue b/src/client/app/desktop/views/pages/deck/deck.notifications.vue
index 168ecc14fe..3071795706 100644
--- a/src/client/app/desktop/views/pages/deck/deck.notifications.vue
+++ b/src/client/app/desktop/views/pages/deck/deck.notifications.vue
@@ -17,19 +17,21 @@
 		</template>
 	</component>
 	<button class="more" :class="{ fetching: fetchingMoreNotifications }" v-if="moreNotifications" @click="fetchMoreNotifications" :disabled="fetchingMoreNotifications">
-		<template v-if="fetchingMoreNotifications"><fa icon="spinner .pulse" fixed-width/></template>{{ fetchingMoreNotifications ? '%i18n:common.loading%' : '%i18n:@more%' }}
+		<template v-if="fetchingMoreNotifications"><fa icon="spinner .pulse" fixed-width/></template>{{ fetchingMoreNotifications ? this.$t('@.loading') : this.$t('@.load-more') }}
 	</button>
-	<p class="empty" v-if="notifications.length == 0 && !fetching">%i18n:@empty%</p>
+	<p class="empty" v-if="notifications.length == 0 && !fetching">{{ $t('empty') }}</p>
 </div>
 </template>
 
 <script lang="ts">
 import Vue from 'vue';
+import i18n from '../../../../i18n';
 import XNotification from './deck.notification.vue';
 
 const displayLimit = 20;
 
 export default Vue.extend({
+	i18n: i18n(),
 	components: {
 		XNotification
 	},
@@ -53,7 +55,7 @@ export default Vue.extend({
 				const date = new Date(notification.createdAt).getDate();
 				const month = new Date(notification.createdAt).getMonth() + 1;
 				notification._date = date;
-				notification._datetext = '%i18n:common.month-and-day%'.replace('{month}', month.toString()).replace('{day}', date.toString());
+				notification._datetext = this.$t('@.month-and-day').replace('{month}', month.toString()).replace('{day}', date.toString());
 				return notification;
 			});
 		}
diff --git a/src/client/app/desktop/views/pages/deck/deck.tl-column.vue b/src/client/app/desktop/views/pages/deck/deck.tl-column.vue
index 44ef4eda1c..c962dd0ec8 100644
--- a/src/client/app/desktop/views/pages/deck/deck.tl-column.vue
+++ b/src/client/app/desktop/views/pages/deck/deck.tl-column.vue
@@ -11,8 +11,8 @@
 	</span>
 
 	<div class="editor" style="padding:0 12px" v-if="edit">
-		<ui-switch v-model="column.isMediaOnly" @change="onChangeSettings">%i18n:@is-media-only%</ui-switch>
-		<ui-switch v-model="column.isMediaView" @change="onChangeSettings">%i18n:@is-media-view%</ui-switch>
+		<ui-switch v-model="column.isMediaOnly" @change="onChangeSettings">{{ $t('is-media-only') }}</ui-switch>
+		<ui-switch v-model="column.isMediaView" @change="onChangeSettings">{{ $t('is-media-view') }}</ui-switch>
 	</div>
 
 	<x-list-tl v-if="column.type == 'list'"
@@ -38,12 +38,14 @@
 
 <script lang="ts">
 import Vue from 'vue';
+import i18n from '../../../../i18n';
 import XColumn from './deck.column.vue';
 import XTl from './deck.tl.vue';
 import XListTl from './deck.list-tl.vue';
 import XHashtagTl from './deck.hashtag-tl.vue';
 
 export default Vue.extend({
+	i18n: i18n('deck/deck.tl-column.vue'),
 	components: {
 		XColumn,
 		XTl,
@@ -67,7 +69,7 @@ export default Vue.extend({
 			edit: false,
 			menu: [{
 				icon: 'cog',
-				text: '%i18n:@edit%',
+				text: this.$t('edit'),
 				action: () => {
 					this.edit = !this.edit;
 				}
@@ -80,10 +82,10 @@ export default Vue.extend({
 			if (this.column.name) return this.column.name;
 
 			switch (this.column.type) {
-				case 'home': return '%i18n:common.deck.home%';
-				case 'local': return '%i18n:common.deck.local%';
-				case 'hybrid': return '%i18n:common.deck.hybrid%';
-				case 'global': return '%i18n:common.deck.global%';
+				case 'home': return this.$t('@deck.home');
+				case 'local': return this.$t('@deck.local');
+				case 'hybrid': return this.$t('@deck.hybrid');
+				case 'global': return this.$t('@deck.global');
 				case 'list': return this.column.list.title;
 				case 'hashtag': return this.$store.state.settings.tagTimelines.find(x => x.id == this.column.tagTlId).title;
 			}
diff --git a/src/client/app/desktop/views/pages/deck/deck.user-column.vue b/src/client/app/desktop/views/pages/deck/deck.user-column.vue
index 8bd5421718..c4da00f3d3 100644
--- a/src/client/app/desktop/views/pages/deck/deck.user-column.vue
+++ b/src/client/app/desktop/views/pages/deck/deck.user-column.vue
@@ -7,8 +7,8 @@
 	<div class="zubukjlciycdsyynicqrnlsmdwmymzqu" v-if="user">
 		<div class="is-remote" v-if="user.host != null">
 			<details>
-				<summary><fa icon="exclamation-triangle"/> %i18n:common.is-remote-user%</summary>
-				<a :href="user.url || user.uri" target="_blank">%i18n:common.view-on-remote%</a>
+				<summary><fa icon="exclamation-triangle"/> {{ $t('@.is-remote-user') }}</summary>
+				<a :href="user.url || user.uri" target="_blank">{{ $t('@.view-on-remote') }}</a>
 			</details>
 		</div>
 		<header :style="bannerStyle">
@@ -27,20 +27,20 @@
 			<div class="counts">
 				<div>
 					<b>{{ user.notesCount | number }}</b>
-					<span>%i18n:@posts%</span>
+					<span>{{ $t('posts') }}</span>
 				</div>
 				<div>
 					<b>{{ user.followingCount | number }}</b>
-					<span>%i18n:@following%</span>
+					<span>{{ $t('following') }}</span>
 				</div>
 				<div>
 					<b>{{ user.followersCount | number }}</b>
-					<span>%i18n:@followers%</span>
+					<span>{{ $t('followers') }}</span>
 				</div>
 			</div>
 		</div>
 		<div class="pinned" v-if="user.pinnedNotes && user.pinnedNotes.length > 0">
-			<p class="caption" @click="toggleShowPinned"><fa icon="thumbtack"/> %i18n:@pinned-notes%</p>
+			<p class="caption" @click="toggleShowPinned"><fa icon="thumbtack"/> {{ $t('pinned-notes') }}</p>
 			<span class="angle" v-if="showPinned"><fa icon="angle-up"/></span>
 			<span class="angle" v-else><fa icon="angle-down"/></span>
 			<div class="notes" v-show="showPinned">
@@ -48,7 +48,7 @@
 			</div>
 		</div>
 		<div class="images" v-if="images.length > 0">
-			<p class="caption" @click="toggleShowImages"><fa :icon="['far', 'images']"/> %i18n:@images%</p>
+			<p class="caption" @click="toggleShowImages"><fa :icon="['far', 'images']"/> {{ $t('images') }}</p>
 			<span class="angle" v-if="showImages"><fa icon="angle-up"/></span>
 			<span class="angle" v-else><fa icon="angle-down"/></span>
 			<div v-show="showImages">
@@ -61,7 +61,7 @@
 			</div>
 		</div>
 		<div class="activity">
-			<p class="caption" @click="toggleShowActivity"><fa :icon="['far', 'chart-bar']"/> %i18n:@activity%</p>
+			<p class="caption" @click="toggleShowActivity"><fa :icon="['far', 'chart-bar']"/> {{ $t('activity') }}</p>
 			<span class="angle" v-if="showActivity"><fa icon="angle-up"/></span>
 			<span class="angle" v-else><fa icon="angle-down"/></span>
 			<div v-show="showActivity">
@@ -69,7 +69,7 @@
 			</div>
 		</div>
 		<div class="tl">
-			<p class="caption"><fa :icon="['far', 'comment-alt']"/> %i18n:@timeline%</p>
+			<p class="caption"><fa :icon="['far', 'comment-alt']"/> {{ $t('timeline') }}</p>
 			<div>
 				<x-notes ref="timeline" :more="existMore ? fetchMoreNotes : null"/>
 			</div>
@@ -80,6 +80,7 @@
 
 <script lang="ts">
 import Vue from 'vue';
+import i18n from '../../../../i18n';
 import parseAcct from '../../../../../../misc/acct/parse';
 import XColumn from './deck.column.vue';
 import XNotes from './deck.notes.vue';
@@ -93,6 +94,7 @@ import * as ApexCharts from 'apexcharts';
 const fetchLimit = 10;
 
 export default Vue.extend({
+	i18n: i18n('deck/deck.user-column.vue'),
 	components: {
 		XColumn,
 		XNotes,
@@ -295,7 +297,7 @@ export default Vue.extend({
 		menu() {
 			let menu = [{
 				icon: 'list',
-				text: '%i18n:@push-to-a-list%',
+				text: this.$t('push-to-a-list'),
 				action: () => {
 					const w = (this as any).os.new(MkUserListsWindow);
 					w.$once('choosen', async list => {
diff --git a/src/client/app/desktop/views/pages/deck/deck.vue b/src/client/app/desktop/views/pages/deck/deck.vue
index e1b9e76813..7badd2a9fe 100644
--- a/src/client/app/desktop/views/pages/deck/deck.vue
+++ b/src/client/app/desktop/views/pages/deck/deck.vue
@@ -14,13 +14,14 @@
 			<x-note-column v-else-if="temporaryColumn.type == 'note'" :note-id="temporaryColumn.noteId" :key="temporaryColumn.noteId"/>
 			<x-hashtag-column v-else-if="temporaryColumn.type == 'tag'" :tag="temporaryColumn.tag" :key="temporaryColumn.tag"/>
 		</template>
-		<button ref="add" @click="add" title="%i18n:common.deck.add-column%"><fa icon="plus"/></button>
+		<button ref="add" @click="add" :title="$t('@deck.add-column')"><fa icon="plus"/></button>
 	</div>
 </mk-ui>
 </template>
 
 <script lang="ts">
 import Vue from 'vue';
+import i18n from '../../../../i18n';
 import XColumnCore from './deck.column-core.vue';
 import Menu from '../../../../common/views/components/menu.vue';
 import MkUserListsWindow from '../../components/user-lists-window.vue';
@@ -31,6 +32,7 @@ import XHashtagColumn from './deck.hashtag-column.vue';
 import * as uuid from 'uuid';
 
 export default Vue.extend({
+	i18n: i18n('deck'),
 	components: {
 		XColumnCore,
 		XUserColumn,
@@ -183,7 +185,7 @@ export default Vue.extend({
 				compact: true,
 				items: [{
 					icon: 'home',
-					text: '%i18n:common.deck.home%',
+					text: this.$t('@deck.home'),
 					action: () => {
 						this.$store.dispatch('settings/addDeckColumn', {
 							id: uuid(),
@@ -192,7 +194,7 @@ export default Vue.extend({
 					}
 				}, {
 					icon: ['far', 'comments'],
-					text: '%i18n:common.deck.local%',
+					text: this.$t('@deck.local'),
 					action: () => {
 						this.$store.dispatch('settings/addDeckColumn', {
 							id: uuid(),
@@ -201,7 +203,7 @@ export default Vue.extend({
 					}
 				}, {
 					icon: 'share-alt',
-					text: '%i18n:common.deck.hybrid%',
+					text: this.$t('@deck.hybrid'),
 					action: () => {
 						this.$store.dispatch('settings/addDeckColumn', {
 							id: uuid(),
@@ -210,7 +212,7 @@ export default Vue.extend({
 					}
 				}, {
 					icon: 'globe',
-					text: '%i18n:common.deck.global%',
+					text: this.$t('@deck.global'),
 					action: () => {
 						this.$store.dispatch('settings/addDeckColumn', {
 							id: uuid(),
@@ -219,7 +221,7 @@ export default Vue.extend({
 					}
 				}, {
 					icon: 'at',
-					text: '%i18n:common.deck.mentions%',
+					text: this.$t('@deck.mentions'),
 					action: () => {
 						this.$store.dispatch('settings/addDeckColumn', {
 							id: uuid(),
@@ -228,7 +230,7 @@ export default Vue.extend({
 					}
 				}, {
 					icon: ['far', 'envelope'],
-					text: '%i18n:common.deck.direct%',
+					text: this.$t('@deck.direct'),
 					action: () => {
 						this.$store.dispatch('settings/addDeckColumn', {
 							id: uuid(),
@@ -237,7 +239,7 @@ export default Vue.extend({
 					}
 				}, {
 					icon: 'list',
-					text: '%i18n:common.deck.list%',
+					text: this.$t('@deck.list'),
 					action: () => {
 						const w = (this as any).os.new(MkUserListsWindow);
 						w.$once('choosen', list => {
@@ -251,10 +253,10 @@ export default Vue.extend({
 					}
 				}, {
 					icon: 'hashtag',
-					text: '%i18n:common.deck.hashtag%',
+					text: this.$t('@deck.hashtag'),
 					action: () => {
 						(this as any).apis.input({
-							title: '%i18n:@enter-hashtag-tl-title%'
+							title: this.$t('enter-hashtag-tl-title')
 						}).then(title => {
 							this.$store.dispatch('settings/addDeckColumn', {
 								id: uuid(),
@@ -265,7 +267,7 @@ export default Vue.extend({
 					}
 				}, {
 					icon: ['far', 'bell'],
-					text: '%i18n:common.deck.notifications%',
+					text: this.$t('@deck.notifications'),
 					action: () => {
 						this.$store.dispatch('settings/addDeckColumn', {
 							id: uuid(),
@@ -274,7 +276,7 @@ export default Vue.extend({
 					}
 				}, {
 					icon: 'calculator',
-					text: '%i18n:common.deck.widgets%',
+					text: this.$t('@deck.widgets'),
 					action: () => {
 						this.$store.dispatch('settings/addDeckColumn', {
 							id: uuid(),
diff --git a/src/client/app/desktop/views/pages/deck/deck.widgets-column.vue b/src/client/app/desktop/views/pages/deck/deck.widgets-column.vue
index 34b2af7d1b..2cdef50991 100644
--- a/src/client/app/desktop/views/pages/deck/deck.widgets-column.vue
+++ b/src/client/app/desktop/views/pages/deck/deck.widgets-column.vue
@@ -6,29 +6,29 @@
 		<template v-if="edit">
 			<header>
 				<select v-model="widgetAdderSelected" @change="addWidget">
-					<option value="profile">%i18n:common.widgets.profile%</option>
-					<option value="analog-clock">%i18n:common.widgets.analog-clock%</option>
-					<option value="calendar">%i18n:common.widgets.calendar%</option>
-					<option value="timemachine">%i18n:common.widgets.timemachine%</option>
-					<option value="activity">%i18n:common.widgets.activity%</option>
-					<option value="rss">%i18n:common.widgets.rss%</option>
-					<option value="trends">%i18n:common.widgets.trends%</option>
-					<option value="photo-stream">%i18n:common.widgets.photo-stream%</option>
-					<option value="slideshow">%i18n:common.widgets.slideshow%</option>
-					<option value="version">%i18n:common.widgets.version%</option>
-					<option value="broadcast">%i18n:common.widgets.broadcast%</option>
-					<option value="notifications">%i18n:common.widgets.notifications%</option>
-					<option value="users">%i18n:common.widgets.users%</option>
-					<option value="polls">%i18n:common.widgets.polls%</option>
-					<option value="post-form">%i18n:common.widgets.post-form%</option>
-					<option value="messaging">%i18n:common.widgets.messaging%</option>
-					<option value="memo">%i18n:common.widgets.memo%</option>
-					<option value="hashtags">%i18n:common.widgets.hashtags%</option>
-					<option value="posts-monitor">%i18n:common.widgets.posts-monitor%</option>
-					<option value="server">%i18n:common.widgets.server%</option>
-					<option value="donation">%i18n:common.widgets.donation%</option>
-					<option value="nav">%i18n:common.widgets.nav%</option>
-					<option value="tips">%i18n:common.widgets.tips%</option>
+					<option value="profile">{{ $t('@.widgets.profile') }}</option>
+					<option value="analog-clock">{{ $t('@.widgets.analog-clock') }}</option>
+					<option value="calendar">{{ $t('@.widgets.calendar') }}</option>
+					<option value="timemachine">{{ $t('@.widgets.timemachine') }}</option>
+					<option value="activity">{{ $t('@.widgets.activity') }}</option>
+					<option value="rss">{{ $t('@.widgets.rss') }}</option>
+					<option value="trends">{{ $t('@.widgets.trends') }}</option>
+					<option value="photo-stream">{{ $t('@.widgets.photo-stream') }}</option>
+					<option value="slideshow">{{ $t('@.widgets.slideshow') }}</option>
+					<option value="version">{{ $t('@.widgets.version') }}</option>
+					<option value="broadcast">{{ $t('@.widgets.broadcast') }}</option>
+					<option value="notifications">{{ $t('@.widgets.notifications') }}</option>
+					<option value="users">{{ $t('@.widgets.users') }}</option>
+					<option value="polls">{{ $t('@.widgets.polls') }}</option>
+					<option value="post-form">{{ $t('@.widgets.post-form') }}</option>
+					<option value="messaging">{{ $t('@.widgets.messaging') }}</option>
+					<option value="memo">{{ $t('@.widgets.memo') }}</option>
+					<option value="hashtags">{{ $t('@.widgets.hashtags') }}</option>
+					<option value="posts-monitor">{{ $t('@.widgets.posts-monitor') }}</option>
+					<option value="server">{{ $t('@.widgets.server') }}</option>
+					<option value="donation">{{ $t('@.widgets.donation') }}</option>
+					<option value="nav">{{ $t('@.widgets.nav') }}</option>
+					<option value="tips">{{ $t('@.widgets.tips') }}</option>
 				</select>
 			</header>
 			<x-draggable
@@ -51,11 +51,13 @@
 
 <script lang="ts">
 import Vue from 'vue';
+import i18n from '../../../../i18n';
 import XColumn from './deck.column.vue';
 import * as XDraggable from 'vuedraggable';
 import * as uuid from 'uuid';
 
 export default Vue.extend({
+	i18n: i18n(),
 	components: {
 		XColumn,
 		XDraggable
@@ -83,14 +85,14 @@ export default Vue.extend({
 	computed: {
 		name(): string {
 			if (this.column.name) return this.column.name;
-			return '%i18n:common.deck.widgets%';
+			return this.$t('@deck.widgets');
 		}
 	},
 
 	created() {
 		this.menu = [{
 			icon: 'cog',
-			text: '%i18n:@edit%',
+			text: this.$t('edit'),
 			action: () => {
 				this.edit = !this.edit;
 			}
diff --git a/src/client/app/desktop/views/pages/drive.vue b/src/client/app/desktop/views/pages/drive.vue
index dec6c4551a..46a3333c79 100644
--- a/src/client/app/desktop/views/pages/drive.vue
+++ b/src/client/app/desktop/views/pages/drive.vue
@@ -6,7 +6,10 @@
 
 <script lang="ts">
 import Vue from 'vue';
+import i18n from '../../../i18n';
+
 export default Vue.extend({
+	i18n: i18n('desktop/views/pages/drive.vue'),
 	data() {
 		return {
 			folder: null
@@ -16,11 +19,11 @@ export default Vue.extend({
 		this.folder = this.$route.params.folder;
 	},
 	mounted() {
-		document.title = '%i18n:@title%';
+		document.title = this.$t('title');
 	},
 	methods: {
 		onMoveRoot() {
-			const title = '%i18n:@title%';
+			const title = this.$t('title');
 
 			// Rewrite URL
 			history.pushState(null, title, '/i/drive');
diff --git a/src/client/app/desktop/views/pages/favorites.vue b/src/client/app/desktop/views/pages/favorites.vue
index d3bb70f037..1463dc2d6b 100644
--- a/src/client/app/desktop/views/pages/favorites.vue
+++ b/src/client/app/desktop/views/pages/favorites.vue
@@ -4,16 +4,18 @@
 		<template v-for="favorite in favorites">
 			<mk-note-detail class="post" :note="favorite.note" :key="favorite.note.id"/>
 		</template>
-		<a v-if="existMore" @click="more">%i18n:@more%</a>
+		<a v-if="existMore" @click="more">{{ $t('@.load-more') }}</a>
 	</main>
 </mk-ui>
 </template>
 
 <script lang="ts">
 import Vue from 'vue';
+import i18n from '../../../i18n';
 import Progress from '../../../common/scripts/loading';
 
 export default Vue.extend({
+	i18n: i18n('.vue'),
 	data() {
 		return {
 			fetching: true,
diff --git a/src/client/app/desktop/views/pages/home-customize.vue b/src/client/app/desktop/views/pages/home-customize.vue
index 174fbf45ed..e0460abcd0 100644
--- a/src/client/app/desktop/views/pages/home-customize.vue
+++ b/src/client/app/desktop/views/pages/home-customize.vue
@@ -1,13 +1,3 @@
 <template>
 <mk-home customize/>
 </template>
-
-<script lang="ts">
-import Vue from 'vue';
-
-export default Vue.extend({
-	mounted() {
-		document.title = `${(this as any).os.instanceName} - %i18n:@title%`;
-	}
-});
-</script>
diff --git a/src/client/app/desktop/views/pages/messaging-room.vue b/src/client/app/desktop/views/pages/messaging-room.vue
index 4be33dda04..ad8bc290f6 100644
--- a/src/client/app/desktop/views/pages/messaging-room.vue
+++ b/src/client/app/desktop/views/pages/messaging-room.vue
@@ -6,11 +6,13 @@
 
 <script lang="ts">
 import Vue from 'vue';
+import i18n from '../../../i18n';
 import Progress from '../../../common/scripts/loading';
 import parseAcct from '../../../../../misc/acct/parse';
 import getUserName from '../../../../../misc/get-user-name';
 
 export default Vue.extend({
+	i18n: i18n('.vue'),
 	data() {
 		return {
 			fetching: true,
diff --git a/src/client/app/desktop/views/pages/note.vue b/src/client/app/desktop/views/pages/note.vue
index 1e53b97cf6..8aed7839cd 100644
--- a/src/client/app/desktop/views/pages/note.vue
+++ b/src/client/app/desktop/views/pages/note.vue
@@ -3,8 +3,8 @@
 	<main v-if="!fetching">
 		<mk-note-detail :note="note"/>
 		<footer>
-			<router-link v-if="note.next" :to="note.next"><fa icon="angle-left"/> %i18n:@next%</router-link>
-			<router-link v-if="note.prev" :to="note.prev">%i18n:@prev% <fa icon="angle-right"/></router-link>
+			<router-link v-if="note.next" :to="note.next"><fa icon="angle-left"/> {{ $t('next') }}</router-link>
+			<router-link v-if="note.prev" :to="note.prev">{{ $t('prev') }} <fa icon="angle-right"/></router-link>
 		</footer>
 	</main>
 </mk-ui>
@@ -12,9 +12,11 @@
 
 <script lang="ts">
 import Vue from 'vue';
+import i18n from '../../../i18n';
 import Progress from '../../../common/scripts/loading';
 
 export default Vue.extend({
+	i18n: i18n('desktop/views/pages/note.vue'),
 	data() {
 		return {
 			fetching: true,
diff --git a/src/client/app/desktop/views/pages/search.vue b/src/client/app/desktop/views/pages/search.vue
index 25046531e7..5af103c04e 100644
--- a/src/client/app/desktop/views/pages/search.vue
+++ b/src/client/app/desktop/views/pages/search.vue
@@ -3,19 +3,21 @@
 	<header :class="$style.header">
 		<h1>{{ q }}</h1>
 	</header>
-	<p :class="$style.notAvailable" v-if="!fetching && notAvailable">%i18n:@not-available%</p>
-	<p :class="$style.empty" v-if="!fetching && empty"><fa icon="search"/> {{ '%i18n:not-found%'.split('{}')[0] }}{{ q }}{{ '%i18n:not-found%'.split('{}')[1] }}</p>
+	<p :class="$style.notAvailable" v-if="!fetching && notAvailable">{{ $t('not-available') }}</p>
+	<p :class="$style.empty" v-if="!fetching && empty"><fa icon="search"/> {{ $t('not-found', { q }) }}</p>
 	<mk-notes ref="timeline" :class="$style.notes" :more="existMore ? more : null"/>
 </mk-ui>
 </template>
 
 <script lang="ts">
 import Vue from 'vue';
+import i18n from '../../../i18n';
 import Progress from '../../../common/scripts/loading';
 
 const limit = 20;
 
 export default Vue.extend({
+	i18n: i18n('desktop/views/pages/search.vue'),
 	data() {
 		return {
 			fetching: true,
diff --git a/src/client/app/desktop/views/pages/selectdrive.vue b/src/client/app/desktop/views/pages/selectdrive.vue
index 64be867a06..422acb9890 100644
--- a/src/client/app/desktop/views/pages/selectdrive.vue
+++ b/src/client/app/desktop/views/pages/selectdrive.vue
@@ -6,17 +6,19 @@
 		@change-selection="onChangeSelection"
 	/>
 	<footer>
-		<button class="upload" title="%i18n:@upload%" @click="upload"><fa icon="upload"/></button>
-		<button class="cancel" @click="close">%i18n:@cancel%</button>
-		<button class="ok" @click="ok">%i18n:@ok%</button>
+		<button class="upload" :title="$t('upload')" @click="upload"><fa icon="upload"/></button>
+		<button class="cancel" @click="close">{{ $t('cancel') }}</button>
+		<button class="ok" @click="ok">{{ $t('ok') }}</button>
 	</footer>
 </div>
 </template>
 
 <script lang="ts">
 import Vue from 'vue';
+import i18n from '../../../i18n';
 
 export default Vue.extend({
+	i18n: i18n('desktop/views/pages/selectdrive.vue'),
 	data() {
 		return {
 			files: []
@@ -29,7 +31,7 @@ export default Vue.extend({
 		}
 	},
 	mounted() {
-		document.title = '%i18n:@title%';
+		document.title = this.$t('title');
 	},
 	methods: {
 		onSelected(file) {
diff --git a/src/client/app/desktop/views/pages/share.vue b/src/client/app/desktop/views/pages/share.vue
index d5e38fed46..b5429067f9 100644
--- a/src/client/app/desktop/views/pages/share.vue
+++ b/src/client/app/desktop/views/pages/share.vue
@@ -1,19 +1,21 @@
 <template>
 <div class="pptjhabgjtt7kwskbfv4y3uml6fpuhmr">
-	<h1>{{ '%i18n:@share-with%'.replace('{}', name) }}</h1>
+	<h1>{{ this.$t('share-with', { name }) }}</h1>
 	<div>
 		<mk-signin v-if="!$store.getters.isSignedIn"/>
 		<mk-post-form v-else-if="!posted" :initial-text="text" :instant="true" @posted="posted = true"/>
 		<p v-if="posted" class="posted"><fa icon="check"/></p>
 	</div>
-	<button v-if="posted" class="ui button" @click="close">%i18n:common.close%</button>
+	<button v-if="posted" class="ui button" @click="close">{{ $t('@.close') }}</button>
 </div>
 </template>
 
 <script lang="ts">
 import Vue from 'vue';
+import i18n from '../../../i18n';
 
 export default Vue.extend({
+	i18n: i18n('desktop/views/pages/share.vue'),
 	data() {
 		return {
 			name: null,
diff --git a/src/client/app/desktop/views/pages/tag.vue b/src/client/app/desktop/views/pages/tag.vue
index f72c629eff..c46186d782 100644
--- a/src/client/app/desktop/views/pages/tag.vue
+++ b/src/client/app/desktop/views/pages/tag.vue
@@ -3,18 +3,20 @@
 	<header :class="$style.header">
 		<h1>#{{ $route.params.tag }}</h1>
 	</header>
-	<p :class="$style.empty" v-if="!fetching && empty"><fa icon="search"/> {{ '%i18n:no-posts-found%'.split('{}')[0] }}{{ q }}{{ '%i18n:no-posts-found%'.split('{}')[1] }}</p>
+	<p :class="$style.empty" v-if="!fetching && empty"><fa icon="search"/> {{ $t('no-posts-found', { q }) }}</p>
 	<mk-notes ref="timeline" :class="$style.notes" :more="existMore ? more : null"/>
 </mk-ui>
 </template>
 
 <script lang="ts">
 import Vue from 'vue';
+import i18n from '../../../i18n';
 import Progress from '../../../common/scripts/loading';
 
 const limit = 20;
 
 export default Vue.extend({
+	i18n: i18n('desktop/views/pages/tag.vue'),
 	data() {
 		return {
 			fetching: true,
diff --git a/src/client/app/desktop/views/pages/user-following-or-followers.vue b/src/client/app/desktop/views/pages/user-following-or-followers.vue
index db0de20b64..c48e35837a 100644
--- a/src/client/app/desktop/views/pages/user-following-or-followers.vue
+++ b/src/client/app/desktop/views/pages/user-following-or-followers.vue
@@ -11,7 +11,7 @@
 			<mk-user-card v-for="user in users" :user="user" :key="user.id"/>
 		</div>
 		<div class="more" v-if="next">
-			<ui-button inline @click="fetchMore">%i18n:@load-more%</ui-button>
+			<ui-button inline @click="fetchMore">{{ $t('@.load-more') }}</ui-button>
 		</div>
 	</div>
 </mk-ui>
@@ -19,22 +19,14 @@
 
 <script lang="ts">
 import Vue from 'vue';
-import VueI18n from 'vue-i18n';
+import i18n from '../../../i18n';
 import parseAcct from '../../../../../misc/acct/parse';
 import Progress from '../../../common/scripts/loading';
-import { lang, locale } from '../../../config';
 
 const limit = 16;
 
-const i18n = new VueI18n({
-	locale: lang,
-	messages: {
-		[lang]: locale['desktop/views/pages/user-following-or-followers.vue']
-	}
-});
-
 export default Vue.extend({
-	i18n,
+	i18n: i18n('desktop/views/pages/user-following-or-followers.vue'),
 
 	data() {
 		return {
diff --git a/src/client/app/desktop/views/pages/user/user.followers-you-know.vue b/src/client/app/desktop/views/pages/user/user.followers-you-know.vue
index 14765450aa..8e0be39ac3 100644
--- a/src/client/app/desktop/views/pages/user/user.followers-you-know.vue
+++ b/src/client/app/desktop/views/pages/user/user.followers-you-know.vue
@@ -1,20 +1,22 @@
 <template>
 <div class="vahgrswmbzfdlmomxnqftuueyvwaafth">
-	<p class="title"><fa icon="users"/>%i18n:@title%</p>
-	<p class="initializing" v-if="fetching"><fa icon="spinner .pulse" fixed-width/>%i18n:@loading%<mk-ellipsis/></p>
+	<p class="title"><fa icon="users"/>{{ $t('title') }}</p>
+	<p class="initializing" v-if="fetching"><fa icon="spinner .pulse" fixed-width/>{{ $t('loading') }}<mk-ellipsis/></p>
 	<div v-if="!fetching && users.length > 0">
 	<router-link v-for="user in users" :to="user | userPage" :key="user.id">
 		<img :src="user.avatarUrl" :alt="user | userName" v-user-preview="user.id"/>
 	</router-link>
 	</div>
-	<p class="empty" v-if="!fetching && users.length == 0">%i18n:@no-users%</p>
+	<p class="empty" v-if="!fetching && users.length == 0">{{ $t('no-users') }}</p>
 </div>
 </template>
 
 <script lang="ts">
 import Vue from 'vue';
+import i18n from '../../../../i18n';
 
 export default Vue.extend({
+	i18n: i18n('desktop/views/pages/user/user.followers-you-know.vue'),
 	props: ['user'],
 	data() {
 		return {
diff --git a/src/client/app/desktop/views/pages/user/user.friends.vue b/src/client/app/desktop/views/pages/user/user.friends.vue
index 60bfc3e4fd..103fd97aa4 100644
--- a/src/client/app/desktop/views/pages/user/user.friends.vue
+++ b/src/client/app/desktop/views/pages/user/user.friends.vue
@@ -1,7 +1,7 @@
 <template>
 <div class="hozptpaliadatkehcmcayizwzwwctpbc">
-	<p class="title"><fa icon="users"/>%i18n:@title%</p>
-	<p class="initializing" v-if="fetching"><fa icon="spinner .pulse" fixed-width/>%i18n:@loading%<mk-ellipsis/></p>
+	<p class="title"><fa icon="users"/>{{ $t('title') }}</p>
+	<p class="initializing" v-if="fetching"><fa icon="spinner .pulse" fixed-width/>{{ $t('loading') }}<mk-ellipsis/></p>
 	<template v-if="!fetching && users.length != 0">
 		<div class="user" v-for="friend in users">
 			<mk-avatar class="avatar" :user="friend"/>
@@ -12,14 +12,16 @@
 			<mk-follow-button :user="friend"/>
 		</div>
 	</template>
-	<p class="empty" v-if="!fetching && users.length == 0">%i18n:@no-users%</p>
+	<p class="empty" v-if="!fetching && users.length == 0">{{ $t('no-users') }}</p>
 </div>
 </template>
 
 <script lang="ts">
 import Vue from 'vue';
+import i18n from '../../../../i18n';
 
 export default Vue.extend({
+	i18n: i18n('desktop/views/pages/user/user.friends.vue'),
 	props: ['user'],
 	data() {
 		return {
diff --git a/src/client/app/desktop/views/pages/user/user.header.vue b/src/client/app/desktop/views/pages/user/user.header.vue
index 6d7827d1ae..c433c0f507 100644
--- a/src/client/app/desktop/views/pages/user/user.header.vue
+++ b/src/client/app/desktop/views/pages/user/user.header.vue
@@ -7,7 +7,7 @@
 			<p class="name">{{ user | userName }}</p>
 			<div>
 				<span class="username"><mk-acct :user="user" :detail="true" /></span>
-				<span v-if="user.isBot" title="%i18n:@is-bot%"><fa icon="robot"/></span>
+				<span v-if="user.isBot" :title="$t('title')"><fa icon="robot"/></span>
 			</div>
 		</div>
 	</div>
@@ -18,12 +18,12 @@
 		</div>
 		<div class="info">
 			<span class="location" v-if="user.host === null && user.profile.location"><fa icon="map-marker"/> {{ user.profile.location }}</span>
-			<span class="birthday" v-if="user.host === null && user.profile.birthday"><fa icon="birthday-cake"/> {{ user.profile.birthday.replace('-', '%i18n:@year%').replace('-', '%i18n:@month%') + '%i18n:@day%' }} ({{ age }}%i18n:@years-old%)</span>
+			<span class="birthday" v-if="user.host === null && user.profile.birthday"><fa icon="birthday-cake"/> {{ user.profile.birthday.replace('-', this.$t('year')).replace('-', this.$t('month')) + this.$t('day') }} ({{ age }}%i18n:@years-old%)</span>
 		</div>
 		<div class="status">
-			<span class="notes-count"><b>{{ user.notesCount | number }}</b>%i18n:@posts%</span>
-			<router-link :to="user | userPage('following')" class="following clickable" @click="showFollowing"><b>{{ user.followingCount | number }}</b>%i18n:@following%</router-link>
-			<router-link :to="user | userPage('followers')" class="followers clickable" @click="showFollowers"><b>{{ user.followersCount | number }}</b>%i18n:@followers%</router-link>
+			<span class="notes-count"><b>{{ user.notesCount | number }}</b>{{ $t('posts') }}</span>
+			<router-link :to="user | userPage('following')" class="following clickable" @click="showFollowing"><b>{{ user.followingCount | number }}</b>{{ $t('following') }}</router-link>
+			<router-link :to="user | userPage('followers')" class="followers clickable" @click="showFollowers"><b>{{ user.followersCount | number }}</b>{{ $t('followers') }}</router-link>
 		</div>
 	</div>
 </div>
@@ -31,9 +31,11 @@
 
 <script lang="ts">
 import Vue from 'vue';
+import i18n from '../../../../i18n';
 import * as age from 's-age';
 
 export default Vue.extend({
+	i18n: i18n('desktop/views/pages/user/user.header.vue'),
 	props: ['user'],
 	computed: {
 		style(): any {
diff --git a/src/client/app/desktop/views/pages/user/user.photos.vue b/src/client/app/desktop/views/pages/user/user.photos.vue
index 6608bd5051..9b56156f45 100644
--- a/src/client/app/desktop/views/pages/user/user.photos.vue
+++ b/src/client/app/desktop/views/pages/user/user.photos.vue
@@ -1,19 +1,21 @@
 <template>
 <div class="dzsuvbsrrrwobdxifudxuefculdfiaxd">
-	<p class="title"><fa icon="camera"/>%i18n:@title%</p>
-	<p class="initializing" v-if="fetching"><fa icon="spinner .pulse" fixed-width/>%i18n:@loading%<mk-ellipsis/></p>
+	<p class="title"><fa icon="camera"/>{{ $t('title') }}</p>
+	<p class="initializing" v-if="fetching"><fa icon="spinner .pulse" fixed-width/>{{ $t('loading') }}<mk-ellipsis/></p>
 	<div class="stream" v-if="!fetching && images.length > 0">
 		<div v-for="image in images" class="img"
 			:style="`background-image: url(${image.thumbnailUrl})`"
 		></div>
 	</div>
-	<p class="empty" v-if="!fetching && images.length == 0">%i18n:@no-photos%</p>
+	<p class="empty" v-if="!fetching && images.length == 0">{{ $t('no-photos') }}</p>
 </div>
 </template>
 
 <script lang="ts">
 import Vue from 'vue';
+import i18n from '../../../../i18n';
 export default Vue.extend({
+	i18n: i18n('desktop/views/pages/user/user.photos.vue'),
 	props: ['user'],
 	data() {
 		return {
diff --git a/src/client/app/desktop/views/pages/user/user.profile.vue b/src/client/app/desktop/views/pages/user/user.profile.vue
index bde57e5bcf..d0585bfab4 100644
--- a/src/client/app/desktop/views/pages/user/user.profile.vue
+++ b/src/client/app/desktop/views/pages/user/user.profile.vue
@@ -2,31 +2,33 @@
 <div class="profile" v-if="$store.getters.isSignedIn">
 	<div class="friend-form" v-if="$store.state.i.id != user.id">
 		<mk-follow-button :user="user" size="big"/>
-		<p class="followed" v-if="user.isFollowed">%i18n:@follows-you%</p>
+		<p class="followed" v-if="user.isFollowed">{{ $t('follows-you') }}</p>
 		<p class="stalk" v-if="user.isFollowing">
-			<span v-if="user.isStalking">%i18n:@stalking% <a @click="unstalk"><fa icon="meh"/> %i18n:@unstalk%</a></span>
-			<span v-if="!user.isStalking"><a @click="stalk"><fa icon="user-secret"/> %i18n:@stalk%</a></span>
+			<span v-if="user.isStalking">{{ $t('stalking% <a @click="unstalk"><fa icon="meh"/> %i18n:@unstalk') }}</a></span>
+			<span v-if="!user.isStalking"><a @click="stalk"><fa icon="user-secret"/> {{ $t('stalk') }}</a></span>
 		</p>
 	</div>
 	<div class="action-form">
 		<ui-button @click="user.isMuted ? unmute() : mute()" v-if="$store.state.i.id != user.id">
-			<span v-if="user.isMuted"><fa icon="eye"/> %i18n:@unmute%</span>
-			<span v-else><fa icon="eye-slash"/> %i18n:@mute%</span>
+			<span v-if="user.isMuted"><fa icon="eye"/> {{ $t('unmute') }}</span>
+			<span v-else><fa icon="eye-slash"/> {{ $t('mute') }}</span>
 		</ui-button>
 		<ui-button @click="user.isBlocking ? unblock() : block()" v-if="$store.state.i.id != user.id">
-			<span v-if="user.isBlocking"><fa icon="user"/> %i18n:@unblock%</span>
-			<span v-else><fa icon="user-slash"/> %i18n:@block%</span>
+			<span v-if="user.isBlocking"><fa icon="user"/> {{ $t('unblock') }}</span>
+			<span v-else><fa icon="user-slash"/> {{ $t('block') }}</span>
 		</ui-button>
-		<ui-button @click="list"><fa icon="list"/> %i18n:@push-to-a-list%</ui-button>
+		<ui-button @click="list"><fa icon="list"/> {{ $t('push-to-a-list') }}</ui-button>
 	</div>
 </div>
 </template>
 
 <script lang="ts">
 import Vue from 'vue';
+import i18n from '../../../../i18n';
 import MkUserListsWindow from '../../components/user-lists-window.vue';
 
 export default Vue.extend({
+	i18n: i18n('desktop/views/pages/user/user.profile.vue'),
 	props: ['user'],
 
 	methods: {
@@ -71,7 +73,7 @@ export default Vue.extend({
 		},
 
 		block() {
-			if (!window.confirm('%i18n:@block-confirm%')) return;
+			if (!window.confirm(this.$t('block-confirm'))) return;
 			(this as any).api('blocking/create', {
 				userId: this.user.id
 			}).then(() => {
@@ -101,7 +103,7 @@ export default Vue.extend({
 				});
 				(this as any).apis.dialog({
 					title: 'Done!',
-					text: '%i18n:@list-pushed%'.replace('{user}', this.user.name).replace('{list}', list.title)
+					text: this.$t('list-pushed').replace('{user}', this.user.name).replace('{list}', list.title)
 				});
 			});
 		}
diff --git a/src/client/app/desktop/views/pages/user/user.timeline.vue b/src/client/app/desktop/views/pages/user/user.timeline.vue
index 8df546aada..84624bc9e7 100644
--- a/src/client/app/desktop/views/pages/user/user.timeline.vue
+++ b/src/client/app/desktop/views/pages/user/user.timeline.vue
@@ -1,22 +1,24 @@
 <template>
 <div class="oh5y2r7l5lx8j6jj791ykeiwgihheguk">
 	<header>
-		<span :data-active="mode == 'default'" @click="mode = 'default'"><fa :icon="['far', 'comment-alt']"/> %i18n:@default%</span>
-		<span :data-active="mode == 'with-replies'" @click="mode = 'with-replies'"><fa icon="comments"/> %i18n:@with-replies%</span>
-		<span :data-active="mode == 'with-media'" @click="mode = 'with-media'"><fa icon="images"/> %i18n:@with-media%</span>
+		<span :data-active="mode == 'default'" @click="mode = 'default'"><fa :icon="['far', 'comment-alt']"/> {{ $t('default') }}</span>
+		<span :data-active="mode == 'with-replies'" @click="mode = 'with-replies'"><fa icon="comments"/> {{ $t('with-replies') }}</span>
+		<span :data-active="mode == 'with-media'" @click="mode = 'with-media'"><fa icon="images"/> {{ $t('with-media') }}</span>
 	</header>
 	<mk-notes ref="timeline" :more="existMore ? more : null">
-		<p class="empty" slot="empty"><fa :icon="['far', 'comments']"/>%i18n:@empty%</p>
+		<p class="empty" slot="empty"><fa :icon="['far', 'comments']"/>{{ $t('empty') }}</p>
 	</mk-notes>
 </div>
 </template>
 
 <script lang="ts">
 import Vue from 'vue';
+import i18n from '../../../../i18n';
 
 const fetchLimit = 10;
 
 export default Vue.extend({
+	i18n: i18n('desktop/views/pages/user/user.timeline.vue'),
 	props: ['user'],
 
 	data() {
diff --git a/src/client/app/desktop/views/pages/user/user.vue b/src/client/app/desktop/views/pages/user/user.vue
index e36be6ac5f..7c430cbcbd 100644
--- a/src/client/app/desktop/views/pages/user/user.vue
+++ b/src/client/app/desktop/views/pages/user/user.vue
@@ -1,8 +1,8 @@
 <template>
 <mk-ui>
 	<div class="xygkxeaeontfaokvqmiblezmhvhostak" v-if="!fetching">
-		<div class="is-suspended" v-if="user.isSuspended"><fa icon="exclamation-triangle"/> %i18n:@is-suspended%</div>
-		<div class="is-remote" v-if="user.host != null"><fa icon="exclamation-triangle"/> %i18n:common.is-remote-user%<a :href="user.url || user.uri" target="_blank">%i18n:common.view-on-remote%</a></div>
+		<div class="is-suspended" v-if="user.isSuspended"><fa icon="exclamation-triangle"/> {{ $t('@.is-suspended') }}</div>
+		<div class="is-remote" v-if="user.host != null"><fa icon="exclamation-triangle"/> {{ $t('@.is-remote-user') }}<a :href="user.url || user.uri" target="_blank">{{ $t('@.view-on-remote') }}</a></div>
 		<main>
 			<div class="main">
 				<x-header :user="user"/>
@@ -20,7 +20,6 @@
 				<x-friends :user="user"/>
 				<x-followers-you-know v-if="$store.getters.isSignedIn && $store.state.i.id != user.id" :user="user"/>
 				<div class="nav"><mk-nav/></div>
-				<p v-if="!user.host">%i18n:@last-used-at%: <b><mk-time :time="user.lastUsedAt"/></b></p>
 			</div>
 		</main>
 	</div>
@@ -29,6 +28,7 @@
 
 <script lang="ts">
 import Vue from 'vue';
+import i18n from '../../../../i18n';
 import parseAcct from '../../../../../../misc/acct/parse';
 import Progress from '../../../../common/scripts/loading';
 import XHeader from './user.header.vue';
@@ -41,6 +41,7 @@ import XTwitter from './user.twitter.vue';
 import XGithub from './user.github.vue'; // ?MEM: Don't fix the intentional typo. (XGitHub -> `<x-git-hub>`)
 
 export default Vue.extend({
+	i18n: i18n(),
 	components: {
 		XHeader,
 		XTimeline,
diff --git a/src/client/app/desktop/views/pages/welcome.vue b/src/client/app/desktop/views/pages/welcome.vue
index 8825e518d7..7285647405 100644
--- a/src/client/app/desktop/views/pages/welcome.vue
+++ b/src/client/app/desktop/views/pages/welcome.vue
@@ -17,7 +17,7 @@
 					<h1 v-else><img svg-inline src="../../../../assets/title.svg" :alt="name"></h1>
 
 					<div class="info">
-						<span><b>{{ host }}</b> - <span v-html="'%i18n:@powered-by-misskey%'"></span></span>
+						<span><b>{{ host }}</b> - <span v-html="$t('powered-by-misskey')"></span></span>
 						<span class="stats" v-if="stats">
 							<span><fa icon="user"/> {{ stats.originalUsersCount | number }}</span>
 							<span><fa icon="pencil-alt"/> {{ stats.originalNotesCount | number }}</span>
@@ -25,14 +25,14 @@
 					</div>
 
 					<div class="desc">
-						<span class="desc" v-html="description || '%i18n:common.about%'"></span>
-						<a class="about" @click="about">%i18n:@about%</a>
+						<span class="desc" v-html="description || $t('@.about')"></span>
+						<a class="about" @click="about">{{ $t('about') }}</a>
 					</div>
 
 					<p class="sign">
-						<span class="signup" @click="signup">%i18n:@signup%</span>
+						<span class="signup" @click="signup">{{ $t('signup') }}</span>
 						<span class="divider">|</span>
-						<span class="signin" @click="signin">%i18n:@signin%</span>
+						<span class="signin" @click="signin">{{ $t('signin') }}</span>
 					</p>
 
 					<img src="/assets/ai.png" alt="" title="藍" class="char">
@@ -40,7 +40,7 @@
 			</div>
 
 			<div class="announcements block">
-				<header><fa icon="broadcast-tower"/> %i18n:@announcements%</header>
+				<header><fa icon="broadcast-tower"/> {{ $t('announcements') }}</header>
 				<div v-if="announcements && announcements.length > 0">
 					<div v-for="announcement in announcements">
 						<h1 v-html="announcement.title"></h1>
@@ -50,7 +50,7 @@
 			</div>
 
 			<div class="photos block">
-				<header><fa icon="images"/> %i18n:@photos%</header>
+				<header><fa icon="images"/> {{ $t('photos') }}</header>
 				<div>
 					<div v-for="photo in photos" :style="`background-image: url(${photo.thumbnailUrl})`"></div>
 				</div>
@@ -76,14 +76,14 @@
 				</div>
 
 				<div class="tl block">
-					<header><fa :icon="['far', 'comment-alt']"/> %i18n:@timeline%</header>
+					<header><fa :icon="['far', 'comment-alt']"/> {{ $t('timeline') }}</header>
 					<div>
 						<mk-welcome-timeline class="tl" :max="20"/>
 					</div>
 				</div>
 
 				<div class="info block">
-					<header><fa icon="info-circle"/> %i18n:@info%</header>
+					<header><fa icon="info-circle"/> {{ $t('info') }}</header>
 					<div>
 						<div v-if="meta" class="body">
 							<p>Version: <b>{{ meta.version }}</b></p>
@@ -97,50 +97,50 @@
 
 	<modal name="about" class="about modal" width="800px" height="auto" scrollable>
 		<article class="fpdezooorhntlzyeszemrsqdlgbysvxq">
-			<h1>%i18n:common.intro.title%</h1>
-			<p v-html="'%i18n:common.intro.about%'"></p>
+			<h1>{{ $t('@.intro.title') }}</h1>
+			<p v-html="this.$t('@.intro.about')"></p>
 			<section>
-				<h2>%i18n:common.intro.features%</h2>
+				<h2>{{ $t('@.intro.features') }}</h2>
 				<section>
 					<div class="body">
-						<h3>%i18n:common.intro.rich-contents%</h3>
-						<p v-html="'%i18n:common.intro.rich-contents-desc%'"></p>
+						<h3>{{ $t('@.intro.rich-contents') }}</h3>
+						<p v-html="this.$t('@.intro.rich-contents-desc')"></p>
 					</div>
 					<div class="image"><img src="/assets/about/post.png" alt=""></div>
 				</section>
 				<section>
 					<div class="body">
-						<h3>%i18n:common.intro.reaction%</h3>
-						<p v-html="'%i18n:common.intro.reaction-desc%'"></p>
+						<h3>{{ $t('@.intro.reaction') }}</h3>
+						<p v-html="this.$t('@.intro.reaction-desc')"></p>
 					</div>
 					<div class="image"><img src="/assets/about/reaction.png" alt=""></div>
 				</section>
 				<section>
 					<div class="body">
-						<h3>%i18n:common.intro.ui%</h3>
-						<p v-html="'%i18n:common.intro.ui-desc%'"></p>
+						<h3>{{ $t('@.intro.ui') }}</h3>
+						<p v-html="this.$t('@.intro.ui-desc')"></p>
 					</div>
 					<div class="image"><img src="/assets/about/ui.png" alt=""></div>
 				</section>
 				<section>
 					<div class="body">
-						<h3>%i18n:common.intro.drive%</h3>
-						<p v-html="'%i18n:common.intro.drive-desc%'"></p>
+						<h3>{{ $t('@.intro.drive') }}</h3>
+						<p v-html="this.$t('@.intro.drive-desc')"></p>
 					</div>
 					<div class="image"><img src="/assets/about/drive.png" alt=""></div>
 				</section>
 			</section>
-			<p v-html="'%i18n:common.intro.outro%'"></p>
+			<p v-html="this.$t('@.intro.outro')"></p>
 		</article>
 	</modal>
 
 	<modal name="signup" class="modal" width="450px" height="auto" scrollable>
-		<header class="formHeader">%i18n:@signup%</header>
+		<header class="formHeader">{{ $t('signup') }}</header>
 		<mk-signup class="form"/>
 	</modal>
 
 	<modal name="signin" class="modal" width="450px" height="auto" scrollable>
-		<header class="formHeader">%i18n:@signin%</header>
+		<header class="formHeader">{{ $t('signin') }}</header>
 		<mk-signin class="form"/>
 	</modal>
 </div>
@@ -148,10 +148,12 @@
 
 <script lang="ts">
 import Vue from 'vue';
+import i18n from '../../../i18n';
 import { host, copyright } from '../../../config';
 import { concat } from '../../../../../prelude/array';
 
 export default Vue.extend({
+	i18n: i18n('desktop/views/pages/welcome.vue'),
 	data() {
 		return {
 			meta: null,
diff --git a/src/client/app/desktop/views/widgets/messaging.vue b/src/client/app/desktop/views/widgets/messaging.vue
index 4ab18034f6..35e15d1b33 100644
--- a/src/client/app/desktop/views/widgets/messaging.vue
+++ b/src/client/app/desktop/views/widgets/messaging.vue
@@ -1,7 +1,7 @@
 <template>
 <div class="mkw-messaging">
 	<mk-widget-container :show-header="props.design == 0">
-		<template slot="header"><fa icon="comments"/>%i18n:@title%</template>
+		<template slot="header"><fa icon="comments"/>{{ $t('title') }}</template>
 		<button slot="func" @click="add"><fa icon="plus"/></button>
 
 		<mk-messaging ref="index" compact @navigate="navigate"/>
@@ -11,6 +11,7 @@
 
 <script lang="ts">
 import define from '../../../common/define-widget';
+import i18n from '../../../i18n';
 import MkMessagingRoomWindow from '../components/messaging-room-window.vue';
 import MkMessagingWindow from '../components/messaging-window.vue';
 
@@ -20,6 +21,7 @@ export default define({
 		design: 0
 	})
 }).extend({
+	i18n: i18n('desktop/views/widgets/messaging.vue'),
 	methods: {
 		navigate(user) {
 			(this as any).os.new(MkMessagingRoomWindow, {
diff --git a/src/client/app/desktop/views/widgets/notifications.vue b/src/client/app/desktop/views/widgets/notifications.vue
index bcbec0c949..b4d1241e00 100644
--- a/src/client/app/desktop/views/widgets/notifications.vue
+++ b/src/client/app/desktop/views/widgets/notifications.vue
@@ -1,8 +1,8 @@
 <template>
 <div class="mkw-notifications">
 	<mk-widget-container :show-header="!props.compact">
-		<template slot="header"><fa :icon="['far', 'bell']"/>%i18n:@title%</template>
-		<!-- <button slot="func" title="%i18n:@settings%" @click="settings"><fa icon="cog"/></button> -->
+		<template slot="header"><fa :icon="['far', 'bell']"/>{{ $t('title') }}</template>
+		<!-- <button slot="func" :title="$t('title')" @click="settings"><fa icon="cog"/></button> -->
 
 		<mk-notifications :class="$style.notifications"/>
 	</mk-widget-container>
@@ -11,12 +11,15 @@
 
 <script lang="ts">
 import define from '../../../common/define-widget';
+import i18n from '../../../i18n';
+
 export default define({
 	name: 'notifications',
 	props: () => ({
 		compact: false
 	})
 }).extend({
+	i18n: i18n('desktop/views/widgets/notifications.vue'),
 	methods: {
 		settings() {
 			alert('not implemented yet');
diff --git a/src/client/app/desktop/views/widgets/polls.vue b/src/client/app/desktop/views/widgets/polls.vue
index 368c4c4a8b..71e07ce145 100644
--- a/src/client/app/desktop/views/widgets/polls.vue
+++ b/src/client/app/desktop/views/widgets/polls.vue
@@ -1,8 +1,8 @@
 <template>
 <div class="mkw-polls">
 	<mk-widget-container :show-header="!props.compact">
-		<template slot="header"><fa icon="chart-pie"/>%i18n:@title%</template>
-		<button slot="func" title="%i18n:@refresh%" @click="fetch"><fa icon="sync"/></button>
+		<template slot="header"><fa icon="chart-pie"/>{{ $t('title') }}</template>
+		<button slot="func" :title="$t('title')" @click="fetch"><fa icon="sync"/></button>
 
 		<div class="mkw-polls--body">
 			<div class="poll" v-if="!fetching && poll != null">
@@ -10,8 +10,8 @@
 				<p v-if="!poll.text"><router-link :to="poll | notePage"><fa icon="link"/></router-link></p>
 				<mk-poll :note="poll"/>
 			</div>
-			<p class="empty" v-if="!fetching && poll == null">%i18n:@nothing%</p>
-			<p class="fetching" v-if="fetching"><fa icon="spinner .pulse" fixed-width/>%i18n:common.loading%<mk-ellipsis/></p>
+			<p class="empty" v-if="!fetching && poll == null">{{ $t('nothing') }}</p>
+			<p class="fetching" v-if="fetching"><fa icon="spinner .pulse" fixed-width/>{{ $t('@.loading') }}<mk-ellipsis/></p>
 		</div>
 	</mk-widget-container>
 </div>
@@ -19,6 +19,7 @@
 
 <script lang="ts">
 import define from '../../../common/define-widget';
+import i18n from '../../../i18n';
 
 export default define({
 	name: 'polls',
@@ -26,6 +27,7 @@ export default define({
 		compact: false
 	})
 }).extend({
+	i18n: i18n('desktop/views/widgets/polls.vue'),
 	data() {
 		return {
 			poll: null,
diff --git a/src/client/app/desktop/views/widgets/post-form.vue b/src/client/app/desktop/views/widgets/post-form.vue
index 01c8afec2b..f7526d7f8d 100644
--- a/src/client/app/desktop/views/widgets/post-form.vue
+++ b/src/client/app/desktop/views/widgets/post-form.vue
@@ -1,21 +1,24 @@
 <template>
 <div class="mkw-post-form">
 	<template v-if="props.design == 0">
-		<p class="title"><fa icon="pencil-alt"/>%i18n:@title%</p>
+		<p class="title"><fa icon="pencil-alt"/>{{ $t('title') }}</p>
 	</template>
 	<textarea :disabled="posting" v-model="text" @keydown="onKeydown" :placeholder="placeholder"></textarea>
-	<button @click="post" :disabled="posting">%i18n:@note%</button>
+	<button @click="post" :disabled="posting">{{ $t('note') }}</button>
 </div>
 </template>
 
 <script lang="ts">
 import define from '../../../common/define-widget';
+import i18n from '../../../i18n';
+
 export default define({
 	name: 'post-form',
 	props: () => ({
 		design: 0
 	})
 }).extend({
+	i18n: i18n('desktop/views/widgets/post-form.vue'),
 	data() {
 		return {
 			posting: false,
@@ -25,12 +28,12 @@ export default define({
 	computed: {
 		placeholder(): string {
 			const xs = [
-				'%i18n:common.note-placeholders.a%',
-				'%i18n:common.note-placeholders.b%',
-				'%i18n:common.note-placeholders.c%',
-				'%i18n:common.note-placeholders.d%',
-				'%i18n:common.note-placeholders.e%',
-				'%i18n:common.note-placeholders.f%'
+				this.$t('@.note-placeholders.a'),
+				this.$t('@.note-placeholders.b'),
+				this.$t('@.note-placeholders.c'),
+				this.$t('@.note-placeholders.d'),
+				this.$t('@.note-placeholders.e'),
+				this.$t('@.note-placeholders.f')
 			];
 			return xs[Math.floor(Math.random() * xs.length)];
 		}
diff --git a/src/client/app/desktop/views/widgets/profile.vue b/src/client/app/desktop/views/widgets/profile.vue
index 30b7b95d35..47429cd586 100644
--- a/src/client/app/desktop/views/widgets/profile.vue
+++ b/src/client/app/desktop/views/widgets/profile.vue
@@ -7,13 +7,13 @@
 		>
 			<div class="banner"
 				:style="$store.state.i.bannerUrl ? `background-image: url(${$store.state.i.bannerUrl})` : ''"
-				title="%i18n:@update-banner%"
+				:title="$t('update-banner')"
 				@click="() => os.apis.updateBanner()"
 			></div>
 			<mk-avatar class="avatar" :user="$store.state.i"
 				:disable-link="true"
 				@click="() => os.apis.updateAvatar()"
-				title="%i18n:@update-avatar%"
+				:title="$t('update-avatar')"
 			/>
 			<router-link class="name" :to="$store.state.i | userPage">{{ $store.state.i | userName }}</router-link>
 			<p class="username">@{{ $store.state.i | acct }}</p>
@@ -24,6 +24,7 @@
 
 <script lang="ts">
 import define from '../../../common/define-widget';
+import i18n from '../../../i18n';
 
 export default define({
 	name: 'profile',
@@ -31,6 +32,7 @@ export default define({
 		design: 0
 	})
 }).extend({
+	i18n: i18n('desktop/views/widgets/profile.vue'),
 	methods: {
 		func() {
 			if (this.props.design == 2) {
diff --git a/src/client/app/desktop/views/widgets/trends.vue b/src/client/app/desktop/views/widgets/trends.vue
index b20063d541..33a9177629 100644
--- a/src/client/app/desktop/views/widgets/trends.vue
+++ b/src/client/app/desktop/views/widgets/trends.vue
@@ -1,16 +1,16 @@
 <template>
 <div class="mkw-trends">
 	<mk-widget-container :show-header="!props.compact">
-		<template slot="header"><fa icon="fire"/>%i18n:@title%</template>
-		<button slot="func" title="%i18n:@refresh%" @click="fetch"><fa icon="sync"/></button>
+		<template slot="header"><fa icon="fire"/>{{ $t('title') }}</template>
+		<button slot="func" :title="$t('title')" @click="fetch"><fa icon="sync"/></button>
 
 		<div class="mkw-trends--body">
-			<p class="fetching" v-if="fetching"><fa icon="spinner .pulse" fixed-width/>%i18n:common.loading%<mk-ellipsis/></p>
+			<p class="fetching" v-if="fetching"><fa icon="spinner .pulse" fixed-width/>{{ $t('@.loading') }}<mk-ellipsis/></p>
 			<div class="note" v-else-if="note != null">
 				<p class="text"><router-link :to="note | notePage">{{ note.text }}</router-link></p>
 				<p class="author">―<router-link :to="note.user | userPage">@{{ note.user | acct }}</router-link></p>
 			</div>
-			<p class="empty" v-else>%i18n:@nothing%</p>
+			<p class="empty" v-else>{{ $t('nothing') }}</p>
 		</div>
 	</mk-widget-container>
 </div>
@@ -18,6 +18,7 @@
 
 <script lang="ts">
 import define from '../../../common/define-widget';
+import i18n from '../../../i18n';
 
 export default define({
 	name: 'trends',
@@ -25,6 +26,7 @@ export default define({
 		compact: false
 	})
 }).extend({
+	i18n: i18n('desktop/views/widgets/trends.vue'),
 	data() {
 		return {
 			note: null,
diff --git a/src/client/app/desktop/views/widgets/users.vue b/src/client/app/desktop/views/widgets/users.vue
index ca55ca8936..1e76bab315 100644
--- a/src/client/app/desktop/views/widgets/users.vue
+++ b/src/client/app/desktop/views/widgets/users.vue
@@ -1,11 +1,11 @@
 <template>
 <div class="mkw-users">
 	<mk-widget-container :show-header="!props.compact">
-		<template slot="header"><fa icon="users"/>%i18n:@title%</template>
-		<button slot="func" title="%i18n:@refresh%" @click="refresh"><fa icon="sync"/></button>
+		<template slot="header"><fa icon="users"/>{{ $t('title') }}</template>
+		<button slot="func" :title="$t('title')" @click="refresh"><fa icon="sync"/></button>
 
 		<div class="mkw-users--body">
-			<p class="fetching" v-if="fetching"><fa icon="spinner .pulse" fixed-width/>%i18n:common.loading%<mk-ellipsis/></p>
+			<p class="fetching" v-if="fetching"><fa icon="spinner .pulse" fixed-width/>{{ $t('@.loading') }}<mk-ellipsis/></p>
 			<template v-else-if="users.length != 0">
 				<div class="user" v-for="_user in users">
 					<mk-avatar class="avatar" :user="_user"/>
@@ -15,7 +15,7 @@
 					</div>
 				</div>
 			</template>
-			<p class="empty" v-else>%i18n:@no-one%</p>
+			<p class="empty" v-else>{{ $t('no-one') }}</p>
 		</div>
 	</mk-widget-container>
 </div>
@@ -23,6 +23,7 @@
 
 <script lang="ts">
 import define from '../../../common/define-widget';
+import i18n from '../../../i18n';
 
 const limit = 3;
 
@@ -32,6 +33,7 @@ export default define({
 		compact: false
 	})
 }).extend({
+	i18n: i18n('desktop/views/widgets/users.vue'),
 	data() {
 		return {
 			users: [],
diff --git a/src/client/app/dev/views/app.vue b/src/client/app/dev/views/app.vue
index 54d99ec82a..4aced2274b 100644
--- a/src/client/app/dev/views/app.vue
+++ b/src/client/app/dev/views/app.vue
@@ -1,6 +1,6 @@
 <template>
 <mk-ui>
-	<p v-if="fetching">%i18n:common.loading%</p>
+	<p v-if="fetching">{{ $t('@.loading') }}</p>
 	<b-card v-if="!fetching" :header="app.name">
 		<b-form-group label="App Secret">
 			<b-input :value="app.secret" readonly/>
@@ -11,7 +11,9 @@
 
 <script lang="ts">
 import Vue from 'vue';
+import i18n from '../../i18n';
 export default Vue.extend({
+	i18n: i18n(),
 	data() {
 		return {
 			fetching: true,
diff --git a/src/client/app/dev/views/apps.vue b/src/client/app/dev/views/apps.vue
index d68191aa6c..4c8de9d48e 100644
--- a/src/client/app/dev/views/apps.vue
+++ b/src/client/app/dev/views/apps.vue
@@ -1,12 +1,12 @@
 <template>
 <mk-ui>
-	<b-card header="%i18n:@manage-apps%">
-		<b-button to="/app/new" variant="primary">%i18n:@create-app%</b-button>
+	<b-card :header="$t('header')">
+		<b-button to="/app/new" variant="primary">{{ $t('create-app') }}</b-button>
 		<hr>
 		<div class="apps">
-			<p v-if="fetching">%i18n:common.loading%</p>
+			<p v-if="fetching">{{ $t('@.loading') }}</p>
 			<template v-if="!fetching">
-				<b-alert v-if="apps.length == 0">%i18n:@app-missing%</b-alert>
+				<b-alert v-if="apps.length == 0">{{ $t('app-missing') }}</b-alert>
 				<b-list-group v-else>
 					<b-list-group-item v-for="app in apps" :key="app.id" :to="`/app/${app.id}`">
 						{{ app.name }}
@@ -20,7 +20,9 @@
 
 <script lang="ts">
 import Vue from 'vue';
+import i18n from '../../i18n';
 export default Vue.extend({
+	i18n: i18n('dev/views/apps.vue'),
 	data() {
 		return {
 			fetching: true,
diff --git a/src/client/app/dev/views/index.vue b/src/client/app/dev/views/index.vue
index bb137481f4..db0e4d57c2 100644
--- a/src/client/app/dev/views/index.vue
+++ b/src/client/app/dev/views/index.vue
@@ -1,10 +1,13 @@
 <template>
 <mk-ui>
-	<b-button to="/apps" variant="primary">%i18n:@manage-apps%</b-button>
+	<b-button to="/apps" variant="primary">{{ $t('manage-apps') }}</b-button>
 </mk-ui>
 </template>
 
 <script lang="ts">
 import Vue from 'vue';
-export default Vue.extend();
+import i18n from '../../i18n';
+export default Vue.extend({
+	i18n: i18n('dev/views/index.vue')
+});
 </script>
diff --git a/src/client/app/dev/views/new-app.vue b/src/client/app/dev/views/new-app.vue
index 6c1844b6cf..382d54fd67 100644
--- a/src/client/app/dev/views/new-app.vue
+++ b/src/client/app/dev/views/new-app.vue
@@ -1,34 +1,34 @@
 <template>
 <mk-ui>
-	<b-card header="%i18n:@create-app%">
+	<b-card :header="$t('header')">
 		<b-form @submit.prevent="onSubmit" autocomplete="off">
-			<b-form-group label="%i18n:@app-name%" description="%i18n:@app-name-desc%">
-				<b-form-input v-model="name" type="text" placeholder="%i18n:@app-name-ex%" autocomplete="off" required/>
+			<b-form-group :label="$t('label')" :description="$t('description')">
+				<b-form-input v-model="name" type="text" :placeholder="$t('placeholder')" autocomplete="off" required/>
 			</b-form-group>
-			<b-form-group label="%i18n:@app-overview%" description="%i18n:@app-desc%">
-				<b-textarea v-model="description" placeholder="%i18n:@app-desc-ex%" autocomplete="off" required></b-textarea>
+			<b-form-group :label="$t('label')" :description="$t('description')">
+				<b-textarea v-model="description" :placeholder="$t('placeholder')" autocomplete="off" required></b-textarea>
 			</b-form-group>
-			<b-form-group label="%i18n:@callback-url%" description="%i18n:@callback-url-desc%">
+			<b-form-group :label="$t('label')" :description="$t('description')">
 				<b-input v-model="cb" type="url" placeholder="ex) https://your.app.example.com/callback.php" autocomplete="off"/>
 			</b-form-group>
-			<b-card header="%i18n:@authority%">
-				<b-form-group description="%i18n:@authority-desc%">
-					<b-alert show variant="warning"><fa icon="exclamation-triangle"/> %i18n:@authority-warning%</b-alert>
+			<b-card :header="$t('header')">
+				<b-form-group :description="$t('description')">
+					<b-alert show variant="warning"><fa icon="exclamation-triangle"/> {{ $t('authority-warning') }}</b-alert>
 					<b-form-checkbox-group v-model="permission" stacked>
-						<b-form-checkbox value="account-read">%i18n:@account-read%</b-form-checkbox>
-						<b-form-checkbox value="account-write">%i18n:@account-write%</b-form-checkbox>
-						<b-form-checkbox value="note-write">%i18n:@note-write%</b-form-checkbox>
-						<b-form-checkbox value="reaction-write">%i18n:@reaction-write%</b-form-checkbox>
-						<b-form-checkbox value="following-write">%i18n:@following-write%</b-form-checkbox>
-						<b-form-checkbox value="drive-read">%i18n:@drive-read%</b-form-checkbox>
-						<b-form-checkbox value="drive-write">%i18n:@drive-write%</b-form-checkbox>
-						<b-form-checkbox value="notification-read">%i18n:@notification-read%</b-form-checkbox>
-						<b-form-checkbox value="notification-write">%i18n:@notification-write%</b-form-checkbox>
+						<b-form-checkbox value="account-read">{{ $t('account-read') }}</b-form-checkbox>
+						<b-form-checkbox value="account-write">{{ $t('account-write') }}</b-form-checkbox>
+						<b-form-checkbox value="note-write">{{ $t('note-write') }}</b-form-checkbox>
+						<b-form-checkbox value="reaction-write">{{ $t('reaction-write') }}</b-form-checkbox>
+						<b-form-checkbox value="following-write">{{ $t('following-write') }}</b-form-checkbox>
+						<b-form-checkbox value="drive-read">{{ $t('drive-read') }}</b-form-checkbox>
+						<b-form-checkbox value="drive-write">{{ $t('drive-write') }}</b-form-checkbox>
+						<b-form-checkbox value="notification-read">{{ $t('notification-read') }}</b-form-checkbox>
+						<b-form-checkbox value="notification-write">{{ $t('notification-write') }}</b-form-checkbox>
 					</b-form-checkbox-group>
 				</b-form-group>
 			</b-card>
 			<hr>
-			<b-button type="submit" variant="primary">%i18n:@create-app%</b-button>
+			<b-button type="submit" variant="primary">{{ $t('create-app') }}</b-button>
 		</b-form>
 	</b-card>
 </mk-ui>
@@ -36,7 +36,9 @@
 
 <script lang="ts">
 import Vue from 'vue';
+import i18n from '../../i18n';
 export default Vue.extend({
+	i18n: i18n('dev/views/new-app.vue'),
 	data() {
 		return {
 			name: '',
@@ -56,7 +58,7 @@ export default Vue.extend({
 			}).then(() => {
 				location.href = '/dev/apps';
 			}).catch(() => {
-				alert('%i18n:common.dev.failed-to-create%');
+				alert(this.$t('@.dev.failed-to-create'));
 			});
 		}
 	}
diff --git a/src/client/app/i18n.ts b/src/client/app/i18n.ts
new file mode 100644
index 0000000000..2d0d9ba550
--- /dev/null
+++ b/src/client/app/i18n.ts
@@ -0,0 +1,14 @@
+import { lang, locale } from './config';
+
+export default function(scope?: string) {
+	const texts = scope ? locale[scope] || {} : {};
+	texts['@'] = locale['common'];
+	texts['@deck'] = locale['deck'];
+	return {
+		sync: false,
+		locale: lang,
+		messages: {
+			[lang]: texts
+		}
+	};
+}
diff --git a/src/client/app/init.ts b/src/client/app/init.ts
index 3d1560633a..87406f296f 100644
--- a/src/client/app/init.ts
+++ b/src/client/app/init.ts
@@ -273,7 +273,14 @@ export default (callback: (launch: (router: VueRouter, api?: (os: MiOS) => API)
 			Vue.mixin({
 				data() {
 					return {
-						os,
+						//os,
+						os: {
+							stream: os.stream,
+							getMeta: os.getMeta,
+							getMetaSync: os.getMetaSync,
+							new: os.new,
+							windows: os.windows
+						},
 						api: os.api,
 						apis: os.apis
 					};
@@ -281,6 +288,13 @@ export default (callback: (launch: (router: VueRouter, api?: (os: MiOS) => API)
 			});
 
 			const app = new Vue({
+				i18n: new VueI18n({
+					sync: false,
+					locale: lang,
+					messages: {
+						[lang]: {}
+					}
+				}),
 				store: os.store,
 				router,
 				render: createEl => createEl(App)
diff --git a/src/client/app/mios.ts b/src/client/app/mios.ts
index 6b10992c61..88dae04622 100644
--- a/src/client/app/mios.ts
+++ b/src/client/app/mios.ts
@@ -70,6 +70,7 @@ export default class MiOS extends EventEmitter {
 
 	public app: Vue;
 
+	@autobind
 	public new(vm, props) {
 		const x = new vm({
 			parent: this.app,
diff --git a/src/client/app/mobile/views/components/drive-file-chooser.vue b/src/client/app/mobile/views/components/drive-file-chooser.vue
index c1e53c83d2..92919c73c0 100644
--- a/src/client/app/mobile/views/components/drive-file-chooser.vue
+++ b/src/client/app/mobile/views/components/drive-file-chooser.vue
@@ -2,7 +2,7 @@
 <div class="cdxzvcfawjxdyxsekbxbfgtplebnoneb">
 	<div class="body">
 		<header>
-			<h1>%i18n:@select-file%<span class="count" v-if="files.length > 0">({{ files.length }})</span></h1>
+			<h1>{{ $t('select-file') }}<span class="count" v-if="files.length > 0">({{ files.length }})</span></h1>
 			<button class="close" @click="cancel"><fa icon="times"/></button>
 			<button v-if="multiple" class="ok" @click="ok"><fa icon="check"/></button>
 		</header>
@@ -18,7 +18,10 @@
 
 <script lang="ts">
 import Vue from 'vue';
+import i18n from '../../../i18n';
+
 export default Vue.extend({
+	i18n: i18n('mobile/views/components/drive-file-chooser.vue'),
 	props: ['multiple'],
 	data() {
 		return {
diff --git a/src/client/app/mobile/views/components/drive-folder-chooser.vue b/src/client/app/mobile/views/components/drive-folder-chooser.vue
index da312841dd..4d08679624 100644
--- a/src/client/app/mobile/views/components/drive-folder-chooser.vue
+++ b/src/client/app/mobile/views/components/drive-folder-chooser.vue
@@ -2,7 +2,7 @@
 <div class="mk-drive-folder-chooser">
 	<div class="body">
 		<header>
-			<h1>%i18n:@select-folder%</h1>
+			<h1>{{ $t('select-folder') }}</h1>
 			<button class="close" @click="cancel"><fa icon="times"/></button>
 			<button class="ok" @click="ok"><fa icon="check"/></button>
 		</header>
@@ -15,7 +15,9 @@
 
 <script lang="ts">
 import Vue from 'vue';
+import i18n from '../../../i18n';
 export default Vue.extend({
+	i18n: i18n('mobile/views/components/drive-folder-chooser.vue'),
 	methods: {
 		cancel() {
 			this.$emit('canceled');
diff --git a/src/client/app/mobile/views/components/drive.file-detail.vue b/src/client/app/mobile/views/components/drive.file-detail.vue
index 11c0d33a5b..cda907fcd1 100644
--- a/src/client/app/mobile/views/components/drive.file-detail.vue
+++ b/src/client/app/mobile/views/components/drive.file-detail.vue
@@ -31,24 +31,24 @@
 			<span class="created-at" @click="showCreatedAt"><fa :icon="['far', 'clock']"/><mk-time :time="file.createdAt"/></span>
 			<template v-if="file.isSensitive">
 				<span class="separator"></span>
-				<span class="nsfw"><fa icon="eye-slash"/> %i18n:@nsfw%</span>
+				<span class="nsfw"><fa icon="eye-slash"/> {{ $t('nsfw') }}</span>
 			</template>
 		</div>
 	</div>
 	<div class="menu">
 		<div>
-			<ui-button link :href="`${file.url}?download`" :download="file.name"><fa icon="download"/> %i18n:@download%</ui-button>
-			<ui-button @click="rename"><fa icon="pencil-alt"/> %i18n:@rename%</ui-button>
-			<ui-button @click="move"><fa :icon="['far', 'folder-open']"/> %i18n:@move%</ui-button>
-			<ui-button @click="toggleSensitive" v-if="file.isSensitive"><fa :icon="['far', 'eye']"/> %i18n:@unmark-as-sensitive%</ui-button>
-			<ui-button @click="toggleSensitive" v-else><fa :icon="['far', 'eye-slash']"/> %i18n:@mark-as-sensitive%</ui-button>
-			<ui-button @click="del"><fa :icon="['far', 'trash-alt']"/> %i18n:@delete%</ui-button>
+			<ui-button link :href="`${file.url}?download`" :download="file.name"><fa icon="download"/> {{ $t('download') }}</ui-button>
+			<ui-button @click="rename"><fa icon="pencil-alt"/> {{ $t('rename') }}</ui-button>
+			<ui-button @click="move"><fa :icon="['far', 'folder-open']"/> {{ $t('move') }}</ui-button>
+			<ui-button @click="toggleSensitive" v-if="file.isSensitive"><fa :icon="['far', 'eye']"/> {{ $t('unmark-as-sensitive') }}</ui-button>
+			<ui-button @click="toggleSensitive" v-else><fa :icon="['far', 'eye-slash']"/> {{ $t('mark-as-sensitive') }}</ui-button>
+			<ui-button @click="del"><fa :icon="['far', 'trash-alt']"/> {{ $t('delete') }}</ui-button>
 		</div>
 	</div>
 	<div class="hash">
 		<div>
 			<p>
-				<fa icon="hashtag"/>%i18n:@hash%
+				<fa icon="hashtag"/>{{ $t('hash') }}
 			</p>
 			<code>{{ file.md5 }}</code>
 		</div>
@@ -58,9 +58,11 @@
 
 <script lang="ts">
 import Vue from 'vue';
+import i18n from '../../../i18n';
 import { gcd } from '../../../../../prelude/math';
 
 export default Vue.extend({
+	i18n: i18n('mobile/views/components/drive.file-detail.vue'),
 	props: ['file'],
 
 	data() {
@@ -88,7 +90,7 @@ export default Vue.extend({
 
 	methods: {
 		rename() {
-			const name = window.prompt('%i18n:@rename%', this.file.name);
+			const name = window.prompt(this.$t('rename'), this.file.name);
 			if (name == null || name == '' || name == this.file.name) return;
 			(this as any).api('drive/files/update', {
 				fileId: this.file.id,
diff --git a/src/client/app/mobile/views/components/drive.file.vue b/src/client/app/mobile/views/components/drive.file.vue
index 3ad208f12b..6be3dc342f 100644
--- a/src/client/app/mobile/views/components/drive.file.vue
+++ b/src/client/app/mobile/views/components/drive.file.vue
@@ -15,7 +15,7 @@
 				<span class="created-at"><fa :icon="['far', 'clock']"/><mk-time :time="file.createdAt"/></span>
 				<template v-if="file.isSensitive">
 					<span class="separator"></span>
-					<span class="nsfw"><fa icon="eye-slash"/> %i18n:@nsfw%</span>
+					<span class="nsfw"><fa icon="eye-slash"/> {{ $t('nsfw') }}</span>
 				</template>
 			</footer>
 		</div>
@@ -25,7 +25,9 @@
 
 <script lang="ts">
 import Vue from 'vue';
+import i18n from '../../../i18n';
 export default Vue.extend({
+	i18n: i18n('mobile/views/components/drive.file.vue'),
 	props: ['file'],
 	data() {
 		return {
diff --git a/src/client/app/mobile/views/components/drive.vue b/src/client/app/mobile/views/components/drive.vue
index dd9870a930..b61af722cf 100644
--- a/src/client/app/mobile/views/components/drive.vue
+++ b/src/client/app/mobile/views/components/drive.vue
@@ -1,7 +1,7 @@
 <template>
 <div class="kmmwchoexgckptowjmjgfsygeltxfeqs">
 	<nav ref="nav">
-		<a @click.prevent="goRoot()" href="/i/drive"><fa icon="cloud"/>%i18n:common.drive%</a>
+		<a @click.prevent="goRoot()" href="/i/drive"><fa icon="cloud"/>{{ $t('@.drive') }}</a>
 		<template v-for="folder in hierarchyFolders">
 			<span :key="folder.id + '>'"><fa icon="angle-right"/></span>
 			<a :key="folder.id" @click.prevent="cd(folder)" :href="`/i/drive/folder/${folder.id}`">{{ folder.name }}</a>
@@ -18,26 +18,26 @@
 	<mk-uploader ref="uploader"/>
 	<div class="browser" :class="{ fetching }" v-if="file == null">
 		<div class="info" v-if="info">
-			<p v-if="folder == null">{{ (info.usage / info.capacity * 100).toFixed(1) }}% %i18n:@used%</p>
+			<p v-if="folder == null">{{ (info.usage / info.capacity * 100).toFixed(1) }}% {{ $t('used') }}</p>
 			<p v-if="folder != null && (folder.foldersCount > 0 || folder.filesCount > 0)">
-				<template v-if="folder.foldersCount > 0">{{ folder.foldersCount }} %i18n:@folder-count%</template>
-				<template v-if="folder.foldersCount > 0 && folder.filesCount > 0">%i18n:@count-separator%</template>
-				<template v-if="folder.filesCount > 0">{{ folder.filesCount }} %i18n:@file-count%</template>
+				<template v-if="folder.foldersCount > 0">{{ folder.foldersCount }} {{ $t('folder-count') }}</template>
+				<template v-if="folder.foldersCount > 0 && folder.filesCount > 0">{{ $t('count-separator') }}</template>
+				<template v-if="folder.filesCount > 0">{{ folder.filesCount }} {{ $t('file-count') }}</template>
 			</p>
 		</div>
 		<div class="folders" v-if="folders.length > 0">
 			<x-folder class="folder" v-for="folder in folders" :key="folder.id" :folder="folder"/>
-			<p v-if="moreFolders">%i18n:@load-more%</p>
+			<p v-if="moreFolders">{{ $t('@.load-more') }}</p>
 		</div>
 		<div class="files" v-if="files.length > 0">
 			<x-file class="file" v-for="file in files" :key="file.id" :file="file"/>
 			<button class="more" v-if="moreFiles" @click="fetchMoreFiles">
-				{{ fetchingMoreFiles ? '%i18n:common.loading%' : '%i18n:@load-more%' }}
+				{{ fetchingMoreFiles ? this.$t('@.loading') : this.$t('@.load-more') }}
 			</button>
 		</div>
 		<div class="empty" v-if="files.length == 0 && folders.length == 0 && !fetching">
-			<p v-if="folder == null">%i18n:@nothing-in-drive%</p>
-			<p v-if="folder != null">%i18n:@folder-is-empty%</p>
+			<p v-if="folder == null">{{ $t('nothing-in-drive') }}</p>
+			<p v-if="folder != null">{{ $t('folder-is-empty') }}</p>
 		</div>
 	</div>
 	<div class="fetching" v-if="fetching && file == null && files.length == 0 && folders.length == 0">
@@ -53,11 +53,13 @@
 
 <script lang="ts">
 import Vue from 'vue';
+import i18n from '../../../i18n';
 import XFolder from './drive.folder.vue';
 import XFile from './drive.file.vue';
 import XFileDetail from './drive.file-detail.vue';
 
 export default Vue.extend({
+	i18n: i18n('mobile/views/components/drive.vue'),
 	components: {
 		XFolder,
 		XFile,
@@ -378,7 +380,7 @@ export default Vue.extend({
 		},
 
 		openContextMenu() {
-			const fn = window.prompt('%i18n:@prompt%');
+			const fn = window.prompt(this.$t('prompt'));
 			if (fn == null || fn == '') return;
 			switch (fn) {
 				case '1':
@@ -397,7 +399,7 @@ export default Vue.extend({
 					this.moveFolder();
 					break;
 				case '6':
-					alert('%i18n:@deletion-alert%');
+					alert(this.$t('deletion-alert'));
 					break;
 			}
 		},
@@ -407,7 +409,7 @@ export default Vue.extend({
 		},
 
 		createFolder() {
-			const name = window.prompt('%i18n:@folder-name%');
+			const name = window.prompt(this.$t('folder-name'));
 			if (name == null || name == '') return;
 			(this as any).api('drive/folders/create', {
 				name: name,
@@ -419,10 +421,10 @@ export default Vue.extend({
 
 		renameFolder() {
 			if (this.folder == null) {
-				alert('%i18n:@root-rename-alert%');
+				alert(this.$t('root-rename-alert'));
 				return;
 			}
-			const name = window.prompt('%i18n:@folder-name%', this.folder.name);
+			const name = window.prompt(this.$t('folder-name'), this.folder.name);
 			if (name == null || name == '') return;
 			(this as any).api('drive/folders/update', {
 				name: name,
@@ -434,7 +436,7 @@ export default Vue.extend({
 
 		moveFolder() {
 			if (this.folder == null) {
-				alert('%i18n:@root-move-alert%');
+				alert(this.$t('root-move-alert'));
 				return;
 			}
 			(this as any).apis.chooseDriveFolder().then(folder => {
@@ -448,13 +450,13 @@ export default Vue.extend({
 		},
 
 		urlUpload() {
-			const url = window.prompt('%i18n:@url-prompt%');
+			const url = window.prompt(this.$t('url-prompt'));
 			if (url == null || url == '') return;
 			(this as any).api('drive/files/upload_from_url', {
 				url: url,
 				folderId: this.folder ? this.folder.id : undefined
 			});
-			alert('%i18n:@uploading%');
+			alert(this.$t('uploading'));
 		},
 
 		onChangeLocalFile() {
diff --git a/src/client/app/mobile/views/components/follow-button.vue b/src/client/app/mobile/views/components/follow-button.vue
index d319ed5572..a13e2fab95 100644
--- a/src/client/app/mobile/views/components/follow-button.vue
+++ b/src/client/app/mobile/views/components/follow-button.vue
@@ -5,11 +5,11 @@
 	:disabled="wait"
 >
 	<template v-if="!wait">
-		<template v-if="u.hasPendingFollowRequestFromYou && u.isLocked"><fa icon="hourglass-half"/> %i18n:@request-pending%</template>
-		<template v-else-if="u.hasPendingFollowRequestFromYou && !u.isLocked"><fa icon="hourglass-start"/> %i18n:@follow-processing%</template>
-		<template v-else-if="u.isFollowing"><fa icon="minus"/> %i18n:@following%</template>
-		<template v-else-if="!u.isFollowing && u.isLocked"><fa icon="plus"/> %i18n:@follow-request%</template>
-		<template v-else-if="!u.isFollowing && !u.isLocked"><fa icon="plus"/> %i18n:@follow%</template>
+		<template v-if="u.hasPendingFollowRequestFromYou && u.isLocked"><fa icon="hourglass-half"/> {{ $t('request-pending') }}</template>
+		<template v-else-if="u.hasPendingFollowRequestFromYou && !u.isLocked"><fa icon="hourglass-start"/> {{ $t('follow-processing') }}</template>
+		<template v-else-if="u.isFollowing"><fa icon="minus"/> {{ $t('following') }}</template>
+		<template v-else-if="!u.isFollowing && u.isLocked"><fa icon="plus"/> {{ $t('follow-request') }}</template>
+		<template v-else-if="!u.isFollowing && !u.isLocked"><fa icon="plus"/> {{ $t('follow') }}</template>
 	</template>
 	<template v-else><fa icon="spinner .pulse" fixed-width/></template>
 </button>
@@ -17,8 +17,10 @@
 
 <script lang="ts">
 import Vue from 'vue';
+import i18n from '../../../i18n';
 
 export default Vue.extend({
+	i18n: i18n('mobile/views/components/follow-button.vue'),
 	props: {
 		user: {
 			type: Object,
diff --git a/src/client/app/mobile/views/components/friends-maker.vue b/src/client/app/mobile/views/components/friends-maker.vue
index f27e7dd28f..d1b9690d68 100644
--- a/src/client/app/mobile/views/components/friends-maker.vue
+++ b/src/client/app/mobile/views/components/friends-maker.vue
@@ -1,19 +1,21 @@
 <template>
 <div class="mk-friends-maker">
-	<p class="title">%i18n:@title%:</p>
+	<p class="title">{{ $t('title') }}:</p>
 	<div class="users" v-if="!fetching && users.length > 0">
 		<mk-user-card v-for="user in users" :key="user.id" :user="user"/>
 	</div>
-	<p class="empty" v-if="!fetching && users.length == 0">%i18n:@empty%</p>
-	<p class="fetching" v-if="fetching"><fa icon="spinner .pulse" fixed-width/>%i18n:@fetching%<mk-ellipsis/></p>
-	<a class="refresh" @click="refresh">%i18n:@refresh%</a>
-	<button class="close" @click="close" title="%i18n:@close%"><fa icon="times"/></button>
+	<p class="empty" v-if="!fetching && users.length == 0">{{ $t('empty') }}</p>
+	<p class="fetching" v-if="fetching"><fa icon="spinner .pulse" fixed-width/>{{ $t('fetching') }}<mk-ellipsis/></p>
+	<a class="refresh" @click="refresh">{{ $t('refresh') }}</a>
+	<button class="close" @click="close" :title="$t('title')"><fa icon="times"/></button>
 </div>
 </template>
 
 <script lang="ts">
 import Vue from 'vue';
+import i18n from '../../../i18n';
 export default Vue.extend({
+	i18n: i18n('mobile/views/components/friends-maker.vue'),
 	data() {
 		return {
 			users: [],
diff --git a/src/client/app/mobile/views/components/media-image.vue b/src/client/app/mobile/views/components/media-image.vue
index c555d6a611..ddbe4cb401 100644
--- a/src/client/app/mobile/views/components/media-image.vue
+++ b/src/client/app/mobile/views/components/media-image.vue
@@ -1,8 +1,8 @@
 <template>
 <div class="qjewsnkgzzxlxtzncydssfbgjibiehcy" v-if="image.isSensitive && hide && !$store.state.device.alwaysShowNsfw" @click="hide = false">
 	<div>
-		<b><fa icon="exclamation-triangle"/> %i18n:@sensitive%</b>
-		<span>%i18n:@click-to-show%</span>
+		<b><fa icon="exclamation-triangle"/> {{ $t('sensitive') }}</b>
+		<span>{{ $t('click-to-show') }}</span>
 	</div>
 </div>
 <a class="gqnyydlzavusgskkfvwvjiattxdzsqlf" v-else :href="image.url" target="_blank" :style="style" :title="image.name" @click.prevent="onClick"></a>
@@ -10,9 +10,11 @@
 
 <script lang="ts">
 import Vue from 'vue';
+import i18n from '../../../i18n';
 import ImageViewer from '../../../common/views/components/image-viewer.vue';
 
 export default Vue.extend({
+	i18n: i18n('mobile/views/components/media-image.vue'),
 	props: {
 		image: {
 			type: Object,
diff --git a/src/client/app/mobile/views/components/media-video.vue b/src/client/app/mobile/views/components/media-video.vue
index 5e1c8a2500..8cd9971b77 100644
--- a/src/client/app/mobile/views/components/media-video.vue
+++ b/src/client/app/mobile/views/components/media-video.vue
@@ -1,8 +1,8 @@
 <template>
 <div class="icozogqfvdetwohsdglrbswgrejoxbdj" v-if="video.isSensitive && hide" @click="hide = false">
 	<div>
-		<b><fa icon="exclamation-triangle"/> %i18n:@sensitive%</b>
-		<span>%i18n:@click-to-show%</span>
+		<b><fa icon="exclamation-triangle"/> {{ $t('sensitive') }}</b>
+		<span>{{ $t('click-to-show') }}</span>
 	</div>
 </div>
 <a class="kkjnbbplepmiyuadieoenjgutgcmtsvu" v-else
@@ -17,8 +17,10 @@
 
 <script lang="ts">
 import Vue from 'vue';
+import i18n from '../../../i18n';
 
 export default Vue.extend({
+	i18n: i18n('mobile/views/components/media-video.vue'),
 	props: {
 		video: {
 			type: Object,
diff --git a/src/client/app/mobile/views/components/note-detail.vue b/src/client/app/mobile/views/components/note-detail.vue
index 66fc91e784..2931f5c36c 100644
--- a/src/client/app/mobile/views/components/note-detail.vue
+++ b/src/client/app/mobile/views/components/note-detail.vue
@@ -20,9 +20,9 @@
 			<mk-avatar class="avatar" :user="note.user"/>
 			<fa icon="retweet"/>
 			<router-link class="name" :href="note.user | userPage">{{ note.user | userName }}</router-link>
-			<span>{{ '%i18n:@reposted-by%'.substr(0, '%i18n:@reposted-by%'.indexOf('{')) }}</span>
+			<span>{{ this.$t('reposted-by').substr(0, this.$t('reposted-by').indexOf('{')) }}</span>
 			<a class="name" :href="note.user | userPage" v-user-preview="note.userId">{{ note.user | userName }}</a>
-			<span>{{ '%i18n:@reposted-by%'.substr('%i18n:@reposted-by%'.indexOf('}') + 1) }}</span>
+			<span>{{ this.$t('reposted-by').substr(this.$t('reposted-by').indexOf('}') + 1) }}</span>
 			<mk-time :time="note.createdAt"/>
 		</p>
 	</div>
@@ -41,8 +41,8 @@
 			</p>
 			<div class="content" v-show="p.cw == null || showContent">
 				<div class="text">
-					<span v-if="p.isHidden" style="opacity: 0.5">(%i18n:@private%)</span>
-					<span v-if="p.deletedAt" style="opacity: 0.5">(%i18n:@deleted%)</span>
+					<span v-if="p.isHidden" style="opacity: 0.5">({{ $t('private') }})</span>
+					<span v-if="p.deletedAt" style="opacity: 0.5">({{ $t('deleted') }})</span>
 					<misskey-flavored-markdown v-if="p.text" :text="p.text" :i="$store.state.i" :customEmojis="p.emojis"/>
 				</div>
 				<div class="files" v-if="p.files.length > 0">
@@ -50,7 +50,7 @@
 				</div>
 				<mk-poll v-if="p.poll" :note="p"/>
 				<mk-url-preview v-for="url in urls" :url="url" :key="url" :detail="true"/>
-				<a class="location" v-if="p.geo" :href="`https://maps.google.com/maps?q=${p.geo.coordinates[1]},${p.geo.coordinates[0]}`" target="_blank"><fa icon="map-marker-alt"/> %i18n:@location%</a>
+				<a class="location" v-if="p.geo" :href="`https://maps.google.com/maps?q=${p.geo.coordinates[1]},${p.geo.coordinates[0]}`" target="_blank"><fa icon="map-marker-alt"/> {{ $t('location') }}</a>
 				<div class="map" v-if="p.geo" ref="map"></div>
 				<div class="renote" v-if="p.renote">
 					<mk-note-preview :note="p.renote"/>
@@ -62,7 +62,7 @@
 		</router-link>
 		<footer>
 			<mk-reactions-viewer :note="p"/>
-			<button @click="reply" title="%i18n:@reply%">
+			<button @click="reply" :title="$t('title')">
 				<template v-if="p.reply"><fa icon="reply-all"/></template>
 				<template v-else><fa icon="reply"/></template>
 				<p class="count" v-if="p.repliesCount > 0">{{ p.repliesCount }}</p>
@@ -70,7 +70,7 @@
 			<button @click="renote" title="Renote">
 				<fa icon="retweet"/><p class="count" v-if="p.renoteCount > 0">{{ p.renoteCount }}</p>
 			</button>
-			<button :class="{ reacted: p.myReaction != null }" @click="react" ref="reactButton" title="%i18n:@reaction%">
+			<button :class="{ reacted: p.myReaction != null }" @click="react" ref="reactButton" :title="$t('title')">
 				<fa icon="plus"/><p class="count" v-if="p.reactions_count > 0">{{ p.reactions_count }}</p>
 			</button>
 			<button @click="menu" ref="menuButton">
@@ -86,6 +86,7 @@
 
 <script lang="ts">
 import Vue from 'vue';
+import i18n from '../../../i18n';
 import parse from '../../../../../mfm/parse';
 
 import MkNoteMenu from '../../../common/views/components/note-menu.vue';
@@ -95,6 +96,7 @@ import { sum } from '../../../../../prelude/array';
 import noteSubscriber from '../../../common/scripts/note-subscriber';
 
 export default Vue.extend({
+	i18n: i18n('mobile/views/components/note-detail.vue'),
 	components: {
 		XSub
 	},
diff --git a/src/client/app/mobile/views/components/note.vue b/src/client/app/mobile/views/components/note.vue
index 44b717145b..08628da30b 100644
--- a/src/client/app/mobile/views/components/note.vue
+++ b/src/client/app/mobile/views/components/note.vue
@@ -12,9 +12,9 @@
 	<div class="renote" v-if="isRenote">
 		<mk-avatar class="avatar" :user="note.user"/>
 		<fa icon="retweet"/>
-		<span>{{ '%i18n:@reposted-by%'.substr(0, '%i18n:@reposted-by%'.indexOf('{')) }}</span>
+		<span>{{ this.$t('reposted-by').substr(0, this.$t('reposted-by').indexOf('{')) }}</span>
 		<router-link class="name" :to="note.user | userPage">{{ note.user | userName }}</router-link>
-		<span>{{ '%i18n:@reposted-by%'.substr('%i18n:@reposted-by%'.indexOf('}') + 1) }}</span>
+		<span>{{ this.$t('reposted-by').substr(this.$t('reposted-by').indexOf('}') + 1) }}</span>
 		<mk-time :time="note.createdAt"/>
 	</div>
 	<article>
@@ -28,7 +28,7 @@
 				</p>
 				<div class="content" v-show="appearNote.cw == null || showContent">
 					<div class="text">
-						<span v-if="appearNote.isHidden" style="opacity: 0.5">(%i18n:@private%)</span>
+						<span v-if="appearNote.isHidden" style="opacity: 0.5">({{ $t('private') }})</span>
 						<a class="reply" v-if="appearNote.reply"><fa icon="reply"/></a>
 						<misskey-flavored-markdown v-if="appearNote.text" :text="appearNote.text" :i="$store.state.i" :class="$style.text" :customEmojis="appearNote.emojis"/>
 						<a class="rp" v-if="appearNote.renote != null">RN:</a>
@@ -38,7 +38,7 @@
 					</div>
 					<mk-poll v-if="appearNote.poll" :note="appearNote" ref="pollViewer"/>
 					<mk-url-preview v-for="url in urls" :url="url" :key="url"/>
-					<a class="location" v-if="appearNote.geo" :href="`https://maps.google.com/maps?q=${appearNote.geo.coordinates[1]},${appearNote.geo.coordinates[0]}`" target="_blank"><fa icon="map-marker-alt"/> %i18n:@location%</a>
+					<a class="location" v-if="appearNote.geo" :href="`https://maps.google.com/maps?q=${appearNote.geo.coordinates[1]},${appearNote.geo.coordinates[0]}`" target="_blank"><fa icon="map-marker-alt"/> {{ $t('location') }}</a>
 					<div class="renote" v-if="appearNote.renote"><mk-note-preview :note="appearNote.renote"/></div>
 				</div>
 				<span class="app" v-if="appearNote.app">via <b>{{ appearNote.app.name }}</b></span>
@@ -67,12 +67,14 @@
 
 <script lang="ts">
 import Vue from 'vue';
+import i18n from '../../../i18n';
 
 import XSub from './note.sub.vue';
 import noteMixin from '../../../common/scripts/note-mixin';
 import noteSubscriber from '../../../common/scripts/note-subscriber';
 
 export default Vue.extend({
+	i18n: i18n('mobile/views/components/note.vue'),
 	components: {
 		XSub
 	},
diff --git a/src/client/app/mobile/views/components/notes.vue b/src/client/app/mobile/views/components/notes.vue
index fb09276891..e363d219a6 100644
--- a/src/client/app/mobile/views/components/notes.vue
+++ b/src/client/app/mobile/views/components/notes.vue
@@ -10,10 +10,7 @@
 		</template>
 	</div>
 
-	<div v-if="!fetching && requestInitPromise != null">
-		<p>%i18n:@failed%</p>
-		<button @click="resolveInitPromise">%i18n:@retry%</button>
-	</div>
+	<mk-error v-if="!fetching && requestInitPromise != null" @retry="resolveInitPromise"/>
 
 	<!-- トランジションを有効にするとなぜかメモリリークする -->
 	<component :is="!$store.state.device.reduceMotion ? 'transition-group' : 'div'" name="mk-notes" class="transition" tag="div">
@@ -28,7 +25,7 @@
 
 	<footer v-if="more">
 		<button @click="loadMore" :disabled="moreFetching" :style="{ cursor: moreFetching ? 'wait' : 'pointer' }">
-			<template v-if="!moreFetching">%i18n:@load-more%</template>
+			<template v-if="!moreFetching">{{ $t('@.load-more') }}</template>
 			<template v-if="moreFetching"><fa icon="spinner .pulse" fixed-width/></template>
 		</button>
 	</footer>
@@ -37,10 +34,12 @@
 
 <script lang="ts">
 import Vue from 'vue';
+import i18n from '../../../i18n';
 
 const displayLimit = 30;
 
 export default Vue.extend({
+	i18n: i18n(),
 	props: {
 		more: {
 			type: Function,
@@ -64,7 +63,7 @@ export default Vue.extend({
 				const date = new Date(note.createdAt).getDate();
 				const month = new Date(note.createdAt).getMonth() + 1;
 				note._date = date;
-				note._datetext = '%i18n:common.month-and-day%'.replace('{month}', month.toString()).replace('{day}', date.toString());
+				note._datetext = this.$t('@.month-and-day').replace('{month}', month.toString()).replace('{day}', date.toString());
 				return note;
 			});
 		}
diff --git a/src/client/app/mobile/views/components/notifications.vue b/src/client/app/mobile/views/components/notifications.vue
index 3669e84ea2..3a95e0d158 100644
--- a/src/client/app/mobile/views/components/notifications.vue
+++ b/src/client/app/mobile/views/components/notifications.vue
@@ -19,17 +19,19 @@
 
 	<button class="more" v-if="moreNotifications" @click="fetchMoreNotifications" :disabled="fetchingMoreNotifications">
 		<template v-if="fetchingMoreNotifications"><fa icon="spinner .pulse" fixed-width/></template>
-		{{ fetchingMoreNotifications ? '%i18n:common.loading%' : '%i18n:@more%' }}
+		{{ fetchingMoreNotifications ? $t('@.loading') : $t('@.load-more') }}
 	</button>
 
-	<p class="empty" v-if="notifications.length == 0 && !fetching">%i18n:@empty%</p>
+	<p class="empty" v-if="notifications.length == 0 && !fetching">{{ $t('empty') }}</p>
 </div>
 </template>
 
 <script lang="ts">
 import Vue from 'vue';
+import i18n from '../../../i18n';
 
 export default Vue.extend({
+	i18n: i18n('mobile/views/components/notifications.vue'),
 	data() {
 		return {
 			fetching: true,
@@ -46,7 +48,7 @@ export default Vue.extend({
 				const date = new Date(notification.createdAt).getDate();
 				const month = new Date(notification.createdAt).getMonth() + 1;
 				notification._date = date;
-				notification._datetext = '%i18n:common.month-and-day%'.replace('{month}', month.toString()).replace('{day}', date.toString());
+				notification._datetext = this.$t('@.month-and-day').replace('{month}', month.toString()).replace('{day}', date.toString());
 				return notification;
 			});
 		}
diff --git a/src/client/app/mobile/views/components/post-form.vue b/src/client/app/mobile/views/components/post-form.vue
index 4a50ed982d..8715046acb 100644
--- a/src/client/app/mobile/views/components/post-form.vue
+++ b/src/client/app/mobile/views/components/post-form.vue
@@ -14,9 +14,9 @@
 			<mk-note-preview class="preview" v-if="renote" :note="renote"/>
 			<div v-if="visibility == 'specified'" class="visibleUsers">
 				<span v-for="u in visibleUsers">{{ u | userName }}<a @click="removeVisibleUser(u)">[x]</a></span>
-				<a @click="addVisibleUser">+%i18n:@add-visible-user%</a>
+				<a @click="addVisibleUser">+{{ $t('add-visible-user') }}</a>
 			</div>
-			<input v-show="useCw" v-model="cw" placeholder="%i18n:@cw-placeholder%">
+			<input v-show="useCw" v-model="cw" :placeholder="$t('placeholder')">
 			<textarea v-model="text" ref="text" :disabled="posting" :placeholder="placeholder" v-autocomplete="'text'"></textarea>
 			<div class="attaches" v-show="files.length != 0">
 				<x-draggable class="files" :list="files" :options="{ animation: 150 }">
@@ -53,6 +53,7 @@
 
 <script lang="ts">
 import Vue from 'vue';
+import i18n from '../../../i18n';
 import insertTextAtCursor from 'insert-text-at-cursor';
 import * as XDraggable from 'vuedraggable';
 import MkVisibilityChooser from '../../../common/views/components/visibility-chooser.vue';
@@ -65,6 +66,7 @@ import parseAcct from '../../../../../misc/acct/parse';
 import { toASCII } from 'punycode';
 
 export default Vue.extend({
+	i18n: i18n('mobile/views/components/post-form.vue'),
 	components: {
 		XDraggable,
 		MkVisibilityChooser
@@ -124,28 +126,28 @@ export default Vue.extend({
 
 		placeholder(): string {
 			const xs = [
-				'%i18n:common.note-placeholders.a%',
-				'%i18n:common.note-placeholders.b%',
-				'%i18n:common.note-placeholders.c%',
-				'%i18n:common.note-placeholders.d%',
-				'%i18n:common.note-placeholders.e%',
-				'%i18n:common.note-placeholders.f%'
+				this.$t('@.note-placeholders.a'),
+				this.$t('@.note-placeholders.b'),
+				this.$t('@.note-placeholders.c'),
+				this.$t('@.note-placeholders.d'),
+				this.$t('@.note-placeholders.e'),
+				this.$t('@.note-placeholders.f')
 			];
 			const x = xs[Math.floor(Math.random() * xs.length)];
 
 			return this.renote
-				? '%i18n:@quote-placeholder%'
+				? this.$t('quote-placeholder')
 				: this.reply
-					? '%i18n:@reply-placeholder%'
+					? this.$t('reply-placeholder')
 					: x;
 		},
 
 		submitText(): string {
 			return this.renote
-				? '%i18n:@renote%'
+				? this.$t('renote')
 				: this.reply
-					? '%i18n:@reply%'
-					: '%i18n:@submit%';
+					? this.$t('reply')
+					: this.$t('submit');
 		},
 
 		canPost(): boolean {
@@ -249,7 +251,7 @@ export default Vue.extend({
 
 		setGeo() {
 			if (navigator.geolocation == null) {
-				alert('%i18n:@location-alert%');
+				alert(this.$t('location-alert'));
 				return;
 			}
 
@@ -278,7 +280,7 @@ export default Vue.extend({
 
 		addVisibleUser() {
 			(this as any).apis.input({
-				title: '%i18n:@username-prompt%'
+				title: this.$t('username-prompt')
 			}).then(acct => {
 				if (acct.startsWith('@')) acct = acct.substr(1);
 				(this as any).api('users/show', parseAcct(acct)).then(user => {
diff --git a/src/client/app/mobile/views/components/sub-note-content.vue b/src/client/app/mobile/views/components/sub-note-content.vue
index 6a478130ce..f4c86f19d2 100644
--- a/src/client/app/mobile/views/components/sub-note-content.vue
+++ b/src/client/app/mobile/views/components/sub-note-content.vue
@@ -1,18 +1,18 @@
 <template>
 <div class="mk-sub-note-content">
 	<div class="body">
-		<span v-if="note.isHidden" style="opacity: 0.5">(%i18n:@private%)</span>
-		<span v-if="note.deletedAt" style="opacity: 0.5">(%i18n:@deleted%)</span>
+		<span v-if="note.isHidden" style="opacity: 0.5">({{ $t('private') }})</span>
+		<span v-if="note.deletedAt" style="opacity: 0.5">({{ $t('deleted') }})</span>
 		<a class="reply" v-if="note.replyId"><fa icon="reply"/></a>
-		<misskey-flavored-markdown v-if="note.text" :text="note.text" :i="$store.state.i" :customEmojis="note.emojis"/>
+		<misskey-flavored-markdown v-if="note.text" :text="note.text" :i="$store.state.i" :custom-emojis="note.emojis"/>
 		<a class="rp" v-if="note.renoteId">RN: ...</a>
 	</div>
 	<details v-if="note.files.length > 0">
-		<summary>({{ '%i18n:@media-count%'.replace('{}', note.files.length) }})</summary>
+		<summary>({{ $t('media-count').replace('{}', note.files.length) }})</summary>
 		<mk-media-list :media-list="note.files"/>
 	</details>
 	<details v-if="note.poll">
-		<summary>%i18n:@poll%</summary>
+		<summary>{{ $t('poll') }}</summary>
 		<mk-poll :note="note"/>
 	</details>
 </div>
@@ -20,7 +20,9 @@
 
 <script lang="ts">
 import Vue from 'vue';
+import i18n from '../../../i18n';
 export default Vue.extend({
+	i18n: i18n('mobile/views/components/sub-note-content.vue'),
 	props: ['note']
 });
 </script>
diff --git a/src/client/app/mobile/views/components/ui.header.vue b/src/client/app/mobile/views/components/ui.header.vue
index e360e1c88a..9dbc1f9d02 100644
--- a/src/client/app/mobile/views/components/ui.header.vue
+++ b/src/client/app/mobile/views/components/ui.header.vue
@@ -1,7 +1,6 @@
 <template>
 <div class="header" ref="root">
-	<p class="warn" v-if="env != 'production'">%i18n:common.do-not-use-in-production%</p>
-	<mk-special-message/>
+	<p class="warn" v-if="env != 'production'">{{ $t('@.do-not-use-in-production') }}</p>
 	<div class="main" ref="main">
 		<div class="backdrop"></div>
 		<div class="content" ref="mainContainer">
@@ -19,10 +18,12 @@
 
 <script lang="ts">
 import Vue from 'vue';
+import i18n from '../../../i18n';
 import * as anime from 'animejs';
 import { env } from '../../../config';
 
 export default Vue.extend({
+	i18n: i18n(),
 	props: ['func'],
 
 	data() {
diff --git a/src/client/app/mobile/views/components/ui.nav.vue b/src/client/app/mobile/views/components/ui.nav.vue
index d7f0d58ae2..5a582c7aca 100644
--- a/src/client/app/mobile/views/components/ui.nav.vue
+++ b/src/client/app/mobile/views/components/ui.nav.vue
@@ -15,23 +15,23 @@
 			</router-link>
 			<div class="links">
 				<ul>
-					<li><router-link to="/" :data-active="$route.name == 'index'"><i><fa icon="home"/></i>%i18n:@timeline%<i><fa icon="angle-right"/></i></router-link></li>
-					<li><router-link to="/i/notifications" :data-active="$route.name == 'notifications'"><i><fa :icon="['far', 'bell']"/></i>%i18n:@notifications%<i v-if="hasUnreadNotification" class="circle"><fa icon="circle"/></i><i><fa icon="angle-right"/></i></router-link></li>
-					<li><router-link to="/i/messaging" :data-active="$route.name == 'messaging'"><i><fa :icon="['far', 'comments']"/></i>%i18n:@messaging%<i v-if="hasUnreadMessagingMessage" class="circle"><fa icon="circle"/></i><i><fa icon="angle-right"/></i></router-link></li>
-					<li v-if="$store.getters.isSignedIn && ($store.state.i.isLocked || $store.state.i.carefulBot)"><router-link to="/i/received-follow-requests" :data-active="$route.name == 'received-follow-requests'"><i><fa :icon="['far', 'envelope']"/></i>%i18n:@follow-requests%<i v-if="$store.getters.isSignedIn && $store.state.i.pendingReceivedFollowRequestsCount" class="circle"><fa icon="circle"/></i><i><fa icon="angle-right"/></i></router-link></li>
-					<li><router-link to="/reversi" :data-active="$route.name == 'reversi'"><i><fa icon="gamepad"/></i>%i18n:@game%<i v-if="hasGameInvitation" class="circle"><fa icon="circle"/></i><i><fa icon="angle-right"/></i></router-link></li>
+					<li><router-link to="/" :data-active="$route.name == 'index'"><i><fa icon="home"/></i>{{ $t('timeline') }}<i><fa icon="angle-right"/></i></router-link></li>
+					<li><router-link to="/i/notifications" :data-active="$route.name == 'notifications'"><i><fa :icon="['far', 'bell']"/></i>{{ $t('notifications') }}<i v-if="hasUnreadNotification" class="circle"><fa icon="circle"/></i><i><fa icon="angle-right"/></i></router-link></li>
+					<li><router-link to="/i/messaging" :data-active="$route.name == 'messaging'"><i><fa :icon="['far', 'comments']"/></i>{{ $t('@.messaging') }}<i v-if="hasUnreadMessagingMessage" class="circle"><fa icon="circle"/></i><i><fa icon="angle-right"/></i></router-link></li>
+					<li v-if="$store.getters.isSignedIn && ($store.state.i.isLocked || $store.state.i.carefulBot)"><router-link to="/i/received-follow-requests" :data-active="$route.name == 'received-follow-requests'"><i><fa :icon="['far', 'envelope']"/></i>{{ $t('follow-requests') }}<i v-if="$store.getters.isSignedIn && $store.state.i.pendingReceivedFollowRequestsCount" class="circle"><fa icon="circle"/></i><i><fa icon="angle-right"/></i></router-link></li>
+					<li><router-link to="/reversi" :data-active="$route.name == 'reversi'"><i><fa icon="gamepad"/></i>{{ $t('game') }}<i v-if="hasGameInvitation" class="circle"><fa icon="circle"/></i><i><fa icon="angle-right"/></i></router-link></li>
 				</ul>
 				<ul>
-					<li><router-link to="/i/widgets" :data-active="$route.name == 'widgets'"><i><fa :icon="['far', 'calendar-alt']"/></i>%i18n:@widgets%<i><fa icon="angle-right"/></i></router-link></li>
-					<li><router-link to="/i/favorites" :data-active="$route.name == 'favorites'"><i><fa icon="star"/></i>%i18n:@favorites%<i><fa icon="angle-right"/></i></router-link></li>
-					<li><router-link to="/i/lists" :data-active="$route.name == 'user-lists'"><i><fa icon="list"/></i>%i18n:@user-lists%<i><fa icon="angle-right"/></i></router-link></li>
-					<li><router-link to="/i/drive" :data-active="$route.name == 'drive'"><i><fa icon="cloud"/></i>%i18n:common.drive%<i><fa icon="angle-right"/></i></router-link></li>
+					<li><router-link to="/i/widgets" :data-active="$route.name == 'widgets'"><i><fa :icon="['far', 'calendar-alt']"/></i>{{ $t('widgets') }}<i><fa icon="angle-right"/></i></router-link></li>
+					<li><router-link to="/i/favorites" :data-active="$route.name == 'favorites'"><i><fa icon="star"/></i>{{ $t('favorites') }}<i><fa icon="angle-right"/></i></router-link></li>
+					<li><router-link to="/i/lists" :data-active="$route.name == 'user-lists'"><i><fa icon="list"/></i>{{ $t('user-lists') }}<i><fa icon="angle-right"/></i></router-link></li>
+					<li><router-link to="/i/drive" :data-active="$route.name == 'drive'"><i><fa icon="cloud"/></i>{{ $t('@.drive') }}<i><fa icon="angle-right"/></i></router-link></li>
 				</ul>
 				<ul>
-					<li><a @click="search"><i><fa icon="search"/></i>%i18n:@search%<i><fa icon="angle-right"/></i></a></li>
-					<li><router-link to="/i/settings" :data-active="$route.name == 'settings'"><i><fa icon="cog"/></i>%i18n:@settings%<i><fa icon="angle-right"/></i></router-link></li>
-					<li v-if="$store.getters.isSignedIn && $store.state.i.isAdmin"><a href="/admin"><i><fa icon="terminal"/></i><span>%i18n:@admin%</span><i><fa icon="angle-right"/></i></a></li>
-					<li @click="dark"><p><template v-if="$store.state.device.darkmode"><i><fa icon="moon"/></i></template><template v-else><i><fa :icon="['far', 'moon']"/></i></template><span>%i18n:@darkmode%</span></p></li>
+					<li><a @click="search"><i><fa icon="search"/></i>{{ $t('search') }}<i><fa icon="angle-right"/></i></a></li>
+					<li><router-link to="/i/settings" :data-active="$route.name == 'settings'"><i><fa icon="cog"/></i>{{ $t('settings') }}<i><fa icon="angle-right"/></i></router-link></li>
+					<li v-if="$store.getters.isSignedIn && $store.state.i.isAdmin"><a href="/admin"><i><fa icon="terminal"/></i><span>{{ $t('admin') }}</span><i><fa icon="angle-right"/></i></a></li>
+					<li @click="dark"><p><template v-if="$store.state.device.darkmode"><i><fa icon="moon"/></i></template><template v-else><i><fa :icon="['far', 'moon']"/></i></template><span>{{ $t('darkmode') }}</span></p></li>
 				</ul>
 			</div>
 			<div class="announcements" v-if="announcements && announcements.length > 0">
@@ -40,7 +40,7 @@
 					<div v-html="announcement.text"></div>
 				</article>
 			</div>
-			<a :href="aboutUrl"><p class="about">%i18n:@about%</p></a>
+			<a :href="aboutUrl"><p class="about">{{ $t('about') }}</p></a>
 		</div>
 	</transition>
 </div>
@@ -48,9 +48,11 @@
 
 <script lang="ts">
 import Vue from 'vue';
+import i18n from '../../../i18n';
 import { lang } from '../../../config';
 
 export default Vue.extend({
+	i18n: i18n('mobile/views/components/ui.nav.vue'),
 	props: ['isOpen'],
 
 	data() {
@@ -93,7 +95,7 @@ export default Vue.extend({
 
 	methods: {
 		search() {
-			const query = window.prompt('%i18n:@search%');
+			const query = window.prompt(this.$t('search'));
 			if (query == null || query == '') return;
 			this.$router.push(`/search?q=${encodeURIComponent(query)}`);
 		},
diff --git a/src/client/app/mobile/views/components/user-timeline.vue b/src/client/app/mobile/views/components/user-timeline.vue
index 55afda0eab..cfe1522666 100644
--- a/src/client/app/mobile/views/components/user-timeline.vue
+++ b/src/client/app/mobile/views/components/user-timeline.vue
@@ -3,7 +3,7 @@
 	<mk-notes ref="timeline" :more="existMore ? more : null">
 		<div slot="empty">
 			<fa :icon="['far', 'comments']"/>
-			{{ withMedia ? '%i18n:@no-notes-with-media%' : '%i18n:@no-notes%' }}
+			{{ withMedia ? this.$t('no-notes-with-media') : this.$t('no-notes') }}
 		</div>
 	</mk-notes>
 </div>
@@ -11,10 +11,12 @@
 
 <script lang="ts">
 import Vue from 'vue';
+import i18n from '../../../i18n';
 
 const fetchLimit = 10;
 
 export default Vue.extend({
+	i18n: i18n('mobile/views/components/user-timeline.vue'),
 	props: ['user', 'withMedia'],
 
 	data() {
diff --git a/src/client/app/mobile/views/components/users-list.vue b/src/client/app/mobile/views/components/users-list.vue
index 29dd6abaab..a0f831c68b 100644
--- a/src/client/app/mobile/views/components/users-list.vue
+++ b/src/client/app/mobile/views/components/users-list.vue
@@ -1,26 +1,28 @@
 <template>
 <div class="mk-users-list">
 	<nav>
-		<span :data-active="mode == 'all'" @click="mode = 'all'">%i18n:@all%<span>{{ count }}</span></span>
-		<span v-if="$store.getters.isSignedIn && youKnowCount" :data-active="mode == 'iknow'" @click="mode = 'iknow'">%i18n:@known%<span>{{ youKnowCount }}</span></span>
+		<span :data-active="mode == 'all'" @click="mode = 'all'">{{ $t('all') }}<span>{{ count }}</span></span>
+		<span v-if="$store.getters.isSignedIn && youKnowCount" :data-active="mode == 'iknow'" @click="mode = 'iknow'">{{ $t('known') }}<span>{{ youKnowCount }}</span></span>
 	</nav>
 	<div class="users" v-if="!fetching && users.length != 0">
 		<mk-user-preview v-for="u in users" :user="u" :key="u.id"/>
 	</div>
 	<button class="more" v-if="!fetching && next != null" @click="more" :disabled="moreFetching">
-		<span v-if="!moreFetching">%i18n:@load-more%</span>
-		<span v-if="moreFetching">%i18n:common.loading%<mk-ellipsis/></span>
+		<span v-if="!moreFetching">{{ $t('@.load-more') }}</span>
+		<span v-if="moreFetching">{{ $t('@.loading') }}<mk-ellipsis/></span>
 	</button>
 	<p class="no" v-if="!fetching && users.length == 0">
 		<slot></slot>
 	</p>
-	<p class="fetching" v-if="fetching"><fa icon="spinner .pulse" fixed-width/>%i18n:common.loading%<mk-ellipsis/></p>
+	<p class="fetching" v-if="fetching"><fa icon="spinner .pulse" fixed-width/>{{ $t('@.loading') }}<mk-ellipsis/></p>
 </div>
 </template>
 
 <script lang="ts">
 import Vue from 'vue';
+import i18n from '../../../i18n';
 export default Vue.extend({
+	i18n: i18n('mobile/views/components/users-list.vue'),
 	props: ['fetch', 'count', 'youKnowCount'],
 	data() {
 		return {
diff --git a/src/client/app/mobile/views/pages/drive.vue b/src/client/app/mobile/views/pages/drive.vue
index 4c1202f6b4..e9197bcfcf 100644
--- a/src/client/app/mobile/views/pages/drive.vue
+++ b/src/client/app/mobile/views/pages/drive.vue
@@ -3,7 +3,7 @@
 	<span slot="header">
 		<template v-if="folder"><span style="margin-right:4px;"><fa :icon="['far', 'folder-open']"/></span>{{ folder.name }}</template>
 		<template v-if="file"><mk-file-type-icon data-icon :type="file.type" style="margin-right:4px;"/>{{ file.name }}</template>
-		<template v-if="!folder && !file"><span style="margin-right:4px;"><fa icon="cloud"/></span>%i18n:common.drive%</template>
+		<template v-if="!folder && !file"><span style="margin-right:4px;"><fa icon="cloud"/></span>{{ $t('@.drive') }}</template>
 	</span>
 	<template slot="func"><button @click="fn"><fa icon="ellipsis-h"/></button></template>
 	<mk-drive
@@ -24,9 +24,11 @@
 
 <script lang="ts">
 import Vue from 'vue';
+import i18n from '../../../i18n';
 import Progress from '../../../common/scripts/loading';
 
 export default Vue.extend({
+	i18n: i18n(),
 	data() {
 		return {
 			Progress,
diff --git a/src/client/app/mobile/views/pages/favorites.vue b/src/client/app/mobile/views/pages/favorites.vue
index af159788b8..ff652ddf0b 100644
--- a/src/client/app/mobile/views/pages/favorites.vue
+++ b/src/client/app/mobile/views/pages/favorites.vue
@@ -1,21 +1,23 @@
 <template>
 <mk-ui>
-	<span slot="header"><span style="margin-right:4px;"><fa icon="star"/></span>%i18n:@title%</span>
+	<span slot="header"><span style="margin-right:4px;"><fa icon="star"/></span>{{ $t('title') }}</span>
 
 	<main>
 		<template v-for="favorite in favorites">
 			<mk-note-detail class="post" :note="favorite.note" :key="favorite.note.id"/>
 		</template>
-		<a v-if="existMore" @click="more">%i18n:@more%</a>
+		<a v-if="existMore" @click="more">{{ $t('@.load-more') }}</a>
 	</main>
 </mk-ui>
 </template>
 
 <script lang="ts">
 import Vue from 'vue';
+import i18n from '../../../i18n';
 import Progress from '../../../common/scripts/loading';
 
 export default Vue.extend({
+	i18n: i18n('mobile/views/pages/favorites.vue'),
 	data() {
 		return {
 			fetching: true,
diff --git a/src/client/app/mobile/views/pages/followers.vue b/src/client/app/mobile/views/pages/followers.vue
index 601f6670c1..b92c0088fb 100644
--- a/src/client/app/mobile/views/pages/followers.vue
+++ b/src/client/app/mobile/views/pages/followers.vue
@@ -1,8 +1,7 @@
 <template>
 <mk-ui>
 	<template slot="header" v-if="!fetching">
-		<img :src="user.avatarUrl" alt="">
-		{{ '%i18n:@followers-of%'.replace('{}', name) }}
+		<img :src="user.avatarUrl" alt="">{{ $t('followers-of', { name }) }}
 	</template>
 	<mk-users-list
 		v-if="!fetching"
@@ -18,11 +17,13 @@
 
 <script lang="ts">
 import Vue from 'vue';
+import i18n from '../../../i18n';
 import Progress from '../../../common/scripts/loading';
 import parseAcct from '../../../../../misc/acct/parse';
 import getUserName from '../../../../../misc/get-user-name';
 
 export default Vue.extend({
+	i18n: i18n('mobile/views/pages/followers.vue'),
 	data() {
 		return {
 			fetching: true,
@@ -49,7 +50,7 @@ export default Vue.extend({
 				this.user = user;
 				this.fetching = false;
 
-				document.title = `${'%i18n:@followers-of%'.replace('{}', this.name)} | ${(this as any).os.instanceName}`;
+				document.title = `${this.$t('followers-of').replace('{}', this.name)} | ${(this as any).os.instanceName}`;
 			});
 		},
 		onLoaded() {
diff --git a/src/client/app/mobile/views/pages/following.vue b/src/client/app/mobile/views/pages/following.vue
index 0efac6110e..a4ec0ab6c7 100644
--- a/src/client/app/mobile/views/pages/following.vue
+++ b/src/client/app/mobile/views/pages/following.vue
@@ -1,8 +1,7 @@
 <template>
 <mk-ui>
 	<template slot="header" v-if="!fetching">
-		<img :src="user.avatarUrl" alt="">
-		{{ '%i18n:@following-of%'.replace('{}', name) }}
+		<img :src="user.avatarUrl" alt="">{{ $t('following-of', { name }) }}
 	</template>
 	<mk-users-list
 		v-if="!fetching"
@@ -18,10 +17,12 @@
 
 <script lang="ts">
 import Vue from 'vue';
+import i18n from '../../../i18n';
 import Progress from '../../../common/scripts/loading';
 import parseAcct from '../../../../../misc/acct/parse';
 
 export default Vue.extend({
+	i18n: i18n('mobile/views/pages/following.vue'),
 	data() {
 		return {
 			fetching: true,
@@ -48,7 +49,7 @@ export default Vue.extend({
 				this.user = user;
 				this.fetching = false;
 
-				document.title = `${'%i18n:@followers-of%'.replace('{}', this.name)} | ${(this as any).os.instanceName}`;
+				document.title = `${this.$t('followers-of').replace('{}', this.name)} | ${(this as any).os.instanceName}`;
 			});
 		},
 		onLoaded() {
diff --git a/src/client/app/mobile/views/pages/games/reversi.vue b/src/client/app/mobile/views/pages/games/reversi.vue
index d02bc191aa..8a5653fc6e 100644
--- a/src/client/app/mobile/views/pages/games/reversi.vue
+++ b/src/client/app/mobile/views/pages/games/reversi.vue
@@ -1,14 +1,16 @@
 <template>
 <mk-ui>
-	<span slot="header"><span style="margin-right:4px;"><fa icon="gamepad"/></span>%i18n:@reversi%</span>
+	<span slot="header"><span style="margin-right:4px;"><fa icon="gamepad"/></span>{{ $t('reversi') }}</span>
 	<x-reversi :game-id="$route.params.game" @nav="nav" :self-nav="false"/>
 </mk-ui>
 </template>
 
 <script lang="ts">
 import Vue from 'vue';
+import i18n from '../../../../i18n';
 
 export default Vue.extend({
+	i18n: i18n('mobile/views/pages/games/reversi.vue'),
 	components: {
 		XReversi: () => import('../../../../common/views/components/games/reversi/reversi.vue')
 	},
diff --git a/src/client/app/mobile/views/pages/home.vue b/src/client/app/mobile/views/pages/home.vue
index 12c1c7f405..d197f1a44f 100644
--- a/src/client/app/mobile/views/pages/home.vue
+++ b/src/client/app/mobile/views/pages/home.vue
@@ -2,12 +2,12 @@
 <mk-ui>
 	<span slot="header" @click="showNav = true">
 		<span :class="$style.title">
-			<span v-if="src == 'home'"><fa icon="home"/>%i18n:@home%</span>
-			<span v-if="src == 'local'"><fa :icon="['far', 'comments']"/>%i18n:@local%</span>
-			<span v-if="src == 'hybrid'"><fa icon="share-alt"/>%i18n:@hybrid%</span>
-			<span v-if="src == 'global'"><fa icon="globe"/>%i18n:@global%</span>
-			<span v-if="src == 'mentions'"><fa icon="at"/>%i18n:@mentions%</span>
-			<span v-if="src == 'messages'"><fa :icon="['far', 'envelope']"/>%i18n:@messages%</span>
+			<span v-if="src == 'home'"><fa icon="home"/>{{ $t('home') }}</span>
+			<span v-if="src == 'local'"><fa :icon="['far', 'comments']"/>{{ $t('local') }}</span>
+			<span v-if="src == 'hybrid'"><fa icon="share-alt"/>{{ $t('hybrid') }}</span>
+			<span v-if="src == 'global'"><fa icon="globe"/>{{ $t('global') }}</span>
+			<span v-if="src == 'mentions'"><fa icon="at"/>{{ $t('mentions') }}</span>
+			<span v-if="src == 'messages'"><fa :icon="['far', 'envelope']"/>{{ $t('messages') }}</span>
 			<span v-if="src == 'list'"><fa icon="list"/>{{ list.title }}</span>
 			<span v-if="src == 'tag'"><fa icon="hashtag"/>{{ tagTl.title }}</span>
 		</span>
@@ -28,13 +28,13 @@
 			<div class="pointer"></div>
 			<div class="body">
 				<div>
-					<span :data-active="src == 'home'" @click="src = 'home'"><fa icon="home"/> %i18n:@home%</span>
-					<span :data-active="src == 'local'" @click="src = 'local'" v-if="enableLocalTimeline"><fa :icon="['far', 'comments']"/> %i18n:@local%</span>
-					<span :data-active="src == 'hybrid'" @click="src = 'hybrid'" v-if="enableLocalTimeline"><fa icon="share-alt"/> %i18n:@hybrid%</span>
-					<span :data-active="src == 'global'" @click="src = 'global'"><fa icon="globe"/> %i18n:@global%</span>
+					<span :data-active="src == 'home'" @click="src = 'home'"><fa icon="home"/> {{ $t('home') }}</span>
+					<span :data-active="src == 'local'" @click="src = 'local'" v-if="enableLocalTimeline"><fa :icon="['far', 'comments']"/> {{ $t('local') }}</span>
+					<span :data-active="src == 'hybrid'" @click="src = 'hybrid'" v-if="enableLocalTimeline"><fa icon="share-alt"/> {{ $t('hybrid') }}</span>
+					<span :data-active="src == 'global'" @click="src = 'global'"><fa icon="globe"/> {{ $t('global') }}</span>
 					<div class="hr"></div>
-					<span :data-active="src == 'mentions'" @click="src = 'mentions'"><fa icon="at"/> %i18n:@mentions%<i class="badge" v-if="$store.state.i.hasUnreadMentions"><fa icon="circle"/></i></span>
-					<span :data-active="src == 'messages'" @click="src = 'messages'"><fa :icon="['far', 'envelope']"/> %i18n:@messages%<i class="badge" v-if="$store.state.i.hasUnreadSpecifiedNotes"><fa icon="circle"/></i></span>
+					<span :data-active="src == 'mentions'" @click="src = 'mentions'"><fa icon="at"/> {{ $t('mentions') }}<i class="badge" v-if="$store.state.i.hasUnreadMentions"><fa icon="circle"/></i></span>
+					<span :data-active="src == 'messages'" @click="src = 'messages'"><fa :icon="['far', 'envelope']"/> {{ $t('messages') }}<i class="badge" v-if="$store.state.i.hasUnreadSpecifiedNotes"><fa icon="circle"/></i></span>
 					<template v-if="lists">
 						<div class="hr" v-if="lists.length > 0"></div>
 						<span v-for="l in lists" :data-active="src == 'list' && list == l" @click="src = 'list'; list = l" :key="l.id"><fa icon="list"/> {{ l.title }}</span>
@@ -61,10 +61,13 @@
 
 <script lang="ts">
 import Vue from 'vue';
+import i18n from '../../../i18n';
 import Progress from '../../../common/scripts/loading';
 import XTl from './home.timeline.vue';
 
 export default Vue.extend({
+	i18n: i18n('mobile/views/pages/home.vue'),
+
 	components: {
 		XTl
 	},
diff --git a/src/client/app/mobile/views/pages/messaging-room.vue b/src/client/app/mobile/views/pages/messaging-room.vue
index 9974b5689a..64feba43a2 100644
--- a/src/client/app/mobile/views/pages/messaging-room.vue
+++ b/src/client/app/mobile/views/pages/messaging-room.vue
@@ -10,9 +10,11 @@
 
 <script lang="ts">
 import Vue from 'vue';
+import i18n from '../../../i18n';
 import parseAcct from '../../../../../misc/acct/parse';
 
 export default Vue.extend({
+	i18n: i18n(),
 	data() {
 		return {
 			fetching: true,
@@ -47,7 +49,7 @@ export default Vue.extend({
 				this.user = user;
 				this.fetching = false;
 
-				document.title = `%i18n:@messaging%: ${Vue.filter('userName')(this.user)} | ${(this as any).os.instanceName}`;
+				document.title = `${this.$t('@.messaging')}: ${Vue.filter('userName')(this.user)} | ${(this as any).os.instanceName}`;
 			});
 		}
 	}
diff --git a/src/client/app/mobile/views/pages/messaging.vue b/src/client/app/mobile/views/pages/messaging.vue
index 4131d9ac94..790d2a6254 100644
--- a/src/client/app/mobile/views/pages/messaging.vue
+++ b/src/client/app/mobile/views/pages/messaging.vue
@@ -1,17 +1,19 @@
 <template>
 <mk-ui>
-	<span slot="header"><span style="margin-right:4px;"><fa :icon="['far', 'comments']"/></span>%i18n:@messaging%</span>
+	<span slot="header"><span style="margin-right:4px;"><fa :icon="['far', 'comments']"/></span>{{ $t('@.messaging') }}</span>
 	<mk-messaging @navigate="navigate" :header-top="48"/>
 </mk-ui>
 </template>
 
 <script lang="ts">
 import Vue from 'vue';
+import i18n from '../../../i18n';
 import getAcct from '../../../../../misc/acct/render';
 
 export default Vue.extend({
+	i18n: i18n(),
 	mounted() {
-		document.title = `${(this as any).os.instanceName} %i18n:@messaging%`;
+		document.title = `${(this as any).os.instanceName} ${this.$t('@.messaging')}`;
 	},
 	methods: {
 		navigate(user) {
diff --git a/src/client/app/mobile/views/pages/note.vue b/src/client/app/mobile/views/pages/note.vue
index 4e95bcf2be..efe9c0c0ff 100644
--- a/src/client/app/mobile/views/pages/note.vue
+++ b/src/client/app/mobile/views/pages/note.vue
@@ -1,13 +1,13 @@
 <template>
 <mk-ui>
-	<span slot="header"><span style="margin-right:4px;"><fa :icon="['far', 'sticky-note']"/></span>%i18n:@title%</span>
+	<span slot="header"><span style="margin-right:4px;"><fa :icon="['far', 'sticky-note']"/></span>{{ $t('title') }}</span>
 	<main v-if="!fetching">
 		<div>
 			<mk-note-detail :note="note"/>
 		</div>
 		<footer>
-			<router-link v-if="note.prev" :to="note.prev"><fa icon="angle-left"/> %i18n:@prev%</router-link>
-			<router-link v-if="note.next" :to="note.next">%i18n:@next% <fa icon="angle-right"/></router-link>
+			<router-link v-if="note.prev" :to="note.prev"><fa icon="angle-left"/> {{ $t('prev') }}</router-link>
+			<router-link v-if="note.next" :to="note.next">{{ $t('next') }} <fa icon="angle-right"/></router-link>
 		</footer>
 	</main>
 </mk-ui>
@@ -15,9 +15,11 @@
 
 <script lang="ts">
 import Vue from 'vue';
+import i18n from '../../../i18n';
 import Progress from '../../../common/scripts/loading';
 
 export default Vue.extend({
+	i18n: i18n('mobile/views/pages/note.vue'),
 	data() {
 		return {
 			fetching: true,
diff --git a/src/client/app/mobile/views/pages/notifications.vue b/src/client/app/mobile/views/pages/notifications.vue
index 54aa48a155..8fc61ad3cb 100644
--- a/src/client/app/mobile/views/pages/notifications.vue
+++ b/src/client/app/mobile/views/pages/notifications.vue
@@ -1,6 +1,6 @@
 <template>
 <mk-ui>
-	<span slot="header"><span style="margin-right:4px;"><fa :icon="['far', 'bell']"/></span>%i18n:@notifications%</span>
+	<span slot="header"><span style="margin-right:4px;"><fa :icon="['far', 'bell']"/></span>{{ $t('notifications') }}</span>
 	<template slot="func"><button @click="fn"><fa icon="check"/></button></template>
 
 	<main>
@@ -11,17 +11,19 @@
 
 <script lang="ts">
 import Vue from 'vue';
+import i18n from '../../../i18n';
 import Progress from '../../../common/scripts/loading';
 
 export default Vue.extend({
+	i18n: i18n('mobile/views/pages/notifications.vue'),
 	mounted() {
-		document.title = '%i18n:@notifications%';
+		document.title = this.$t('notifications');
 
 		Progress.start();
 	},
 	methods: {
 		fn() {
-			const ok = window.confirm('%i18n:@read-all%');
+			const ok = window.confirm(this.$t('read-all'));
 			if (!ok) return;
 
 			(this as any).api('notifications/mark_all_as_read');
diff --git a/src/client/app/mobile/views/pages/received-follow-requests.vue b/src/client/app/mobile/views/pages/received-follow-requests.vue
index 40e9c1cae2..08fe787921 100644
--- a/src/client/app/mobile/views/pages/received-follow-requests.vue
+++ b/src/client/app/mobile/views/pages/received-follow-requests.vue
@@ -1,12 +1,12 @@
 <template>
 <mk-ui>
-	<span slot="header"><fa :icon="['far', 'envelope']"/>%i18n:@title%</span>
+	<span slot="header"><fa :icon="['far', 'envelope']"/>{{ $t('title') }}</span>
 
 	<main>
 		<div v-for="req in requests">
 			<router-link :key="req.id" :to="req.follower | userPage">{{ req.follower | userName }}</router-link>
 			<span>
-				<a @click="accept(req.follower)">%i18n:@accept%</a>|<a @click="reject(req.follower)">%i18n:@reject%</a>
+				<a @click="accept(req.follower)">{{ $t('accept') }}</a>|<a @click="reject(req.follower)">{{ $t('reject') }}</a>
 			</span>
 		</div>
 	</main>
@@ -15,9 +15,11 @@
 
 <script lang="ts">
 import Vue from 'vue';
+import i18n from '../../../i18n';
 import Progress from '../../../common/scripts/loading';
 
 export default Vue.extend({
+	i18n: i18n('mobile/views/pages/received-follow-requests.vue'),
 	data() {
 		return {
 			fetching: true,
@@ -25,7 +27,7 @@ export default Vue.extend({
 		};
 	},
 	mounted() {
-		document.title = '%i18n:@title%';
+		document.title = this.$t('title');
 
 		Progress.start();
 
diff --git a/src/client/app/mobile/views/pages/search.vue b/src/client/app/mobile/views/pages/search.vue
index 0f680ac571..13b0469933 100644
--- a/src/client/app/mobile/views/pages/search.vue
+++ b/src/client/app/mobile/views/pages/search.vue
@@ -3,7 +3,7 @@
 	<span slot="header"><fa icon="search"/> {{ q }}</span>
 
 	<main>
-		<p :class="$style.empty" v-if="!fetching && empty"><fa icon="search"/> {{ '%i18n:not-found%'.split('{}')[0] }}{{ q }}{{ '%i18n:not-found%'.split('{}')[1] }}</p>
+		<p :class="$style.empty" v-if="!fetching && empty"><fa icon="search"/> {{ $t('not-found', { q }) }}</p>
 		<mk-notes ref="timeline" :more="existMore ? more : null"/>
 	</main>
 </mk-ui>
@@ -11,11 +11,13 @@
 
 <script lang="ts">
 import Vue from 'vue';
+import i18n from '../../../i18n';
 import Progress from '../../../common/scripts/loading';
 
 const limit = 20;
 
 export default Vue.extend({
+	i18n: i18n('mobile/views/pages/search.vue'),
 	data() {
 		return {
 			fetching: true,
diff --git a/src/client/app/mobile/views/pages/selectdrive.vue b/src/client/app/mobile/views/pages/selectdrive.vue
index 4083624200..633c83e40d 100644
--- a/src/client/app/mobile/views/pages/selectdrive.vue
+++ b/src/client/app/mobile/views/pages/selectdrive.vue
@@ -1,7 +1,7 @@
 <template>
 <div class="mk-selectdrive">
 	<header>
-		<h1>%i18n:@select-file%<span class="count" v-if="files.length > 0">({{ files.length }})</span></h1>
+		<h1>{{ $t('select-file') }}<span class="count" v-if="files.length > 0">({{ files.length }})</span></h1>
 		<button class="upload" @click="upload"><fa icon="upload"/></button>
 		<button v-if="multiple" class="ok" @click="ok"><fa icon="check"/></button>
 	</header>
@@ -11,8 +11,10 @@
 
 <script lang="ts">
 import Vue from 'vue';
+import i18n from '../../../i18n';
 
 export default Vue.extend({
+	i18n: i18n('mobile/views/pages/selectdrive.vue'),
 	data() {
 		return {
 			files: []
@@ -25,7 +27,7 @@ export default Vue.extend({
 		}
 	},
 	mounted() {
-		document.title = '%i18n:@title%';
+		document.title = this.$t('title');
 	},
 	methods: {
 		onSelected(file) {
diff --git a/src/client/app/mobile/views/pages/settings.vue b/src/client/app/mobile/views/pages/settings.vue
index fa80188904..702e5879e9 100644
--- a/src/client/app/mobile/views/pages/settings.vue
+++ b/src/client/app/mobile/views/pages/settings.vue
@@ -1,84 +1,84 @@
 <template>
 <mk-ui>
-	<span slot="header"><span style="margin-right:4px;"><fa icon="cog"/></span>%i18n:@settings%</span>
+	<span slot="header"><span style="margin-right:4px;"><fa icon="cog"/></span>{{ $t('settings') }}</span>
 	<main>
-		<div class="signin-as" v-html="'%i18n:@signed-in-as%'.replace('{}', `<b>${name}</b>`)"></div>
+		<div class="signin-as" v-html="this.$t('signed-in-as').replace('{}', `<b>${name}</b>`)"></div>
 
 		<div>
 			<mk-profile-editor/>
 
 			<ui-card>
-				<div slot="title"><fa icon="palette"/> %i18n:@theme%</div>
+				<div slot="title"><fa icon="palette"/> {{ $t('theme') }}</div>
 				<section>
 					<mk-theme/>
 				</section>
 			</ui-card>
 
 			<ui-card>
-				<div slot="title"><fa icon="poll-h"/> %i18n:@design%</div>
+				<div slot="title"><fa icon="poll-h"/> {{ $t('design') }}</div>
 
 				<section>
-					<ui-switch v-model="darkmode">%i18n:@dark-mode%</ui-switch>
-					<ui-switch v-model="circleIcons">%i18n:@circle-icons%</ui-switch>
-					<ui-switch v-model="reduceMotion">%i18n:common.reduce-motion% (%i18n:common.this-setting-is-this-device-only%)</ui-switch>
-					<ui-switch v-model="contrastedAcct">%i18n:@contrasted-acct%</ui-switch>
-					<ui-switch v-model="showFullAcct">%i18n:common.show-full-acct%</ui-switch>
-					<ui-switch v-model="useOsDefaultEmojis">%i18n:common.use-os-default-emojis%</ui-switch>
-					<ui-switch v-model="iLikeSushi">%i18n:common.i-like-sushi%</ui-switch>
-					<ui-switch v-model="disableAnimatedMfm">%i18n:common.disable-animated-mfm%</ui-switch>
-					<ui-switch v-model="alwaysShowNsfw">%i18n:common.always-show-nsfw% (%i18n:common.this-setting-is-this-device-only%)</ui-switch>
+					<ui-switch v-model="darkmode">{{ $t('dark-mode') }}</ui-switch>
+					<ui-switch v-model="circleIcons">{{ $t('circle-icons') }}</ui-switch>
+					<ui-switch v-model="reduceMotion">{{ $t('@.reduce-motion') }} ({{ $t('@.this-setting-is-this-device-only') }})</ui-switch>
+					<ui-switch v-model="contrastedAcct">{{ $t('contrasted-acct') }}</ui-switch>
+					<ui-switch v-model="showFullAcct">{{ $t('@.show-full-acct') }}</ui-switch>
+					<ui-switch v-model="useOsDefaultEmojis">{{ $t('@.use-os-default-emojis') }}</ui-switch>
+					<ui-switch v-model="iLikeSushi">{{ $t('@.i-like-sushi') }}</ui-switch>
+					<ui-switch v-model="disableAnimatedMfm">{{ $t('@.disable-animated-mfm') }}</ui-switch>
+					<ui-switch v-model="alwaysShowNsfw">{{ $t('@.always-show-nsfw') }} ({{ $t('@.this-setting-is-this-device-only') }})</ui-switch>
 				</section>
 
 				<section>
-					<ui-switch v-model="games_reversi_showBoardLabels">%i18n:common.show-reversi-board-labels%</ui-switch>
-					<ui-switch v-model="games_reversi_useContrastStones">%i18n:common.use-contrast-reversi-stones%</ui-switch>
+					<ui-switch v-model="games_reversi_showBoardLabels">{{ $t('@.show-reversi-board-labels') }}</ui-switch>
+					<ui-switch v-model="games_reversi_useContrastStones">{{ $t('@.use-contrast-reversi-stones') }}</ui-switch>
 				</section>
 
 				<section>
-					<header>%i18n:@timeline%</header>
+					<header>{{ $t('timeline') }}</header>
 					<div>
-						<ui-switch v-model="showReplyTarget">%i18n:@show-reply-target%</ui-switch>
-						<ui-switch v-model="showMyRenotes">%i18n:@show-my-renotes%</ui-switch>
-						<ui-switch v-model="showRenotedMyNotes">%i18n:@show-renoted-my-notes%</ui-switch>
-						<ui-switch v-model="showLocalRenotes">%i18n:@show-local-renotes%</ui-switch>
+						<ui-switch v-model="showReplyTarget">{{ $t('show-reply-target') }}</ui-switch>
+						<ui-switch v-model="showMyRenotes">{{ $t('show-my-renotes') }}</ui-switch>
+						<ui-switch v-model="showRenotedMyNotes">{{ $t('show-renoted-my-notes') }}</ui-switch>
+						<ui-switch v-model="showLocalRenotes">{{ $t('show-local-renotes') }}</ui-switch>
 					</div>
 				</section>
 
 				<section>
-					<header>%i18n:@post-style%</header>
-					<ui-radio v-model="postStyle" value="standard">%i18n:@post-style-standard%</ui-radio>
-					<ui-radio v-model="postStyle" value="smart">%i18n:@post-style-smart%</ui-radio>
+					<header>{{ $t('post-style') }}</header>
+					<ui-radio v-model="postStyle" value="standard">{{ $t('post-style-standard') }}</ui-radio>
+					<ui-radio v-model="postStyle" value="smart">{{ $t('post-style-smart') }}</ui-radio>
 				</section>
 
 				<section>
-					<header>%i18n:@notification-position%</header>
-					<ui-radio v-model="mobileNotificationPosition" value="bottom">%i18n:@notification-position-bottom%</ui-radio>
-					<ui-radio v-model="mobileNotificationPosition" value="top">%i18n:@notification-position-top%</ui-radio>
+					<header>{{ $t('notification-position') }}</header>
+					<ui-radio v-model="mobileNotificationPosition" value="bottom">{{ $t('notification-position-bottom') }}</ui-radio>
+					<ui-radio v-model="mobileNotificationPosition" value="top">{{ $t('notification-position-top') }}</ui-radio>
 				</section>
 			</ui-card>
 
 			<ui-card>
-				<div slot="title"><fa icon="sliders-h"/> %i18n:@behavior%</div>
+				<div slot="title"><fa icon="sliders-h"/> {{ $t('behavior') }}</div>
 
 				<section>
-					<ui-switch v-model="fetchOnScroll">%i18n:@fetch-on-scroll%</ui-switch>
-					<ui-switch v-model="disableViaMobile">%i18n:@disable-via-mobile%</ui-switch>
-					<ui-switch v-model="loadRawImages">%i18n:@load-raw-images%</ui-switch>
-					<ui-switch v-model="loadRemoteMedia">%i18n:@load-remote-media%</ui-switch>
-					<ui-switch v-model="lightmode">%i18n:@i-am-under-limited-internet%</ui-switch>
+					<ui-switch v-model="fetchOnScroll">{{ $t('fetch-on-scroll') }}</ui-switch>
+					<ui-switch v-model="disableViaMobile">{{ $t('disable-via-mobile') }}</ui-switch>
+					<ui-switch v-model="loadRawImages">{{ $t('load-raw-images') }}</ui-switch>
+					<ui-switch v-model="loadRemoteMedia">{{ $t('load-remote-media') }}</ui-switch>
+					<ui-switch v-model="lightmode">{{ $t('i-am-under-limited-internet') }}</ui-switch>
 				</section>
 
 				<section>
-					<header>%i18n:@note-visibility%</header>
-					<ui-switch v-model="rememberNoteVisibility">%i18n:@remember-note-visibility%</ui-switch>
+					<header>{{ $t('note-visibility') }}</header>
+					<ui-switch v-model="rememberNoteVisibility">{{ $t('remember-note-visibility') }}</ui-switch>
 					<section>
-						<header>%i18n:@default-note-visibility%</header>
+						<header>{{ $t('default-note-visibility') }}</header>
 						<ui-select v-model="defaultNoteVisibility">
-							<option value="public">%i18n:common.note-visibility.public%</option>
-							<option value="home">%i18n:common.note-visibility.home%</option>
-							<option value="followers">%i18n:common.note-visibility.followers%</option>
-							<option value="specified">%i18n:common.note-visibility.specified%</option>
-							<option value="private">%i18n:common.note-visibility.private%</option>
+							<option value="public">{{ $t('@.note-visibility.public') }}</option>
+							<option value="home">{{ $t('@.note-visibility.home') }}</option>
+							<option value="followers">{{ $t('@.note-visibility.followers') }}</option>
+							<option value="specified">{{ $t('@.note-visibility.specified') }}</option>
+							<option value="private">{{ $t('@.note-visibility.private') }}</option>
 						</ui-select>
 					</section>
 				</section>
@@ -89,52 +89,52 @@
 			<mk-mute-and-block/>
 
 			<ui-card>
-				<div slot="title"><fa icon="volume-up"/> %i18n:@sound%</div>
+				<div slot="title"><fa icon="volume-up"/> {{ $t('sound') }}</div>
 
 				<section>
-					<ui-switch v-model="enableSounds">%i18n:@enable-sounds%</ui-switch>
+					<ui-switch v-model="enableSounds">{{ $t('enable-sounds') }}</ui-switch>
 				</section>
 			</ui-card>
 
 			<ui-card>
-				<div slot="title"><fa icon="language"/> %i18n:@lang%</div>
+				<div slot="title"><fa icon="language"/> {{ $t('lang') }}</div>
 
 				<section class="fit-top">
-					<ui-select v-model="lang" placeholder="%i18n:@auto%">
-						<optgroup label="%i18n:@recommended%">
-							<option value="">%i18n:@auto%</option>
+					<ui-select v-model="lang" :placeholder="$t('placeholder')">
+						<optgroup :label="$t('label')">
+							<option value="">{{ $t('auto') }}</option>
 						</optgroup>
 
-						<optgroup label="%i18n:@specify-language%">
+						<optgroup :label="$t('label')">
 							<option v-for="x in langs" :value="x[0]" :key="x[0]">{{ x[1] }}</option>
 						</optgroup>
 					</ui-select>
-					<span><fa icon="info-circle"/> %i18n:@lang-tip%</span>
+					<span><fa icon="info-circle"/> {{ $t('lang-tip') }}</span>
 				</section>
 			</ui-card>
 
 			<ui-card>
-				<div slot="title"><fa :icon="['fab', 'twitter']"/> %i18n:@twitter%</div>
+				<div slot="title"><fa :icon="['fab', 'twitter']"/> {{ $t('twitter') }}</div>
 
 				<section>
 					<p class="account" v-if="$store.state.i.twitter"><a :href="`https://twitter.com/${$store.state.i.twitter.screenName}`" target="_blank">@{{ $store.state.i.twitter.screenName }}</a></p>
 					<p>
-						<a :href="`${apiUrl}/connect/twitter`" target="_blank">{{ $store.state.i.twitter ? '%i18n:@twitter-reconnect%' : '%i18n:@twitter-connect%' }}</a>
+						<a :href="`${apiUrl}/connect/twitter`" target="_blank">{{ $store.state.i.twitter ? this.$t('twitter-reconnect') : this.$t('twitter-connect') }}</a>
 						<span v-if="$store.state.i.twitter"> or </span>
-						<a :href="`${apiUrl}/disconnect/twitter`" target="_blank" v-if="$store.state.i.twitter">%i18n:@twitter-disconnect%</a>
+						<a :href="`${apiUrl}/disconnect/twitter`" target="_blank" v-if="$store.state.i.twitter">{{ $t('twitter-disconnect') }}</a>
 					</p>
 				</section>
 			</ui-card>
 
 			<ui-card>
-				<div slot="title"><fa :icon="['fab', 'github']"/> %i18n:@github%</div>
+				<div slot="title"><fa :icon="['fab', 'github']"/> {{ $t('github') }}</div>
 
 				<section>
 					<p class="account" v-if="$store.state.i.github"><a :href="`https://github.com/${$store.state.i.github.login}`" target="_blank">@{{ $store.state.i.github.login }}</a></p>
 					<p>
-						<a :href="`${apiUrl}/connect/github`" target="_blank">{{ $store.state.i.github ? '%i18n:@github-reconnect%' : '%i18n:@github-connect%' }}</a>
+						<a :href="`${apiUrl}/connect/github`" target="_blank">{{ $store.state.i.github ? this.$t('github-reconnect') : this.$t('github-connect') }}</a>
 						<span v-if="$store.state.i.github"> or </span>
-						<a :href="`${apiUrl}/disconnect/github`" target="_blank" v-if="$store.state.i.github">%i18n:@github-disconnect%</a>
+						<a :href="`${apiUrl}/disconnect/github`" target="_blank" v-if="$store.state.i.github">{{ $t('github-disconnect') }}</a>
 					</p>
 				</section>
 			</ui-card>
@@ -142,29 +142,29 @@
 			<mk-api-settings />
 
 			<ui-card>
-				<div slot="title"><fa icon="unlock-alt"/> %i18n:@password%</div>
+				<div slot="title"><fa icon="unlock-alt"/> {{ $t('password') }}</div>
 				<section>
 					<mk-password-settings/>
 				</section>
 			</ui-card>
 
 			<ui-card>
-				<div slot="title"><fa icon="sync-alt"/> %i18n:@update%</div>
+				<div slot="title"><fa icon="sync-alt"/> {{ $t('update') }}</div>
 
 				<section>
-					<div>%i18n:@version% <i>{{ version }}</i></div>
+					<div>{{ $t('version') }} <i>{{ version }}</i></div>
 					<template v-if="latestVersion !== undefined">
-						<div>%i18n:@latest-version% <i>{{ latestVersion ? latestVersion : version }}</i></div>
+						<div>{{ $t('latest-version') }} <i>{{ latestVersion ? latestVersion : version }}</i></div>
 					</template>
 					<ui-button @click="checkForUpdate" :disabled="checkingForUpdate">
-						<template v-if="checkingForUpdate">%i18n:@update-checking%<mk-ellipsis/></template>
-						<template v-else>%i18n:@check-for-updates%</template>
+						<template v-if="checkingForUpdate">{{ $t('update-checking') }}<mk-ellipsis/></template>
+						<template v-else>{{ $t('check-for-updates') }}</template>
 					</ui-button>
 				</section>
 			</ui-card>
 		</div>
 
-		<div class="signout" @click="signout">%i18n:@signout%</div>
+		<div class="signout" @click="signout">{{ $t('signout') }}</div>
 
 		<footer>
 			<small>ver {{ version }} ({{ codename }})</small>
@@ -175,10 +175,12 @@
 
 <script lang="ts">
 import Vue from 'vue';
+import i18n from '../../../i18n';
 import { apiUrl, clientVersion as version, codename, langs } from '../../../config';
 import checkForUpdate from '../../../common/scripts/check-for-update';
 
 export default Vue.extend({
+	i18n: i18n('mobile/views/pages/settings.vue'),
 	data() {
 		return {
 			apiUrl,
@@ -327,7 +329,7 @@ export default Vue.extend({
 	},
 
 	mounted() {
-		document.title = '%i18n:@settings%';
+		document.title = this.$t('settings');
 	},
 
 	methods: {
@@ -342,13 +344,13 @@ export default Vue.extend({
 				this.latestVersion = newer;
 				if (newer == null) {
 					(this as any).apis.dialog({
-						title: '%i18n:@no-updates%',
-						text: '%i18n:@no-updates-desc%'
+						title: this.$t('no-updates'),
+						text: this.$t('no-updates-desc')
 					});
 				} else {
 					(this as any).apis.dialog({
-						title: '%i18n:@update-available%',
-						text: '%i18n:@update-available-desc%'
+						title: this.$t('update-available'),
+						text: this.$t('update-available-desc')
 					});
 				}
 			});
diff --git a/src/client/app/mobile/views/pages/share.vue b/src/client/app/mobile/views/pages/share.vue
index 246a8afa4d..83535b39c1 100644
--- a/src/client/app/mobile/views/pages/share.vue
+++ b/src/client/app/mobile/views/pages/share.vue
@@ -1,19 +1,21 @@
 <template>
 <div class="azibmfpleajagva420swmu4c3r7ni7iw">
-	<h1>{{ '%i18n:@share-with%'.replace('{}', name) }}</h1>
+	<h1>{{ $t('share-with', { name }) }}</h1>
 	<div>
 		<mk-signin v-if="!$store.getters.isSignedIn"/>
 		<mk-post-form v-else-if="!posted" :initial-text="text" :instant="true" @posted="posted = true"/>
 		<p v-if="posted" class="posted"><fa icon="check"/></p>
 	</div>
-	<ui-button class="close" v-if="posted" @click="close">%i18n:common.close%</ui-button>
+	<ui-button class="close" v-if="posted" @click="close">{{ $t('@.close') }}</ui-button>
 </div>
 </template>
 
 <script lang="ts">
 import Vue from 'vue';
+import i18n from '../../../i18n';
 
 export default Vue.extend({
+	i18n: i18n('mobile/views/pages/share.vue'),
 	data() {
 		return {
 			name: null,
diff --git a/src/client/app/mobile/views/pages/signup.vue b/src/client/app/mobile/views/pages/signup.vue
index aac2ee431d..65b02e967d 100644
--- a/src/client/app/mobile/views/pages/signup.vue
+++ b/src/client/app/mobile/views/pages/signup.vue
@@ -1,13 +1,15 @@
 <template>
 <div class="signup">
-	<h1>%i18n:@lets-start%</h1>
+	<h1>{{ $t('lets-start') }}</h1>
 	<mk-signup/>
 </div>
 </template>
 
 <script lang="ts">
 import Vue from 'vue';
-export default Vue.extend({});
+import i18n from '../../../i18n';
+export default Vue.extend({
+	i18n: i18n('mobile/views/pages/signup.vue'),});
 </script>
 
 <style lang="stylus" scoped>
diff --git a/src/client/app/mobile/views/pages/tag.vue b/src/client/app/mobile/views/pages/tag.vue
index 8f70a647ec..9a0c891920 100644
--- a/src/client/app/mobile/views/pages/tag.vue
+++ b/src/client/app/mobile/views/pages/tag.vue
@@ -3,7 +3,7 @@
 	<span slot="header"><span style="margin-right:4px;"><fa icon="hashtag"/></span>{{ $route.params.tag }}</span>
 
 	<main>
-		<p v-if="!fetching && empty"><fa icon="search"/> {{ '%i18n:no-posts-found%'.split('{}')[0] }}{{ q }}{{ '%i18n:no-posts-found%'.split('{}')[1] }}</p>
+		<p v-if="!fetching && empty"><fa icon="search"/> {{ $t('no-posts-found', { q }) }}</p>
 		<mk-notes ref="timeline" :more="existMore ? more : null"/>
 	</main>
 </mk-ui>
@@ -11,11 +11,13 @@
 
 <script lang="ts">
 import Vue from 'vue';
+import i18n from '../../../i18n';
 import Progress from '../../../common/scripts/loading';
 
 const limit = 20;
 
 export default Vue.extend({
+	i18n: i18n('mobile/views/pages/tag.vue'),
 	data() {
 		return {
 			fetching: true,
diff --git a/src/client/app/mobile/views/pages/user-lists.vue b/src/client/app/mobile/views/pages/user-lists.vue
index 84f07e8a99..3844bbbf7e 100644
--- a/src/client/app/mobile/views/pages/user-lists.vue
+++ b/src/client/app/mobile/views/pages/user-lists.vue
@@ -1,6 +1,6 @@
 <template>
 <mk-ui>
-	<span slot="header"><fa icon="list"/>%i18n:@title%</span>
+	<span slot="header"><fa icon="list"/>{{ $t('title') }}</span>
 	<template slot="func"><button @click="fn"><fa icon="plus"/></button></template>
 
 	<main>
@@ -13,9 +13,11 @@
 
 <script lang="ts">
 import Vue from 'vue';
+import i18n from '../../../i18n';
 import Progress from '../../../common/scripts/loading';
 
 export default Vue.extend({
+	i18n: i18n('mobile/views/pages/user-lists.vue'),
 	data() {
 		return {
 			fetching: true,
@@ -23,7 +25,7 @@ export default Vue.extend({
 		};
 	},
 	mounted() {
-		document.title = '%i18n:@title%';
+		document.title = this.$t('title');
 
 		Progress.start();
 
@@ -37,7 +39,7 @@ export default Vue.extend({
 	methods: {
 		fn() {
 			(this as any).apis.input({
-				title: '%i18n:@enter-list-name%',
+				title: this.$t('enter-list-name'),
 			}).then(async title => {
 				const list = await (this as any).api('users/lists/create', {
 					title
diff --git a/src/client/app/mobile/views/pages/user.vue b/src/client/app/mobile/views/pages/user.vue
index a3295d5ccf..9e79115896 100644
--- a/src/client/app/mobile/views/pages/user.vue
+++ b/src/client/app/mobile/views/pages/user.vue
@@ -2,8 +2,8 @@
 <mk-ui>
 	<template slot="header" v-if="!fetching"><img :src="user.avatarUrl" alt="">{{ user | userName }}</template>
 	<main v-if="!fetching">
-		<div class="is-suspended" v-if="user.isSuspended"><p><fa icon="exclamation-triangle"/> %i18n:@is-suspended%</p></div>
-		<div class="is-remote" v-if="user.host != null"><p><fa icon="exclamation-triangle"/> %i18n:common.is-remote-user%<a :href="user.url || user.uri" target="_blank">%i18n:common.view-on-remote%</a></p></div>
+		<div class="is-suspended" v-if="user.isSuspended"><p><fa icon="exclamation-triangle"/> {{ $t('is-suspended') }}</p></div>
+		<div class="is-remote" v-if="user.host != null"><p><fa icon="exclamation-triangle"/> {{ $t('@.is-remote-user') }}<a :href="user.url || user.uri" target="_blank">{{ $t('@.view-on-remote') }}</a></p></div>
 		<header>
 			<div class="banner" :style="style"></div>
 			<div class="body">
@@ -17,7 +17,7 @@
 				<div class="title">
 					<h1>{{ user | userName }}</h1>
 					<span class="username"><mk-acct :user="user" :detail="true" /></span>
-					<span class="followed" v-if="user.isFollowed">%i18n:@follows-you%</span>
+					<span class="followed" v-if="user.isFollowed">{{ $t('follows-you') }}</span>
 				</div>
 				<div class="description">
 					<misskey-flavored-markdown v-if="user.description" :text="user.description" :i="$store.state.i"/>
@@ -33,24 +33,24 @@
 				<div class="status">
 					<a>
 						<b>{{ user.notesCount | number }}</b>
-						<i>%i18n:@notes%</i>
+						<i>{{ $t('notes') }}</i>
 					</a>
 					<a :href="user | userPage('following')">
 						<b>{{ user.followingCount | number }}</b>
-						<i>%i18n:@following%</i>
+						<i>{{ $t('following') }}</i>
 					</a>
 					<a :href="user | userPage('followers')">
 						<b>{{ user.followersCount | number }}</b>
-						<i>%i18n:@followers%</i>
+						<i>{{ $t('followers') }}</i>
 					</a>
 				</div>
 			</div>
 		</header>
 		<nav>
 			<div class="nav-container">
-				<a :data-active="page == 'home'" @click="page = 'home'"><fa icon="home"/> %i18n:@overview%</a>
-				<a :data-active="page == 'notes'" @click="page = 'notes'"><fa :icon="['far', 'comment-alt']"/> %i18n:@timeline%</a>
-				<a :data-active="page == 'media'" @click="page = 'media'"><fa icon="image"/> %i18n:@media%</a>
+				<a :data-active="page == 'home'" @click="page = 'home'"><fa icon="home"/> {{ $t('overview') }}</a>
+				<a :data-active="page == 'notes'" @click="page = 'notes'"><fa :icon="['far', 'comment-alt']"/> {{ $t('timeline') }}</a>
+				<a :data-active="page == 'media'" @click="page = 'media'"><fa icon="image"/> {{ $t('media') }}</a>
 			</div>
 		</nav>
 		<div class="body">
@@ -64,6 +64,7 @@
 
 <script lang="ts">
 import Vue from 'vue';
+import i18n from '../../../i18n';
 import * as age from 's-age';
 import parseAcct from '../../../../../misc/acct/parse';
 import Progress from '../../../common/scripts/loading';
@@ -71,6 +72,7 @@ import Menu from '../../../common/views/components/menu.vue';
 import XHome from './user/home.vue';
 
 export default Vue.extend({
+	i18n: i18n('mobile/views/pages/user.vue'),
 	components: {
 		XHome
 	},
@@ -115,7 +117,7 @@ export default Vue.extend({
 		menu() {
 			let menu = [{
 				icon: this.user.isMuted ? '<fa icon="eye"/>' : '<fa icon="eye-slash"/>',
-				text: this.user.isMuted ? '%i18n:@unmute%' : '%i18n:@mute%',
+				text: this.user.isMuted ? this.$t('unmute') : this.$t('mute'),
 				action: () => {
 					if (this.user.isMuted) {
 						(this as any).api('mute/delete', {
@@ -137,7 +139,7 @@ export default Vue.extend({
 				}
 			}, {
 				icon: this.user.isBlocking ? '<fa icon="user"/>' : '<fa icon="user-slash"/>',
-				text: this.user.isBlocking ? '%i18n:@unblock%' : '%i18n:@block%',
+				text: this.user.isBlocking ? this.$t('unblock') : this.$t('block'),
 				action: () => {
 					if (this.user.isBlocking) {
 						(this as any).api('blocking/delete', {
diff --git a/src/client/app/mobile/views/pages/user/home.followers-you-know.vue b/src/client/app/mobile/views/pages/user/home.followers-you-know.vue
index d25d3889ef..938a565aef 100644
--- a/src/client/app/mobile/views/pages/user/home.followers-you-know.vue
+++ b/src/client/app/mobile/views/pages/user/home.followers-you-know.vue
@@ -1,19 +1,21 @@
 <template>
 <div class="root followers-you-know">
-	<p class="initializing" v-if="fetching"><fa icon="spinner .pulse" fixed-width/>%i18n:@loading%<mk-ellipsis/></p>
+	<p class="initializing" v-if="fetching"><fa icon="spinner .pulse" fixed-width/>{{ $t('@.loading') }}<mk-ellipsis/></p>
 	<div v-if="!fetching && users.length > 0">
 		<a v-for="user in users" :key="user.id" :href="user | userPage">
 			<img :src="user.avatarUrl" :alt="user | userName"/>
 		</a>
 	</div>
-	<p class="empty" v-if="!fetching && users.length == 0">%i18n:@no-users%</p>
+	<p class="empty" v-if="!fetching && users.length == 0">{{ $t('no-users') }}</p>
 </div>
 </template>
 
 <script lang="ts">
 import Vue from 'vue';
+import i18n from '../../../../i18n';
 
 export default Vue.extend({
+	i18n: i18n('mobile/views/pages/user/home.followers-you-know.vue'),
 	props: ['user'],
 	data() {
 		return {
diff --git a/src/client/app/mobile/views/pages/user/home.friends.vue b/src/client/app/mobile/views/pages/user/home.friends.vue
index 29f64116bc..d18a1e049e 100644
--- a/src/client/app/mobile/views/pages/user/home.friends.vue
+++ b/src/client/app/mobile/views/pages/user/home.friends.vue
@@ -1,16 +1,18 @@
 <template>
 <div class="root friends">
-	<p class="fetching" v-if="fetching"><fa icon="spinner .pulse" fixed-width/>%i18n:@loading%<mk-ellipsis/></p>
+	<p class="fetching" v-if="fetching"><fa icon="spinner .pulse" fixed-width/>{{ $t('@.loading') }}<mk-ellipsis/></p>
 	<div v-if="!fetching && users.length > 0">
 		<mk-user-card v-for="user in users" :key="user.id" :user="user"/>
 	</div>
-	<p class="empty" v-if="!fetching && users.length == 0">%i18n:@no-users%</p>
+	<p class="empty" v-if="!fetching && users.length == 0">{{ $t('no-users') }}</p>
 </div>
 </template>
 
 <script lang="ts">
 import Vue from 'vue';
+import i18n from '../../../../i18n';
 export default Vue.extend({
+	i18n: i18n('mobile/views/pages/user/home.friends.vue'),
 	props: ['user'],
 	data() {
 		return {
diff --git a/src/client/app/mobile/views/pages/user/home.notes.vue b/src/client/app/mobile/views/pages/user/home.notes.vue
index bb7132cc4a..d2ffa275a5 100644
--- a/src/client/app/mobile/views/pages/user/home.notes.vue
+++ b/src/client/app/mobile/views/pages/user/home.notes.vue
@@ -1,16 +1,18 @@
 <template>
 <div class="root notes">
-	<p class="fetching" v-if="fetching"><fa icon="spinner .pulse" fixed-width/>%i18n:@loading%<mk-ellipsis/></p>
+	<p class="fetching" v-if="fetching"><fa icon="spinner .pulse" fixed-width/>{{ $t('@.loading') }}<mk-ellipsis/></p>
 	<div v-if="!fetching && notes.length > 0">
 		<mk-note-card v-for="note in notes" :key="note.id" :note="note"/>
 	</div>
-	<p class="empty" v-if="!fetching && notes.length == 0">%i18n:@no-notes%</p>
+	<p class="empty" v-if="!fetching && notes.length == 0">{{ $t('no-notes') }}</p>
 </div>
 </template>
 
 <script lang="ts">
 import Vue from 'vue';
+import i18n from '../../../../i18n';
 export default Vue.extend({
+	i18n: i18n('mobile/views/pages/user/home.notes.vue'),
 	props: ['user'],
 	data() {
 		return {
diff --git a/src/client/app/mobile/views/pages/user/home.photos.vue b/src/client/app/mobile/views/pages/user/home.photos.vue
index e30b16cc7f..6aa78a5414 100644
--- a/src/client/app/mobile/views/pages/user/home.photos.vue
+++ b/src/client/app/mobile/views/pages/user/home.photos.vue
@@ -1,6 +1,6 @@
 <template>
 <div class="root photos">
-	<p class="initializing" v-if="fetching"><fa icon="spinner .pulse" fixed-width/>%i18n:@loading%<mk-ellipsis/></p>
+	<p class="initializing" v-if="fetching"><fa icon="spinner .pulse" fixed-width/>{{ $t('@.loading') }}<mk-ellipsis/></p>
 	<div class="stream" v-if="!fetching && images.length > 0">
 		<a v-for="image in images"
 			class="img"
@@ -8,14 +8,16 @@
 			:href="image.note | notePage"
 		></a>
 	</div>
-	<p class="empty" v-if="!fetching && images.length == 0">%i18n:@no-photos%</p>
+	<p class="empty" v-if="!fetching && images.length == 0">{{ $t('no-photos') }}</p>
 </div>
 </template>
 
 <script lang="ts">
 import Vue from 'vue';
+import i18n from '../../../../i18n';
 
 export default Vue.extend({
+	i18n: i18n('mobile/views/pages/user/home.photos.vue'),
 	props: ['user'],
 	data() {
 		return {
diff --git a/src/client/app/mobile/views/pages/user/home.vue b/src/client/app/mobile/views/pages/user/home.vue
index 27d751260a..f89b04ae9d 100644
--- a/src/client/app/mobile/views/pages/user/home.vue
+++ b/src/client/app/mobile/views/pages/user/home.vue
@@ -2,47 +2,49 @@
 <div class="root home">
 	<mk-note-detail v-for="n in user.pinnedNotes" :key="n.id" :note="n" :compact="true"/>
 	<section class="recent-notes">
-		<h2><fa :icon="['far', 'comments']"/>%i18n:@recent-notes%</h2>
+		<h2><fa :icon="['far', 'comments']"/>{{ $t('recent-notes') }}</h2>
 		<div>
 			<x-notes :user="user"/>
 		</div>
 	</section>
 	<section class="images">
-		<h2><fa icon="image"/>%i18n:@images%</h2>
+		<h2><fa icon="image"/>{{ $t('images') }}</h2>
 		<div>
 			<x-photos :user="user"/>
 		</div>
 	</section>
 	<section class="activity">
-		<h2><fa icon="chart-bar"/>%i18n:@activity%</h2>
+		<h2><fa icon="chart-bar"/>{{ $t('activity') }}</h2>
 		<div>
 			<mk-activity :user="user"/>
 		</div>
 	</section>
 	<section class="frequently-replied-users">
-		<h2><fa icon="users"/>%i18n:@frequently-replied-users%</h2>
+		<h2><fa icon="users"/>{{ $t('frequently-replied-users') }}</h2>
 		<div>
 			<x-friends :user="user"/>
 		</div>
 	</section>
 	<section class="followers-you-know" v-if="$store.getters.isSignedIn && $store.state.i.id !== user.id">
-		<h2><fa icon="users"/>%i18n:@followers-you-know%</h2>
+		<h2><fa icon="users"/>{{ $t('followers-you-know') }}</h2>
 		<div>
 			<x-followers-you-know :user="user"/>
 		</div>
 	</section>
-	<p v-if="user.host === null">%i18n:@last-used-at%: <b><mk-time :time="user.lastUsedAt"/></b></p>
+	<p v-if="user.host === null">{{ $t('last-used-at') }}: <b><mk-time :time="user.lastUsedAt"/></b></p>
 </div>
 </template>
 
 <script lang="ts">
 import Vue from 'vue';
+import i18n from '../../../../i18n';
 import XNotes from './home.notes.vue';
 import XPhotos from './home.photos.vue';
 import XFriends from './home.friends.vue';
 import XFollowersYouKnow from './home.followers-you-know.vue';
 
 export default Vue.extend({
+	i18n: i18n('mobile/views/pages/user/home.vue'),
 	components: {
 		XNotes,
 		XPhotos,
diff --git a/src/client/app/mobile/views/pages/welcome.vue b/src/client/app/mobile/views/pages/welcome.vue
index c235cac690..c09ba1a78a 100644
--- a/src/client/app/mobile/views/pages/welcome.vue
+++ b/src/client/app/mobile/views/pages/welcome.vue
@@ -7,8 +7,8 @@
 		<p class="host">{{ host }}</p>
 		<div class="about">
 			<h2>{{ name }}</h2>
-			<p v-html="description || '%i18n:common.about%'"></p>
-			<router-link class="signup" to="/signup">%i18n:@signup%</router-link>
+			<p v-html="description || this.$t('@.about')"></p>
+			<router-link class="signup" to="/signup">{{ $t('signup') }}</router-link>
 		</div>
 		<div class="login">
 			<mk-signin :with-avatar="false"/>
@@ -33,32 +33,32 @@
 			</article>
 		</div>
 		<article class="about-misskey">
-			<h1>%i18n:common.intro.title%</h1>
-			<p v-html="'%i18n:common.intro.about%'"></p>
+			<h1>{{ $t('@.intro.title') }}</h1>
+			<p v-html="this.$t('@.intro.about')"></p>
 			<section>
-				<h2>%i18n:common.intro.features%</h2>
+				<h2>{{ $t('@.intro.features') }}</h2>
 				<section>
-					<h3>%i18n:common.intro.rich-contents%</h3>
+					<h3>{{ $t('@.intro.rich-contents') }}</h3>
 					<div class="image"><img src="/assets/about/post.png" alt=""></div>
-					<p v-html="'%i18n:common.intro.rich-contents-desc%'"></p>
+					<p v-html="this.$t('@.intro.rich-contents-desc')"></p>
 				</section>
 				<section>
-					<h3>%i18n:common.intro.reaction%</h3>
+					<h3>{{ $t('@.intro.reaction') }}</h3>
 					<div class="image"><img src="/assets/about/reaction.png" alt=""></div>
-					<p v-html="'%i18n:common.intro.reaction-desc%'"></p>
+					<p v-html="this.$t('@.intro.reaction-desc')"></p>
 				</section>
 				<section>
-					<h3>%i18n:common.intro.ui%</h3>
+					<h3>{{ $t('@.intro.ui') }}</h3>
 					<div class="image"><img src="/assets/about/ui.png" alt=""></div>
-					<p v-html="'%i18n:common.intro.ui-desc%'"></p>
+					<p v-html="this.$t('@.intro.ui-desc')"></p>
 				</section>
 				<section>
-					<h3>%i18n:common.intro.drive%</h3>
+					<h3>{{ $t('@.intro.drive') }}</h3>
 					<div class="image"><img src="/assets/about/drive.png" alt=""></div>
-					<p v-html="'%i18n:common.intro.drive-desc%'"></p>
+					<p v-html="this.$t('@.intro.drive-desc')"></p>
 				</section>
 			</section>
-			<p v-html="'%i18n:common.intro.outro%'"></p>
+			<p v-html="this.$t('@.intro.outro')"></p>
 		</article>
 		<div class="info" v-if="meta">
 			<p>Version: <b>{{ meta.version }}</b></p>
@@ -73,10 +73,12 @@
 
 <script lang="ts">
 import Vue from 'vue';
+import i18n from '../../../i18n';
 import { copyright, host } from '../../../config';
 import { concat } from '../../../../../prelude/array';
 
 export default Vue.extend({
+	i18n: i18n('mobile/views/pages/welcome.vue'),
 	data() {
 		return {
 			meta: null,
diff --git a/src/client/app/mobile/views/pages/widgets.vue b/src/client/app/mobile/views/pages/widgets.vue
index cd4c75f15a..be6604b8a9 100644
--- a/src/client/app/mobile/views/pages/widgets.vue
+++ b/src/client/app/mobile/views/pages/widgets.vue
@@ -1,6 +1,6 @@
 <template>
 <mk-ui>
-	<span slot="header"><span style="margin-right:4px;"><fa icon="home"/></span>%i18n:@dashboard%</span>
+	<span slot="header"><span style="margin-right:4px;"><fa icon="home"/></span>{{ $t('dashboard') }}</span>
 	<template slot="func">
 		<button @click="customizing = !customizing"><fa icon="cog"/></button>
 	</template>
@@ -8,24 +8,24 @@
 		<template v-if="customizing">
 			<header>
 				<select v-model="widgetAdderSelected">
-					<option value="profile">%i18n:common.widgets.profile%</option>
-					<option value="analog-clock">%i18n:common.widgets.analog-clock%</option>
-					<option value="calendar">%i18n:common.widgets.calendar%</option>
-					<option value="activity">%i18n:common.widgets.activity%</option>
-					<option value="rss">%i18n:common.widgets.rss%</option>
-					<option value="photo-stream">%i18n:common.widgets.photo-stream%</option>
-					<option value="slideshow">%i18n:common.widgets.slideshow%</option>
-					<option value="hashtags">%i18n:common.widgets.hashtags%</option>
-					<option value="posts-monitor">%i18n:common.widgets.posts-monitor%</option>
-					<option value="version">%i18n:common.widgets.version%</option>
-					<option value="server">%i18n:common.widgets.server%</option>
-					<option value="memo">%i18n:common.widgets.memo%</option>
-					<option value="donation">%i18n:common.widgets.donation%</option>
-					<option value="nav">%i18n:common.widgets.nav%</option>
-					<option value="tips">%i18n:common.widgets.tips%</option>
+					<option value="profile">{{ $t('@.widgets.profile') }}</option>
+					<option value="analog-clock">{{ $t('@.widgets.analog-clock') }}</option>
+					<option value="calendar">{{ $t('@.widgets.calendar') }}</option>
+					<option value="activity">{{ $t('@.widgets.activity') }}</option>
+					<option value="rss">{{ $t('@.widgets.rss') }}</option>
+					<option value="photo-stream">{{ $t('@.widgets.photo-stream') }}</option>
+					<option value="slideshow">{{ $t('@.widgets.slideshow') }}</option>
+					<option value="hashtags">{{ $t('@.widgets.hashtags') }}</option>
+					<option value="posts-monitor">{{ $t('@.widgets.posts-monitor') }}</option>
+					<option value="version">{{ $t('@.widgets.version') }}</option>
+					<option value="server">{{ $t('@.widgets.server') }}</option>
+					<option value="memo">{{ $t('@.widgets.memo') }}</option>
+					<option value="donation">{{ $t('@.widgets.donation') }}</option>
+					<option value="nav">{{ $t('@.widgets.nav') }}</option>
+					<option value="tips">{{ $t('@.widgets.tips') }}</option>
 				</select>
-				<button @click="addWidget">%i18n:add-widget%</button>
-				<p><a @click="hint">%i18n:customization-tips%</a></p>
+				<button @click="addWidget">{{ $t('add-widget') }}</button>
+				<p><a @click="hint">{{ $t('customization-tips') }}</a></p>
 			</header>
 			<x-draggable
 				:list="widgets"
@@ -51,10 +51,12 @@
 
 <script lang="ts">
 import Vue from 'vue';
+import i18n from '../../../i18n';
 import * as XDraggable from 'vuedraggable';
 import * as uuid from 'uuid';
 
 export default Vue.extend({
+	i18n: i18n('mobile/views/pages/widgets.vue'),
 	components: {
 		XDraggable
 	},
@@ -107,7 +109,7 @@ export default Vue.extend({
 
 	methods: {
 		hint() {
-			alert('%i18n:@widgets-hints%');
+			alert(this.$t('widgets-hints'));
 		},
 
 		widgetFunc(id) {
diff --git a/src/client/app/mobile/views/widgets/activity.vue b/src/client/app/mobile/views/widgets/activity.vue
index d35613b080..c67d846673 100644
--- a/src/client/app/mobile/views/widgets/activity.vue
+++ b/src/client/app/mobile/views/widgets/activity.vue
@@ -1,7 +1,7 @@
 <template>
 <div class="mkw-activity">
 	<mk-widget-container :show-header="!props.compact">
-		<template slot="header"><fa icon="chart-bar"/>%i18n:@activity%</template>
+		<template slot="header"><fa icon="chart-bar"/>{{ $t('activity') }}</template>
 		<div :class="$style.body">
 			<mk-activity :user="$store.state.i"/>
 		</div>
@@ -11,6 +11,7 @@
 
 <script lang="ts">
 import define from '../../../common/define-widget';
+import i18n from '../../../i18n';
 
 export default define({
 	name: 'activity',
@@ -18,6 +19,7 @@ export default define({
 		compact: false
 	})
 }).extend({
+	i18n: i18n(),
 	methods: {
 		func() {
 			this.props.compact = !this.props.compact;
diff --git a/src/misc/i18n.ts b/src/misc/i18n.ts
deleted file mode 100644
index 0e429f1276..0000000000
--- a/src/misc/i18n.ts
+++ /dev/null
@@ -1,73 +0,0 @@
-/**
- * Replace i18n texts
- */
-
-const locale = require('../../locales');
-
-export default class Replacer {
-	private lang: string;
-
-	public pattern = /%i18n:([a-z0-9_\-\.\/\|]+?)%/g;
-
-	constructor(lang: string) {
-		this.lang = lang;
-
-		this.get = this.get.bind(this);
-		this.replacement = this.replacement.bind(this);
-	}
-
-	public get(path: string, key: string): string {
-		if (!(this.lang in locale)) {
-			console.warn(`lang '${this.lang}' is not supported`);
-			return key; // Fallback
-		}
-
-		const texts = locale[this.lang];
-
-		let text = texts;
-
-		if (path) {
-			path = path.replace('.ts', '');
-
-			if (text.hasOwnProperty(path)) {
-				text = text[path];
-			} else {
-				if (this.lang === 'ja-JP') console.warn(`path '${path}' not found`);
-				return key; // Fallback
-			}
-		}
-
-		// Check the key existance
-		const error = key.split('.').some(k => {
-			if (text.hasOwnProperty(k)) {
-				text = text[k];
-				return false;
-			} else {
-				return true;
-			}
-		});
-
-		if (error) {
-			if (this.lang === 'ja-JP') console.warn(`key '${key}' not found in '${path}'`);
-			return key; // Fallback
-		} else if (typeof text !== 'string') {
-			if (this.lang === 'ja-JP') console.warn(`key '${key}' is not string in '${path}'`);
-			return key; // Fallback
-		} else {
-			return text.replace(/\n/g, ' ');
-		}
-	}
-
-	public replacement(match: string, key: string) {
-		let path = null;
-
-		if (key.indexOf('|') != -1) {
-			path = key.split('|')[0];
-			key = key.split('|')[1];
-		}
-
-		const txt = this.get(path, key);
-
-		return txt.replace(/'/g, '\\x27').replace(/"/g, '\\x22');
-	}
-}
diff --git a/src/server/web/docs.ts b/src/server/web/docs.ts
index 1fd0157795..292f3135b5 100644
--- a/src/server/web/docs.ts
+++ b/src/server/web/docs.ts
@@ -13,7 +13,6 @@ import { Context, ObjectContext } from 'cafy';
 import * as glob from 'glob';
 import * as yaml from 'js-yaml';
 import config from '../../config';
-import I18n from '../../misc/i18n';
 import { licenseHtml } from '../../misc/license';
 const constants = require('../../const.json');
 import endpoints from '../api/endpoints';
@@ -54,8 +53,8 @@ async function genVars(lang: string): Promise<{ [key: string]: any }> {
 
 	vars['license'] = licenseHtml;
 
-	const i18n = new I18n(lang);
-	vars['i18n'] = (key: string) => i18n.get(null, key);
+	//const i18n = new I18n(lang);
+	//vars['i18n'] = (key: string) => i18n.get(null, key);
 
 	return vars;
 }
diff --git a/webpack.config.ts b/webpack.config.ts
index 37faec0069..7d1f914c4e 100644
--- a/webpack.config.ts
+++ b/webpack.config.ts
@@ -11,8 +11,6 @@ const WebpackOnBuildPlugin = require('on-build-webpack');
 //const HardSourceWebpackPlugin = require('hard-source-webpack-plugin');
 const ProgressBarPlugin = require('progress-bar-webpack-plugin');
 
-import I18nReplacer from './src/misc/i18n';
-import { pattern as i18nPattern, replacement as i18nReplacement } from './webpack/i18n';
 const constants = require('./src/const.json');
 
 const locales = require('./locales');
@@ -22,7 +20,6 @@ const codename = meta.codename;
 
 declare var global: {
 	collapseSpacesReplacement: any;
-	i18nReplacement: typeof i18nReplacement;
 };
 
 //#region Replacer definitions
@@ -33,9 +30,6 @@ global['collapseSpacesReplacement'] = (html: string) => {
 		keepClosingSlash: true
 	}).replace(/\t/g, '');
 };
-
-global['i18nReplacement'] = i18nReplacement;
-
 //#endregion
 
 const langs = Object.keys(locales);
@@ -97,9 +91,6 @@ const plugins = [
 			Object.keys(entry).forEach(file => {
 				let src = fs.readFileSync(`${__dirname}/built/client/assets/${file}.${version}.-.js`, 'utf-8');
 
-				const i18nReplacer = new I18nReplacer(lang);
-
-				src = src.replace(i18nReplacer.pattern, i18nReplacer.replacement);
 				src = src.replace('%lang%', lang);
 				src = src.replace('"%locale%"', JSON.stringify(locales[lang]));
 
@@ -135,10 +126,6 @@ module.exports = {
 				loader: 'replace',
 				query: {
 					qs: [{
-						search: i18nPattern.toString(),
-						replace: 'i18nReplacement',
-						i18n: true
-					}, {
 						search: /^<template>([\s\S]+?)\r?\n<\/template>/.toString(),
 						replace: 'collapseSpacesReplacement'
 					}]
@@ -198,15 +185,6 @@ module.exports = {
 					configFile: __dirname + '/src/client/app/tsconfig.json',
 					appendTsSuffixTo: [/\.vue$/]
 				}
-			}, {
-				loader: 'replace',
-				query: {
-					qs: [{
-						search: i18nPattern.toString(),
-						replace: 'i18nReplacement',
-						i18n: true
-					}]
-				}
 			}]
 		}]
 	},
diff --git a/webpack/i18n.ts b/webpack/i18n.ts
deleted file mode 100644
index 4fd439d1d5..0000000000
--- a/webpack/i18n.ts
+++ /dev/null
@@ -1,19 +0,0 @@
-/**
- * Replace i18n texts
- */
-
-export const pattern = /%i18n:([a-z0-9_\-@\.]+?)%/g;
-
-export const replacement = (ctx: any, _: any, key: string) => {
-	const client = '/src/client/app/';
-	let name = null;
-
-	if (key.startsWith('@')) {
-		name = ctx.src.substr(ctx.src.indexOf(client) + client.length);
-		key = key.substr(1);
-	}
-
-	const path = name ? `${name}|${key}` : key;
-
-	return `%i18n:${path}%`;
-};