diff --git a/.eslintrc b/.eslintrc
index 0943cb4b64..dc1426eb1f 100644
--- a/.eslintrc
+++ b/.eslintrc
@@ -22,6 +22,7 @@
 	"globals": {
 		"ENV": true,
 		"VERSION": true,
-		"API": true
+		"API": true,
+		"LANGS": true
 	}
 }
diff --git a/.gitignore b/.gitignore
index 53808240b7..41fef982c4 100644
--- a/.gitignore
+++ b/.gitignore
@@ -5,6 +5,7 @@
 /build
 /built
 /data
+/.cache-loader
 npm-debug.log
 *.pem
 run.bat
diff --git a/.travis.yml b/.travis.yml
index c86b737d21..f52fe7e3f5 100644
--- a/.travis.yml
+++ b/.travis.yml
@@ -4,6 +4,10 @@
 notifications:
   email: false
 
+branches:
+  except:
+    - l10n_master
+
 language: node_js
 
 node_js:
diff --git a/assets/title-dark.svg b/assets/title-dark.svg
new file mode 100644
index 0000000000..10139024ad
--- /dev/null
+++ b/assets/title-dark.svg
@@ -0,0 +1,140 @@
+<?xml version="1.0" encoding="UTF-8" standalone="no"?>
+<!-- Created with Inkscape (http://www.inkscape.org/) -->
+
+<svg
+   xmlns:dc="http://purl.org/dc/elements/1.1/"
+   xmlns:cc="http://creativecommons.org/ns#"
+   xmlns:rdf="http://www.w3.org/1999/02/22-rdf-syntax-ns#"
+   xmlns:svg="http://www.w3.org/2000/svg"
+   xmlns="http://www.w3.org/2000/svg"
+   xmlns:sodipodi="http://sodipodi.sourceforge.net/DTD/sodipodi-0.dtd"
+   xmlns:inkscape="http://www.inkscape.org/namespaces/inkscape"
+   width="614.71039"
+   height="205.08009"
+   viewBox="0 0 162.64213 54.260776"
+   version="1.1"
+   id="svg8"
+   inkscape:version="0.92.1 r15371"
+   sodipodi:docname="misskey.svg"
+   inkscape:export-filename="C:\Users\Takumiya_Cho\Desktop\misskey.png"
+   inkscape:export-xdpi="96"
+   inkscape:export-ydpi="96">
+  <defs
+     id="defs2">
+    <inkscape:path-effect
+       effect="simplify"
+       id="path-effect5115"
+       is_visible="true"
+       steps="1"
+       threshold="0.000408163"
+       smooth_angles="360"
+       helper_size="0"
+       simplify_individual_paths="false"
+       simplify_just_coalesce="false"
+       simplifyindividualpaths="false"
+       simplifyJustCoalesce="false" />
+    <inkscape:path-effect
+       effect="simplify"
+       id="path-effect5104"
+       is_visible="true"
+       steps="1"
+       threshold="0.000408163"
+       smooth_angles="360"
+       helper_size="0"
+       simplify_individual_paths="false"
+       simplify_just_coalesce="false"
+       simplifyindividualpaths="false"
+       simplifyJustCoalesce="false" />
+  </defs>
+  <sodipodi:namedview
+     id="base"
+     pagecolor="#ffffff"
+     bordercolor="#666666"
+     borderopacity="1.0"
+     inkscape:pageopacity="0.0"
+     inkscape:pageshadow="2"
+     inkscape:zoom="0.9899495"
+     inkscape:cx="370.82839"
+     inkscape:cy="79.043895"
+     inkscape:document-units="mm"
+     inkscape:current-layer="layer1"
+     showgrid="false"
+     units="px"
+     inkscape:snap-bbox="true"
+     inkscape:bbox-nodes="true"
+     inkscape:snap-bbox-edge-midpoints="false"
+     inkscape:snap-smooth-nodes="true"
+     inkscape:snap-center="true"
+     inkscape:snap-page="true"
+     inkscape:window-width="1920"
+     inkscape:window-height="1017"
+     inkscape:window-x="-8"
+     inkscape:window-y="1072"
+     inkscape:window-maximized="1"
+     inkscape:object-paths="true"
+     inkscape:bbox-paths="true"
+     fit-margin-top="50"
+     fit-margin-left="50"
+     fit-margin-bottom="20"
+     fit-margin-right="50" />
+  <metadata
+     id="metadata5">
+    <rdf:RDF>
+      <cc:Work
+         rdf:about="">
+        <dc:format>image/svg+xml</dc:format>
+        <dc:type
+           rdf:resource="http://purl.org/dc/dcmitype/StillImage" />
+        <dc:title></dc:title>
+      </cc:Work>
+    </rdf:RDF>
+  </metadata>
+  <g
+     inkscape:label="レイヤー 1"
+     inkscape:groupmode="layer"
+     id="layer1"
+     transform="translate(-11.097531,-173.29664)">
+    <g
+       transform="matrix(0.28612302,0,0,0.28612302,17.176981,141.74334)"
+       id="text4489-6"
+       style="font-style:normal;font-variant:normal;font-weight:normal;font-stretch:normal;font-size:141.03404236px;line-height:476.69509888px;font-family:'OTADESIGN Rounded';-inkscape-font-specification:'OTADESIGN Rounded';letter-spacing:0px;word-spacing:0px;fill:#fff;fill-opacity:1;stroke:none;stroke-width:0.92471898px;stroke-linecap:butt;stroke-linejoin:miter;stroke-opacity:1"
+       aria-label="Mi">
+      <path
+         sodipodi:nodetypes="zccssscssccscczzzccsccsscscsccz"
+         inkscape:connector-curvature="0"
+         id="path5210"
+         style="font-style:normal;font-variant:normal;font-weight:normal;font-stretch:normal;font-family:'OTADESIGN Rounded';-inkscape-font-specification:'OTADESIGN Rounded';fill:#fff;fill-opacity:1;stroke-width:0.92471898px"
+         d="m 75.196381,231.17126 c -5.855419,0.0202 -10.885068,-3.50766 -13.2572,-7.61584 -1.266603,-1.79454 -3.772419,-2.43291 -3.807919,0 v 11.2332 c 0,4.51309 -1.645397,8.41504 -4.936191,11.70583 -3.196772,3.19677 -7.098714,4.79516 -11.705826,4.79516 -4.513089,0 -8.415031,-1.59839 -11.705825,-4.79516 -3.196772,-3.29079 -4.795158,-7.19274 -4.795158,-11.70583 v -61.7729 c 0,-3.47884 0.987238,-6.6286 2.961715,-9.44928 2.068499,-2.91471 4.701135,-4.9362 7.897906,-6.06447 1.786431,-0.65816 3.666885,-0.98724 5.641362,-0.98724 5.077225,0 9.308247,1.97448 12.693064,5.92343 1.786431,1.97448 2.820681,3.00873 3.102749,3.10275 0,0 13.408119,16.21319 13.78421,16.49526 0.376091,0.28206 1.480789,2.43848 4.127113,2.43848 2.646324,0 3.89218,-2.15642 4.26827,-2.43848 0.376091,-0.28207 13.784088,-16.49526 13.784088,-16.49526 0.09402,0.094 1.081261,-0.94022 2.961715,-3.10275 3.478837,-3.94895 7.756866,-5.92343 12.834096,-5.92343 1.88045,0 3.76091,0.32908 5.64136,0.98724 3.19677,1.12827 5.7824,3.14976 7.75688,6.06447 2.06849,2.82068 3.10274,5.97044 3.10274,9.44928 v 61.7729 c 0,4.51309 -1.6454,8.41504 -4.93619,11.70583 -3.19677,3.19677 -7.09871,4.79516 -11.70582,4.79516 -4.51309,0 -8.41504,-1.59839 -11.705828,-4.79516 -3.196772,-3.29079 -4.795158,-7.19274 -4.795158,-11.70583 v -11.2332 c -0.277898,-3.06563 -2.987588,-1.13379 -3.948953,0 -2.538613,4.70114 -7.401781,7.59567 -13.2572,7.61584 z" />
+      <path
+         inkscape:connector-curvature="0"
+         id="path5212"
+         style="font-style:normal;font-variant:normal;font-weight:normal;font-stretch:normal;font-family:'OTADESIGN Rounded';-inkscape-font-specification:'OTADESIGN Rounded';fill:#fff;fill-opacity:1;stroke-width:0.92471898px"
+         d="m 145.83461,185.00361 q -5.92343,0 -10.15445,-4.08999 -4.08999,-4.23102 -4.08999,-10.15445 0,-5.92343 4.08999,-10.01342 4.23102,-4.23102 10.15445,-4.23102 5.92343,0 10.15445,4.23102 4.23102,4.08999 4.23102,10.01342 0,5.92343 -4.23102,10.15445 -4.23102,4.08999 -10.15445,4.08999 z m 0.14103,2.82068 q 5.92343,0 10.01342,4.23102 4.23102,4.23102 4.23102,10.15445 v 34.83541 q 0,5.92343 -4.23102,10.15445 -4.08999,4.08999 -10.01342,4.08999 -5.92343,0 -10.15445,-4.08999 -4.23102,-4.23102 -4.23102,-10.15445 v -34.83541 q 0,-5.92343 4.23102,-10.15445 4.23102,-4.23102 10.15445,-4.23102 z" />
+    </g>
+    <path
+       inkscape:connector-curvature="0"
+       id="path5199"
+       style="font-style:normal;font-variant:normal;font-weight:normal;font-stretch:normal;font-size:medium;line-height:136.34428406px;font-family:'OTADESIGN Rounded';-inkscape-font-specification:'OTADESIGN Rounded';letter-spacing:0px;word-spacing:0px;fill:#fff;fill-opacity:1;stroke:none;stroke-width:0.26458335px;stroke-linecap:butt;stroke-linejoin:miter;stroke-opacity:1"
+       d="m 72.022691,200.53715 q 0.968125,0.24203 2.420312,0.5244 2.420313,0.40339 3.791824,1.29083 2.581666,1.69422 2.581666,5.08266 0,2.74302 -1.815234,4.47758 -2.097604,2.01693 -5.849089,2.01693 -2.743021,0 -6.131458,-0.76644 -1.089141,-0.24203 -1.774896,-1.08914 -0.645417,-0.84711 -0.645417,-1.89591 0,-1.29083 0.887448,-2.17828 0.927786,-0.92779 2.178281,-0.92779 0.363047,0 0.685756,0.0807 1.169817,0.24203 4.477578,0.60508 0.443724,0 0.968125,-0.0403 0.201693,0 0.201693,-0.24203 0.04034,-0.20169 -0.242032,-0.28237 -1.37151,-0.24203 -2.541328,-0.5244 -1.331172,-0.28237 -1.895911,-0.48406 -1.12948,-0.32271 -1.895912,-0.84711 -2.581667,-1.69422 -2.622005,-5.08266 0,-2.70268 1.855573,-4.47758 2.258958,-2.17828 6.413828,-1.97659 2.783359,0.12102 5.566719,0.7261 1.048802,0.24203 1.734557,1.08914 0.685756,0.84711 0.685756,1.93625 0,1.25049 -0.927787,2.17828 -0.887448,0.88745 -2.178281,0.88745 -0.322709,0 -0.645417,-0.0807 -1.169818,-0.24203 -4.517917,-0.56474 -0.403385,-0.0403 -0.766432,0 -0.322708,0.0403 -0.322708,0.24203 0.04034,0.24203 0.322708,0.32271 z" />
+    <path
+       inkscape:connector-curvature="0"
+       id="path5201"
+       style="font-style:normal;font-variant:normal;font-weight:normal;font-stretch:normal;font-size:medium;line-height:136.34428406px;font-family:'OTADESIGN Rounded';-inkscape-font-specification:'OTADESIGN Rounded';letter-spacing:0px;word-spacing:0px;fill:#fff;fill-opacity:1;stroke:none;stroke-width:0.26458335px;stroke-linecap:butt;stroke-linejoin:miter;stroke-opacity:1"
+       d="m 89.577027,200.53715 q 0.968125,0.24203 2.420312,0.5244 2.420313,0.40339 3.791823,1.29083 2.581667,1.69422 2.581667,5.08266 0,2.74302 -1.815234,4.47758 -2.097604,2.01693 -5.849089,2.01693 -2.743021,0 -6.131458,-0.76644 -1.089141,-0.24203 -1.774896,-1.08914 -0.645417,-0.84711 -0.645417,-1.89591 0,-1.29083 0.887448,-2.17828 0.927786,-0.92779 2.178281,-0.92779 0.363047,0 0.685755,0.0807 1.169818,0.24203 4.477579,0.60508 0.443724,0 0.968125,-0.0403 0.201692,0 0.201692,-0.24203 0.04034,-0.20169 -0.242031,-0.28237 -1.37151,-0.24203 -2.541328,-0.5244 -1.331172,-0.28237 -1.895912,-0.48406 -1.129479,-0.32271 -1.895911,-0.84711 -2.581667,-1.69422 -2.622005,-5.08266 0,-2.70268 1.855573,-4.47758 2.258958,-2.17828 6.413828,-1.97659 2.783359,0.12102 5.566719,0.7261 1.048802,0.24203 1.734557,1.08914 0.685755,0.84711 0.685755,1.93625 0,1.25049 -0.927786,2.17828 -0.887448,0.88745 -2.178281,0.88745 -0.322709,0 -0.645417,-0.0807 -1.169818,-0.24203 -4.517917,-0.56474 -0.403385,-0.0403 -0.766432,0 -0.322708,0.0403 -0.322708,0.24203 0.04034,0.24203 0.322708,0.32271 z" />
+    <path
+       inkscape:connector-curvature="0"
+       id="path5203"
+       style="font-style:normal;font-variant:normal;font-weight:normal;font-stretch:normal;font-size:medium;line-height:136.34428406px;font-family:'OTADESIGN Rounded';-inkscape-font-specification:'OTADESIGN Rounded';letter-spacing:0px;word-spacing:0px;fill:#fff;fill-opacity:1;stroke:none;stroke-width:0.26458335px;stroke-linecap:butt;stroke-linejoin:miter;stroke-opacity:1"
+       d="m 115.65209,203.87137 q 0.12101,0.0807 2.86404,2.78336 1.25049,1.21016 1.25049,2.94471 0,1.61354 -1.16982,2.86404 -1.16982,1.21016 -2.90437,1.21016 -1.65388,0 -2.86404,-1.16982 l -4.03385,-3.91284 q -0.16136,-0.12102 -0.32271,-0.12102 -0.32271,0 -0.32271,1.21016 0,1.69422 -1.21016,2.90438 -1.21015,1.16981 -2.90437,1.16981 -1.69422,0 -2.90438,-1.16981 -1.169807,-1.21016 -1.169807,-2.90438 v -18.79776 q 0,-1.69422 1.169807,-2.86404 1.21016,-1.21015 2.90438,-1.21015 1.69422,0 2.90437,1.21015 1.21016,1.16982 1.21016,2.86404 v 6.29281 q 0,0.40339 0.28237,0.5244 0.24203,0.12102 0.5244,-0.0807 0.16135,-0.0807 4.84063,-3.18675 1.0488,-0.64542 2.25895,-0.64542 2.21862,0 3.42878,1.81524 0.64542,1.0488 0.64542,2.25896 0,2.21862 -1.81524,3.42877 l -2.54133,1.61354 v 0.0403 l -0.0807,0.0403 q -0.56474,0.36305 -0.0403,0.88745 z" />
+    <path
+       inkscape:connector-curvature="0"
+       id="path5205"
+       style="font-style:normal;font-variant:normal;font-weight:normal;font-stretch:normal;font-size:medium;line-height:136.34428406px;font-family:'OTADESIGN Rounded';-inkscape-font-specification:'OTADESIGN Rounded';letter-spacing:0px;word-spacing:0px;fill:#fff;fill-opacity:1;stroke:none;stroke-width:0.26458335px;stroke-linecap:butt;stroke-linejoin:miter;stroke-opacity:1"
+       d="m 131.25181,213.92955 q -4.19521,0 -7.18026,-2.94472 -2.94472,-2.98505 -2.94472,-7.18026 0,-4.15487 2.94472,-7.09958 2.98505,-2.98505 7.18026,-2.98505 4.15487,0 6.97857,2.78335 0.92778,0.92779 0.92778,2.25896 0,1.33118 -0.92778,2.25896 l -4.67928,4.63893 q -1.00846,1.00847 -2.01692,1.00847 -1.45219,0 -2.25896,-0.80677 -0.80677,-0.80677 -0.80677,-2.13795 0,-1.29083 0.92778,-2.21862 l 0.80678,-0.84711 q 0.16135,-0.12101 0.0807,-0.24203 -0.12101,-0.0807 -0.32271,-0.0403 -0.80677,0.20169 -1.37151,0.80677 -1.12948,1.08914 -1.12948,2.622 0,1.5732 1.08915,2.70268 1.12947,1.08914 2.70268,1.08914 1.53286,0 2.622,-1.12947 0.92779,-0.92779 2.25896,-0.92779 1.33117,0 2.25896,0.92779 0.92779,0.92778 0.92779,2.25895 0,1.33118 -0.92779,2.25896 -2.98505,2.94472 -7.13992,2.94472 z" />
+    <path
+       inkscape:connector-curvature="0"
+       id="path5207"
+       style="font-style:normal;font-variant:normal;font-weight:normal;font-stretch:normal;font-size:medium;line-height:136.34428406px;font-family:'OTADESIGN Rounded';-inkscape-font-specification:'OTADESIGN Rounded';letter-spacing:0px;word-spacing:0px;fill:#fff;fill-opacity:1;stroke:none;stroke-width:0.26458335px;stroke-linecap:butt;stroke-linejoin:miter;stroke-opacity:1"
+       d="m 160.51049,198.1433 v 5.60705 q 0,0.56474 -0.0807,1.21016 v 7.38195 q 0,4.51792 -2.74302,7.2206 -2.70268,2.70269 -7.30128,2.70269 -2.66234,0 -4.80028,-1.00847 -2.13795,-0.96812 -2.13795,-3.3481 0,-0.80677 0.36305,-1.53286 0.96812,-2.17828 3.3481,-2.17828 0.56474,0 1.5732,0.32271 1.00847,0.3227 1.65388,0.3227 1.69422,0 2.21862,-0.72609 0.20169,-0.28237 0.0807,-0.44372 -0.16136,-0.24204 -0.56474,-0.16136 -0.68576,0.12102 -1.49253,0.12102 -4.07419,0 -6.97856,-2.90438 -2.90438,-2.90437 -2.90438,-6.97857 v -5.60705 q 0,-1.69422 1.16982,-2.86404 1.21015,-1.21016 2.90437,-1.21016 1.69422,0 2.90438,1.21016 1.21015,1.16982 1.21015,2.86404 v 5.60705 q 0,0.68576 0.48407,1.21016 0.5244,0.48406 1.21015,0.48406 0.7261,0 1.21016,-0.48406 0.48406,-0.5244 0.48406,-1.21016 v -5.60705 q 0,-1.69422 1.21016,-2.86404 1.21015,-1.21016 2.90437,-1.21016 1.69422,0 2.86404,1.21016 1.21016,1.16982 1.21016,2.86404 z" />
+  </g>
+</svg>
diff --git a/cli/clean-cached-remote-files.js b/cli/clean-cached-remote-files.js
index e4db37ef97..a9c38a4cdf 100644
--- a/cli/clean-cached-remote-files.js
+++ b/cli/clean-cached-remote-files.js
@@ -8,7 +8,8 @@ const { default: User } = require('../built/models/user');
 const q = {
 	'metadata._user.host': {
 		$ne: null
-	}
+	},
+	'metadata.isMetaOnly': false
 };
 
 async function main() {
@@ -56,8 +57,7 @@ async function main() {
 
 					DriveFile.update({ _id: file._id }, {
 						$set: {
-							'metadata.deletedAt': new Date(),
-							'metadata.isExpired': true
+							'metadata.isMetaOnly': true
 						}
 					})
 				]).then(async () => {
diff --git a/docs/setup.en.md b/docs/setup.en.md
index b858a4a2a4..8dde4d00d6 100644
--- a/docs/setup.en.md
+++ b/docs/setup.en.md
@@ -43,13 +43,7 @@ Please install and setup these softwares:
 
 *4.* Prepare configuration
 ----------------------------------------------------------------
-1. Copy `example.yml` of `.config` directory
-2. Rename it to `default.yml`
-3. Edit it
-
----
-
-Or you can generate config file via `npm run config` command.
+You need to generate config file via `npm run config` command.
 
 *5.* Build Misskey
 ----------------------------------------------------------------
diff --git a/docs/setup.ja.md b/docs/setup.ja.md
index c45ebcdca0..0f1e46761b 100644
--- a/docs/setup.ja.md
+++ b/docs/setup.ja.md
@@ -43,18 +43,14 @@ web-push generate-vapid-keys
 
 *4.* 設定ファイルを用意する
 ----------------------------------------------------------------
-1. `.config`ディレクトリ内の`example.yml`をコピー
-2. `default.yml`にリネーム
-3. 編集する
-
----
-
-または、`npm run config`コマンドを利用して、ガイドに従って情報を
-入力して設定ファイルを生成することもできます。
+`npm run config`コマンドを利用して、ガイドに従って情報を入力してください。
 
 *5.* Misskeyのビルド
 ----------------------------------------------------------------
-1. `npm run build`
+1. `npm install -g node-gyp`
+2. `node-gyp configure`
+3. `node-gyp build`
+4. `npm run build`
 
 *6.* 以上です!
 ----------------------------------------------------------------
@@ -78,4 +74,4 @@ VPSなどでビルドする時は、もしかしたらメモリが足りなく
 3. npm run webpack
 4. built/client をサーバーにアップロードする
 5. サーバー上で、npm run gulp
-6. 完了
\ No newline at end of file
+6. 完了
diff --git a/locales/de.yml b/locales/de.yml
index 7d0ffe4084..5395de73ab 100644
--- a/locales/de.yml
+++ b/locales/de.yml
@@ -1,7 +1,7 @@
 ---
 meta:
   lang: "Deutsch"
-  divider: ""
+  divider: " "
 common:
   misskey: "Teile alles mit anderen mithilfe von Misskey"
   time:
diff --git a/locales/en.yml b/locales/en.yml
index adaf433dd2..04f54957e1 100644
--- a/locales/en.yml
+++ b/locales/en.yml
@@ -359,7 +359,7 @@ desktop/views/components/renote-form.vue:
 desktop/views/components/renote-form-window.vue:
   title: "Are you sure you want to renote this note?"
 desktop/views/components/settings-window.vue:
-  settings: "設定"
+  settings: "Settings"
 desktop/views/components/settings.vue:
   profile: "Profile"
   notification: "Notification"
diff --git a/locales/fr.yml b/locales/fr.yml
index 9ed78f6ec2..4a9ddd380e 100644
--- a/locales/fr.yml
+++ b/locales/fr.yml
@@ -1,7 +1,7 @@
 ---
 meta:
-  lang: "日本語"
-  divider: ""
+  lang: "Français"
+  divider: " "
 common:
   misskey: "Partagez avec les autres en utilisant Misskey"
   time:
diff --git a/locales/ja.yml b/locales/ja.yml
index c3ee3e6c9f..0fcbca5361 100644
--- a/locales/ja.yml
+++ b/locales/ja.yml
@@ -173,6 +173,16 @@ common/views/components/twitter-setting.vue:
 common/views/components/uploader.vue:
   waiting: "待機中"
 
+common/views/components/visibility-chooser.vue:
+  public: "公開"
+  home: "ホーム"
+  home-desc: "ホームタイムラインにのみ公開"
+  followers: "フォロワー"
+  followers-desc: "自分のフォロワーにのみ公開"
+  specified: "ダイレクト"
+  specified-desc: "指定したユーザーにのみ公開"
+  private: "非公開"
+
 common/views/widgets/broadcast.vue:
   fetching: "確認中"
   no-broadcasts: "お知らせはありません"
@@ -340,6 +350,14 @@ desktop/views/components/messaging-room-window.vue:
 desktop/views/components/messaging-window.vue:
   title: "メッセージ"
 
+desktop/views/components/note-detail.vue:
+  more: "会話をもっと読み込む"
+  private: "(この投稿は非公開です)"
+  reposted-by: "{}がRenote"
+  location: "位置情報"
+  renote: "Renote"
+  add-reaction: "リアクション"
+
 desktop/views/components/note-detail.sub.vue:
   private: "(この投稿は非公開です)"
 
@@ -399,6 +417,9 @@ desktop/views/components/renote-form.vue:
 desktop/views/components/renote-form-window.vue:
   title: "この投稿をRenoteしますか?"
 
+desktop/views/components/settings-window.vue:
+  settings: "設定"
+
 desktop/views/components/settings.vue:
   profile: "プロフィール"
   notification: "通知"
@@ -477,9 +498,6 @@ desktop/views/components/settings.vue:
   advanced-settings: "高度な設定"
   debug-mode: "デバッグモードを有効にする"
   debug-mode-desc: "この設定はブラウザに記憶されます。"
-  use-raw-script: "生のスクリプトを読み込む"
-  use-raw-script-desc: "圧縮されていない「生の」スクリプトを使用します。サイズが大きいため、読み込みに時間がかかる場合があります。この設定はブラウザに記憶されます。"
-  source-info: "Misskeyはソースマップも提供しています。"
   experimental: "実験的機能を有効にする"
   experimental-desc: "実験的機能を有効にするとMisskeyの動作が不安定になる可能性があります。この設定はブラウザに記憶されます。"
   tools: "ツール"
@@ -535,6 +553,13 @@ desktop/views/components/settings.profile.vue:
   description: "自己紹介"
   birthday: "誕生日"
   save: "保存"
+  is-bot: "このアカウントはBotです"
+  is-cat: "このアカウントはCatです"
+
+desktop/views/components/sub-note-content.vue:
+  hidden: "(この投稿は非公開です)"
+  media: "つのメディア"
+  poll: "投票"
 
 desktop/views/components/taskmanager.vue:
   title: "タスクマネージャ"
@@ -583,6 +608,29 @@ desktop/views/components/users-list.vue:
   load-more: "もっと"
   fetching: "読み込んでいます"
 
+desktop/views/components/users-list-item.vue:
+  followed: "フォローされています"
+
+desktop/views/components/window.vue:
+  popout: "ポップアウト"
+  close: "閉じる"
+
+desktop/views/pages/welcome.vue:
+  signin: "ログイン"
+  signup: "新規登録"
+  signin-button: "やってる"
+  signup-button: "やる"
+  timeline: "タイムライン"
+
+desktop/views/pages/drive.vue:
+  title: "Misskey Drive"
+
+desktop/views/pages/favorites.vue:
+  more: "さらに読み込む"
+
+desktop/views/pages/home-customize.vue:
+  title: "ホームのカスタマイズ"
+
 desktop/views/pages/note.vue:
   prev: "前の投稿"
   next: "次の投稿"
@@ -593,6 +641,11 @@ desktop/views/pages/selectdrive.vue:
   cancel: "キャンセル"
   upload: "PCからドライブにファイルをアップロード"
 
+desktop/views/pages/user-list.users.vue:
+  users: "ユーザー"
+  add-user: "ユーザーを追加"
+  username: "ユーザー名"
+
 desktop/views/pages/user/user.followers-you-know.vue:
   title: "知り合いのフォロワー"
   loading: "読み込み中"
@@ -625,6 +678,12 @@ desktop/views/pages/user/user.profile.vue:
   muted: "ミュートしています"
   unmute: "ミュート解除"
 
+desktop/views/pages/user/user.timeline.vue:
+  default: "投稿"
+  with-replies: "投稿と返信"
+  with-media: "メディア"
+  empty: "このユーザーはまだ何も投稿していないようです。"
+
 desktop/views/widgets/messaging.vue:
   title: "メッセージ"
 
@@ -642,6 +701,10 @@ desktop/views/widgets/post-form.vue:
   note: "投稿"
   placeholder: "いまどうしてる?"
 
+desktop/views/widgets/profile.vue:
+  update-banner: "クリックでバナー編集"
+  update-avatar: "クリックでアバター編集"
+
 desktop/views/widgets/trends.vue:
   title: "トレンド"
   refresh: "他を見る"
@@ -735,7 +798,9 @@ mobile/views/pages/following.vue:
   following-of: "{}のフォロー"
 
 mobile/views/pages/home.vue:
-  timeline: "タイムライン"
+  home: "ホーム"
+  local: "ローカル"
+  global: "グローバル"
 
 mobile/views/pages/messaging.vue:
   messaging: "メッセージ"
@@ -753,20 +818,19 @@ mobile/views/pages/notifications.vue:
   read-all: "すべての通知を既読にしますか?"
 
 mobile/views/pages/settings/settings.profile.vue:
-  title: "プロフィール設定"
-  will-be-published: "これらのプロフィールは公開されます。"
+  title: "プロフィール"
   name: "名前"
+  account: "アカウント"
   location: "場所"
   description: "自己紹介"
   birthday: "誕生日"
   avatar: "アイコン"
   banner: "バナー"
-  avatar-saved: "アイコンを保存しました"
-  banner-saved: "バナーを保存しました"
-  set-avatar: "アイコンを選択する"
-  set-banner: "バナーを選択する"
+  is-cat: "このアカウントはCatです"
   save: "保存"
   saved: "プロフィールを保存しました"
+  uploading: "アップロード中"
+  upload-failed: "アップロードに失敗しました"
 
 mobile/views/pages/search.vue:
   search: "検索"
@@ -777,9 +841,40 @@ mobile/views/pages/selectdrive.vue:
 
 mobile/views/pages/settings.vue:
   signed-in-as: "{}としてサインイン中"
-  profile: "プロフィール"
+  lang: "言語"
+  lang-tip: "変更はページの再読み込み後に反映されます。"
+  recommended: "推奨"
+  auto: "自動"
+  specify-language: "言語を指定"
+  design: "デザインと表示"
+  dark-mode: "ダークモード"
+  i-am-under-limited-internet: "私は通信を制限されている"
+  circle-icons: "円形のアイコンを使用"
+  timeline: "タイムライン"
+  show-reply-target: "リプライ先を表示する"
+  show-my-renotes: "自分の行ったRenoteを表示する"
+  show-renoted-my-notes: "Renoteされた自分の投稿を表示する"
+  post-style: "投稿の表示スタイル"
+  post-style-standard: "標準"
+  post-style-smart: "スマート"
+  behavior: "動作"
+  fetch-on-scroll: "スクロールで自動読み込み"
+  disable-via-mobile: "「モバイルからの投稿」フラグを付けない"
+  load-raw-images: "添付された画像を高画質で表示する"
+  load-remote-media: "リモートサーバーのメディアを表示する"
   twitter: "Twitter連携"
-  signin-history: "サインイン履歴"
+  twitter-connect: "Twitterアカウントに接続する"
+  twitter-reconnect: "再接続する"
+  twitter-disconnect: "切断する"
+  update: "Misskey Update"
+  version: "バージョン:"
+  latest-version: "最新のバージョン:"
+  update-checking: "アップデートを確認中"
+  check-for-updates: "アップデートを確認"
+  no-updates: "利用可能な更新はありません"
+  no-updates-desc: "お使いのMisskeyは最新です。"
+  update-available: "新しいバージョンが利用可能です"
+  update-available-desc: "ページを再度読み込みすると更新が適用されます。"
   settings: "設定"
   signout: "サインアウト"
 
diff --git a/locales/pl.yml b/locales/pl.yml
index b6427bdf09..9324704bb5 100644
--- a/locales/pl.yml
+++ b/locales/pl.yml
@@ -1,7 +1,7 @@
 ---
 meta:
-  lang: "japoński"
-  divider: ""
+  lang: "język polski"
+  divider: " "
 common:
   misskey: "Dziel się zawartością z innymi korzystając z Misskey."
   time:
diff --git a/locales/ru.yml b/locales/ru.yml
index 08551f2db5..290c660ff8 100644
--- a/locales/ru.yml
+++ b/locales/ru.yml
@@ -1,7 +1,7 @@
 ---
 meta:
-  lang: "日本語"
-  divider: ""
+  lang: "Русский язык"
+  divider: " "
 common:
   misskey: "Misskeyで皆と共有しよう。"
   time:
diff --git a/package.json b/package.json
index 3fa261b8a1..534b0c296d 100644
--- a/package.json
+++ b/package.json
@@ -1,8 +1,8 @@
 {
 	"name": "misskey",
 	"author": "syuilo <i@syuilo.com>",
-	"version": "2.10.1",
-	"clientVersion": "1.0.5407",
+	"version": "2.17.0",
+	"clientVersion": "1.0.5731",
 	"codename": "nighthike",
 	"main": "./built/index.js",
 	"private": true,
@@ -65,7 +65,7 @@
 		"@types/mongodb": "3.0.18",
 		"@types/monk": "6.0.0",
 		"@types/ms": "0.7.30",
-		"@types/node": "10.1.0",
+		"@types/node": "10.1.2",
 		"@types/nopt": "3.0.29",
 		"@types/parse5": "3.0.0",
 		"@types/pug": "2.0.4",
@@ -80,7 +80,7 @@
 		"@types/speakeasy": "2.0.2",
 		"@types/tmp": "0.0.33",
 		"@types/uuid": "3.4.3",
-		"@types/webpack": "4.1.7",
+		"@types/webpack": "4.4.0",
 		"@types/webpack-stream": "3.2.10",
 		"@types/websocket": "0.0.39",
 		"@types/ws": "5.1.1",
@@ -98,8 +98,8 @@
 		"deepcopy": "0.6.3",
 		"diskusage": "0.2.4",
 		"dompurify": "1.0.4",
-		"elasticsearch": "14.2.2",
-		"element-ui": "2.3.8",
+		"elasticsearch": "15.0.0",
+		"element-ui": "2.3.9",
 		"emojilib": "2.2.12",
 		"escape-regexp": "0.0.1",
 		"eslint": "4.19.1",
@@ -124,7 +124,7 @@
 		"gulp-typescript": "4.0.2",
 		"gulp-uglify": "3.0.0",
 		"gulp-util": "3.0.8",
-		"hard-source-webpack-plugin": "0.6.7",
+		"hard-source-webpack-plugin": "0.6.9",
 		"highlight.js": "9.12.0",
 		"html-minifier": "3.5.15",
 		"http-signature": "1.2.0",
@@ -146,11 +146,11 @@
 		"koa-slow": "2.1.0",
 		"koa-views": "6.1.4",
 		"kue": "0.11.6",
-		"license-checker": "19.0.0",
+		"license-checker": "20.0.0",
 		"loader-utils": "1.1.0",
 		"mecab-async": "0.1.2",
 		"mkdirp": "0.5.1",
-		"mocha": "5.1.1",
+		"mocha": "5.2.0",
 		"moji": "0.5.1",
 		"mongodb": "3.0.8",
 		"monk": "6.0.6",
@@ -205,12 +205,13 @@
 		"vue-cropperjs": "2.2.0",
 		"vue-js-modal": "1.3.13",
 		"vue-json-tree-view": "2.1.4",
-		"vue-loader": "15.0.11",
+		"vue-loader": "15.1.0",
 		"vue-material": "^1.0.0-beta-10.2",
 		"vue-router": "3.0.1",
 		"vue-template-compiler": "2.5.16",
 		"vuedraggable": "2.16.0",
 		"vuex": "3.0.1",
+		"vuex-persistedstate": "^2.5.4",
 		"web-push": "3.3.1",
 		"webfinger.js": "2.6.6",
 		"webpack": "4.8.3",
diff --git a/src/build/i18n.ts b/src/build/i18n.ts
index addc35ce59..35854055d0 100644
--- a/src/build/i18n.ts
+++ b/src/build/i18n.ts
@@ -7,7 +7,7 @@ import locale from '../../locales';
 export default class Replacer {
 	private lang: string;
 
-	public pattern = /%i18n:([a-z0-9_\-\.\/\|\!]+?)%/g;
+	public pattern = /%i18n:([a-z0-9_\-\.\/\|]+?)%/g;
 
 	constructor(lang: string) {
 		this.lang = lang;
@@ -56,11 +56,6 @@ export default class Replacer {
 	public replacement(match, key) {
 		let path = null;
 
-		const shouldEscape = key[0] == '!';
-		if (shouldEscape) {
-			key = key.substr(1);
-		}
-
 		if (key.indexOf('|') != -1) {
 			path = key.split('|')[0];
 			key = key.split('|')[1];
@@ -68,8 +63,6 @@ export default class Replacer {
 
 		const txt = this.get(path, key);
 
-		return shouldEscape
-			? txt.replace(/'/g, '\\x27').replace(/"/g, '\\x22')
-			: txt.replace(/"/g, '&quot;');
+		return txt.replace(/'/g, '\\x27').replace(/"/g, '\\x22');
 	}
 }
diff --git a/src/client/app/app.styl b/src/client/app/app.styl
index 431b9daa65..ba694b73ae 100644
--- a/src/client/app/app.styl
+++ b/src/client/app/app.styl
@@ -7,6 +7,11 @@ html
 			cursor progress !important
 
 body
+	// for md
+	font-size 16px !important
+	line-height initial !important
+	letter-spacing initial !important
+
 	overflow-wrap break-word
 
 #error
diff --git a/src/client/app/boot.js b/src/client/app/boot.js
index 9338bc501e..7b884c8a54 100644
--- a/src/client/app/boot.js
+++ b/src/client/app/boot.js
@@ -18,6 +18,14 @@
 		return;
 	}
 
+	//#region Load settings
+	let settings = null;
+	const vuex = localStorage.getItem('vuex');
+	if (vuex) {
+		settings = JSON.parse(vuex);
+	}
+	//#endregion
+
 	// Get the current url information
 	const url = new URL(location.href);
 
@@ -29,11 +37,16 @@
 	if (url.pathname == '/auth') app = 'auth';
 	//#endregion
 
-	// Detect the user language
-	// Note: The default language is Japanese
+	//#region Detect the user language
 	let lang = navigator.language.split('-')[0];
+
+	// The default language is English
 	if (!LANGS.includes(lang)) lang = 'en';
-	if (localStorage.getItem('lang')) lang = localStorage.getItem('lang');
+
+	if (settings) {
+		if (settings.device.lang) lang = settings.device.lang;
+	}
+	//#endregion
 
 	// Detect the user agent
 	const ua = navigator.userAgent.toLowerCase();
@@ -61,20 +74,15 @@
 	}
 
 	// Dark/Light
-	if (localStorage.getItem('darkmode') == 'true') {
-		document.documentElement.setAttribute('data-darkmode', 'true');
+	if (settings) {
+		if (settings.device.darkmode) {
+			document.documentElement.setAttribute('data-darkmode', 'true');
+		}
 	}
 
 	// Script version
 	const ver = localStorage.getItem('v') || VERSION;
 
-	// Whether in debug mode
-	const isDebug = localStorage.getItem('debug') == 'true';
-
-	// Whether use raw version script
-	const raw = (localStorage.getItem('useRawScript') == 'true' && isDebug)
-		|| ENV != 'production';
-
 	// Get salt query
 	const salt = localStorage.getItem('salt')
 		? '?salt=' + localStorage.getItem('salt')
@@ -84,7 +92,7 @@
 	// Note: 'async' make it possible to load the script asyncly.
 	//       'defer' make it possible to run the script when the dom loaded.
 	const script = document.createElement('script');
-	script.setAttribute('src', `/assets/${app}.${ver}.${lang}.${raw ? 'raw' : 'min'}.js${salt}`);
+	script.setAttribute('src', `/assets/${app}.${ver}.${lang}.js${salt}`);
 	script.setAttribute('async', 'true');
 	script.setAttribute('defer', 'true');
 	head.appendChild(script);
diff --git a/src/client/app/common/scripts/check-for-update.ts b/src/client/app/common/scripts/check-for-update.ts
index 1e303017eb..b5ba6916d1 100644
--- a/src/client/app/common/scripts/check-for-update.ts
+++ b/src/client/app/common/scripts/check-for-update.ts
@@ -23,7 +23,7 @@ export default async function(mios: MiOS, force = false, silent = false) {
 		}
 
 		if (!silent) {
-			alert('%i18n:!common.update-available%'.replace('{newer}', newer).replace('{current}', current));
+			alert('%i18n:common.update-available%'.replace('{newer}', newer).replace('{current}', current));
 		}
 
 		return newer;
diff --git a/src/client/app/common/scripts/streaming/home.ts b/src/client/app/common/scripts/streaming/home.ts
index 09d830bece..44d07e331a 100644
--- a/src/client/app/common/scripts/streaming/home.ts
+++ b/src/client/app/common/scripts/streaming/home.ts
@@ -62,7 +62,7 @@ export class HomeStream extends Stream {
 		// トークンが再生成されたとき
 		// このままではMisskeyが利用できないので強制的にサインアウトさせる
 		this.on('my_token_regenerated', () => {
-			alert('%i18n:!common.my-token-regenerated%');
+			alert('%i18n:common.my-token-regenerated%');
 			os.signout();
 		});
 	}
diff --git a/src/client/app/common/views/components/avatar.vue b/src/client/app/common/views/components/avatar.vue
index 8ec359e83c..3e1b17635f 100644
--- a/src/client/app/common/views/components/avatar.vue
+++ b/src/client/app/common/views/components/avatar.vue
@@ -21,10 +21,17 @@ export default Vue.extend({
 		}
 	},
 	computed: {
+		lightmode(): boolean {
+			return this.$store.state.device.lightmode;
+		},
 		style(): any {
 			return {
-				backgroundColor: this.user.avatarColor && this.user.avatarColor.length == 3 ? `rgb(${ this.user.avatarColor.join(',') })` : null,
-				backgroundImage: `url(${ this.user.avatarUrl }?thumbnail)`,
+				backgroundColor: this.lightmode
+					? `rgb(${ this.user.avatarColor.slice(0, 3).join(',') })`
+					: this.user.avatarColor && this.user.avatarColor.length == 3
+						? `rgb(${ this.user.avatarColor.join(',') })`
+						: null,
+				backgroundImage: this.lightmode ? null : `url(${ this.user.avatarUrl }?thumbnail)`,
 				borderRadius: (this as any).clientSettings.circleIcons ? '100%' : null
 			};
 		}
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 6a922676b7..6c23cc7969 100644
--- a/src/client/app/common/views/components/connect-failed.troubleshooter.vue
+++ b/src/client/app/common/views/components/connect-failed.troubleshooter.vue
@@ -8,21 +8,21 @@
 					<template v-if="network">%fa:check%</template>
 					<template v-if="!network">%fa:times%</template>
 				</template>
-				{{ network == null ? '%i18n:!@checking-network%' : '%i18n:!@network%' }}<mk-ellipsis v-if="network == null"/>
+				{{ network == null ? '%i18n:@checking-network%' : '%i18n:@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:check%</template>
 					<template v-if="!internet">%fa:times%</template>
 				</template>
-				{{ internet == null ? '%i18n:!@checking-internet%' : '%i18n:!@internet%' }}<mk-ellipsis v-if="internet == null"/>
+				{{ internet == null ? '%i18n:@checking-internet%' : '%i18n:@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:check%</template>
 					<template v-if="!server">%fa:times%</template>
 				</template>
-				{{ server == null ? '%i18n:!@checking-server%' : '%i18n:!@server%' }}<mk-ellipsis v-if="server == null"/>
+				{{ server == null ? '%i18n:@checking-server%' : '%i18n:@server%' }}<mk-ellipsis v-if="server == null"/>
 			</p>
 		</div>
 		<p v-if="!end">%i18n:@finding%<mk-ellipsis/></p>
diff --git a/src/client/app/common/views/components/connect-failed.vue b/src/client/app/common/views/components/connect-failed.vue
index 6c194ff982..0f686926b0 100644
--- a/src/client/app/common/views/components/connect-failed.vue
+++ b/src/client/app/common/views/components/connect-failed.vue
@@ -3,9 +3,9 @@
 	<img src="data:image/jpeg;base64,%base64:/assets/error.jpg%" alt=""/>
 	<h1>%i18n:@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>{{ '%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>
 	</p>
 	<button v-if="!troubleshooting" @click="troubleshooting = true">%i18n:@troubleshoot%</button>
 	<x-troubleshooter v-if="troubleshooting"/>
@@ -28,7 +28,7 @@ export default Vue.extend({
 	},
 	mounted() {
 		document.title = 'Oops!';
-		document.documentElement.style.background = '#f8f8f8';
+		document.documentElement.style.setProperty('background', '#f8f8f8', 'important');
 	},
 	methods: {
 		reload() {
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 32a43ace57..050906cf44 100644
--- a/src/client/app/common/views/components/messaging-room.form.vue
+++ b/src/client/app/common/views/components/messaging-room.form.vue
@@ -197,7 +197,7 @@ export default Vue.extend({
 <style lang="stylus" scoped>
 @import '~const.styl'
 
-.mk-messaging-form
+root(isDark)
 	> textarea
 		cursor auto
 		display block
@@ -209,10 +209,10 @@ export default Vue.extend({
 		padding 8px
 		resize none
 		font-size 1em
-		color #000
+		color isDark ? #fff : #000
 		outline none
 		border none
-		border-top solid 1px #eee
+		border-top solid 1px isDark ? #4b5056 : #eee
 		border-radius 0
 		box-shadow none
 		background transparent
@@ -302,4 +302,10 @@ export default Vue.extend({
 	input[type=file]
 		display none
 
+.mk-messaging-form[data-darkmode]
+	root(true)
+
+.mk-messaging-form:not([data-darkmode])
+	root(false)
+
 </style>
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 ba0ab3209f..ef39199dc4 100644
--- a/src/client/app/common/views/components/messaging-room.message.vue
+++ b/src/client/app/common/views/components/messaging-room.message.vue
@@ -59,8 +59,10 @@ export default Vue.extend({
 </script>
 
 <style lang="stylus" scoped>
-.message
-	$me-balloon-color = #23A7B6
+@import '~const.styl'
+
+root(isDark)
+	$me-balloon-color = $theme-color
 
 	padding 10px 12px 10px 12px
 	background-color transparent
@@ -126,7 +128,7 @@ export default Vue.extend({
 				bottom -4px
 				left -12px
 				margin 0
-				color rgba(#000, 0.5)
+				color isDark ? rgba(#fff, 0.5) : rgba(#000, 0.5)
 				font-size 11px
 
 			> .content
@@ -187,7 +189,7 @@ export default Vue.extend({
 			display block
 			margin 2px 0 0 0
 			font-size 10px
-			color rgba(#000, 0.4)
+			color isDark ? rgba(#fff, 0.4) : rgba(#000, 0.4)
 
 			> [data-fa]
 				margin-left 4px
@@ -200,8 +202,9 @@ export default Vue.extend({
 			padding-left 66px
 
 			> .balloon
+				$color = isDark ? #2d3338 : #eee
 				float left
-				background #eee
+				background $color
 
 				&[data-no-text]
 					background transparent
@@ -209,10 +212,15 @@ export default Vue.extend({
 				&:not([data-no-text]):before
 					left -14px
 					border-top solid 8px transparent
-					border-right solid 8px #eee
+					border-right solid 8px $color
 					border-bottom solid 8px transparent
 					border-left solid 8px transparent
 
+				> .content
+					> .text
+						if isDark
+							color #fff
+
 			> footer
 				text-align left
 
@@ -241,7 +249,7 @@ export default Vue.extend({
 				> .content
 
 					> p.is-deleted
-						color rgba(255, 255, 255, 0.5)
+						color rgba(#fff, 0.5)
 
 					> .text >>>
 						&, *
@@ -254,4 +262,10 @@ export default Vue.extend({
 		> .baloon
 			opacity 0.5
 
+.message[data-darkmode]
+	root(true)
+
+.message:not([data-darkmode])
+	root(false)
+
 </style>
diff --git a/src/client/app/common/views/components/messaging-room.vue b/src/client/app/common/views/components/messaging-room.vue
index a45114e6bb..79756b22eb 100644
--- a/src/client/app/common/views/components/messaging-room.vue
+++ b/src/client/app/common/views/components/messaging-room.vue
@@ -8,7 +8,7 @@
 		<p class="empty" v-if="!init && messages.length == 0">%fa:info-circle%%i18n:@empty%</p>
 		<p class="no-history" v-if="!init && messages.length > 0 && !existMoreMessages">%fa:flag%%i18n:@no-history%</p>
 		<button class="more" :class="{ fetching: fetchingMoreMessages }" v-if="existMoreMessages" @click="fetchMoreMessages" :disabled="fetchingMoreMessages">
-			<template v-if="fetchingMoreMessages">%fa:spinner .pulse .fw%</template>{{ fetchingMoreMessages ? '%i18n:!common.loading%' : '%i18n:!@more%' }}
+			<template v-if="fetchingMoreMessages">%fa:spinner .pulse .fw%</template>{{ fetchingMoreMessages ? '%i18n:common.loading%' : '%i18n:@more%' }}
 		</button>
 		<template v-for="(message, i) in _messages">
 			<x-message :message="message" :key="message.id"/>
@@ -18,7 +18,11 @@
 		</template>
 	</div>
 	<footer>
-		<div ref="notifications" class="notifications"></div>
+		<transition name="fade">
+			<div class="new-message" v-show="showIndicator">
+				<button @click="onIndicatorClick">%fa:arrow-circle-down%%i18n:@new-message%</button>
+			</div>
+		</transition>
 		<x-form :user="user" ref="form"/>
 	</footer>
 </div>
@@ -45,7 +49,9 @@ export default Vue.extend({
 			fetchingMoreMessages: false,
 			messages: [],
 			existMoreMessages: false,
-			connection: null
+			connection: null,
+			showIndicator: false,
+			timer: null
 		};
 	},
 
@@ -149,9 +155,9 @@ export default Vue.extend({
 
 		onMessage(message) {
 			// サウンドを再生する
-			if ((this as any).os.isEnableSounds) {
+			if (this.$store.state.device.enableSounds) {
 				const sound = new Audio(`${url}/assets/message.mp3`);
-				sound.volume = localStorage.getItem('soundVolume') ? parseInt(localStorage.getItem('soundVolume'), 10) / 100 : 0.5;
+				sound.volume = this.$store.state.device.soundVolume;
 				sound.play();
 			}
 
@@ -172,7 +178,7 @@ export default Vue.extend({
 				});
 			} else if (message.userId != (this as any).os.i.id) {
 				// Notify
-				this.notify('%i18n:!@new-message%');
+				this.notifyNewMessage();
 			}
 		},
 
@@ -205,18 +211,18 @@ export default Vue.extend({
 			}
 		},
 
-		notify(message) {
-			const n = document.createElement('p') as any;
-			n.innerHTML = '%fa:arrow-circle-down%' + message;
-			n.onclick = () => {
-				this.scrollToBottom();
-				n.parentNode.removeChild(n);
-			};
-			(this.$refs.notifications as any).appendChild(n);
+		onIndicatorClick() {
+			this.showIndicator = false;
+			this.scrollToBottom();
+		},
 
-			setTimeout(() => {
-				n.style.opacity = 0;
-				setTimeout(() => n.parentNode.removeChild(n), 1000);
+		notifyNewMessage() {
+			this.showIndicator = true;
+
+			if (this.timer) clearTimeout(this.timer);
+
+			this.timer = setTimeout(() => {
+				this.showIndicator = false;
 			}, 4000);
 		},
 
@@ -238,11 +244,12 @@ export default Vue.extend({
 <style lang="stylus" scoped>
 @import '~const.styl'
 
-.mk-messaging-room
+root(isDark)
 	display flex
 	flex 1
 	flex-direction column
 	height 100%
+	background isDark ? #191b22 : #fff
 
 	> .stream
 		width 100%
@@ -256,7 +263,7 @@ export default Vue.extend({
 			padding 16px 8px 8px 8px
 			text-align center
 			font-size 0.8em
-			color rgba(#000, 0.4)
+			color rgba(isDark ? #fff : #000, 0.4)
 
 			[data-fa]
 				margin-right 4px
@@ -267,7 +274,7 @@ export default Vue.extend({
 			padding 16px 8px 8px 8px
 			text-align center
 			font-size 0.8em
-			color rgba(#000, 0.4)
+			color rgba(isDark ? #fff : #000, 0.4)
 
 			[data-fa]
 				margin-right 4px
@@ -278,7 +285,7 @@ export default Vue.extend({
 			padding 16px
 			text-align center
 			font-size 0.8em
-			color rgba(#000, 0.4)
+			color rgba(isDark ? #fff : #000, 0.4)
 
 			[data-fa]
 				margin-right 4px
@@ -322,7 +329,7 @@ export default Vue.extend({
 				left 0
 				right 0
 				margin 0 auto
-				background rgba(#000, 0.1)
+				background rgba(isDark ? #fff : #000, 0.1)
 
 			> span
 				display inline-block
@@ -330,8 +337,8 @@ export default Vue.extend({
 				padding 0 16px
 				//font-weight bold
 				line-height 32px
-				color rgba(#000, 0.3)
-				background #fff
+				color rgba(isDark ? #fff : #000, 0.3)
+				background isDark ? #191b22 : #fff
 
 	> footer
 		position -webkit-sticky
@@ -342,30 +349,32 @@ export default Vue.extend({
 		max-width 600px
 		margin 0 auto
 		padding 0
-		background rgba(255, 255, 255, 0.95)
+		background rgba(isDark ? #282c37 : #fff, 0.95)
 		background-clip content-box
 
-		> .notifications
+		> .new-message
 			position absolute
 			top -48px
 			width 100%
 			padding 8px 0
 			text-align center
 
-			&:empty
-				display none
-
-			> p
+			> button
 				display inline-block
 				margin 0
-				padding 0 12px 0 28px
+				padding 0 12px 0 30px
 				cursor pointer
 				line-height 32px
 				font-size 12px
 				color $theme-color-foreground
 				background $theme-color
 				border-radius 16px
-				transition opacity 1s ease
+
+				&:hover
+					background lighten($theme-color, 10%)
+
+				&:active
+					background darken($theme-color, 10%)
 
 				> [data-fa]
 					position absolute
@@ -374,4 +383,17 @@ export default Vue.extend({
 					line-height 32px
 					font-size 16px
 
+.fade-enter-active, .fade-leave-active
+	transition opacity 0.1s
+
+.fade-enter, .fade-leave-to
+	transition opacity 0.5s
+	opacity 0
+
+.mk-messaging-room[data-darkmode]
+	root(true)
+
+.mk-messaging-room:not([data-darkmode])
+	root(false)
+
 </style>
diff --git a/src/client/app/common/views/components/othello.game.vue b/src/client/app/common/views/components/othello.game.vue
index 8c646cce07..ea75558d10 100644
--- a/src/client/app/common/views/components/othello.game.vue
+++ b/src/client/app/common/views/components/othello.game.vue
@@ -162,9 +162,9 @@ export default Vue.extend({
 			this.o.put(this.myColor, pos);
 
 			// サウンドを再生する
-			if ((this as any).os.isEnableSounds) {
+			if (this.$store.state.device.enableSounds) {
 				const sound = new Audio(`${url}/assets/othello-put-me.mp3`);
-				sound.volume = localStorage.getItem('soundVolume') ? parseInt(localStorage.getItem('soundVolume'), 10) / 100 : 0.5;
+				sound.volume = this.$store.state.device.soundVolume;
 				sound.play();
 			}
 
@@ -186,9 +186,9 @@ export default Vue.extend({
 			this.$forceUpdate();
 
 			// サウンドを再生する
-			if ((this as any).os.isEnableSounds && x.color != this.myColor) {
+			if (this.$store.state.device.enableSounds && x.color != this.myColor) {
 				const sound = new Audio(`${url}/assets/othello-put-you.mp3`);
-				sound.volume = localStorage.getItem('soundVolume') ? parseInt(localStorage.getItem('soundVolume'), 10) / 100 : 0.5;
+				sound.volume = this.$store.state.device.soundVolume;
 				sound.play();
 			}
 		},
diff --git a/src/client/app/common/views/components/poll-editor.vue b/src/client/app/common/views/components/poll-editor.vue
index 95bcba996e..115c934c8b 100644
--- a/src/client/app/common/views/components/poll-editor.vue
+++ b/src/client/app/common/views/components/poll-editor.vue
@@ -5,7 +5,7 @@
 	</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)">
+			<input :value="choice" @input="onInput(i, $event)" :placeholder="'%i18n:@choice-n%'.replace('{}', i + 1)">
 			<button @click="remove(i)" title="%i18n:@remove%">
 				%fa:times%
 			</button>
diff --git a/src/client/app/common/views/components/poll.vue b/src/client/app/common/views/components/poll.vue
index 46e41cbcdb..660247edbc 100644
--- a/src/client/app/common/views/components/poll.vue
+++ b/src/client/app/common/views/components/poll.vue
@@ -1,19 +1,19 @@
 <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 ? '%i18n:@vote-to%'.replace('{}', choice.text) : ''">
 			<div class="backdrop" :style="{ 'width': (showResult ? (choice.votes / total * 100) : 0) + '%' }"></div>
 			<span>
 				<template v-if="choice.isVoted">%fa: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">({{ '%i18n:@vote-count%'.replace('{}', choice.votes) }})</span>
 			</span>
 		</li>
 	</ul>
 	<p v-if="total > 0">
-		<span>{{ '%i18n:!@total-users%'.replace('{}', total) }}</span>
+		<span>{{ '%i18n:@total-users%'.replace('{}', total) }}</span>
 		<span>・</span>
-		<a v-if="!isVoted" @click="toggleShowResult">{{ showResult ? '%i18n:!@vote%' : '%i18n:!@show-result%' }}</a>
+		<a v-if="!isVoted" @click="toggleShowResult">{{ showResult ? '%i18n:@vote%' : '%i18n:@show-result%' }}</a>
 		<span v-if="isVoted">%i18n:@voted%</span>
 	</p>
 </div>
diff --git a/src/client/app/common/views/components/reaction-picker.vue b/src/client/app/common/views/components/reaction-picker.vue
index e2c8a6ed3f..0db6f66b37 100644
--- a/src/client/app/common/views/components/reaction-picker.vue
+++ b/src/client/app/common/views/components/reaction-picker.vue
@@ -22,7 +22,7 @@
 import Vue from 'vue';
 import * as anime from 'animejs';
 
-const placeholder = '%i18n:!@choose-reaction%';
+const placeholder = '%i18n:@choose-reaction%';
 
 export default Vue.extend({
 	props: ['note', 'source', 'compact', 'cb'],
diff --git a/src/client/app/common/views/components/signin.vue b/src/client/app/common/views/components/signin.vue
index 7fb9fc3fd4..6b9d58e0a8 100644
--- a/src/client/app/common/views/components/signin.vue
+++ b/src/client/app/common/views/components/signin.vue
@@ -9,7 +9,7 @@
 	<label class="token" v-if="user && user.twoFactorEnabled">
 		<input v-model="token" type="number" placeholder="%i18n:@token%" required/>%fa:lock%
 	</label>
-	<button type="submit" :disabled="signing">{{ signing ? '%i18n:!@signing-in%' : '%i18n:!@signin%' }}</button>
+	<button type="submit" :disabled="signing">{{ signing ? '%i18n:@signing-in%' : '%i18n:@signin%' }}</button>
 	もしくは <a :href="`${apiUrl}/signin/twitter`">Twitterでログイン</a>
 </form>
 </template>
diff --git a/src/client/app/common/views/components/signup.vue b/src/client/app/common/views/components/signup.vue
index 516979acd0..f8bf7dd798 100644
--- a/src/client/app/common/views/components/signup.vue
+++ b/src/client/app/common/views/components/signup.vue
@@ -127,7 +127,7 @@ export default Vue.extend({
 					location.href = '/';
 				});
 			}).catch(() => {
-				alert('%i18n:!@some-error%');
+				alert('%i18n:@some-error%');
 
 				(window as any).grecaptcha.reset();
 				this.recaptchaed = false;
diff --git a/src/client/app/common/views/components/time.vue b/src/client/app/common/views/components/time.vue
index 533958697c..6e0d2b0dcb 100644
--- a/src/client/app/common/views/components/time.vue
+++ b/src/client/app/common/views/components/time.vue
@@ -44,16 +44,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 ? '%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%');
 		}
 	},
 	created() {
diff --git a/src/client/app/common/views/components/twitter-setting.vue b/src/client/app/common/views/components/twitter-setting.vue
index ab07e6d09a..9a2a1c3d40 100644
--- a/src/client/app/common/views/components/twitter-setting.vue
+++ b/src/client/app/common/views/components/twitter-setting.vue
@@ -3,7 +3,7 @@
 	<p>%i18n:@description%<a :href="`${docsUrl}/link-to-twitter`" target="_blank">%i18n:@detail%</a></p>
 	<p class="account" v-if="os.i.twitter" :title="`Twitter ID: ${os.i.twitter.userId}`">%i18n:@connected-to%: <a :href="`https://twitter.com/${os.i.twitter.screenName}`" target="_blank">@{{ os.i.twitter.screenName }}</a></p>
 	<p>
-		<a :href="`${apiUrl}/connect/twitter`" target="_blank" @click.prevent="connect">{{ os.i.twitter ? '%i18n:!@reconnect%' : '%i18n:!@connect%' }}</a>
+		<a :href="`${apiUrl}/connect/twitter`" target="_blank" @click.prevent="connect">{{ os.i.twitter ? '%i18n:@reconnect%' : '%i18n:@connect%' }}</a>
 		<span v-if="os.i.twitter"> or </span>
 		<a :href="`${apiUrl}/disconnect/twitter`" target="_blank" v-if="os.i.twitter" @click.prevent="disconnect">%i18n:@disconnect%</a>
 	</p>
diff --git a/src/client/app/common/views/components/visibility-chooser.vue b/src/client/app/common/views/components/visibility-chooser.vue
index 50f0877ae9..592367cd6d 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:globe%</div>
 			<div>
-				<span>公開</span>
+				<span>%i18n:@public%</span>
 			</div>
 		</div>
 		<div @click="choose('home')" :class="{ active: v == 'home' }">
 			<div>%fa:home%</div>
 			<div>
-				<span>ホーム</span>
-				<span>ホームタイムラインにのみ公開</span>
+				<span>%i18n:@home%</span>
+				<span>%i18n:@home-desc%</span>
 			</div>
 		</div>
 		<div @click="choose('followers')" :class="{ active: v == 'followers' }">
 			<div>%fa:unlock%</div>
 			<div>
-				<span>フォロワー</span>
-				<span>自分のフォロワーにのみ公開</span>
+				<span>%i18n:@followers%</span>
+				<span>%i18n:@followers-desc%</span>
 			</div>
 		</div>
 		<div @click="choose('specified')" :class="{ active: v == 'specified' }">
 			<div>%fa:envelope%</div>
 			<div>
-				<span>ダイレクト</span>
-				<span>指定したユーザーにのみ公開</span>
+				<span>%i18n:@specified%</span>
+				<span>%i18n:@specified-desc%</span>
 			</div>
 		</div>
 		<div @click="choose('private')" :class="{ active: v == 'private' }">
 			<div>%fa:lock%</div>
 			<div>
-				<span>非公開</span>
+				<span>%i18n:@private%</span>
 			</div>
 		</div>
 	</div>
diff --git a/src/client/app/common/views/components/welcome-timeline.vue b/src/client/app/common/views/components/welcome-timeline.vue
index 6fadb030c3..cad59d24f0 100644
--- a/src/client/app/common/views/components/welcome-timeline.vue
+++ b/src/client/app/common/views/components/welcome-timeline.vue
@@ -37,6 +37,7 @@ export default Vue.extend({
 		fetch(cb?) {
 			this.fetching = true;
 			(this as any).api('notes', {
+				local: true,
 				reply: false,
 				renote: false,
 				media: false,
@@ -52,15 +53,15 @@ export default Vue.extend({
 </script>
 
 <style lang="stylus" scoped>
-.mk-welcome-timeline
-	background #fff
+root(isDark)
+	background isDark ? #282C37 : #fff
 
 	> div
 		padding 16px
 		overflow-wrap break-word
 		font-size .9em
-		color #4C4C4C
-		border-bottom 1px solid rgba(#000, 0.05)
+		color isDark ? #fff : #4C4C4C
+		border-bottom 1px solid isDark ? rgba(#000, 0.1) : rgba(#000, 0.05)
 
 		&:after
 			content ""
@@ -95,17 +96,23 @@ export default Vue.extend({
 					overflow hidden
 					font-weight bold
 					text-overflow ellipsis
-					color #627079
+					color isDark ? #fff : #627079
 
 				> .username
 					margin 0 .5em 0 0
-					color #ccc
+					color isDark ? #606984 : #ccc
 
 				> .info
 					margin-left auto
 					font-size 0.9em
 
 					> .created-at
-						color #c0c0c0
+						color isDark ? #606984 : #c0c0c0
+
+.mk-welcome-timeline[data-darkmode]
+	root(true)
+
+.mk-welcome-timeline:not([data-darkmode])
+	root(false)
 
 </style>
diff --git a/src/client/app/common/views/widgets/broadcast.vue b/src/client/app/common/views/widgets/broadcast.vue
index 75b1d60524..f337cec853 100644
--- a/src/client/app/common/views/widgets/broadcast.vue
+++ b/src/client/app/common/views/widgets/broadcast.vue
@@ -14,7 +14,7 @@
 		</svg>
 	</div>
 	<p class="fetching" v-if="fetching">%i18n:@fetching%<mk-ellipsis/></p>
-	<h1 v-if="!fetching">{{ broadcasts.length == 0 ? '%i18n:!@no-broadcasts%' : broadcasts[i].title }}</h1>
+	<h1 v-if="!fetching">{{ broadcasts.length == 0 ? '%i18n:@no-broadcasts%' : broadcasts[i].title }}</h1>
 	<p v-if="!fetching">
 		<span v-if="broadcasts.length != 0" v-html="broadcasts[i].text"></span>
 		<template v-if="broadcasts.length == 0">%i18n:@have-a-nice-day%</template>
diff --git a/src/client/app/common/views/widgets/donation.vue b/src/client/app/common/views/widgets/donation.vue
index e35462611d..75f5db808a 100644
--- a/src/client/app/common/views/widgets/donation.vue
+++ b/src/client/app/common/views/widgets/donation.vue
@@ -3,9 +3,9 @@
 	<article>
 		<h1>%fa:heart%%i18n:@title%</h1>
 		<p>
-			{{ '%i18n:!@text%'.substr(0, '%i18n:!@text%'.indexOf('{')) }}
+			{{ '%i18n:@text%'.substr(0, '%i18n:@text%'.indexOf('{')) }}
 			<a href="https://syuilo.com">@syuilo</a>
-			{{ '%i18n:!@text%'.substr('%i18n:!@text%'.indexOf('}') + 1) }}
+			{{ '%i18n:@text%'.substr('%i18n:@text%'.indexOf('}') + 1) }}
 		</p>
 	</article>
 </div>
diff --git a/src/client/app/config.ts b/src/client/app/config.ts
index 522d7ff056..70c085de1c 100644
--- a/src/client/app/config.ts
+++ b/src/client/app/config.ts
@@ -8,6 +8,7 @@ declare const _STATS_URL_: string;
 declare const _STATUS_URL_: string;
 declare const _DEV_URL_: string;
 declare const _LANG_: string;
+declare const _LANGS_: string;
 declare const _RECAPTCHA_SITEKEY_: string;
 declare const _SW_PUBLICKEY_: string;
 declare const _THEME_COLOR_: string;
@@ -27,6 +28,7 @@ export const statsUrl = _STATS_URL_;
 export const statusUrl = _STATUS_URL_;
 export const devUrl = _DEV_URL_;
 export const lang = _LANG_;
+export const langs = _LANGS_;
 export const recaptchaSitekey = _RECAPTCHA_SITEKEY_;
 export const swPublickey = _SW_PUBLICKEY_;
 export const themeColor = _THEME_COLOR_;
diff --git a/src/client/app/desktop/views/components/calendar.vue b/src/client/app/desktop/views/components/calendar.vue
index 757eefac7e..9a93841e52 100644
--- a/src/client/app/desktop/views/components/calendar.vue
+++ b/src/client/app/desktop/views/components/calendar.vue
@@ -2,7 +2,7 @@
 <div class="mk-calendar" :data-melt="design == 4 || design == 5">
 	<template v-if="design == 0 || design == 1">
 		<button @click="prev" title="%i18n:@prev%">%fa:chevron-circle-left%</button>
-		<p class="title">{{ '%i18n:!@title%'.replace('{1}', year).replace('{2}', month) }}</p>
+		<p class="title">{{ '%i18n:@title%'.replace('{1}', year).replace('{2}', month) }}</p>
 		<button @click="next" title="%i18n:@next%">%fa:chevron-circle-right%</button>
 	</template>
 
@@ -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 : '%i18n:@go%'"
 		>
 			<div>{{ i + 1 }}</div>
 		</div>
@@ -58,13 +58,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%'
+				'%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%'
 			]
 		};
 	},
diff --git a/src/client/app/desktop/views/components/drive.file.vue b/src/client/app/desktop/views/components/drive.file.vue
index d8b8420ece..fb553e1ae7 100644
--- a/src/client/app/desktop/views/components/drive.file.vue
+++ b/src/client/app/desktop/views/components/drive.file.vue
@@ -64,46 +64,46 @@ export default Vue.extend({
 			this.isContextmenuShowing = true;
 			contextmenu(e, [{
 				type: 'item',
-				text: '%i18n:!@contextmenu.rename%',
+				text: '%i18n:@contextmenu.rename%',
 				icon: '%fa:i-cursor%',
 				onClick: this.rename
 			}, {
 				type: 'item',
-				text: '%i18n:!@contextmenu.copy-url%',
+				text: '%i18n:@contextmenu.copy-url%',
 				icon: '%fa:link%',
 				onClick: this.copyUrl
 			}, {
 				type: 'link',
 				href: `${this.file.url}?download`,
-				text: '%i18n:!@contextmenu.download%',
+				text: '%i18n:@contextmenu.download%',
 				icon: '%fa:download%',
 			}, {
 				type: 'divider',
 			}, {
 				type: 'item',
-				text: '%i18n:!common.delete%',
+				text: '%i18n:common.delete%',
 				icon: '%fa:R trash-alt%',
 				onClick: this.deleteFile
 			}, {
 				type: 'divider',
 			}, {
 				type: 'nest',
-				text: '%i18n:!@contextmenu.else-files%',
+				text: '%i18n:@contextmenu.else-files%',
 				menu: [{
 					type: 'item',
-					text: '%i18n:!@contextmenu.set-as-avatar%',
+					text: '%i18n:@contextmenu.set-as-avatar%',
 					onClick: this.setAsAvatar
 				}, {
 					type: 'item',
-					text: '%i18n:!@contextmenu.set-as-banner%',
+					text: '%i18n:@contextmenu.set-as-banner%',
 					onClick: this.setAsBanner
 				}]
 			}, {
 				type: 'nest',
-				text: '%i18n:!@contextmenu.open-in-app%',
+				text: '%i18n:@contextmenu.open-in-app%',
 				menu: [{
 					type: 'item',
-					text: '%i18n:!@contextmenu.add-app%...',
+					text: '%i18n:@contextmenu.add-app%...',
 					onClick: this.addApp
 				}]
 			}], {
@@ -141,8 +141,8 @@ export default Vue.extend({
 
 		rename() {
 			(this as any).apis.input({
-				title: '%i18n:!@contextmenu.rename-file%',
-				placeholder: '%i18n:!@contextmenu.input-new-file-name%',
+				title: '%i18n:@contextmenu.rename-file%',
+				placeholder: '%i18n:@contextmenu.input-new-file-name%',
 				default: this.file.name,
 				allowEmpty: false
 			}).then(name => {
@@ -157,9 +157,9 @@ export default Vue.extend({
 			copyToClipboard(this.file.url);
 			(this as any).apis.dialog({
 				title: '%fa:check%%i18n:@contextmenu.copied%',
-				text: '%i18n:!@contextmenu.copied-url-to-clipboard%',
+				text: '%i18n:@contextmenu.copied-url-to-clipboard%',
 				actions: [{
-					text: '%i18n:!common.ok%'
+					text: '%i18n:common.ok%'
 				}]
 			});
 		},
diff --git a/src/client/app/desktop/views/components/drive.folder.vue b/src/client/app/desktop/views/components/drive.folder.vue
index 0761ffb1a1..16f474f4e0 100644
--- a/src/client/app/desktop/views/components/drive.folder.vue
+++ b/src/client/app/desktop/views/components/drive.folder.vue
@@ -54,26 +54,26 @@ export default Vue.extend({
 			this.isContextmenuShowing = true;
 			contextmenu(e, [{
 				type: 'item',
-				text: '%i18n:!@contextmenu.move-to-this-folder%',
+				text: '%i18n:@contextmenu.move-to-this-folder%',
 				icon: '%fa:arrow-right%',
 				onClick: this.go
 			}, {
 				type: 'item',
-				text: '%i18n:!@contextmenu.show-in-new-window%',
+				text: '%i18n:@contextmenu.show-in-new-window%',
 				icon: '%fa:R window-restore%',
 				onClick: this.newWindow
 			}, {
 				type: 'divider',
 			}, {
 				type: 'item',
-				text: '%i18n:!@contextmenu.rename%',
+				text: '%i18n:@contextmenu.rename%',
 				icon: '%fa:i-cursor%',
 				onClick: this.rename
 			}, {
 				type: 'divider',
 			}, {
 				type: 'item',
-				text: '%i18n:!common.delete%',
+				text: '%i18n:common.delete%',
 				icon: '%fa:R trash-alt%',
 				onClick: this.deleteFolder
 			}], {
@@ -159,15 +159,15 @@ export default Vue.extend({
 					switch (err) {
 						case 'detected-circular-definition':
 							(this as any).apis.dialog({
-								title: '%fa:exclamation-triangle%%i18n:!@unable-to-process%',
-								text: '%i18n:!@circular-reference-detected%',
+								title: '%fa:exclamation-triangle%%i18n:@unable-to-process%',
+								text: '%i18n:@circular-reference-detected%',
 								actions: [{
-									text: '%i18n:!common.ok%'
+									text: '%i18n:common.ok%'
 								}]
 							});
 							break;
 						default:
-							alert('%i18n:!@unhandled-error% ' + err);
+							alert('%i18n:@unhandled-error% ' + err);
 					}
 				});
 			}
@@ -199,8 +199,8 @@ export default Vue.extend({
 
 		rename() {
 			(this as any).apis.input({
-				title: '%i18n:!@contextmenu.rename-folder%',
-				placeholder: '%i18n:!@contextmenu.input-new-folder-name%',
+				title: '%i18n:@contextmenu.rename-folder%',
+				placeholder: '%i18n:@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 71b2e419d9..40f620875e 100644
--- a/src/client/app/desktop/views/components/drive.nav-folder.vue
+++ b/src/client/app/desktop/views/components/drive.nav-folder.vue
@@ -8,7 +8,7 @@
 	@drop.stop="onDrop"
 >
 	<template v-if="folder == null">%fa:cloud%</template>
-	<span>{{ folder == null ? '%i18n:!@drive%' : folder.name }}</span>
+	<span>{{ folder == null ? '%i18n:@drive%' : folder.name }}</span>
 </div>
 </template>
 
diff --git a/src/client/app/desktop/views/components/drive.vue b/src/client/app/desktop/views/components/drive.vue
index 973df1014d..cae40f306c 100644
--- a/src/client/app/desktop/views/components/drive.vue
+++ b/src/client/app/desktop/views/components/drive.vue
@@ -138,17 +138,17 @@ export default Vue.extend({
 		onContextmenu(e) {
 			contextmenu(e, [{
 				type: 'item',
-				text: '%i18n:!@contextmenu.create-folder%',
+				text: '%i18n:@contextmenu.create-folder%',
 				icon: '%fa:R folder%',
 				onClick: this.createFolder
 			}, {
 				type: 'item',
-				text: '%i18n:!@contextmenu.upload%',
+				text: '%i18n:@contextmenu.upload%',
 				icon: '%fa:upload%',
 				onClick: this.selectLocalFile
 			}, {
 				type: 'item',
-				text: '%i18n:!@contextmenu.url-upload%',
+				text: '%i18n:@contextmenu.url-upload%',
 				icon: '%fa:cloud-upload-alt%',
 				onClick: this.urlUpload
 			}]);
@@ -306,15 +306,15 @@ export default Vue.extend({
 					switch (err) {
 						case 'detected-circular-definition':
 							(this as any).apis.dialog({
-								title: '%fa:exclamation-triangle%%i18n:!@unable-to-process%',
-								text: '%i18n:!@circular-reference-detected%',
+								title: '%fa:exclamation-triangle%%i18n:@unable-to-process%',
+								text: '%i18n:@circular-reference-detected%',
 								actions: [{
-									text: '%i18n:!common.ok%'
+									text: '%i18n:common.ok%'
 								}]
 							});
 							break;
 						default:
-							alert('%i18n:!@unhandled-error% ' + err);
+							alert('%i18n:@unhandled-error% ' + err);
 					}
 				});
 			}
@@ -327,8 +327,8 @@ export default Vue.extend({
 
 		urlUpload() {
 			(this as any).apis.input({
-				title: '%i18n:!@url-upload%',
-				placeholder: '%i18n:!@url-of-file%'
+				title: '%i18n:@url-upload%',
+				placeholder: '%i18n:@url-of-file%'
 			}).then(url => {
 				(this as any).api('drive/files/upload_from_url', {
 					url: url,
@@ -337,9 +337,9 @@ export default Vue.extend({
 
 				(this as any).apis.dialog({
 					title: '%fa:check%%i18n:@url-upload-requested%',
-					text: '%i18n:!@may-take-time%',
+					text: '%i18n:@may-take-time%',
 					actions: [{
-						text: '%i18n:!common.ok%'
+						text: '%i18n:common.ok%'
 					}]
 				});
 			});
@@ -347,8 +347,8 @@ export default Vue.extend({
 
 		createFolder() {
 			(this as any).apis.input({
-				title: '%i18n:!@create-folder%',
-				placeholder: '%i18n:!@folder-name%'
+				title: '%i18n:@create-folder%',
+				placeholder: '%i18n:@folder-name%'
 			}).then(name => {
 				(this as any).api('drive/folders/create', {
 					name: name,
diff --git a/src/client/app/desktop/views/components/followers-window.vue b/src/client/app/desktop/views/components/followers-window.vue
index f3eec13e0b..7ed31315f1 100644
--- a/src/client/app/desktop/views/components/followers-window.vue
+++ b/src/client/app/desktop/views/components/followers-window.vue
@@ -1,7 +1,7 @@
 <template>
 <mk-window width="400px" height="550px" @closed="$destroy">
 	<span slot="header" :class="$style.header">
-		<img :src="`${user.avatarUrl}?thumbnail&size=64`" alt=""/>%i18n:!@followers%.replace('{}', {{ user | userName }})
+		<img :src="`${user.avatarUrl}?thumbnail&size=64`" alt=""/>{{ '%i18n:@followers%'.replace('{}', name) }}
 	</span>
 	<mk-followers :user="user"/>
 </mk-window>
@@ -11,7 +11,12 @@
 import Vue from 'vue';
 
 export default Vue.extend({
-	props: ['user']
+	props: ['user'],
+	computed: {
+		name(): string {
+			return Vue.filter('userName')(this.user);
+		}
+	}
 });
 </script>
 
diff --git a/src/client/app/desktop/views/components/following-window.vue b/src/client/app/desktop/views/components/following-window.vue
index 153819b12e..b97f21e2a3 100644
--- a/src/client/app/desktop/views/components/following-window.vue
+++ b/src/client/app/desktop/views/components/following-window.vue
@@ -1,7 +1,7 @@
 <template>
 <mk-window width="400px" height="550px" @closed="$destroy">
 	<span slot="header" :class="$style.header">
-		<img :src="`${user.avatarUrl}?thumbnail&size=64`" alt=""/>%i18n:!@following%.replace('{}', {{ user | userName }})
+		<img :src="`${user.avatarUrl}?thumbnail&size=64`" alt=""/>{{ '%i18n:@following%'.replace('{}', name) }}
 	</span>
 	<mk-following :user="user"/>
 </mk-window>
@@ -11,7 +11,12 @@
 import Vue from 'vue';
 
 export default Vue.extend({
-	props: ['user']
+	props: ['user'],
+	computed: {
+		name(): string {
+			return Vue.filter('userName')(this.user);
+		}
+	}
 });
 </script>
 
diff --git a/src/client/app/desktop/views/components/home.vue b/src/client/app/desktop/views/components/home.vue
index 87dae5a806..d84c1e404f 100644
--- a/src/client/app/desktop/views/components/home.vue
+++ b/src/client/app/desktop/views/components/home.vue
@@ -102,7 +102,7 @@ export default Vue.extend({
 
 	computed: {
 		home(): any[] {
-			return this.$store.state.settings.data.home;
+			return this.$store.state.settings.home;
 		},
 		left(): any[] {
 			return this.home.filter(w => w.place == 'left');
diff --git a/src/client/app/desktop/views/components/note-detail.vue b/src/client/app/desktop/views/components/note-detail.vue
index bda53db918..5b48b7a1ba 100644
--- a/src/client/app/desktop/views/components/note-detail.vue
+++ b/src/client/app/desktop/views/components/note-detail.vue
@@ -2,16 +2,16 @@
 <div class="mk-note-detail" :title="title">
 	<button
 		class="read-more"
-		v-if="p.reply && p.reply.replyId && context.length == 0"
-		title="会話をもっと読み込む"
-		@click="fetchContext"
-		:disabled="contextFetching"
+		v-if="p.reply && p.reply.replyId && conversation.length == 0"
+		title="%i18n:@more%"
+		@click="fetchConversation"
+		:disabled="conversationFetching"
 	>
-		<template v-if="!contextFetching">%fa:ellipsis-v%</template>
-		<template v-if="contextFetching">%fa:spinner .pulse%</template>
+		<template v-if="!conversationFetching">%fa:ellipsis-v%</template>
+		<template v-if="conversationFetching">%fa:spinner .pulse%</template>
 	</button>
-	<div class="context">
-		<x-sub v-for="note in context" :key="note.id" :note="note"/>
+	<div class="conversation">
+		<x-sub v-for="note in conversation" :key="note.id" :note="note"/>
 	</div>
 	<div class="reply-to" v-if="p.reply">
 		<x-sub :note="p.reply"/>
@@ -21,7 +21,10 @@
 			<mk-avatar class="avatar" :user="note.user"/>
 			%fa:retweet%
 			<router-link class="name" :href="note.user | userPage">{{ note.user | userName }}</router-link>
-			がRenote
+			<span>{{ '%i18n:@reposted-by%'.substr(0, '%i18n:@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>
+			<mk-time :time="note.createdAt"/>
 		</p>
 	</div>
 	<article>
@@ -35,7 +38,7 @@
 		</header>
 		<div class="body">
 			<div class="text">
-				<span v-if="p.isHidden" style="opacity: 0.5">(この投稿は非公開です)</span>
+				<span v-if="p.isHidden" style="opacity: 0.5">%i18n:@private%</span>
 				<mk-note-html v-if="p.text" :text="p.text" :i="os.i"/>
 			</div>
 			<div class="media" v-if="p.media.length > 0">
@@ -46,7 +49,7 @@
 			<div class="tags" v-if="p.tags && p.tags.length > 0">
 				<router-link v-for="tag in p.tags" :key="tag" :to="`/search?q=#${tag}`">{{ tag }}</router-link>
 			</div>
-			<a class="location" v-if="p.geo" :href="`http://maps.google.com/maps?q=${p.geo.coordinates[1]},${p.geo.coordinates[0]}`" target="_blank">%fa:map-marker-alt% 位置情報</a>
+			<a class="location" v-if="p.geo" :href="`http://maps.google.com/maps?q=${p.geo.coordinates[1]},${p.geo.coordinates[0]}`" target="_blank">%fa:map-marker-alt% %i18n:@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"/>
@@ -54,15 +57,15 @@
 		</div>
 		<footer>
 			<mk-reactions-viewer :note="p"/>
-			<button @click="reply" title="返信">
+			<button @click="reply" title="">
 				<template v-if="p.reply">%fa:reply-all%</template>
 				<template v-else>%fa:reply%</template>
 				<p class="count" v-if="p.repliesCount > 0">{{ p.repliesCount }}</p>
 			</button>
-			<button @click="renote" title="Renote">
+			<button @click="renote" title="%i18n:@renote%">
 				%fa:retweet%<p class="count" v-if="p.renoteCount > 0">{{ p.renoteCount }}</p>
 			</button>
-			<button :class="{ reacted: p.myReaction != null }" @click="react" ref="reactButton" title="リアクション">
+			<button :class="{ reacted: p.myReaction != null }" @click="react" ref="reactButton" title="%i18n:@add-reaction%">
 				%fa:plus%<p class="count" v-if="p.reactions_count > 0">{{ p.reactions_count }}</p>
 			</button>
 			<button @click="menu" ref="menuButton">
@@ -104,8 +107,8 @@ export default Vue.extend({
 
 	data() {
 		return {
-			context: [],
-			contextFetching: false,
+			conversation: [],
+			conversationFetching: false,
 			replies: []
 		};
 	},
@@ -173,15 +176,15 @@ export default Vue.extend({
 	},
 
 	methods: {
-		fetchContext() {
-			this.contextFetching = true;
+		fetchConversation() {
+			this.conversationFetching = true;
 
-			// Fetch context
-			(this as any).api('notes/context', {
+			// Fetch conversation
+			(this as any).api('notes/conversation', {
 				noteId: this.p.replyId
-			}).then(context => {
-				this.contextFetching = false;
-				this.context = context.reverse();
+			}).then(conversation => {
+				this.conversationFetching = false;
+				this.conversation = conversation.reverse();
 			});
 		},
 		reply() {
@@ -246,7 +249,7 @@ root(isDark)
 		&:disabled
 			color isDark ? #21242b : #ccc
 
-	> .context
+	> .conversation
 		> *
 			border-bottom 1px solid isDark ? #1c2023 : #eef0f2
 
diff --git a/src/client/app/desktop/views/components/notes.note.sub.vue b/src/client/app/desktop/views/components/notes.note.sub.vue
index 503982b1a8..5f0c46b4c6 100644
--- a/src/client/app/desktop/views/components/notes.note.sub.vue
+++ b/src/client/app/desktop/views/components/notes.note.sub.vue
@@ -4,6 +4,9 @@
 	<div class="main">
 		<header>
 			<router-link class="name" :to="note.user | userPage" v-user-preview="note.userId">{{ note.user | userName }}</router-link>
+			<span class="is-admin" v-if="note.user.isAdmin">admin</span>
+			<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>
 			<div class="info">
 				<span class="mobile" v-if="note.viaMobile">%fa:mobile-alt%</span>
@@ -68,7 +71,6 @@ root(isDark)
 			align-items baseline
 			margin-bottom 2px
 			white-space nowrap
-			line-height 21px
 
 			> .name
 				display block
@@ -84,6 +86,20 @@ root(isDark)
 				&:hover
 					text-decoration underline
 
+			> .is-admin
+			> .is-bot
+			> .is-cat
+				margin 0 0.5em 0 0
+				padding 1px 5px
+				font-size 10px
+				color isDark ? #758188 : #aaa
+				border solid 1px isDark ? #57616f : #ddd
+				border-radius 3px
+
+				&.is-admin
+					border-color isDark ? #d42c41 : #f56a7b
+					color isDark ? #d42c41 : #f56a7b
+
 			> .username
 				margin 0 .5em 0 0
 				color isDark ? #606984 : #d1d8da
diff --git a/src/client/app/desktop/views/components/notes.note.vue b/src/client/app/desktop/views/components/notes.note.vue
index e23d3e5a52..4448820eb9 100644
--- a/src/client/app/desktop/views/components/notes.note.vue
+++ b/src/client/app/desktop/views/components/notes.note.vue
@@ -6,9 +6,9 @@
 	<div class="renote" v-if="isRenote">
 		<mk-avatar class="avatar" :user="note.user"/>
 		%fa:retweet%
-		<span>{{ '%i18n:!@reposted-by%'.substr(0, '%i18n:!@reposted-by%'.indexOf('{')) }}</span>
+		<span>{{ '%i18n:@reposted-by%'.substr(0, '%i18n:@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>{{ '%i18n:@reposted-by%'.substr('%i18n:@reposted-by%'.indexOf('}') + 1) }}</span>
 		<mk-time :time="note.createdAt"/>
 	</div>
 	<article>
@@ -16,7 +16,9 @@
 		<div class="main">
 			<header>
 				<router-link class="name" :to="p.user | userPage" v-user-preview="p.user.id">{{ p.user | userName }}</router-link>
-				<span class="is-bot" v-if="p.user.host === null && p.user.isBot">bot</span>
+				<span class="is-admin" v-if="p.user.isAdmin">admin</span>
+				<span class="is-bot" v-if="p.user.isBot">bot</span>
+				<span class="is-cat" v-if="p.user.isCat">cat</span>
 				<span class="username"><mk-acct :user="p.user"/></span>
 				<div class="info">
 					<span class="app" v-if="p.app">via <b>{{ p.app.name }}</b></span>
@@ -430,7 +432,9 @@ root(isDark)
 					&:hover
 						text-decoration underline
 
+				> .is-admin
 				> .is-bot
+				> .is-cat
 					margin 0 .5em 0 0
 					padding 1px 6px
 					font-size 12px
@@ -438,6 +442,10 @@ root(isDark)
 					border solid 1px isDark ? #57616f : #ddd
 					border-radius 3px
 
+					&.is-admin
+						border-color isDark ? #d42c41 : #f56a7b
+						color isDark ? #d42c41 : #f56a7b
+
 				> .username
 					margin 0 .5em 0 0
 					overflow hidden
diff --git a/src/client/app/desktop/views/components/notes.vue b/src/client/app/desktop/views/components/notes.vue
index c041e5278c..55b0de3fbd 100644
--- a/src/client/app/desktop/views/components/notes.vue
+++ b/src/client/app/desktop/views/components/notes.vue
@@ -145,9 +145,9 @@ export default Vue.extend({
 				this.notes.unshift(note);
 
 				// サウンドを再生する
-				if ((this as any).os.isEnableSounds && !silent) {
+				if (this.$store.state.device.enableSounds && !silent) {
 					const sound = new Audio(`${url}/assets/post.mp3`);
-					sound.volume = localStorage.getItem('soundVolume') ? parseInt(localStorage.getItem('soundVolume'), 10) / 100 : 0.5;
+					sound.volume = this.$store.state.device.soundVolume;
 					sound.play();
 				}
 
diff --git a/src/client/app/desktop/views/components/notifications.vue b/src/client/app/desktop/views/components/notifications.vue
index 7923d1a62d..5564dad623 100644
--- a/src/client/app/desktop/views/components/notifications.vue
+++ b/src/client/app/desktop/views/components/notifications.vue
@@ -81,7 +81,7 @@
 		</transition-group>
 	</div>
 	<button class="more" :class="{ fetching: fetchingMoreNotifications }" v-if="moreNotifications" @click="fetchMoreNotifications" :disabled="fetchingMoreNotifications">
-		<template v-if="fetchingMoreNotifications">%fa:spinner .pulse .fw%</template>{{ fetchingMoreNotifications ? '%i18n:!common.loading%' : '%i18n:!@more%' }}
+		<template v-if="fetchingMoreNotifications">%fa:spinner .pulse .fw%</template>{{ fetchingMoreNotifications ? '%i18n:common.loading%' : '%i18n:@more%' }}
 	</button>
 	<p class="empty" v-if="notifications.length == 0 && !fetching">%i18n:@empty%</p>
 	<p class="loading" v-if="fetching">%fa:spinner .pulse .fw%%i18n:common.loading%<mk-ellipsis/></p>
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 1f0fbff760..18bb39f9bc 100644
--- a/src/client/app/desktop/views/components/post-form-window.vue
+++ b/src/client/app/desktop/views/components/post-form-window.vue
@@ -4,8 +4,8 @@
 		<span :class="$style.icon" v-if="geo">%fa:map-marker-alt%</span>
 		<span v-if="!reply">%i18n:@note%</span>
 		<span v-if="reply">%i18n:@reply%</span>
-		<span :class="$style.count" v-if="media.length != 0">{{ '%i18n:!@attaches%'.replace('{}', media.length) }}</span>
-		<span :class="$style.count" v-if="uploadings.length != 0">{{ '%i18n:!@uploading-media%'.replace('{}', uploadings.length) }}<mk-ellipsis/></span>
+		<span :class="$style.count" v-if="media.length != 0">{{ '%i18n:@attaches%'.replace('{}', media.length) }}</span>
+		<span :class="$style.count" v-if="uploadings.length != 0">{{ '%i18n:@uploading-media%'.replace('{}', uploadings.length) }}<mk-ellipsis/></span>
 	</span>
 
 	<mk-note-preview v-if="reply" :class="$style.notePreview" :note="reply"/>
diff --git a/src/client/app/desktop/views/components/post-form.vue b/src/client/app/desktop/views/components/post-form.vue
index 984fc9866c..0696d4e82b 100644
--- a/src/client/app/desktop/views/components/post-form.vue
+++ b/src/client/app/desktop/views/components/post-form.vue
@@ -37,7 +37,7 @@
 	<button class="visibility" title="公開範囲" @click="setVisibility" ref="visibilityButton">%fa:lock%</button>
 	<p class="text-count" :class="{ over: text.length > 1000 }">{{ 1000 - text.length }}</p>
 	<button :class="{ posting }" class="submit" :disabled="!canPost" @click="post">
-		{{ posting ? '%i18n:!@posting%' : submitText }}<mk-ellipsis v-if="posting"/>
+		{{ posting ? '%i18n:@posting%' : submitText }}<mk-ellipsis v-if="posting"/>
 	</button>
 	<input ref="file" type="file" accept="image/*" multiple="multiple" tabindex="-1" @change="onChangeFile"/>
 	<div class="dropzone" v-if="draghover"></div>
@@ -86,18 +86,18 @@ export default Vue.extend({
 
 		placeholder(): string {
 			return this.renote
-				? '%i18n:!@quote-placeholder%'
+				? '%i18n:@quote-placeholder%'
 				: this.reply
-					? '%i18n:!@reply-placeholder%'
-					: '%i18n:!@note-placeholder%';
+					? '%i18n:@reply-placeholder%'
+					: '%i18n:@note-placeholder%';
 		},
 
 		submitText(): string {
 			return this.renote
-				? '%i18n:!@renote%'
+				? '%i18n:@renote%'
 				: this.reply
-					? '%i18n:!@reply%'
-					: '%i18n:!@note%';
+					? '%i18n:@reply%'
+					: '%i18n:@note%';
 		},
 
 		canPost(): boolean {
@@ -304,16 +304,16 @@ export default Vue.extend({
 				this.deleteDraft();
 				this.$emit('posted');
 				(this as any).apis.notify(this.renote
-					? '%i18n:!@reposted%'
+					? '%i18n:@reposted%'
 					: this.reply
-						? '%i18n:!@replied%'
-						: '%i18n:!@posted%');
+						? '%i18n:@replied%'
+						: '%i18n:@posted%');
 			}).catch(err => {
 				(this as any).apis.notify(this.renote
-					? '%i18n:!@renote-failed%'
+					? '%i18n:@renote-failed%'
 					: this.reply
-						? '%i18n:!@reply-failed%'
-						: '%i18n:!@note-failed%');
+						? '%i18n:@reply-failed%'
+						: '%i18n:@note-failed%');
 			}).then(() => {
 				this.posting = false;
 			});
diff --git a/src/client/app/desktop/views/components/renote-form.vue b/src/client/app/desktop/views/components/renote-form.vue
index 9c0154211b..38eab3362f 100644
--- a/src/client/app/desktop/views/components/renote-form.vue
+++ b/src/client/app/desktop/views/components/renote-form.vue
@@ -5,7 +5,7 @@
 		<footer>
 			<a class="quote" v-if="!quote" @click="onQuote">%i18n:@quote%</a>
 			<button class="ui cancel" @click="cancel">%i18n:@cancel%</button>
-			<button class="ui primary ok" @click="ok" :disabled="wait">{{ wait ? '%i18n:!@reposting%' : '%i18n:!@renote%' }}</button>
+			<button class="ui primary ok" @click="ok" :disabled="wait">{{ wait ? '%i18n:@reposting%' : '%i18n:@renote%' }}</button>
 		</footer>
 	</template>
 	<template v-if="quote">
@@ -32,9 +32,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('%i18n:@success%');
 			}).catch(err => {
-				(this as any).apis.notify('%i18n:!@failure%');
+				(this as any).apis.notify('%i18n:@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 d5be177dcc..deb865b102 100644
--- a/src/client/app/desktop/views/components/settings-window.vue
+++ b/src/client/app/desktop/views/components/settings-window.vue
@@ -1,6 +1,6 @@
 <template>
 <mk-window ref="window" is-modal width="700px" height="550px" @closed="$destroy">
-	<span slot="header" :class="$style.header">%fa:cog%設定</span>
+	<span slot="header" :class="$style.header">%fa:cog%%i18n:@settings%</span>
 	<mk-settings @done="close"/>
 </mk-window>
 </template>
diff --git a/src/client/app/desktop/views/components/settings.2fa.vue b/src/client/app/desktop/views/components/settings.2fa.vue
index 99b6cb947c..0809dd798c 100644
--- a/src/client/app/desktop/views/components/settings.2fa.vue
+++ b/src/client/app/desktop/views/components/settings.2fa.vue
@@ -34,7 +34,7 @@ export default Vue.extend({
 	methods: {
 		register() {
 			(this as any).apis.input({
-				title: '%i18n:!@enter-password%',
+				title: '%i18n:@enter-password%',
 				type: 'password'
 			}).then(password => {
 				(this as any).api('i/2fa/register', {
@@ -47,13 +47,13 @@ export default Vue.extend({
 
 		unregister() {
 			(this as any).apis.input({
-				title: '%i18n:!@enter-password%',
+				title: '%i18n:@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('%i18n:@unregistered%');
 					(this as any).os.i.twoFactorEnabled = false;
 				});
 			});
@@ -63,10 +63,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('%i18n:@success%');
 				(this as any).os.i.twoFactorEnabled = true;
 			}).catch(() => {
-				(this as any).apis.notify('%i18n:!@failed%');
+				(this as any).apis.notify('%i18n:@failed%');
 			});
 		}
 	}
diff --git a/src/client/app/desktop/views/components/settings.api.vue b/src/client/app/desktop/views/components/settings.api.vue
index b22ee6cdab..b8eef3de63 100644
--- a/src/client/app/desktop/views/components/settings.api.vue
+++ b/src/client/app/desktop/views/components/settings.api.vue
@@ -15,7 +15,7 @@ export default Vue.extend({
 	methods: {
 		regenerateToken() {
 			(this as any).apis.input({
-				title: '%i18n:!@enter-password%',
+				title: '%i18n:@enter-password%',
 				type: 'password'
 			}).then(password => {
 				(this as any).api('i/regenerate_token', {
diff --git a/src/client/app/desktop/views/components/settings.password.vue b/src/client/app/desktop/views/components/settings.password.vue
index 9e89bc0f6e..39896daf67 100644
--- a/src/client/app/desktop/views/components/settings.password.vue
+++ b/src/client/app/desktop/views/components/settings.password.vue
@@ -11,21 +11,21 @@ export default Vue.extend({
 	methods: {
 		reset() {
 			(this as any).apis.input({
-				title: '%i18n:!@enter-current-password%',
+				title: '%i18n:@enter-current-password%',
 				type: 'password'
 			}).then(currentPassword => {
 				(this as any).apis.input({
-					title: '%i18n:!@enter-new-password%',
+					title: '%i18n:@enter-new-password%',
 					type: 'password'
 				}).then(newPassword => {
 					(this as any).apis.input({
-						title: '%i18n:!@enter-new-password-again%',
+						title: '%i18n:@enter-new-password-again%',
 						type: 'password'
 					}).then(newPassword2 => {
 						if (newPassword !== newPassword2) {
 							(this as any).apis.dialog({
 								title: null,
-								text: '%i18n:!@not-match%',
+								text: '%i18n:@not-match%',
 								actions: [{
 									text: 'OK'
 								}]
@@ -36,7 +36,7 @@ export default Vue.extend({
 							currentPasword: currentPassword,
 							newPassword: newPassword
 						}).then(() => {
-							(this as any).apis.notify('%i18n:!@changed%');
+							(this as any).apis.notify('%i18n:@changed%');
 						});
 					});
 				});
diff --git a/src/client/app/desktop/views/components/settings.profile.vue b/src/client/app/desktop/views/components/settings.profile.vue
index 84b09eb988..132ab12f1c 100644
--- a/src/client/app/desktop/views/components/settings.profile.vue
+++ b/src/client/app/desktop/views/components/settings.profile.vue
@@ -24,7 +24,8 @@
 	<button class="ui primary" @click="save">%i18n:@save%</button>
 	<section>
 		<h2>その他</h2>
-		<mk-switch v-model="os.i.isBot" @change="onChangeIsBot" text="このアカウントはbotです"/>
+		<mk-switch v-model="os.i.isBot" @change="onChangeIsBot" text="%i18n:@is-bot%"/>
+		<mk-switch v-model="os.i.isCat" @change="onChangeIsCat" text="%i18n:@is-cat%"/>
 	</section>
 </div>
 </template>
@@ -65,6 +66,11 @@ export default Vue.extend({
 			(this as any).api('i/update', {
 				isBot: (this as any).os.i.isBot
 			});
+		},
+		onChangeIsCat() {
+			(this as any).api('i/update', {
+				isCat: (this as any).os.i.isCat
+			});
 		}
 	}
 });
diff --git a/src/client/app/desktop/views/components/settings.vue b/src/client/app/desktop/views/components/settings.vue
index 6652a8ac37..dac5fe67cb 100644
--- a/src/client/app/desktop/views/components/settings.vue
+++ b/src/client/app/desktop/views/components/settings.vue
@@ -62,8 +62,10 @@
 			<el-slider
 				v-model="soundVolume"
 				:show-input="true"
-				:format-tooltip="v => `${v}%`"
+				:format-tooltip="v => `${v * 100}%`"
 				:disabled="!enableSounds"
+				:max="1"
+				:step="0.1"
 			/>
 			<button class="ui button" @click="soundTest">%fa:volume-up% %i18n:@test%</button>
 		</section>
@@ -77,14 +79,10 @@
 			<h1>%i18n:@language%</h1>
 			<el-select v-model="lang" placeholder="%i18n:@pick-language%">
 				<el-option-group label="%i18n:@recommended%">
-					<el-option label="%i18n:@auto%" value=""/>
+					<el-option label="%i18n:@auto%" :value="null"/>
 				</el-option-group>
 				<el-option-group label="%i18n:@specify-language%">
-					<el-option label="日本語" value="ja"/>
-					<el-option label="English" value="en"/>
-					<el-option label="Français" value="fr"/>
-					<el-option label="Polski" value="pl"/>
-					<el-option label="Deutsch" value="de"/>
+					<el-option v-for="x in langs" :label="x[1]" :value="x[0]" :key="x[0]"/>
 				</el-option-group>
 			</el-select>
 			<div class="none ui info">
@@ -178,15 +176,7 @@
 			<mk-switch v-model="debug" text="%i18n:@debug-mode%">
 				<span>%i18n:@debug-mode-desc%</span>
 			</mk-switch>
-			<template v-if="debug">
-				<mk-switch v-model="useRawScript" text="%i18n:@use-raw-script%">
-					<span>%i18n:@use-raw-script-desc%</span>
-				</mk-switch>
-				<div class="none ui info">
-				<p>%fa:info-circle%%i18n:@source-info%</p>
-			</div>
-			</template>
-			<mk-switch v-model="enableExperimental" text="%i18n:@experimental%">
+			<mk-switch v-model="enableExperimentalFeatures" text="%i18n:@experimental%">
 				<span>%i18n:@experimental-desc%</span>
 			</mk-switch>
 			<details v-if="debug">
@@ -214,7 +204,7 @@ import XApi from './settings.api.vue';
 import XApps from './settings.apps.vue';
 import XSignins from './settings.signins.vue';
 import XDrive from './settings.drive.vue';
-import { url, docsUrl, license, lang, version } from '../../../config';
+import { url, docsUrl, license, lang, langs, version } from '../../../config';
 import checkForUpdate from '../../../common/scripts/check-for-update';
 import MkTaskManager from './taskmanager.vue';
 
@@ -235,55 +225,59 @@ export default Vue.extend({
 			meta: null,
 			license,
 			version,
+			langs,
 			latestVersion: undefined,
-			checkingForUpdate: false,
-			darkmode: localStorage.getItem('darkmode') == 'true',
-			enableSounds: localStorage.getItem('enableSounds') == 'true',
-			autoPopout: localStorage.getItem('autoPopout') == 'true',
-			apiViaStream: localStorage.getItem('apiViaStream') ? localStorage.getItem('apiViaStream') == 'true' : true,
-			soundVolume: localStorage.getItem('soundVolume') ? parseInt(localStorage.getItem('soundVolume'), 10) : 50,
-			lang: localStorage.getItem('lang') || '',
-			preventUpdate: localStorage.getItem('preventUpdate') == 'true',
-			debug: localStorage.getItem('debug') == 'true',
-			useRawScript: localStorage.getItem('useRawScript') == 'true',
-			enableExperimental: localStorage.getItem('enableExperimental') == 'true'
+			checkingForUpdate: false
 		};
 	},
 	computed: {
 		licenseUrl(): string {
 			return `${docsUrl}/${lang}/license`;
-		}
-	},
-	watch: {
-		autoPopout() {
-			localStorage.setItem('autoPopout', this.autoPopout ? 'true' : 'false');
 		},
-		apiViaStream() {
-			localStorage.setItem('apiViaStream', this.apiViaStream ? 'true' : 'false');
+
+		apiViaStream: {
+			get() { return this.$store.state.device.apiViaStream; },
+			set(value) { this.$store.commit('device/set', { key: 'apiViaStream', value }); }
 		},
-		darkmode() {
-			(this as any)._updateDarkmode_(this.darkmode);
+
+		autoPopout: {
+			get() { return this.$store.state.device.autoPopout; },
+			set(value) { this.$store.commit('device/set', { key: 'autoPopout', value }); }
 		},
-		enableSounds() {
-			localStorage.setItem('enableSounds', this.enableSounds ? 'true' : 'false');
+
+		darkmode: {
+			get() { return this.$store.state.device.darkmode; },
+			set(value) { this.$store.commit('device/set', { key: 'darkmode', value }); }
 		},
-		soundVolume() {
-			localStorage.setItem('soundVolume', this.soundVolume.toString());
+
+		enableSounds: {
+			get() { return this.$store.state.device.enableSounds; },
+			set(value) { this.$store.commit('device/set', { key: 'enableSounds', value }); }
 		},
-		lang() {
-			localStorage.setItem('lang', this.lang);
+
+		soundVolume: {
+			get() { return this.$store.state.device.soundVolume; },
+			set(value) { this.$store.commit('device/set', { key: 'soundVolume', value }); }
 		},
-		preventUpdate() {
-			localStorage.setItem('preventUpdate', this.preventUpdate ? 'true' : 'false');
+
+		lang: {
+			get() { return this.$store.state.device.lang; },
+			set(value) { this.$store.commit('device/set', { key: 'lang', value }); }
 		},
-		debug() {
-			localStorage.setItem('debug', this.debug ? 'true' : 'false');
+
+		preventUpdate: {
+			get() { return this.$store.state.device.preventUpdate; },
+			set(value) { this.$store.commit('device/set', { key: 'preventUpdate', value }); }
 		},
-		useRawScript() {
-			localStorage.setItem('useRawScript', this.useRawScript ? 'true' : 'false');
+
+		debug: {
+			get() { return this.$store.state.device.debug; },
+			set(value) { this.$store.commit('device/set', { key: 'debug', value }); }
 		},
-		enableExperimental() {
-			localStorage.setItem('enableExperimental', this.enableExperimental ? 'true' : 'false');
+
+		enableExperimentalFeatures: {
+			get() { return this.$store.state.device.enableExperimentalFeatures; },
+			set(value) { this.$store.commit('device/set', { key: 'enableExperimentalFeatures', value }); }
 		}
 	},
 	created() {
@@ -371,13 +365,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: '%i18n:@no-updates%',
+						text: '%i18n:@no-updates-desc%'
 					});
 				} else {
 					(this as any).apis.dialog({
-						title: '%i18n:!@update-available%',
-						text: '%i18n:!@update-available-desc%'
+						title: '%i18n:@update-available%',
+						text: '%i18n:@update-available-desc%'
 					});
 				}
 			});
@@ -385,13 +379,13 @@ export default Vue.extend({
 		clean() {
 			localStorage.clear();
 			(this as any).apis.dialog({
-				title: '%i18n:!@cache-cleared%',
-				text: '%i18n:!@caache-cleared-desc%'
+				title: '%i18n:@cache-cleared%',
+				text: '%i18n:@caache-cleared-desc%'
 			});
 		},
 		soundTest() {
 			const sound = new Audio(`${url}/assets/message.mp3`);
-			sound.volume = localStorage.getItem('soundVolume') ? parseInt(localStorage.getItem('soundVolume'), 10) / 100 : 0.5;
+			sound.volume = this.$store.state.device.soundVolume;
 			sound.play();
 		}
 	}
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 dd4012039b..03b634b0ff 100644
--- a/src/client/app/desktop/views/components/sub-note-content.vue
+++ b/src/client/app/desktop/views/components/sub-note-content.vue
@@ -1,17 +1,17 @@
 <template>
 <div class="mk-sub-note-content">
 	<div class="body">
-		<span v-if="note.isHidden" style="opacity: 0.5">(この投稿は非公開です)</span>
+		<span v-if="note.isHidden" style="opacity: 0.5">%i18n:@hidden%</span>
 		<a class="reply" v-if="note.replyId">%fa:reply%</a>
 		<mk-note-html :text="note.text" :i="os.i"/>
 		<a class="rp" v-if="note.renoteId" :href="`/note:${note.renoteId}`">RP: ...</a>
 	</div>
 	<details v-if="note.media.length > 0">
-		<summary>({{ note.media.length }}つのメディア)</summary>
+		<summary>({{ note.media.length }}%i18n:@media%)</summary>
 		<mk-media-list :media-list="note.media"/>
 	</details>
 	<details v-if="note.poll">
-		<summary>投票</summary>
+		<summary>%i18n:@poll%</summary>
 		<mk-poll :note="note"/>
 	</details>
 </div>
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 fd15ea6006..f3f6539496 100644
--- a/src/client/app/desktop/views/components/ui.header.account.vue
+++ b/src/client/app/desktop/views/components/ui.header.account.vue
@@ -35,7 +35,7 @@
 			</ul>
 			<ul>
 				<li @click="dark">
-					<p><span>%i18n:@dark%</span><template v-if="_darkmode_">%fa:moon%</template><template v-else>%fa:R moon%</template></p>
+					<p><span>%i18n:@dark%</span><template v-if="$store.state.device.darkmode">%fa:moon%</template><template v-else>%fa:R moon%</template></p>
 				</li>
 			</ul>
 		</div>
@@ -99,7 +99,10 @@ export default Vue.extend({
 			(this as any).os.signout();
 		},
 		dark() {
-			(this as any)._updateDarkmode_(!(this as any)._darkmode_);
+			this.$store.commit('device/set', {
+				key: 'darkmode',
+				value: !this.$store.state.device.darkmode
+			});
 		}
 	}
 });
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 585c0a864f..454c725d20 100644
--- a/src/client/app/desktop/views/components/user-lists-window.vue
+++ b/src/client/app/desktop/views/components/user-lists-window.vue
@@ -2,7 +2,7 @@
 <mk-window ref="window" is-modal width="450px" height="500px" @closed="$destroy">
 	<span slot="header">%fa:list% リスト</span>
 
-	<div data-id="6e4caea3-d8f9-4ab7-96de-ab67fe8d5c82" :data-darkmode="_darkmode_">
+	<div data-id="6e4caea3-d8f9-4ab7-96de-ab67fe8d5c82" :data-darkmode="$store.state.device.darkmode">
 		<button class="ui" @click="add">%i18n:@create-list%</button>
 		<a v-for="list in lists" :key="list.id" @click="choice(list)">{{ list.title }}</a>
 	</div>
diff --git a/src/client/app/desktop/views/components/users-list.item.vue b/src/client/app/desktop/views/components/users-list.item.vue
index dbad295178..262fd38cd1 100644
--- a/src/client/app/desktop/views/components/users-list.item.vue
+++ b/src/client/app/desktop/views/components/users-list.item.vue
@@ -7,7 +7,7 @@
 			<span class="username">@{{ user | acct }}</span>
 		</header>
 		<div class="body">
-			<p class="followed" v-if="user.isFollowed">フォローされています</p>
+			<p class="followed" v-if="user.isFollowed">%i18n:@followed%</p>
 			<div class="description">{{ user.description }}</div>
 		</div>
 	</div>
diff --git a/src/client/app/desktop/views/components/window.vue b/src/client/app/desktop/views/components/window.vue
index 2e7eb557b4..ac06ac8e57 100644
--- a/src/client/app/desktop/views/components/window.vue
+++ b/src/client/app/desktop/views/components/window.vue
@@ -9,8 +9,8 @@
 			>
 				<h1><slot name="header"></slot></h1>
 				<div>
-					<button class="popout" v-if="popoutUrl" @mousedown.stop="() => {}" @click="popout" title="ポップアウト">%fa:R window-restore%</button>
-					<button class="close" v-if="canClose" @mousedown.stop="() => {}" @click="close" title="閉じる">%fa:times%</button>
+					<button class="popout" v-if="popoutUrl" @mousedown.stop="() => {}" @click="popout" title="%i18n:@popout%">%fa:R window-restore%</button>
+					<button class="close" v-if="canClose" @mousedown.stop="() => {}" @click="close" title="%i18n:@close%">%fa:times%</button>
 				</div>
 			</header>
 			<div class="content">
@@ -95,7 +95,7 @@ export default Vue.extend({
 	},
 
 	created() {
-		if (localStorage.getItem('autoPopout') == 'true' && this.popoutUrl) {
+		if ((this as any).os.store.state.device.autoPopout && this.popoutUrl) {
 			this.popout();
 			this.preventMount = true;
 		} else {
diff --git a/src/client/app/desktop/views/pages/drive.vue b/src/client/app/desktop/views/pages/drive.vue
index 353f59b703..217dcb7751 100644
--- a/src/client/app/desktop/views/pages/drive.vue
+++ b/src/client/app/desktop/views/pages/drive.vue
@@ -16,11 +16,11 @@ export default Vue.extend({
 		this.folder = this.$route.params.folder;
 	},
 	mounted() {
-		document.title = 'Misskey Drive';
+		document.title = '%i18n:@title%';
 	},
 	methods: {
 		onMoveRoot() {
-			const title = 'Misskey Drive';
+			const title = '%i18n:@title%';
 
 			// Rewrite URL
 			history.pushState(null, title, '/i/drive');
@@ -28,7 +28,7 @@ export default Vue.extend({
 			document.title = title;
 		},
 		onOpenFolder(folder) {
-			const title = folder.name + ' | Misskey Drive';
+			const title = folder.name + ' | %i18n:@title%';
 
 			// Rewrite URL
 			history.pushState(null, title, '/i/drive/folder/' + folder.id);
@@ -49,4 +49,3 @@ export default Vue.extend({
 	> .mk-drive
 		height 100%
 </style>
-
diff --git a/src/client/app/desktop/views/pages/favorites.vue b/src/client/app/desktop/views/pages/favorites.vue
index d908c08f7c..71d36cdf2b 100644
--- a/src/client/app/desktop/views/pages/favorites.vue
+++ b/src/client/app/desktop/views/pages/favorites.vue
@@ -4,7 +4,7 @@
 		<template v-for="favorite in favorites">
 			<mk-note-detail :note="favorite.note" :key="favorite.note.id"/>
 		</template>
-		<a v-if="existMore" @click="more">さらに読み込む</a>
+		<a v-if="existMore" @click="more">%i18n:@more%</a>
 	</main>
 </mk-ui>
 </template>
diff --git a/src/client/app/desktop/views/pages/home-customize.vue b/src/client/app/desktop/views/pages/home-customize.vue
index 8aa06be57f..da5f15bb69 100644
--- a/src/client/app/desktop/views/pages/home-customize.vue
+++ b/src/client/app/desktop/views/pages/home-customize.vue
@@ -6,7 +6,7 @@
 import Vue from 'vue';
 export default Vue.extend({
 	mounted() {
-		document.title = 'Misskey - ホームのカスタマイズ';
+		document.title = 'Misskey - %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 1cc8d8a778..06c32776c9 100644
--- a/src/client/app/desktop/views/pages/messaging-room.vue
+++ b/src/client/app/desktop/views/pages/messaging-room.vue
@@ -21,10 +21,21 @@ export default Vue.extend({
 		$route: 'fetch'
 	},
 	created() {
+		const applyBg = v =>
+			document.documentElement.style.setProperty('background', v ? '#191b22' : '#fff', 'important');
+
+		applyBg(this.$store.state.device.darkmode);
+
+		this.unwatchDarkmode = this.$store.watch(s => {
+			return s.device.darkmode;
+		}, applyBg);
+
 		this.fetch();
 	},
-	mounted() {
-		document.documentElement.style.background = '#fff';
+	beforeDestroy() {
+		document.documentElement.style.removeProperty('background');
+		document.documentElement.style.removeProperty('background-color'); // for safari's bug
+		this.unwatchDarkmode();
 	},
 	methods: {
 		fetch() {
@@ -50,6 +61,5 @@ export default Vue.extend({
 	flex 1
 	flex-direction column
 	min-height 100%
-	background #fff
 
 </style>
diff --git a/src/client/app/desktop/views/pages/selectdrive.vue b/src/client/app/desktop/views/pages/selectdrive.vue
index 7a00896640..c846f2418f 100644
--- a/src/client/app/desktop/views/pages/selectdrive.vue
+++ b/src/client/app/desktop/views/pages/selectdrive.vue
@@ -29,7 +29,7 @@ export default Vue.extend({
 		}
 	},
 	mounted() {
-		document.title = '%i18n:!@title%';
+		document.title = '%i18n:@title%';
 	},
 	methods: {
 		onSelected(file) {
diff --git a/src/client/app/desktop/views/pages/user-list.users.vue b/src/client/app/desktop/views/pages/user-list.users.vue
index 4236cdbb14..517fe89750 100644
--- a/src/client/app/desktop/views/pages/user-list.users.vue
+++ b/src/client/app/desktop/views/pages/user-list.users.vue
@@ -1,8 +1,8 @@
 <template>
 <div>
 	<mk-widget-container>
-		<template slot="header">%fa:users% ユーザー</template>
-		<button slot="func" title="ユーザーを追加" @click="add">%fa:plus%</button>
+		<template slot="header">%fa:users% %i18n:@users%</template>
+		<button slot="func" title="%i18n:@add-user%" @click="add">%fa:plus%</button>
 
 		<div data-id="d0b63759-a822-4556-a5ce-373ab966e08a">
 			<p class="fetching" v-if="fetching">%fa:spinner .pulse .fw% %i18n:common.loading%<mk-ellipsis/></p>
@@ -48,7 +48,7 @@ export default Vue.extend({
 	methods: {
 		add() {
 			(this as any).apis.input({
-				title: 'ユーザー名',
+				title: '%i18n:@username%',
 			}).then(async username => {
 				const user = await (this as any).api('users/show', {
 					username
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 9c9840c190..576a285104 100644
--- a/src/client/app/desktop/views/pages/user/user.timeline.vue
+++ b/src/client/app/desktop/views/pages/user/user.timeline.vue
@@ -1,15 +1,15 @@
 <template>
 <div class="timeline">
 	<header>
-		<span :data-active="mode == 'default'" @click="mode = 'default'">投稿</span>
-		<span :data-active="mode == 'with-replies'" @click="mode = 'with-replies'">投稿と返信</span>
-		<span :data-active="mode == 'with-media'" @click="mode = 'with-media'">メディア</span>
+		<span :data-active="mode == 'default'" @click="mode = 'default'">%i18n:@default%</span>
+		<span :data-active="mode == 'with-replies'" @click="mode = 'with-replies'">%i18n:@with-replies%</span>
+		<span :data-active="mode == 'with-media'" @click="mode = 'with-media'">%i18n:@with-media%</span>
 	</header>
 	<div class="loading" v-if="fetching">
 		<mk-ellipsis-icon/>
 	</div>
 	<mk-notes ref="timeline" :more="existMore ? more : null">
-		<p class="empty" slot="empty">%fa:R comments%このユーザーはまだ何も投稿していないようです。</p>
+		<p class="empty" slot="empty">%fa:R comments%%i18n:@empty%</p>
 	</mk-notes>
 </div>
 </template>
diff --git a/src/client/app/desktop/views/pages/welcome.vue b/src/client/app/desktop/views/pages/welcome.vue
index 898b6b2179..91ad4b61c3 100644
--- a/src/client/app/desktop/views/pages/welcome.vue
+++ b/src/client/app/desktop/views/pages/welcome.vue
@@ -1,23 +1,16 @@
 <template>
 <div class="mk-welcome">
+	<button @click="dark">
+		<template v-if="$store.state.device.darkmode">%fa:moon%</template>
+		<template v-else>%fa:R moon%</template>
+	</button>
 	<main>
-		<div class="top">
-			<div>
-				<div>
-					<h1>Share<br><span ref="share">Everything!</span><span class="cursor">_</span></h1>
-					<p>ようこそ! <b>Misskey</b>はTwitter風ミニブログSNSです。思ったことや皆と共有したいことを投稿しましょう。タイムラインを見れば、皆の関心事をすぐにチェックすることもできます。<a :href="aboutUrl">詳しく...</a></p>
-					<p><button class="signup" @click="signup">はじめる</button><button class="signin" @click="signin">ログイン</button></p>
-					<div class="users">
-						<mk-avatar class="avatar" v-for="user in users" :key="user.id" :user="user"/>
-					</div>
-				</div>
-				<div>
-					<div>
-						<header>%fa:comments R% タイムライン<div><span></span><span></span><span></span></div></header>
-						<mk-welcome-timeline/>
-					</div>
-				</div>
-			</div>
+		<img :src="$store.state.device.darkmode ? 'assets/title-dark.svg' : 'assets/title.svg'" alt="Misskey">
+		<p><button class="signup" @click="signup">%i18n:@signup-button%</button><button class="signin" @click="signin">%i18n:@signin-button%</button></p>
+
+		<div class="tl">
+			<header>%fa:comments R% %i18n:@timeline%<div><span></span><span></span><span></span></div></header>
+			<mk-welcome-timeline/>
 		</div>
 	</main>
 	<mk-forkit/>
@@ -28,11 +21,11 @@
 		</div>
 	</footer>
 	<modal name="signup" width="500px" height="auto" scrollable>
-		<header :class="$style.signupFormHeader">新規登録</header>
+		<header :class="$style.signupFormHeader">%i18n:@signup%</header>
 		<mk-signup :class="$style.signupForm"/>
 	</modal>
 	<modal name="signin" width="500px" height="auto" scrollable>
-		<header :class="$style.signinFormHeader">ログイン</header>
+		<header :class="$style.signinFormHeader">%i18n:@signin%</header>
 		<mk-signin :class="$style.signinForm"/>
 	</modal>
 </div>
@@ -42,64 +35,25 @@
 import Vue from 'vue';
 import { docsUrl, copyright, lang } from '../../../config';
 
-const shares = [
-	'Everything!',
-	'Webpages',
-	'Photos',
-	'Interests',
-	'Favorites'
-];
-
 export default Vue.extend({
 	data() {
 		return {
 			aboutUrl: `${docsUrl}/${lang}/about`,
-			copyright,
-			users: [],
-			clock: null,
-			i: 0
+			copyright
 		};
 	},
-	mounted() {
-		(this as any).api('users', {
-			sort: '+follower',
-			limit: 20
-		}).then(users => {
-			this.users = users;
-		});
-
-		this.clock = setInterval(() => {
-			if (++this.i == shares.length) this.i = 0;
-			const speed = 70;
-			const text = (this.$refs.share as any).innerText;
-			for (let i = 0; i < text.length; i++) {
-				setTimeout(() => {
-					if (this.$refs.share) {
-						(this.$refs.share as any).innerText = text.substr(0, text.length - i);
-					}
-				}, i * speed)
-			}
-			setTimeout(() => {
-				const newText = shares[this.i];
-				for (let i = 0; i <= newText.length; i++) {
-					setTimeout(() => {
-						if (this.$refs.share) {
-							(this.$refs.share as any).innerText = newText.substr(0, i);
-						}
-					}, i * speed)
-				}
-			}, text.length * speed);
-		}, 4000);
-	},
-	beforeDestroy() {
-		clearInterval(this.clock);
-	},
 	methods: {
 		signup() {
 			this.$modal.show('signup');
 		},
 		signin() {
 			this.$modal.show('signin');
+		},
+		dark() {
+			this.$store.commit('device/set', {
+				key: 'darkmode',
+				value: !this.$store.state.device.darkmode
+			});
 		}
 	}
 });
@@ -115,161 +69,107 @@ export default Vue.extend({
 <style lang="stylus" scoped>
 @import '~const.styl'
 
-@import url('https://fonts.googleapis.com/css?family=Sarpanch:700')
-
-.mk-welcome
+root(isDark)
 	display flex
 	flex-direction column
 	flex 1
-	$width = 1000px
 
-	background linear-gradient(to bottom, #1e1d65, #bd6659)
-	//background-image url('/assets/welcome-bg.svg')
-	background-size cover
-	background-position top center
-
-	&:before
-		content ""
-		display block
-		position fixed
-		bottom 0
+	> button
+		position absolute
+		z-index 1
+		top 0
 		left 0
-		width 100%
-		height 100%
-		background-image url('/assets/welcome-fg.svg')
-		background-size cover
-		background-position bottom center
+		padding 16px
+		font-size 18px
+		color isDark ? #fff : #555
 
 	> main
-		display flex
 		flex 1
+		padding 64px 0 0 0
+		text-align center
+		color isDark ? #9aa4b3 : #555
 
-		> .top
-			display flex
-			width 100%
+		> img
+			width 350px
 
-			> div
-				display flex
-				max-width $width + 64px
-				margin 0 auto
-				padding 80px 32px 0 32px
+		> p
+			margin 8px 0
+			line-height 2em
 
-				> *
-					margin-bottom 48px
+			button
+				padding 8px 16px
+				font-size inherit
 
-				> div:first-child
-					margin-right 48px
-					color #fff
-					text-shadow 0 0 12px #172062
+			.signup
+				color $theme-color
+				border solid 2px $theme-color
+				border-radius 4px
 
-					> h1
-						margin 0
-						font-weight bold
-						//font-variant small-caps
-						letter-spacing 12px
-						font-family 'Sarpanch', sans-serif
-						font-size 42px
-						line-height 48px
+				&:focus
+					box-shadow 0 0 0 3px rgba($theme-color, 0.2)
 
-						> .cursor
-							animation cursor 1s infinite linear both
+				&:hover
+					color $theme-color-foreground
+					background $theme-color
 
-							@keyframes cursor
-								0%
-									opacity 1
-								50%
-									opacity 0
+				&:active
+					color $theme-color-foreground
+					background darken($theme-color, 10%)
+					border-color darken($theme-color, 10%)
 
-					> p
-						margin 1em 0
-						line-height 2em
+			.signin
+				&:hover
+					color isDark ? #fff : #000
 
-					button
-						padding 8px 16px
-						font-size inherit
+		> .tl
+			margin 32px auto 0 auto
+			width 410px
+			text-align left
+			background isDark ? #313543 : #fff
+			border-radius 8px
+			box-shadow 0 8px 32px rgba(#000, 0.15)
+			overflow hidden
 
-					.signup
-						color $theme-color
-						border solid 2px $theme-color
-						border-radius 4px
+			> header
+				z-index 1
+				padding 12px 16px
+				color isDark ? #e3e5e8 : #888d94
+				box-shadow 0 1px 0px rgba(#000, 0.1)
 
-						&:focus
-							box-shadow 0 0 0 3px rgba($theme-color, 0.2)
+				> div
+					position absolute
+					top 0
+					right 0
+					padding inherit
 
-						&:hover
-							color $theme-color-foreground
-							background $theme-color
+					> span
+						display inline-block
+						height 11px
+						width 11px
+						margin-left 6px
+						border-radius 100%
+						vertical-align middle
 
-						&:active
-							color $theme-color-foreground
-							background darken($theme-color, 10%)
-							border-color darken($theme-color, 10%)
+						&:nth-child(1)
+							background #5BCC8B
 
-					.signin
-						&:hover
-							color #fff
+						&:nth-child(2)
+							background #E6BB46
 
-					> .users
-						margin 16px 0 0 0
+						&:nth-child(3)
+							background #DF7065
 
-						> *
-							display inline-block
-							margin 4px
-							width 38px
-							height 38px
-							border-radius 6px
-
-				> div:last-child
-
-					> div
-						width 410px
-						background #fff
-						border-radius 8px
-						box-shadow 0 0 0 12px rgba(#000, 0.1)
-						overflow hidden
-
-						> header
-							z-index 1
-							padding 12px 16px
-							color #888d94
-							box-shadow 0 1px 0px rgba(#000, 0.1)
-
-							> div
-								position absolute
-								top 0
-								right 0
-								padding inherit
-
-								> span
-									display inline-block
-									height 11px
-									width 11px
-									margin-left 6px
-									background #ccc
-									border-radius 100%
-									vertical-align middle
-
-									&:nth-child(1)
-										background #5BCC8B
-
-									&:nth-child(2)
-										background #E6BB46
-
-									&:nth-child(3)
-										background #DF7065
-
-						> .mk-welcome-timeline
-							max-height 350px
-							overflow auto
+			> .mk-welcome-timeline
+				max-height 350px
+				overflow auto
 
 	> footer
 		font-size 12px
-		color #949ea5
+		color isDark ? #949ea5 : #737c82
 
 		> div
-			max-width $width
 			margin 0 auto
-			padding 0 0 42px 0
+			padding 64px
 			text-align center
 
 			> .c
@@ -277,6 +177,12 @@ export default Vue.extend({
 				font-size 10px
 				opacity 0.7
 
+.mk-welcome[data-darkmode]
+	root(true)
+
+.mk-welcome:not([data-darkmode])
+	root(false)
+
 </style>
 
 <style lang="stylus" module>
diff --git a/src/client/app/desktop/views/widgets/polls.vue b/src/client/app/desktop/views/widgets/polls.vue
index 36fcc20636..7421a81102 100644
--- a/src/client/app/desktop/views/widgets/polls.vue
+++ b/src/client/app/desktop/views/widgets/polls.vue
@@ -4,7 +4,7 @@
 		<template slot="header">%fa:chart-pie%%i18n:@title%</template>
 		<button slot="func" title="%i18n:@refresh%" @click="fetch">%fa:sync%</button>
 
-		<div class="mkw-polls--body" :data-darkmode="_darkmode_">
+		<div class="mkw-polls--body" :data-darkmode="$store.state.device.darkmode">
 			<div class="poll" v-if="!fetching && poll != null">
 				<p v-if="poll.text"><router-link to="poll | notePage">{{ poll.text }}</router-link></p>
 				<p v-if="!poll.text"><router-link to="poll | notePage">%fa:link%</router-link></p>
diff --git a/src/client/app/desktop/views/widgets/profile.vue b/src/client/app/desktop/views/widgets/profile.vue
index 3b01ed034d..5af5b88e23 100644
--- a/src/client/app/desktop/views/widgets/profile.vue
+++ b/src/client/app/desktop/views/widgets/profile.vue
@@ -5,12 +5,12 @@
 >
 	<div class="banner"
 		:style="os.i.bannerUrl ? `background-image: url(${os.i.bannerUrl}?thumbnail&size=256)` : ''"
-		title="クリックでバナー編集"
+		title="%i18n:@update-banner%"
 		@click="os.apis.updateBanner"
 	></div>
 	<mk-avatar class="avatar" :user="os.i"
 		@click="os.apis.updateAvatar"
-		title="クリックでアバター編集"
+		title="%i18n:@update-avatar%"
 	/>
 	<router-link class="name" :to="os.i | userPage">{{ os.i | userName }}</router-link>
 	<p class="username">@{{ os.i | acct }}</p>
diff --git a/src/client/app/init.ts b/src/client/app/init.ts
index 4908b73b23..560ab1a096 100644
--- a/src/client/app/init.ts
+++ b/src/client/app/init.ts
@@ -49,48 +49,6 @@ Vue.mixin({
 	}
 });
 
-// Dark/Light
-const bus = new Vue();
-Vue.mixin({
-	data() {
-		return {
-			_darkmode_: localStorage.getItem('darkmode') == 'true'
-		};
-	},
-	beforeCreate() {
-		// なぜか警告が出るので
-		this._darkmode_ = localStorage.getItem('darkmode') == 'true';
-	},
-	beforeDestroy() {
-		bus.$off('updated', this._onDarkmodeUpdated_);
-	},
-	mounted() {
-		this._onDarkmodeUpdated_(this._darkmode_);
-		bus.$on('updated', this._onDarkmodeUpdated_);
-	},
-	methods: {
-		_updateDarkmode_(v) {
-			localStorage.setItem('darkmode', v.toString());
-			if (v) {
-				document.documentElement.setAttribute('data-darkmode', 'true');
-			} else {
-				document.documentElement.removeAttribute('data-darkmode');
-			}
-			bus.$emit('updated', v);
-		},
-		_onDarkmodeUpdated_(v) {
-			if (!this.$el || !this.$el.setAttribute) return;
-			if (v) {
-				this.$el.setAttribute('data-darkmode', 'true');
-			} else {
-				this.$el.removeAttribute('data-darkmode');
-			}
-			this._darkmode_ = v;
-			this.$forceUpdate();
-		}
-	}
-});
-
 /**
  * APP ENTRY POINT!
  */
@@ -113,7 +71,7 @@ html.setAttribute('lang', lang);
 const head = document.getElementsByTagName('head')[0];
 const meta = document.createElement('meta');
 meta.setAttribute('name', 'description');
-meta.setAttribute('content', '%i18n:!common.misskey%');
+meta.setAttribute('content', '%i18n:common.misskey%');
 head.appendChild(meta);
 //#endregion
 
@@ -141,13 +99,52 @@ export default (callback: (launch: (router: VueRouter, api?: (os: MiOS) => API)
 		const launch = (router: VueRouter, api?: (os: MiOS) => API) => {
 			os.apis = api ? api(os) : null;
 
+			//#region Dark/Light
+			Vue.mixin({
+				data() {
+					return {
+						_unwatchDarkmode_: null
+					};
+				},
+				mounted() {
+					const apply = v => {
+						if (this.$el.setAttribute == null) return;
+						if (v) {
+							this.$el.setAttribute('data-darkmode', 'true');
+						} else {
+							this.$el.removeAttribute('data-darkmode');
+						}
+					};
+
+					apply(os.store.state.device.darkmode);
+
+					this._unwatchDarkmode_ = os.store.watch(s => {
+						return s.device.darkmode;
+					}, apply);
+				},
+				beforeDestroy() {
+					this._unwatchDarkmode_();
+				}
+			});
+
+			os.store.watch(s => {
+				return s.device.darkmode;
+			}, v => {
+				if (v) {
+					document.documentElement.setAttribute('data-darkmode', 'true');
+				} else {
+					document.documentElement.removeAttribute('data-darkmode');
+				}
+			});
+			//#endregion
+
 			Vue.mixin({
 				data() {
 					return {
 						os,
 						api: os.api,
 						apis: os.apis,
-						clientSettings: os.store.state.settings.data
+						clientSettings: os.store.state.settings
 					};
 				}
 			});
@@ -173,7 +170,7 @@ export default (callback: (launch: (router: VueRouter, api?: (os: MiOS) => API)
 		}
 
 		//#region 更新チェック
-		const preventUpdate = localStorage.getItem('preventUpdate') == 'true';
+		const preventUpdate = os.store.state.device.preventUpdate;
 		if (!preventUpdate) {
 			setTimeout(() => {
 				checkForUpdate(os);
diff --git a/src/client/app/mios.ts b/src/client/app/mios.ts
index 2373b0d8d2..a5a38a5414 100644
--- a/src/client/app/mios.ts
+++ b/src/client/app/mios.ts
@@ -98,14 +98,7 @@ export default class MiOS extends EventEmitter {
 	 * Whether is debug mode
 	 */
 	public get debug() {
-		return localStorage.getItem('debug') == 'true';
-	}
-
-	/**
-	 * Whether enable sounds
-	 */
-	public get isEnableSounds() {
-		return localStorage.getItem('enableSounds') == 'true';
+		return this.store ? this.store.state.device.debug : false;
 	}
 
 	public store: ReturnType<typeof initStore>;
@@ -435,12 +428,8 @@ export default class MiOS extends EventEmitter {
 			});
 		});
 
-		// Whether use raw version script
-		const raw = (localStorage.getItem('useRawScript') == 'true' && this.debug)
-			|| process.env.NODE_ENV != 'production';
-
 		// The path of service worker script
-		const sw = `/sw.${version}.${lang}.${raw ? 'raw' : 'min'}.js`;
+		const sw = `/sw.${version}.${lang}.js`;
 
 		// Register service worker
 		navigator.serviceWorker.register(sw).then(registration => {
@@ -471,8 +460,7 @@ export default class MiOS extends EventEmitter {
 		};
 
 		const promise = new Promise((resolve, reject) => {
-			const viaStream = this.stream && this.stream.hasConnection &&
-				(localStorage.getItem('apiViaStream') ? localStorage.getItem('apiViaStream') == 'true' : true);
+			const viaStream = this.stream && this.stream.hasConnection && this.store.state.device.apiViaStream;
 
 			if (viaStream) {
 				const stream = this.stream.borrow();
diff --git a/src/client/app/mobile/script.ts b/src/client/app/mobile/script.ts
index 427c177a14..5418aac090 100644
--- a/src/client/app/mobile/script.ts
+++ b/src/client/app/mobile/script.ts
@@ -5,7 +5,7 @@
 import Vue from 'vue';
 import VueRouter from 'vue-router';
 
-import { MdCard, MdButton, MdField, MdMenu, MdList, MdSwitch } from 'vue-material/dist/components';
+import { MdCard, MdButton, MdField, MdMenu, MdList, MdSwitch, MdSubheader, MdDialog, MdDialogAlert, MdRadio } from 'vue-material/dist/components';
 import 'vue-material/dist/vue-material.min.css';
 import 'vue-material/dist/theme/default.css';
 
@@ -37,7 +37,6 @@ import MkSearch from './views/pages/search.vue';
 import MkFollowers from './views/pages/followers.vue';
 import MkFollowing from './views/pages/following.vue';
 import MkSettings from './views/pages/settings.vue';
-import MkProfileSetting from './views/pages/profile-setting.vue';
 import MkOthello from './views/pages/othello.vue';
 
 Vue.use(MdCard);
@@ -46,6 +45,10 @@ Vue.use(MdField);
 Vue.use(MdMenu);
 Vue.use(MdList);
 Vue.use(MdSwitch);
+Vue.use(MdSubheader);
+Vue.use(MdDialog);
+Vue.use(MdDialogAlert);
+Vue.use(MdRadio);
 
 /**
  * init
@@ -67,8 +70,7 @@ init((launch) => {
 		routes: [
 			{ path: '/', name: 'index', component: MkIndex },
 			{ path: '/signup', name: 'signup', component: MkSignup },
-			{ path: '/i/settings', component: MkSettings },
-			{ path: '/i/settings/profile', component: MkProfileSetting },
+			{ path: '/i/settings', name: 'settings', component: MkSettings },
 			{ path: '/i/notifications', name: 'notifications', component: MkNotifications },
 			{ path: '/i/widgets', name: 'widgets', component: MkWidgets },
 			{ path: '/i/messaging', name: 'messaging', component: MkMessaging },
diff --git a/src/client/app/mobile/views/components/drive.vue b/src/client/app/mobile/views/components/drive.vue
index ef3432a3ec..8e35e6c88b 100644
--- a/src/client/app/mobile/views/components/drive.vue
+++ b/src/client/app/mobile/views/components/drive.vue
@@ -32,7 +32,7 @@
 		<div class="files" v-if="files.length > 0">
 			<x-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 ? '%i18n:common.loading%' : '%i18n:@load-more%' }}
 			</button>
 		</div>
 		<div class="empty" v-if="files.length == 0 && folders.length == 0 && !fetching">
diff --git a/src/client/app/mobile/views/components/follow-button.vue b/src/client/app/mobile/views/components/follow-button.vue
index 5d6b8ebf84..a6b5cf0556 100644
--- a/src/client/app/mobile/views/components/follow-button.vue
+++ b/src/client/app/mobile/views/components/follow-button.vue
@@ -7,7 +7,7 @@
 	<template v-if="!wait && user.isFollowing">%fa:minus%</template>
 	<template v-if="!wait && !user.isFollowing">%fa:plus%</template>
 	<template v-if="wait">%fa:spinner .pulse .fw%</template>
-	{{ user.isFollowing ? '%i18n:!@unfollow%' : '%i18n:!@follow%' }}
+	{{ user.isFollowing ? '%i18n:@unfollow%' : '%i18n:@follow%' }}
 </button>
 </template>
 
diff --git a/src/client/app/mobile/views/components/media-image.vue b/src/client/app/mobile/views/components/media-image.vue
index 9e0f8e5f7e..c2f9c66e84 100644
--- a/src/client/app/mobile/views/components/media-image.vue
+++ b/src/client/app/mobile/views/components/media-image.vue
@@ -17,9 +17,17 @@ export default Vue.extend({
 	},
 	computed: {
 		style(): any {
+			let url = `url(${this.image.url}?thumbnail)`;
+
+			if (this.$store.state.device.loadRemoteMedia || this.$store.state.device.lightmode) {
+				url = null;
+			} else if (this.raw || this.$store.state.device.loadRawImages) {
+				url = `url(${this.image.url})`;
+			}
+
 			return {
 				'background-color': this.image.properties.avgColor && this.image.properties.avgColor.length == 3 ? `rgb(${this.image.properties.avgColor.join(',')})` : 'transparent',
-				'background-image': this.raw ? `url(${this.image.url})` : `url(${this.image.url}?thumbnail&size=512)`
+				'background-image': url
 			};
 		}
 	}
diff --git a/src/client/app/mobile/views/components/note-detail.vue b/src/client/app/mobile/views/components/note-detail.vue
index c6664a91da..244dbb6c03 100644
--- a/src/client/app/mobile/views/components/note-detail.vue
+++ b/src/client/app/mobile/views/components/note-detail.vue
@@ -2,15 +2,15 @@
 <div class="mk-note-detail">
 	<button
 		class="more"
-		v-if="p.reply && p.reply.replyId && context.length == 0"
-		@click="fetchContext"
-		:disabled="fetchingContext"
+		v-if="p.reply && p.reply.replyId && conversation.length == 0"
+		@click="fetchConversation"
+		:disabled="conversationFetching"
 	>
-		<template v-if="!contextFetching">%fa:ellipsis-v%</template>
-		<template v-if="contextFetching">%fa:spinner .pulse%</template>
+		<template v-if="!conversationFetching">%fa:ellipsis-v%</template>
+		<template v-if="conversationFetching">%fa:spinner .pulse%</template>
 	</button>
-	<div class="context">
-		<x-sub v-for="note in context" :key="note.id" :note="note"/>
+	<div class="conversation">
+		<x-sub v-for="note in conversation" :key="note.id" :note="note"/>
 	</div>
 	<div class="reply-to" v-if="p.reply">
 		<x-sub :note="p.reply"/>
@@ -99,8 +99,8 @@ export default Vue.extend({
 
 	data() {
 		return {
-			context: [],
-			contextFetching: false,
+			conversation: [],
+			conversationFetching: false,
 			replies: []
 		};
 	},
@@ -166,14 +166,14 @@ export default Vue.extend({
 
 	methods: {
 		fetchContext() {
-			this.contextFetching = true;
+			this.conversationFetching = true;
 
-			// Fetch context
-			(this as any).api('notes/context', {
+			// Fetch conversation
+			(this as any).api('notes/conversation', {
 				noteId: this.p.replyId
-			}).then(context => {
-				this.contextFetching = false;
-				this.context = context.reverse();
+			}).then(conversation => {
+				this.conversationFetching = false;
+				this.conversation = conversation.reverse();
 			});
 		},
 		reply() {
@@ -245,7 +245,7 @@ root(isDark)
 		&:disabled
 			color #ccc
 
-	> .context
+	> .conversation
 		> *
 			border-bottom 1px solid isDark ? #1c2023 : #eef0f2
 
diff --git a/src/client/app/mobile/views/components/note-preview.vue b/src/client/app/mobile/views/components/note-preview.vue
index b55cad792d..8fa57768e0 100644
--- a/src/client/app/mobile/views/components/note-preview.vue
+++ b/src/client/app/mobile/views/components/note-preview.vue
@@ -1,9 +1,13 @@
 <template>
-<div class="mk-note-preview">
-	<mk-avatar class="avatar" :user="note.user"/>
+<div class="mk-note-preview" :class="{ smart: $store.state.device.postStyle == 'smart' }">
+	<mk-avatar class="avatar" :user="note.user" v-if="$store.state.device.postStyle != 'smart'"/>
 	<div class="main">
 		<header>
+			<mk-avatar class="avatar" :user="note.user" v-if="$store.state.device.postStyle == 'smart'"/>
 			<router-link class="name" :to="note.user | userPage">{{ note.user | userName }}</router-link>
+			<span class="is-admin" v-if="note.user.isAdmin">admin</span>
+			<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>
 			<router-link class="time" :to="note | notePage">
 				<mk-time :time="note.createdAt"/>
@@ -35,6 +39,13 @@ root(isDark)
 		display block
 		clear both
 
+	&.smart
+		> .main
+			width 100%
+
+			> header
+				align-items center
+
 	> .avatar
 		display block
 		float left
@@ -53,6 +64,13 @@ root(isDark)
 			margin-bottom 4px
 			white-space nowrap
 
+			> .avatar
+				flex-shrink 0
+				margin-right 8px
+				width 18px
+				height 18px
+				border-radius 100%
+
 			> .name
 				display block
 				margin 0 .5em 0 0
@@ -65,8 +83,19 @@ root(isDark)
 				text-decoration none
 				text-overflow ellipsis
 
-				&:hover
-					text-decoration underline
+			> .is-admin
+			> .is-bot
+			> .is-cat
+				margin 0 0.5em 0 0
+				padding 1px 6px
+				font-size 10px
+				color isDark ? #758188 : #aaa
+				border solid 1px isDark ? #57616f : #ddd
+				border-radius 3px
+
+				&.is-admin
+					border-color isDark ? #d42c41 : #f56a7b
+					color isDark ? #d42c41 : #f56a7b
 
 			> .username
 				margin 0 .5em 0 0
diff --git a/src/client/app/mobile/views/components/note.sub.vue b/src/client/app/mobile/views/components/note.sub.vue
index 2fb3b2ffcc..149a78ecde 100644
--- a/src/client/app/mobile/views/components/note.sub.vue
+++ b/src/client/app/mobile/views/components/note.sub.vue
@@ -1,9 +1,13 @@
 <template>
-<div class="sub">
-	<mk-avatar class="avatar" :user="note.user"/>
+<div class="sub" :class="{ smart: $store.state.device.postStyle == 'smart' }">
+	<mk-avatar class="avatar" :user="note.user" v-if="$store.state.device.postStyle != 'smart'"/>
 	<div class="main">
 		<header>
+			<mk-avatar class="avatar" :user="note.user" v-if="$store.state.device.postStyle == 'smart'"/>
 			<router-link class="name" :to="note.user | userPage">{{ note.user | userName }}</router-link>
+			<span class="is-admin" v-if="note.user.isAdmin">admin</span>
+			<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>
 			<div class="info">
 				<span class="mobile" v-if="note.viaMobile">%fa:mobile-alt%</span>
@@ -42,6 +46,13 @@ root(isDark)
 	@media (min-width 600px)
 		padding 24px 32px
 
+	&.smart
+		> .main
+			width 100%
+
+			> header
+				align-items center
+
 	&:after
 		content ""
 		display block
@@ -73,6 +84,13 @@ root(isDark)
 			margin-bottom 2px
 			white-space nowrap
 
+			> .avatar
+				flex-shrink 0
+				margin-right 8px
+				width 18px
+				height 18px
+				border-radius 100%
+
 			> .name
 				display block
 				margin 0 0.5em 0 0
@@ -88,6 +106,20 @@ root(isDark)
 				&:hover
 					text-decoration underline
 
+			> .is-admin
+			> .is-bot
+			> .is-cat
+				margin 0 0.5em 0 0
+				padding 1px 5px
+				font-size 10px
+				color isDark ? #758188 : #aaa
+				border solid 1px isDark ? #57616f : #ddd
+				border-radius 3px
+
+				&.is-admin
+					border-color isDark ? #d42c41 : #f56a7b
+					color isDark ? #d42c41 : #f56a7b
+
 			> .username
 				text-align left
 				margin 0
diff --git a/src/client/app/mobile/views/components/note.vue b/src/client/app/mobile/views/components/note.vue
index 83a957cfbd..2004263d22 100644
--- a/src/client/app/mobile/views/components/note.vue
+++ b/src/client/app/mobile/views/components/note.vue
@@ -1,22 +1,25 @@
 <template>
-<div class="note" :class="{ renote: isRenote }">
+<div class="note" :class="{ renote: isRenote, smart: $store.state.device.postStyle == 'smart' }">
 	<div class="reply-to" v-if="p.reply && (!os.isSignedIn || clientSettings.showReplyTarget)">
 		<x-sub :note="p.reply"/>
 	</div>
 	<div class="renote" v-if="isRenote">
 		<mk-avatar class="avatar" :user="note.user"/>
 		%fa:retweet%
-		<span>{{ '%i18n:!@reposted-by%'.substr(0, '%i18n:!@reposted-by%'.indexOf('{')) }}</span>
+		<span>{{ '%i18n:@reposted-by%'.substr(0, '%i18n:@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>{{ '%i18n:@reposted-by%'.substr('%i18n:@reposted-by%'.indexOf('}') + 1) }}</span>
 		<mk-time :time="note.createdAt"/>
 	</div>
 	<article>
-		<mk-avatar class="avatar" :user="p.user"/>
+		<mk-avatar class="avatar" :user="p.user" v-if="$store.state.device.postStyle != 'smart'"/>
 		<div class="main">
 			<header>
+				<mk-avatar class="avatar" :user="p.user" v-if="$store.state.device.postStyle == 'smart'"/>
 				<router-link class="name" :to="p.user | userPage">{{ p.user | userName }}</router-link>
-				<span class="is-bot" v-if="p.user.host === null && p.user.isBot">bot</span>
+				<span class="is-admin" v-if="p.user.isAdmin">admin</span>
+				<span class="is-bot" v-if="p.user.isBot">bot</span>
+				<span class="is-cat" v-if="p.user.isCat">cat</span>
 				<span class="username"><mk-acct :user="p.user"/></span>
 				<div class="info">
 					<span class="mobile" v-if="p.viaMobile">%fa:mobile-alt%</span>
@@ -262,6 +265,15 @@ root(isDark)
 	@media (min-width 500px)
 		font-size 16px
 
+	&.smart
+		> article
+			> .main
+				width 100%
+
+				> header
+					align-items center
+					margin-bottom 4px
+
 	> .renote
 		display flex
 		align-items center
@@ -278,12 +290,17 @@ root(isDark)
 			padding 16px 32px
 
 		.avatar
+			flex-shrink 0
 			display inline-block
-			width 28px
-			height 28px
+			width 20px
+			height 20px
 			margin 0 8px 0 0
 			border-radius 6px
 
+			@media (min-width 500px)
+				width 28px
+				height 28px
+
 		[data-fa]
 			margin-right 4px
 
@@ -352,21 +369,26 @@ root(isDark)
 				@media (min-width 500px)
 					margin-bottom 2px
 
+				> .avatar
+					flex-shrink 0
+					margin-right 8px
+					width 20px
+					height 20px
+					border-radius 100%
+
 				> .name
 					display block
 					margin 0 0.5em 0 0
 					padding 0
 					overflow hidden
 					color isDark ? #fff : #627079
-					font-size 1em
 					font-weight bold
 					text-decoration none
 					text-overflow ellipsis
 
-					&:hover
-						text-decoration underline
-
+				> .is-admin
 				> .is-bot
+				> .is-cat
 					margin 0 0.5em 0 0
 					padding 1px 6px
 					font-size 12px
@@ -374,6 +396,10 @@ root(isDark)
 					border solid 1px isDark ? #57616f : #ddd
 					border-radius 3px
 
+					&.is-admin
+						border-color isDark ? #d42c41 : #f56a7b
+						color isDark ? #d42c41 : #f56a7b
+
 				> .username
 					margin 0 0.5em 0 0
 					overflow hidden
diff --git a/src/client/app/mobile/views/components/notifications.vue b/src/client/app/mobile/views/components/notifications.vue
index 8ab66940c4..6bb9e9bb2c 100644
--- a/src/client/app/mobile/views/components/notifications.vue
+++ b/src/client/app/mobile/views/components/notifications.vue
@@ -12,7 +12,7 @@
 
 	<button class="more" v-if="moreNotifications" @click="fetchMoreNotifications" :disabled="fetchingMoreNotifications">
 		<template v-if="fetchingMoreNotifications">%fa:spinner .pulse .fw%</template>
-		{{ fetchingMoreNotifications ? '%i18n:!common.loading%' : '%i18n:!@more%' }}
+		{{ fetchingMoreNotifications ? '%i18n:common.loading%' : '%i18n:@more%' }}
 	</button>
 
 	<p class="empty" v-if="notifications.length == 0 && !fetching">%i18n:@empty%</p>
diff --git a/src/client/app/mobile/views/components/post-form.vue b/src/client/app/mobile/views/components/post-form.vue
index 0bb498e5d7..b3b5ffd502 100644
--- a/src/client/app/mobile/views/components/post-form.vue
+++ b/src/client/app/mobile/views/components/post-form.vue
@@ -20,7 +20,7 @@
 			<a @click="addVisibleUser">+ユーザーを追加</a>
 		</div>
 		<input v-show="useCw" v-model="cw" placeholder="内容への注釈 (オプション)">
-		<textarea v-model="text" ref="text" :disabled="posting" :placeholder="reply ? '%i18n:!@reply-placeholder%' : renote ? '%i18n:!@renote-placeholder%' : '%i18n:!@note-placeholder%'"></textarea>
+		<textarea v-model="text" ref="text" :disabled="posting" :placeholder="reply ? '%i18n:@reply-placeholder%' : renote ? '%i18n:@renote-placeholder%' : '%i18n:@note-placeholder%'"></textarea>
 		<div class="attaches" v-show="files.length != 0">
 			<x-draggable class="files" :list="files" :options="{ animation: 150 }">
 				<div class="file" v-for="file in files" :key="file.id">
diff --git a/src/client/app/mobile/views/components/ui.nav.vue b/src/client/app/mobile/views/components/ui.nav.vue
index ec42dbc99d..aa469bd1c8 100644
--- a/src/client/app/mobile/views/components/ui.nav.vue
+++ b/src/client/app/mobile/views/components/ui.nav.vue
@@ -28,8 +28,8 @@
 					<li><a @click="search">%fa:search%%i18n:@search%%fa:angle-right%</a></li>
 				</ul>
 				<ul>
-					<li><router-link to="/i/settings">%fa:cog%%i18n:@settings%%fa:angle-right%</router-link></li>
-					<li @click="dark"><p><template v-if="_darkmode_">%fa:moon%</template><template v-else>%fa:R moon%</template><span>ダークモード</span></p></li>
+					<li><router-link to="/i/settings" :data-active="$route.name == 'settings'">%fa:cog%%i18n:@settings%%fa:angle-right%</router-link></li>
+					<li @click="dark"><p><template v-if="$store.state.device.darkmode">%fa:moon%</template><template v-else>%fa:R moon%</template><span>ダークモード</span></p></li>
 				</ul>
 			</div>
 			<a :href="aboutUrl"><p class="about">%i18n:@about%</p></a>
@@ -94,7 +94,7 @@ export default Vue.extend({
 	},
 	methods: {
 		search() {
-			const query = window.prompt('%i18n:!@search%');
+			const query = window.prompt('%i18n:@search%');
 			if (query == null || query == '') return;
 			this.$router.push('/search?q=' + encodeURIComponent(query));
 		},
@@ -117,7 +117,10 @@ export default Vue.extend({
 			this.hasGameInvitations = false;
 		},
 		dark() {
-			(this as any)._updateDarkmode_(!(this as any)._darkmode_);
+			this.$store.commit('device/set', {
+				key: 'darkmode',
+				value: !this.$store.state.device.darkmode
+			});
 		}
 	}
 });
diff --git a/src/client/app/mobile/views/components/user-card.vue b/src/client/app/mobile/views/components/user-card.vue
index 52c82115bf..808ee72402 100644
--- a/src/client/app/mobile/views/components/user-card.vue
+++ b/src/client/app/mobile/views/components/user-card.vue
@@ -1,9 +1,7 @@
 <template>
 <div class="mk-user-card">
 	<header :style="user.bannerUrl ? `background-image: url(${user.bannerUrl}?thumbnail&size=1024)` : ''">
-		<a :href="user | userPage">
-			<img :src="`${user.avatarUrl}?thumbnail&size=200`" alt="avatar"/>
-		</a>
+		<mk-avatar class="avatar" :user="user"/>
 	</header>
 	<a class="name" :href="user | userPage" target="_blank">{{ user | userName }}</a>
 	<p class="username"><mk-acct :user="user"/></p>
@@ -35,15 +33,14 @@ export default Vue.extend({
 		background-position center
 		border-radius 8px 8px 0 0
 
-		> a
-			> img
-				position absolute
-				top 20px
-				left calc(50% - 40px)
-				width 80px
-				height 80px
-				border solid 2px #fff
-				border-radius 8px
+		> .avatar
+			position absolute
+			top 20px
+			left calc(50% - 40px)
+			width 80px
+			height 80px
+			border solid 2px #fff
+			border-radius 8px
 
 	> .name
 		display block
diff --git a/src/client/app/mobile/views/components/user-timeline.vue b/src/client/app/mobile/views/components/user-timeline.vue
index 3ceb876596..aca6f783b8 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:R comments%
-			{{ withMedia ? '%i18n:!@no-notes-with-media%' : '%i18n:!@no-notes%' }}
+			{{ withMedia ? '%i18n:@no-notes-with-media%' : '%i18n:@no-notes%' }}
 		</div>
 	</mk-notes>
 </div>
diff --git a/src/client/app/mobile/views/pages/followers.vue b/src/client/app/mobile/views/pages/followers.vue
index 33ade94e35..dfb9c62142 100644
--- a/src/client/app/mobile/views/pages/followers.vue
+++ b/src/client/app/mobile/views/pages/followers.vue
@@ -2,7 +2,7 @@
 <mk-ui>
 	<template slot="header" v-if="!fetching">
 		<img :src="`${user.avatarUrl}?thumbnail&size=64`" alt="">
-		{{ '%i18n:!@followers-of%'.replace('{}', name) }}
+		{{ '%i18n:@followers-of%'.replace('{}', name) }}
 	</template>
 	<mk-users-list
 		v-if="!fetching"
@@ -49,7 +49,7 @@ export default Vue.extend({
 				this.user = user;
 				this.fetching = false;
 
-				document.title = '%i18n:!@followers-of%'.replace('{}', this.name) + ' | Misskey';
+				document.title = '%i18n:@followers-of%'.replace('{}', this.name) + ' | Misskey';
 			});
 		},
 		onLoaded() {
diff --git a/src/client/app/mobile/views/pages/following.vue b/src/client/app/mobile/views/pages/following.vue
index c6d6d44281..35461ea2fc 100644
--- a/src/client/app/mobile/views/pages/following.vue
+++ b/src/client/app/mobile/views/pages/following.vue
@@ -2,7 +2,7 @@
 <mk-ui>
 	<template slot="header" v-if="!fetching">
 		<img :src="`${user.avatarUrl}?thumbnail&size=64`" alt="">
-		{{ '%i18n:!@following-of%'.replace('{}', name) }}
+		{{ '%i18n:@following-of%'.replace('{}', name) }}
 	</template>
 	<mk-users-list
 		v-if="!fetching"
@@ -48,7 +48,7 @@ export default Vue.extend({
 				this.user = user;
 				this.fetching = false;
 
-				document.title = '%i18n:!@followers-of%'.replace('{}', this.name) + ' | Misskey';
+				document.title = '%i18n:@followers-of%'.replace('{}', this.name) + ' | Misskey';
 			});
 		},
 		onLoaded() {
diff --git a/src/client/app/mobile/views/pages/home.vue b/src/client/app/mobile/views/pages/home.vue
index ad6d5ed408..5701ff03d5 100644
--- a/src/client/app/mobile/views/pages/home.vue
+++ b/src/client/app/mobile/views/pages/home.vue
@@ -2,9 +2,9 @@
 <mk-ui>
 	<span slot="header" @click="showNav = true">
 		<span>
-			<span v-if="src == 'home'">%fa:home%ホーム</span>
-			<span v-if="src == 'local'">%fa:R comments%ローカル</span>
-			<span v-if="src == 'global'">%fa:globe%グローバル</span>
+			<span v-if="src == 'home'">%fa:home%%i18n:@home%</span>
+			<span v-if="src == 'local'">%fa:R comments%%i18n:@local%</span>
+			<span v-if="src == 'global'">%fa:globe%%i18n:@global%</span>
 			<span v-if="src.startsWith('list')">%fa:list%{{ list.title }}</span>
 		</span>
 		<span style="margin-left:8px">
@@ -17,14 +17,14 @@
 		<button @click="fn">%fa:pencil-alt%</button>
 	</template>
 
-	<main :data-darkmode="_darkmode_">
+	<main :data-darkmode="$store.state.device.darkmode">
 		<div class="nav" v-if="showNav">
 			<div class="bg" @click="showNav = false"></div>
 			<div class="body">
 				<div>
-					<span :data-active="src == 'home'" @click="src = 'home'">%fa:home% ホーム</span>
-					<span :data-active="src == 'local'" @click="src = 'local'">%fa:R comments% ローカル</span>
-					<span :data-active="src == 'global'" @click="src = 'global'">%fa:globe% グローバル</span>
+					<span :data-active="src == 'home'" @click="src = 'home'">%fa:home% %i18n:@home%</span>
+					<span :data-active="src == 'local'" @click="src = 'local'">%fa:R comments% %i18n:@local%</span>
+					<span :data-active="src == 'global'" @click="src = 'global'">%fa:globe% %i18n:@global%</span>
 					<template v-if="lists">
 						<span v-for="l in lists" :data-active="src == 'list:' + l.id" @click="src = 'list:' + l.id; list = l" :key="l.id">%fa:list% {{ l.title }}</span>
 					</template>
diff --git a/src/client/app/mobile/views/pages/messaging-room.vue b/src/client/app/mobile/views/pages/messaging-room.vue
index c26a9b735e..8b82b03fb9 100644
--- a/src/client/app/mobile/views/pages/messaging-room.vue
+++ b/src/client/app/mobile/views/pages/messaging-room.vue
@@ -16,16 +16,30 @@ export default Vue.extend({
 	data() {
 		return {
 			fetching: true,
-			user: null
+			user: null,
+			unwatchDarkmode: null
 		};
 	},
 	watch: {
 		$route: 'fetch'
 	},
 	created() {
-		document.documentElement.style.background = '#fff';
+		const applyBg = v =>
+			document.documentElement.style.setProperty('background', v ? '#191b22' : '#fff', 'important');
+
+		applyBg(this.$store.state.device.darkmode);
+
+		this.unwatchDarkmode = this.$store.watch(s => {
+			return s.device.darkmode;
+		}, applyBg);
+
 		this.fetch();
 	},
+	beforeDestroy() {
+		document.documentElement.style.removeProperty('background');
+		document.documentElement.style.removeProperty('background-color'); // for safari's bug
+		this.unwatchDarkmode();
+	},
 	methods: {
 		fetch() {
 			this.fetching = true;
@@ -39,4 +53,3 @@ export default Vue.extend({
 	}
 });
 </script>
-
diff --git a/src/client/app/mobile/views/pages/messaging.vue b/src/client/app/mobile/views/pages/messaging.vue
index cc328e5a1c..057470efd9 100644
--- a/src/client/app/mobile/views/pages/messaging.vue
+++ b/src/client/app/mobile/views/pages/messaging.vue
@@ -12,7 +12,6 @@ import getAcct from '../../../../../acct/render';
 export default Vue.extend({
 	mounted() {
 		document.title = 'Misskey %i18n:@messaging%';
-		document.documentElement.style.background = '#fff';
 	},
 	methods: {
 		navigate(user) {
diff --git a/src/client/app/mobile/views/pages/notifications.vue b/src/client/app/mobile/views/pages/notifications.vue
index d0c0fe9535..2e98201caa 100644
--- a/src/client/app/mobile/views/pages/notifications.vue
+++ b/src/client/app/mobile/views/pages/notifications.vue
@@ -21,7 +21,7 @@ export default Vue.extend({
 	},
 	methods: {
 		fn() {
-			const ok = window.confirm('%i18n:!@read-all%');
+			const ok = window.confirm('%i18n:@read-all%');
 			if (!ok) return;
 
 			(this as any).api('notifications/markAsRead_all');
diff --git a/src/client/app/mobile/views/pages/profile-setting.vue b/src/client/app/mobile/views/pages/profile-setting.vue
deleted file mode 100644
index 7048cdef31..0000000000
--- a/src/client/app/mobile/views/pages/profile-setting.vue
+++ /dev/null
@@ -1,225 +0,0 @@
-<template>
-<mk-ui>
-	<span slot="header">%fa:user%%i18n:@title%</span>
-	<div :class="$style.content">
-		<p>%fa:info-circle%%i18n:@will-be-published%</p>
-		<div :class="$style.form">
-			<div :style="os.i.bannerUrl ? `background-image: url(${os.i.bannerUrl}?thumbnail&size=1024)` : ''" @click="setBanner">
-				<img :src="`${os.i.avatarUrl}?thumbnail&size=200`" alt="avatar" @click="setAvatar"/>
-			</div>
-			<label>
-				<p>%i18n:@name%</p>
-				<input v-model="name" type="text"/>
-			</label>
-			<label>
-				<p>%i18n:@location%</p>
-				<input v-model="location" type="text"/>
-			</label>
-			<label>
-				<p>%i18n:@description%</p>
-				<textarea v-model="description"></textarea>
-			</label>
-			<label>
-				<p>%i18n:@birthday%</p>
-				<input v-model="birthday" type="date"/>
-			</label>
-			<label>
-				<p>%i18n:@avatar%</p>
-				<button @click="setAvatar" :disabled="avatarSaving">%i18n:@set-avatar%</button>
-			</label>
-			<label>
-				<p>%i18n:@banner%</p>
-				<button @click="setBanner" :disabled="bannerSaving">%i18n:@set-banner%</button>
-			</label>
-		</div>
-		<button :class="$style.save" @click="save" :disabled="saving">%fa:check%%i18n:@save%</button>
-	</div>
-</mk-ui>
-</template>
-
-<script lang="ts">
-import Vue from 'vue';
-export default Vue.extend({
-	data() {
-		return {
-			name: null,
-			location: null,
-			description: null,
-			birthday: null,
-			avatarSaving: false,
-			bannerSaving: false,
-			saving: false
-		};
-	},
-	created() {
-		this.name = (this as any).os.i.name || '';
-		this.location = (this as any).os.i.profile.location;
-		this.description = (this as any).os.i.description;
-		this.birthday = (this as any).os.i.profile.birthday;
-	},
-	mounted() {
-		document.title = 'Misskey | %i18n:@title%';
-	},
-	methods: {
-		setAvatar() {
-			(this as any).apis.chooseDriveFile({
-				multiple: false
-			}).then(file => {
-				this.avatarSaving = true;
-
-				(this as any).api('i/update', {
-					avatarId: file.id
-				}).then(() => {
-					this.avatarSaving = false;
-					alert('%i18n:!@avatar-saved%');
-				});
-			});
-		},
-		setBanner() {
-			(this as any).apis.chooseDriveFile({
-				multiple: false
-			}).then(file => {
-				this.bannerSaving = true;
-
-				(this as any).api('i/update', {
-					bannerId: file.id
-				}).then(() => {
-					this.bannerSaving = false;
-					alert('%i18n:!@banner-saved%');
-				});
-			});
-		},
-		save() {
-			this.saving = true;
-
-			(this as any).api('i/update', {
-				name: this.name || null,
-				location: this.location || null,
-				description: this.description || null,
-				birthday: this.birthday || null
-			}).then(() => {
-				this.saving = false;
-				alert('%i18n:!@saved%');
-			});
-		}
-	}
-});
-</script>
-
-<style lang="stylus" module>
-@import '~const.styl'
-
-.content
-	margin 8px auto
-	max-width 500px
-	width calc(100% - 16px)
-
-	@media (min-width 500px)
-		margin 16px auto
-		width calc(100% - 32px)
-
-	> p
-		display block
-		margin 0 0 8px 0
-		padding 12px 16px
-		font-size 14px
-		color #79d4e6
-		border solid 1px #71afbb
-		//color #276f86
-		//background #f8ffff
-		//border solid 1px #a9d5de
-		border-radius 8px
-
-		> [data-fa]
-			margin-right 6px
-
-.form
-	position relative
-	background #fff
-	box-shadow 0 0 0 1px rgba(#000, 0.2)
-	border-radius 8px
-
-	&:before
-		content ""
-		display block
-		position absolute
-		bottom -20px
-		left calc(50% - 10px)
-		border-top solid 10px rgba(#000, 0.2)
-		border-right solid 10px transparent
-		border-bottom solid 10px transparent
-		border-left solid 10px transparent
-
-	&:after
-		content ""
-		display block
-		position absolute
-		bottom -16px
-		left calc(50% - 8px)
-		border-top solid 8px #fff
-		border-right solid 8px transparent
-		border-bottom solid 8px transparent
-		border-left solid 8px transparent
-
-	> div
-		height 128px
-		background-color #e4e4e4
-		background-size cover
-		background-position center
-		border-radius 8px 8px 0 0
-
-		> img
-			position absolute
-			top 25px
-			left calc(50% - 40px)
-			width 80px
-			height 80px
-			border solid 2px #fff
-			border-radius 8px
-
-	> label
-		display block
-		margin 0
-		padding 16px
-		border-bottom solid 1px #eee
-
-		&:last-of-type
-			border none
-
-		> p:first-child
-			display block
-			margin 0
-			padding 0 0 4px 0
-			font-weight bold
-			color #2f3c42
-
-		> input[type="text"]
-		> textarea
-			display block
-			width 100%
-			padding 12px
-			font-size 16px
-			color #192427
-			border solid 2px #ddd
-			border-radius 4px
-
-		> textarea
-			min-height 80px
-
-.save
-	display block
-	margin 8px 0 0 0
-	padding 16px
-	width 100%
-	font-size 16px
-	color $theme-color-foreground
-	background $theme-color
-	border-radius 8px
-
-	&:disabled
-		opacity 0.7
-
-	> [data-fa]
-		margin-right 4px
-
-</style>
diff --git a/src/client/app/mobile/views/pages/search.vue b/src/client/app/mobile/views/pages/search.vue
index f038a6f81f..9850fbcfb4 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:search% {{ q }}</span>
 	<main v-if="!fetching">
 		<mk-notes :class="$style.notes" :notes="notes">
-			<span v-if="notes.length == 0">{{ '%i18n:!@empty%'.replace('{}', q) }}</span>
+			<span v-if="notes.length == 0">{{ '%i18n:@empty%'.replace('{}', q) }}</span>
 			<button v-if="existMore" @click="more" :disabled="fetching" slot="tail">
 				<span v-if="!fetching">%i18n:@load-more%</span>
 				<span v-if="fetching">%i18n:common.loading%<mk-ellipsis/></span>
diff --git a/src/client/app/mobile/views/pages/selectdrive.vue b/src/client/app/mobile/views/pages/selectdrive.vue
index d730e4fcff..1a162b346c 100644
--- a/src/client/app/mobile/views/pages/selectdrive.vue
+++ b/src/client/app/mobile/views/pages/selectdrive.vue
@@ -25,7 +25,7 @@ export default Vue.extend({
 		}
 	},
 	mounted() {
-		document.title = '%i18n:!@title%';
+		document.title = '%i18n:@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 b16860d62c..3bb25f88f8 100644
--- a/src/client/app/mobile/views/pages/settings.vue
+++ b/src/client/app/mobile/views/pages/settings.vue
@@ -2,13 +2,13 @@
 <mk-ui>
 	<span slot="header">%fa:cog%%i18n:@settings%</span>
 	<main>
-		<p v-html="'%i18n:!@signed-in-as%'.replace('{}', '<b>' + name + '</b>')"></p>
+		<p v-html="'%i18n:@signed-in-as%'.replace('{}', '<b>' + name + '</b>')"></p>
 		<div>
 			<x-profile/>
 
-			<md-card class="md-layout-item md-size-50 md-small-size-100">
+			<md-card>
 				<md-card-header>
-					<div class="md-title">%i18n:@design%</div>
+					<div class="md-title">%fa:palette% %i18n:@design%</div>
 				</md-card-header>
 
 				<md-card-content>
@@ -19,6 +19,110 @@
 					<div>
 						<md-switch v-model="clientSettings.circleIcons" @change="onChangeCircleIcons">%i18n:@circle-icons%</md-switch>
 					</div>
+
+					<div>
+						<div class="md-body-2">%i18n:@timeline%</div>
+
+						<div>
+							<md-switch v-model="clientSettings.showReplyTarget" @change="onChangeShowReplyTarget">%i18n:@show-reply-target%</md-switch>
+						</div>
+
+						<div>
+							<md-switch v-model="clientSettings.showMyRenotes" @change="onChangeShowMyRenotes">%i18n:@show-my-renotes%</md-switch>
+						</div>
+
+						<div>
+							<md-switch v-model="clientSettings.showRenotedMyNotes" @change="onChangeShowRenotedMyNotes">%i18n:@show-renoted-my-notes%</md-switch>
+						</div>
+					</div>
+
+					<div>
+						<div class="md-body-2">%i18n:@post-style%</div>
+
+						<md-radio v-model="postStyle" value="standard">%i18n:@post-style-standard%</md-radio>
+						<md-radio v-model="postStyle" value="smart">%i18n:@post-style-smart%</md-radio>
+					</div>
+				</md-card-content>
+			</md-card>
+
+			<md-card>
+				<md-card-header>
+					<div class="md-title">%fa:cog% %i18n:@behavior%</div>
+				</md-card-header>
+
+				<md-card-content>
+					<div>
+						<md-switch v-model="clientSettings.fetchOnScroll" @change="onChangeFetchOnScroll">%i18n:@fetch-on-scroll%</md-switch>
+					</div>
+
+					<div>
+						<md-switch v-model="clientSettings.disableViaMobile" @change="onChangeDisableViaMobile">%i18n:@disable-via-mobile%</md-switch>
+					</div>
+
+					<div>
+						<md-switch v-model="loadRawImages">%i18n:@load-raw-images%</md-switch>
+					</div>
+
+					<div>
+						<md-switch v-model="clientSettings.loadRemoteMedia" @change="onChangeLoadRemoteMedia">%i18n:@load-remote-media%</md-switch>
+					</div>
+
+					<div>
+						<md-switch v-model="lightmode">%i18n:@i-am-under-limited-internet%</md-switch>
+					</div>
+				</md-card-content>
+			</md-card>
+
+			<md-card>
+				<md-card-header>
+					<div class="md-title">%fa:language% %i18n:@lang%</div>
+				</md-card-header>
+
+				<md-card-content>
+					<md-field>
+						<md-select v-model="lang" placeholder="%i18n:@auto%">
+							<md-optgroup label="%i18n:@recommended%">
+								<md-option value="">%i18n:@auto%</md-option>
+							</md-optgroup>
+
+							<md-optgroup label="%i18n:@specify-language%">
+								<md-option v-for="x in langs" :value="x[0]" :key="x[0]">{{ x[1] }}</md-option>
+							</md-optgroup>
+						</md-select>
+					</md-field>
+					<span class="md-helper-text">%fa:info-circle% %i18n:@lang-tip%</span>
+				</md-card-content>
+			</md-card>
+
+			<md-card>
+				<md-card-header>
+					<div class="md-title">%fa:B twitter% %i18n:@twitter%</div>
+				</md-card-header>
+
+				<md-card-content>
+					<p class="account" v-if="os.i.twitter"><a :href="`https://twitter.com/${os.i.twitter.screenName}`" target="_blank">@{{ os.i.twitter.screenName }}</a></p>
+					<p>
+						<a :href="`${apiUrl}/connect/twitter`" target="_blank">{{ os.i.twitter ? '%i18n:@twitter-reconnect%' : '%i18n:@twitter-connect%' }}</a>
+						<span v-if="os.i.twitter"> or </span>
+						<a :href="`${apiUrl}/disconnect/twitter`" target="_blank" v-if="os.i.twitter">%i18n:@twitter-disconnect%</a>
+					</p>
+				</md-card-content>
+			</md-card>
+
+			<md-card>
+				<md-card-header>
+					<div class="md-title">%fa:sync-alt% %i18n:@update%</div>
+				</md-card-header>
+
+				<md-card-content>
+					<div>%i18n:@version% <i>{{ version }}</i></div>
+					<template v-if="latestVersion !== undefined">
+						<div>%i18n:@latest-version% <i>{{ latestVersion ? latestVersion : version }}</i></div>
+					</template>
+					<md-button class="md-raised md-primary" @click="checkForUpdate" :disabled="checkingForUpdate">
+						<template v-if="checkingForUpdate">%i18n:@update-checking%<mk-ellipsis/></template>
+						<template v-else>%i18n:@check-for-updates%</template>
+					</md-button>
 				</md-card-content>
 			</md-card>
 		</div>
@@ -29,7 +133,8 @@
 
 <script lang="ts">
 import Vue from 'vue';
-import { version, codename } from '../../../config';
+import { apiUrl, version, codename, langs } from '../../../config';
+import checkForUpdate from '../../../common/scripts/check-for-update';
 
 import XProfile from './settings/settings.profile.vue';
 
@@ -40,22 +145,44 @@ export default Vue.extend({
 
 	data() {
 		return {
+			apiUrl,
 			version,
 			codename,
-			darkmode: localStorage.getItem('darkmode') == 'true'
+			langs,
+			latestVersion: undefined,
+			checkingForUpdate: false
 		};
 	},
 
 	computed: {
 		name(): string {
 			return Vue.filter('userName')((this as any).os.i);
-		}
-	},
+		},
 
-	watch: {
-		darkmode() {
-			(this as any)._updateDarkmode_(this.darkmode);
-		}
+		darkmode: {
+			get() { return this.$store.state.device.darkmode; },
+			set(value) { this.$store.commit('device/set', { key: 'darkmode', value }); }
+		},
+
+		postStyle: {
+			get() { return this.$store.state.device.postStyle; },
+			set(value) { this.$store.commit('device/set', { key: 'postStyle', value }); }
+		},
+
+		lightmode: {
+			get() { return this.$store.state.device.lightmode; },
+			set(value) { this.$store.commit('device/set', { key: 'lightmode', value }); }
+		},
+
+		loadRawImages: {
+			get() { return this.$store.state.device.loadRawImages; },
+			set(value) { this.$store.commit('device/set', { key: 'loadRawImages', value }); }
+		},
+
+		lang: {
+			get() { return this.$store.state.device.lang; },
+			set(value) { this.$store.commit('device/set', { key: 'lang', value }); }
+		},
 	},
 
 	mounted() {
@@ -67,19 +194,83 @@ export default Vue.extend({
 			(this as any).os.signout();
 		},
 
+		onChangeFetchOnScroll(v) {
+			this.$store.dispatch('settings/set', {
+				key: 'fetchOnScroll',
+				value: v
+			});
+		},
+
+		onChangeDisableViaMobile(v) {
+			this.$store.dispatch('settings/set', {
+				key: 'disableViaMobile',
+				value: v
+			});
+		},
+
+		onChangeLoadRemoteMedia(v) {
+			this.$store.dispatch('settings/set', {
+				key: 'loadRemoteMedia',
+				value: v
+			});
+		},
+
 		onChangeCircleIcons(v) {
 			this.$store.dispatch('settings/set', {
 				key: 'circleIcons',
 				value: v
 			});
+		},
+
+		onChangeShowReplyTarget(v) {
+			this.$store.dispatch('settings/set', {
+				key: 'showReplyTarget',
+				value: v
+			});
+		},
+
+		onChangeShowMyRenotes(v) {
+			this.$store.dispatch('settings/set', {
+				key: 'showMyRenotes',
+				value: v
+			});
+		},
+
+		onChangeShowRenotedMyNotes(v) {
+			this.$store.dispatch('settings/set', {
+				key: 'showRenotedMyNotes',
+				value: v
+			});
+		},
+
+		checkForUpdate() {
+			this.checkingForUpdate = true;
+			checkForUpdate((this as any).os, true, true).then(newer => {
+				this.checkingForUpdate = false;
+				this.latestVersion = newer;
+				if (newer == null) {
+					(this as any).apis.dialog({
+						title: '%i18n:@no-updates%',
+						text: '%i18n:@no-updates-desc%'
+					});
+				} else {
+					(this as any).apis.dialog({
+						title: '%i18n:@update-available%',
+						text: '%i18n:@update-available-desc%'
+					});
+				}
+			});
 		}
 	}
 });
 </script>
 
 <style lang="stylus" scoped>
-main
+root(isDark)
 	padding 0 16px
+	margin 0 auto
+	max-width 500px
+	width 100%
 
 	> div
 		> *
@@ -89,57 +280,12 @@ main
 		display block
 		margin 24px
 		text-align center
-		color #cad2da
+		color isDark ? #cad2da : #a2a9b1
 
-	> ul
-		$radius = 8px
+main[data-darkmode]
+	root(true)
 
-		display block
-		margin 16px auto
-		padding 0
-		max-width 500px
-		width calc(100% - 32px)
-		list-style none
-		background #fff
-		border solid 1px rgba(#000, 0.2)
-		border-radius $radius
-
-		> li
-			display block
-			border-bottom solid 1px #ddd
-
-			&:hover
-				background rgba(#000, 0.1)
-
-			&:first-child
-				border-top-left-radius $radius
-				border-top-right-radius $radius
-
-			&:last-child
-				border-bottom-left-radius $radius
-				border-bottom-right-radius $radius
-				border-bottom none
-
-			> a
-				$height = 48px
-
-				display block
-				position relative
-				padding 0 16px
-				line-height $height
-				color #4d635e
-
-				> [data-fa]:nth-of-type(1)
-					margin-right 4px
-
-				> [data-fa]:nth-of-type(2)
-					display block
-					position absolute
-					top 0
-					right 8px
-					z-index 1
-					padding 0 20px
-					font-size 1.2em
-					line-height $height
+main:not([data-darkmode])
+	root(false)
 
 </style>
diff --git a/src/client/app/mobile/views/pages/settings/settings.profile.vue b/src/client/app/mobile/views/pages/settings/settings.profile.vue
index 6b5d07cce9..c16c44e133 100644
--- a/src/client/app/mobile/views/pages/settings/settings.profile.vue
+++ b/src/client/app/mobile/views/pages/settings/settings.profile.vue
@@ -1,50 +1,55 @@
 <template>
-	<md-card class="md-layout-item md-size-50 md-small-size-100">
+	<md-card>
 		<md-card-header>
-			<div class="md-title">%i18n:@title%</div>
+			<div class="md-title">%fa:pencil-alt% %i18n:@title%</div>
 		</md-card-header>
 
 		<md-card-content>
 			<md-field>
 				<label>%i18n:@name%</label>
-				<md-input v-model="name" :disabled="saving"/>
+				<md-input v-model="name" :disabled="saving" md-counter="30"/>
 			</md-field>
 
 			<md-field>
+				<label>%i18n:@account%</label>
+				<span class="md-prefix">@</span>
+				<md-input v-model="username" readonly></md-input>
+				<span class="md-suffix">@{{ host }}</span>
+			</md-field>
+
+			<md-field>
+				<md-icon>%fa:map-marker-alt%</md-icon>
 				<label>%i18n:@location%</label>
 				<md-input v-model="location" :disabled="saving"/>
 			</md-field>
 
 			<md-field>
-				<label>%i18n:@description%</label>
-				<md-textarea v-model="description" :disabled="saving"/>
-			</md-field>
-
-			<md-field>
+				<md-icon>%fa:birthday-cake%</md-icon>
 				<label>%i18n:@birthday%</label>
 				<md-input type="date" v-model="birthday" :disabled="saving"/>
 			</md-field>
 
-			<div>
-				<div class="md-body-2">%i18n:@avatar%</div>
-				<md-menu md-direction="bottom-end" :md-close-on-select="true">
-					<md-button md-menu-trigger>%i18n:@set-avatar%</md-button>
-					<md-menu-content>
-						<md-menu-item @click="uploadAvatar">%i18n:@upload-avatar%</md-menu-item>
-						<md-menu-item @click="chooseAvatar">%i18n:@choose-avatar%</md-menu-item>
-					</md-menu-content>
-				</md-menu>
-			</div>
+			<md-field>
+				<label>%i18n:@description%</label>
+				<md-textarea v-model="description" :disabled="saving" md-counter="500"/>
+			</md-field>
+
+			<md-field>
+				<label>%i18n:@avatar%</label>
+				<md-file @md-change="onAvatarChange"/>
+			</md-field>
+
+			<md-field>
+				<label>%i18n:@banner%</label>
+				<md-file @md-change="onBannerChange"/>
+			</md-field>
+
+			<md-dialog-alert
+					:md-active.sync="uploading"
+					md-content="%18n:!@uploading%"/>
 
 			<div>
-				<div class="md-body-2">%i18n:@banner%</div>
-				<md-menu md-direction="bottom-end" :md-close-on-select="true">
-					<md-button md-menu-trigger>%i18n:@set-banner%</md-button>
-					<md-menu-content>
-						<md-menu-item @click="uploadAvatar">%i18n:@upload-banner%</md-menu-item>
-						<md-menu-item @click="chooseAvatar">%i18n:@choose-banner%</md-menu-item>
-					</md-menu-content>
-				</md-menu>
+				<md-switch v-model="isCat">%i18n:@is-cat%</md-switch>
 			</div>
 		</md-card-content>
 
@@ -56,58 +61,83 @@
 
 <script lang="ts">
 import Vue from 'vue';
+import { apiUrl, host } from '../../../../config';
 
 export default Vue.extend({
 	data() {
 		return {
+			host,
 			name: null,
+			username: null,
 			location: null,
 			description: null,
 			birthday: null,
-			saving: false
+			avatarId: null,
+			bannerId: null,
+			isBot: false,
+			isCat: false,
+			saving: false,
+			uploading: false
 		};
 	},
+
 	created() {
 		this.name = (this as any).os.i.name || '';
+		this.username = (this as any).os.i.username;
 		this.location = (this as any).os.i.profile.location;
 		this.description = (this as any).os.i.description;
 		this.birthday = (this as any).os.i.profile.birthday;
+		this.avatarId = (this as any).os.i.avatarId;
+		this.bannerId = (this as any).os.i.bannerId;
+		this.isBot = (this as any).os.i.isBot;
+		this.isCat = (this as any).os.i.isCat;
 	},
+
 	methods: {
-		chooseAvatar() {
-			(this as any).apis.chooseDriveFile({
-				multiple: false
-			}).then(file => {
-				this.avatarSaving = true;
+		onAvatarChange([file]) {
+			this.uploading = true;
 
-				(this as any).api('i/update', {
-					avatarId: file.id
-				}).then(() => {
-					this.avatarSaving = false;
-					alert('%i18n:!@avatar-saved%');
-				});
+			const data = new FormData();
+			data.append('file', file);
+			data.append('i', (this as any).os.i.token);
+
+			fetch(apiUrl + '/drive/files/create', {
+				method: 'POST',
+				body: data
+			})
+			.then(response => response.json())
+			.then(f => {
+				this.avatarId = f.id;
+				this.uploading = false;
+			})
+			.catch(e => {
+				this.uploading = false;
+				alert('%18n:!@upload-failed%');
 			});
 		},
-		chooseBanner() {
-			(this as any).apis.chooseDriveFile({
-				multiple: false
-			}).then(file => {
-				this.bannerSaving = true;
 
-				(this as any).api('i/update', {
-					bannerId: file.id
-				}).then(() => {
-					this.bannerSaving = false;
-					alert('%i18n:!@banner-saved%');
-				});
+		onBannerChange([file]) {
+			this.uploading = true;
+
+			const data = new FormData();
+			data.append('file', file);
+			data.append('i', (this as any).os.i.token);
+
+			fetch(apiUrl + '/drive/files/create', {
+				method: 'POST',
+				body: data
+			})
+			.then(response => response.json())
+			.then(f => {
+				this.bannerId = f.id;
+				this.uploading = false;
+			})
+			.catch(e => {
+				this.uploading = false;
+				alert('%18n:!@upload-failed%');
 			});
 		},
-		uploadAvatar() {
-			// a
-		},
-		uploadBanner() {
-			// a
-		},
+
 		save() {
 			this.saving = true;
 
@@ -115,10 +145,19 @@ export default Vue.extend({
 				name: this.name || null,
 				location: this.location || null,
 				description: this.description || null,
-				birthday: this.birthday || null
-			}).then(() => {
+				birthday: this.birthday || null,
+				avatarId: this.avatarId,
+				bannerId: this.bannerId,
+				isBot: this.isBot,
+				isCat: this.isCat
+			}).then(i => {
 				this.saving = false;
-				alert('%i18n:!@saved%');
+				(this as any).os.i.avatarId = i.avatarId;
+				(this as any).os.i.avatarUrl = i.avatarUrl;
+				(this as any).os.i.bannerId = i.bannerId;
+				(this as any).os.i.bannerUrl = i.bannerUrl;
+
+				alert('%i18n:@saved%');
 			});
 		}
 	}
diff --git a/src/client/app/mobile/views/pages/user.vue b/src/client/app/mobile/views/pages/user.vue
index 34adeb03cd..84fd7eda02 100644
--- a/src/client/app/mobile/views/pages/user.vue
+++ b/src/client/app/mobile/views/pages/user.vue
@@ -1,7 +1,7 @@
 <template>
 <mk-ui>
 	<template slot="header" v-if="!fetching"><img :src="`${user.avatarUrl}?thumbnail&size=64`" alt="">{{ user | userName }}</template>
-	<main v-if="!fetching" :data-darkmode="_darkmode_">
+	<main v-if="!fetching" :data-darkmode="$store.state.device.darkmode">
 		<div class="is-suspended" v-if="user.isSuspended"><p>%fa:exclamation-triangle% %i18n:@is-suspended%</p></div>
 		<div class="is-remote" v-if="user.host != null"><p>%fa:exclamation-triangle% %i18n:@is-remote%<a :href="user.url || user.uri" target="_blank">%i18n:@view-remote%</a></p></div>
 		<header>
diff --git a/src/client/app/mobile/views/pages/widgets.vue b/src/client/app/mobile/views/pages/widgets.vue
index f0a0877862..03abcabe8f 100644
--- a/src/client/app/mobile/views/pages/widgets.vue
+++ b/src/client/app/mobile/views/pages/widgets.vue
@@ -65,7 +65,7 @@ export default Vue.extend({
 
 	computed: {
 		widgets(): any[] {
-			return this.$store.state.settings.data.mobileHome;
+			return this.$store.state.settings.mobileHome;
 		}
 	},
 
diff --git a/src/client/app/reset.styl b/src/client/app/reset.styl
index 10bd3113a2..c0a88f27b0 100644
--- a/src/client/app/reset.styl
+++ b/src/client/app/reset.styl
@@ -1,3 +1,6 @@
+input
+	min-width 0px
+
 input:not([type])
 input[type='text']
 input[type='password']
diff --git a/src/client/app/store.ts b/src/client/app/store.ts
index 1f1189054d..e300d31d8d 100644
--- a/src/client/app/store.ts
+++ b/src/client/app/store.ts
@@ -1,4 +1,6 @@
 import Vuex from 'vuex';
+import createPersistedState from 'vuex-persistedstate';
+
 import MiOS from './mios';
 
 const defaultSettings = {
@@ -11,17 +13,36 @@ const defaultSettings = {
 	gradientWindowHeader: false,
 	showReplyTarget: true,
 	showMyRenotes: true,
-	showRenotedMyNotes: true
+	showRenotedMyNotes: true,
+	loadRemoteMedia: true,
+	disableViaMobile: false
+};
+
+const defaultDeviceSettings = {
+	apiViaStream: true,
+	autoPopout: false,
+	darkmode: false,
+	enableSounds: true,
+	soundVolume: 0.5,
+	lang: null,
+	preventUpdate: false,
+	debug: false,
+	lightmode: false,
+	loadRawImages: false,
+	postStyle: 'standard'
 };
 
 export default (os: MiOS) => new Vuex.Store({
 	plugins: [store => {
 		store.subscribe((mutation, state) => {
 			if (mutation.type.startsWith('settings/')) {
-				localStorage.setItem('settings', JSON.stringify(state.settings.data));
+				localStorage.setItem('settings', JSON.stringify(state.settings));
 			}
 		});
-	}],
+	}, createPersistedState({
+		paths: ['device'],
+		filter: mut => mut.type.startsWith('device/')
+	})],
 
 	state: {
 		indicate: false,
@@ -39,50 +60,60 @@ export default (os: MiOS) => new Vuex.Store({
 	},
 
 	modules: {
-		settings: {
+		device: {
 			namespaced: true,
 
-			state: {
-				data: defaultSettings
-			},
+			state: defaultDeviceSettings,
 
 			mutations: {
 				set(state, x: { key: string; value: any }) {
-					state.data[x.key] = x.value;
+					state[x.key] = x.value;
+				}
+			}
+		},
+
+		settings: {
+			namespaced: true,
+
+			state: defaultSettings,
+
+			mutations: {
+				set(state, x: { key: string; value: any }) {
+					state[x.key] = x.value;
 				},
 
 				setHome(state, data) {
-					state.data.home = data;
+					state.home = data;
 				},
 
 				setHomeWidget(state, x) {
-					const w = state.data.home.find(w => w.id == x.id);
+					const w = state.home.find(w => w.id == x.id);
 					if (w) {
 						w.data = x.data;
 					}
 				},
 
 				addHomeWidget(state, widget) {
-					state.data.home.unshift(widget);
+					state.home.unshift(widget);
 				},
 
 				setMobileHome(state, data) {
-					state.data.mobileHome = data;
+					state.mobileHome = data;
 				},
 
 				setMobileHomeWidget(state, x) {
-					const w = state.data.mobileHome.find(w => w.id == x.id);
+					const w = state.mobileHome.find(w => w.id == x.id);
 					if (w) {
 						w.data = x.data;
 					}
 				},
 
 				addMobileHomeWidget(state, widget) {
-					state.data.mobileHome.unshift(widget);
+					state.mobileHome.unshift(widget);
 				},
 
 				removeMobileHomeWidget(state, widget) {
-					state.data.mobileHome = state.data.mobileHome.filter(w => w.id != widget.id);
+					state.mobileHome = state.mobileHome.filter(w => w.id != widget.id);
 				}
 			},
 
@@ -108,7 +139,7 @@ export default (os: MiOS) => new Vuex.Store({
 					ctx.commit('addHomeWidget', widget);
 
 					os.api('i/update_home', {
-						home: ctx.state.data.home
+						home: ctx.state.home
 					});
 				},
 
@@ -116,7 +147,7 @@ export default (os: MiOS) => new Vuex.Store({
 					ctx.commit('addMobileHomeWidget', widget);
 
 					os.api('i/update_mobile_home', {
-						home: ctx.state.data.mobileHome
+						home: ctx.state.mobileHome
 					});
 				},
 
@@ -124,7 +155,7 @@ export default (os: MiOS) => new Vuex.Store({
 					ctx.commit('removeMobileHomeWidget', widget);
 
 					os.api('i/update_mobile_home', {
-						home: ctx.state.data.mobileHome.filter(w => w.id != widget.id)
+						home: ctx.state.mobileHome.filter(w => w.id != widget.id)
 					});
 				}
 			}
diff --git a/src/client/assets/title.svg b/src/client/assets/title.svg
deleted file mode 100644
index 747fcd38b1..0000000000
--- a/src/client/assets/title.svg
+++ /dev/null
@@ -1,25 +0,0 @@
-<?xml version="1.0" encoding="utf-8"?>
-<!-- Generator: Adobe Illustrator 16.0.3, SVG Export Plug-In . SVG Version: 6.00 Build 0)  -->
-<!DOCTYPE svg PUBLIC "-//W3C//DTD SVG 1.1//EN" "http://www.w3.org/Graphics/SVG/1.1/DTD/svg11.dtd">
-<svg version="1.1" id="レイヤー_1" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" x="0px"
-	 y="0px" width="256px" height="256px" viewBox="0 0 256 256" enable-background="new 0 0 256 256" xml:space="preserve">
-<circle fill="#2B2F2D" cx="128" cy="153.6" r="19.201"/>
-<circle fill="#2B2F2D" cx="51.2" cy="153.6" r="19.2"/>
-<circle fill="#2B2F2D" cx="204.8" cy="153.6" r="19.2"/>
-<polyline fill="none" stroke="#2B2F2D" stroke-width="16" stroke-linejoin="round" stroke-miterlimit="10" points="51.2,153.6 
-	89.601,102.4 128,153.6 166.4,102.4 204.799,153.6 "/>
-<circle fill="#2B2F2D" cx="89.6" cy="102.4" r="19.2"/>
-<circle fill="#2B2F2D" cx="166.4" cy="102.4" r="19.199"/>
-<g>
-</g>
-<g>
-</g>
-<g>
-</g>
-<g>
-</g>
-<g>
-</g>
-<g>
-</g>
-</svg>
diff --git a/src/client/assets/version.html b/src/client/assets/version.html
index d8a98279a6..177d37db8f 100644
--- a/src/client/assets/version.html
+++ b/src/client/assets/version.html
@@ -10,11 +10,6 @@
 				localStorage.setItem('v', v);
 			}
 
-			const lang = window.prompt('Enter language (optional):');
-			if (lang && lang.length > 0) {
-				localStorage.setItem('lang', lang);
-			}
-
 			setTimeout(() => {
 				location.href = '/';
 			}, 500);
diff --git a/src/client/md.scss b/src/client/md.scss
index d850863efd..8368365885 100644
--- a/src/client/md.scss
+++ b/src/client/md.scss
@@ -6,7 +6,7 @@
 
 @include md-register-theme("default", (
 	primary: $themeColor,
-	accent: md-get-palette-color(red, A200)
+	accent: $themeColor
 ));
 
 @import "~vue-material/dist/components/MdButton/theme";
diff --git a/src/config/types.ts b/src/config/types.ts
index dff3f7d37c..910c03c2c1 100644
--- a/src/config/types.ts
+++ b/src/config/types.ts
@@ -41,6 +41,8 @@ export type Source = {
 		secret_key: string;
 	};
 
+	preventCacheRemoteFiles: boolean;
+
 	/**
 	 * ゴーストアカウントのID
 	 */
diff --git a/src/index.ts b/src/index.ts
index d633fcbbcb..bcd6561691 100644
--- a/src/index.ts
+++ b/src/index.ts
@@ -194,7 +194,12 @@ cluster.on('exit', worker => {
 // Display detail of unhandled promise rejection
 process.on('unhandledRejection', console.dir);
 
-// Dying away...
-process.on('exit', () => {
-	Logger.info('The process is going exit');
+// Display detail of uncaught exception
+process.on('uncaughtException', err => {
+	console.error(err);
+});
+
+// Dying away...
+process.on('exit', code => {
+	Logger.info(`The process is going exit (${code})`);
 });
diff --git a/src/models/drive-file.ts b/src/models/drive-file.ts
index 8a18567dc6..a3a567038e 100644
--- a/src/models/drive-file.ts
+++ b/src/models/drive-file.ts
@@ -32,7 +32,7 @@ export type IMetadata = {
 	uri?: string;
 	url?: string;
 	deletedAt?: Date;
-	isExpired?: boolean;
+	isMetaOnly?: boolean;
 };
 
 export type IDriveFile = {
@@ -155,7 +155,8 @@ export const pack = (
 	_target = Object.assign(_target, _file.metadata);
 
 	_target.src = _file.metadata.url;
-	_target.url = `${config.drive_url}/${_target.id}/${encodeURIComponent(_target.name)}`;
+	_target.url = _file.metadata.isMetaOnly ? _file.metadata.url : `${config.drive_url}/${_target.id}/${encodeURIComponent(_target.name)}`;
+	_target.isRemote = _file.metadata.isMetaOnly;
 
 	if (_target.properties == null) _target.properties = {};
 
diff --git a/src/models/note.ts b/src/models/note.ts
index 5070923363..1274901d45 100644
--- a/src/models/note.ts
+++ b/src/models/note.ts
@@ -324,6 +324,10 @@ export const pack = async (
 	// resolve promises in _note object
 	_note = await rap(_note);
 
+	if (_note.user.isCat && _note.text) {
+		_note.text = _note.text.replace(/な/g, 'にゃ').replace(/ナ/g, 'ニャ');
+	}
+
 	if (hide) {
 		_note.mediaIds = [];
 		_note.text = null;
diff --git a/src/models/user.ts b/src/models/user.ts
index 108111ceca..477bb232e4 100644
--- a/src/models/user.ts
+++ b/src/models/user.ts
@@ -77,6 +77,7 @@ export interface ILocalUser extends IUserBase {
 	};
 	lastUsedAt: Date;
 	isBot: boolean;
+	isCat: boolean;
 	isPro: boolean;
 	twoFactorSecret: string;
 	twoFactorEnabled: boolean;
diff --git a/src/server/api/common/read-notification.ts b/src/server/api/common/read-notification.ts
index 7b9faf4cf4..cdb87a4114 100644
--- a/src/server/api/common/read-notification.ts
+++ b/src/server/api/common/read-notification.ts
@@ -1,6 +1,7 @@
 import * as mongo from 'mongodb';
 import { default as Notification, INotification } from '../../../models/notification';
 import publishUserStream from '../../../publishers/stream';
+import Mute from '../../../models/mute';
 
 /**
  * Mark as read notification(s)
@@ -26,6 +27,11 @@ export default (
 				? [new mongo.ObjectID(message)]
 				: [(message as INotification)._id];
 
+	const mute = await Mute.find({
+		muterId: userId
+	});
+	const mutedUserIds = mute.map(m => m.muteeId);
+
 	// Update documents
 	await Notification.update({
 		_id: { $in: ids },
@@ -42,6 +48,9 @@ export default (
 	const count = await Notification
 		.count({
 			notifieeId: userId,
+			notifierId: {
+				$nin: mutedUserIds
+			},
 			isRead: false
 		}, {
 			limit: 1
diff --git a/src/server/api/endpoints.ts b/src/server/api/endpoints.ts
index 7647c76d3d..892da3540f 100644
--- a/src/server/api/endpoints.ts
+++ b/src/server/api/endpoints.ts
@@ -482,7 +482,7 @@ const endpoints: Endpoint[] = [
 		name: 'notes/replies'
 	},
 	{
-		name: 'notes/context'
+		name: 'notes/conversation'
 	},
 	{
 		name: 'notes/create',
diff --git a/src/server/api/endpoints/drive/files/create.ts b/src/server/api/endpoints/drive/files/create.ts
index e9348e4e2f..dd748d6bba 100644
--- a/src/server/api/endpoints/drive/files/create.ts
+++ b/src/server/api/endpoints/drive/files/create.ts
@@ -1,6 +1,7 @@
 /**
  * Module dependencies
  */
+import * as fs from 'fs';
 import $ from 'cafy'; import ID from '../../../../../cafy-id';
 import { validateFileName, pack } from '../../../../../models/drive-file';
 import create from '../../../../../services/drive/add-file';
@@ -32,15 +33,23 @@ module.exports = async (file, params, user): Promise<any> => {
 	const [folderId = null, folderIdErr] = $.type(ID).optional().nullable().get(params.folderId);
 	if (folderIdErr) throw 'invalid folderId param';
 
+	function cleanup() {
+		fs.unlink(file.path, () => {});
+	}
+
 	try {
 		// Create file
 		const driveFile = await create(user, file.path, name, null, folderId);
 
+		cleanup();
+
 		// Serialize
 		return pack(driveFile);
 	} catch (e) {
 		console.error(e);
 
+		cleanup();
+
 		throw e;
 	}
 };
diff --git a/src/server/api/endpoints/i/notifications.ts b/src/server/api/endpoints/i/notifications.ts
index 50ed9b27e8..ba9c47508c 100644
--- a/src/server/api/endpoints/i/notifications.ts
+++ b/src/server/api/endpoints/i/notifications.ts
@@ -96,8 +96,7 @@ module.exports = (params, user) => new Promise(async (res, rej) => {
 		});
 
 	// Serialize
-	res(await Promise.all(notifications.map(async notification =>
-		await pack(notification))));
+	res(await Promise.all(notifications.map(notification => pack(notification))));
 
 	// Mark as read all
 	if (notifications.length > 0 && markAsRead) {
diff --git a/src/server/api/endpoints/i/update.ts b/src/server/api/endpoints/i/update.ts
index b7b25d0f65..6e0c5b8515 100644
--- a/src/server/api/endpoints/i/update.ts
+++ b/src/server/api/endpoints/i/update.ts
@@ -47,6 +47,11 @@ module.exports = async (params, user, app) => new Promise(async (res, rej) => {
 	if (isBotErr) return rej('invalid isBot param');
 	if (isBot != null) user.isBot = isBot;
 
+	// Get 'isCat' parameter
+	const [isCat, isCatErr] = $.bool.optional().get(params.isCat);
+	if (isCatErr) return rej('invalid isCat param');
+	if (isCat != null) user.isCat = isCat;
+
 	// Get 'autoWatch' parameter
 	const [autoWatch, autoWatchErr] = $.bool.optional().get(params.autoWatch);
 	if (autoWatchErr) return rej('invalid autoWatch param');
@@ -82,6 +87,7 @@ module.exports = async (params, user, app) => new Promise(async (res, rej) => {
 			bannerColor: user.bannerColor,
 			profile: user.profile,
 			isBot: user.isBot,
+			isCat: user.isCat,
 			settings: user.settings
 		}
 	});
diff --git a/src/server/api/endpoints/notes.ts b/src/server/api/endpoints/notes.ts
index 4ce7613d70..21946d1bd3 100644
--- a/src/server/api/endpoints/notes.ts
+++ b/src/server/api/endpoints/notes.ts
@@ -8,6 +8,10 @@ import Note, { pack } from '../../../models/note';
  * Get all notes
  */
 module.exports = (params) => new Promise(async (res, rej) => {
+	// Get 'local' parameter
+	const [local, localErr] = $.bool.optional().get(params.local);
+	if (localErr) return rej('invalid local param');
+
 	// Get 'reply' parameter
 	const [reply, replyErr] = $.bool.optional().get(params.reply);
 	if (replyErr) return rej('invalid reply param');
@@ -61,6 +65,10 @@ module.exports = (params) => new Promise(async (res, rej) => {
 		};
 	}
 
+	if (local) {
+		query['_user.host'] = null;
+	}
+
 	if (reply != undefined) {
 		query.replyId = reply ? { $exists: true, $ne: null } : null;
 	}
diff --git a/src/server/api/endpoints/notes/context.ts b/src/server/api/endpoints/notes/conversation.ts
similarity index 80%
rename from src/server/api/endpoints/notes/context.ts
rename to src/server/api/endpoints/notes/conversation.ts
index 1cd27250e2..02f7229ccf 100644
--- a/src/server/api/endpoints/notes/context.ts
+++ b/src/server/api/endpoints/notes/conversation.ts
@@ -5,11 +5,7 @@ import $ from 'cafy'; import ID from '../../../../cafy-id';
 import Note, { pack } from '../../../../models/note';
 
 /**
- * Show a context of a note
- *
- * @param {any} params
- * @param {any} user
- * @return {Promise<any>}
+ * Show conversation of a note
  */
 module.exports = (params, user) => new Promise(async (res, rej) => {
 	// Get 'noteId' parameter
@@ -33,7 +29,7 @@ module.exports = (params, user) => new Promise(async (res, rej) => {
 		return rej('note not found');
 	}
 
-	const context = [];
+	const conversation = [];
 	let i = 0;
 
 	async function get(id) {
@@ -41,10 +37,10 @@ module.exports = (params, user) => new Promise(async (res, rej) => {
 		const p = await Note.findOne({ _id: id });
 
 		if (i > offset) {
-			context.push(p);
+			conversation.push(p);
 		}
 
-		if (context.length == limit) {
+		if (conversation.length == limit) {
 			return;
 		}
 
@@ -58,6 +54,5 @@ module.exports = (params, user) => new Promise(async (res, rej) => {
 	}
 
 	// Serialize
-	res(await Promise.all(context.map(async note =>
-		await pack(note, user))));
+	res(await Promise.all(conversation.map(note => pack(note, user))));
 });
diff --git a/src/server/api/endpoints/notifications/get_unread_count.ts b/src/server/api/endpoints/notifications/get_unread_count.ts
index 600a80d194..9766366ff1 100644
--- a/src/server/api/endpoints/notifications/get_unread_count.ts
+++ b/src/server/api/endpoints/notifications/get_unread_count.ts
@@ -9,8 +9,7 @@ import Mute from '../../../../models/mute';
  */
 module.exports = (params, user) => new Promise(async (res, rej) => {
 	const mute = await Mute.find({
-		muterId: user._id,
-		deletedAt: { $exists: false }
+		muterId: user._id
 	});
 	const mutedUserIds = mute.map(m => m.muteeId);
 
diff --git a/src/server/file/send-drive-file.ts b/src/server/file/send-drive-file.ts
index d613a3aa5f..e04400317f 100644
--- a/src/server/file/send-drive-file.ts
+++ b/src/server/file/send-drive-file.ts
@@ -33,11 +33,12 @@ export default async function(ctx: Koa.Context) {
 
 	if (file.metadata.deletedAt) {
 		ctx.status = 410;
-		if (file.metadata.isExpired) {
-			await send(ctx, '/cache-expired.png', { root: assets });
-		} else {
-			await send(ctx, '/tombstone.png', { root: assets });
-		}
+		await send(ctx, '/tombstone.png', { root: assets });
+		return;
+	}
+
+	if (file.metadata.isMetaOnly) {
+		ctx.status = 204;
 		return;
 	}
 
diff --git a/src/server/index.ts b/src/server/index.ts
index ded8f7706e..fc3d252e10 100644
--- a/src/server/index.ts
+++ b/src/server/index.ts
@@ -4,8 +4,7 @@
 
 import * as fs from 'fs';
 import * as http from 'http';
-import * as https from 'https';
-//import * as http2 from 'http2';
+import * as http2 from 'http2';
 import * as zlib from 'zlib';
 import * as Koa from 'koa';
 import * as Router from 'koa-router';
@@ -68,8 +67,7 @@ function createServer() {
 			certs[k] = fs.readFileSync(config.https[k]);
 		});
 		certs['allowHTTP1'] = true;
-		//return http2.createSecureServer(certs, app.callback());
-		return https.createServer(certs, app.callback());
+		return http2.createSecureServer(certs, app.callback());
 	} else {
 		return http.createServer(app.callback());
 	}
diff --git a/src/server/web/index.ts b/src/server/web/index.ts
index 6ceef17c1c..5ce040d083 100644
--- a/src/server/web/index.ts
+++ b/src/server/web/index.ts
@@ -49,8 +49,8 @@ const router = new Router();
 //#region static assets
 
 router.get('/assets/*', async ctx => {
-	// 無圧縮スクリプトを用意するのは大変なので一時的に無効化
-	const path = process.env.NODE_ENV == 'production' ? ctx.path.replace('raw.js', 'min.js') : ctx.path.replace('min.js', 'raw.js');
+	// 互換性のため
+	const path = ctx.path.replace('.raw.js', '.js').replace('.min.js', '.js');
 	await send(ctx, path, {
 		root: client,
 		maxage: ms('7 days'),
diff --git a/src/services/drive/add-file.ts b/src/services/drive/add-file.ts
index bcd5bee512..0e42a00bf6 100644
--- a/src/services/drive/add-file.ts
+++ b/src/services/drive/add-file.ts
@@ -1,6 +1,5 @@
 import { Buffer } from 'buffer';
 import * as fs from 'fs';
-import * as tmp from 'tmp';
 import * as stream from 'stream';
 
 import * as mongodb from 'mongodb';
@@ -14,8 +13,7 @@ import DriveFile, { IMetadata, getDriveFileBucket, IDriveFile, DriveFileChunk }
 import DriveFolder from '../../models/drive-folder';
 import { pack } from '../../models/drive-file';
 import event, { publishDriveStream } from '../../publishers/stream';
-import getAcct from '../../acct/render';
-import { IUser, isLocalUser, isRemoteUser } from '../../models/user';
+import { isLocalUser, IUser, IRemoteUser } from '../../models/user';
 import DriveFileThumbnail, { getDriveFileThumbnailBucket, DriveFileThumbnailChunk } from '../../models/drive-file-thumbnail';
 import genThumbnail from '../../drive/gen-thumbnail';
 
@@ -25,13 +23,6 @@ const gm = _gm.subClass({
 
 const log = debug('misskey:drive:add-file');
 
-const tmpFile = (): Promise<[string, any]> => new Promise((resolve, reject) => {
-	tmp.file((e, path, fd, cleanup) => {
-		if (e) return reject(e);
-		resolve([path, cleanup]);
-	});
-});
-
 const writeChunks = (name: string, readable: stream.Readable, type: string, metadata: any) =>
 	getDriveFileBucket()
 		.then(bucket => new Promise((resolve, reject) => {
@@ -55,64 +46,115 @@ const writeThumbnailChunks = (name: string, readable: stream.Readable, originalI
 			readable.pipe(writeStream);
 		}));
 
-const addFile = async (
+async function deleteOldFile(user: IRemoteUser) {
+	const oldFile = await DriveFile.findOne({
+		_id: {
+			$nin: [user.avatarId, user.bannerId]
+		}
+	}, {
+		sort: {
+			_id: 1
+		}
+	});
+
+	if (oldFile) {
+		// チャンクをすべて削除
+		DriveFileChunk.remove({
+			files_id: oldFile._id
+		});
+
+		DriveFile.update({ _id: oldFile._id }, {
+			$set: {
+				'metadata.deletedAt': new Date(),
+				'metadata.isExpired': true
+			}
+		});
+
+		//#region サムネイルもあれば削除
+		const thumbnail = await DriveFileThumbnail.findOne({
+			'metadata.originalId': oldFile._id
+		});
+
+		if (thumbnail) {
+			DriveFileThumbnailChunk.remove({
+				files_id: thumbnail._id
+			});
+
+			DriveFileThumbnail.remove({ _id: thumbnail._id });
+		}
+		//#endregion
+	}
+}
+
+/**
+ * Add file to drive
+ *
+ * @param user User who wish to add file
+ * @param path File path
+ * @param name Name
+ * @param comment Comment
+ * @param folderId Folder ID
+ * @param force If set to true, forcibly upload the file even if there is a file with the same hash.
+ * @return Created drive file
+ */
+export default async function(
 	user: IUser,
 	path: string,
 	name: string = null,
 	comment: string = null,
 	folderId: mongodb.ObjectID = null,
 	force: boolean = false,
+	metaOnly: boolean = false,
 	url: string = null,
 	uri: string = null
-): Promise<IDriveFile> => {
-	log(`registering ${name} (user: ${getAcct(user)}, path: ${path})`);
-
-	// Calculate hash, get content type and get file size
-	const [hash, [mime, ext], size] = await Promise.all([
-		// hash
-		((): Promise<string> => new Promise((res, rej) => {
-			const readable = fs.createReadStream(path);
-			const hash = crypto.createHash('md5');
-			const chunks = [];
-			readable
-				.on('error', rej)
-				.pipe(hash)
-				.on('error', rej)
-				.on('data', (chunk) => chunks.push(chunk))
-				.on('end', () => {
-					const buffer = Buffer.concat(chunks);
-					res(buffer.toString('hex'));
-				});
-		}))(),
-		// mime
-		((): Promise<[string, string | null]> => new Promise((res, rej) => {
-			const readable = fs.createReadStream(path);
-			readable
-				.on('error', rej)
-				.once('data', (buffer: Buffer) => {
-					readable.destroy();
-					const type = fileType(buffer);
-					if (type) {
-						return res([type.mime, type.ext]);
-					} else {
-						// 種類が同定できなかったら application/octet-stream にする
-						return res(['application/octet-stream', null]);
-					}
-				});
-		}))(),
-		// size
-		((): Promise<number> => new Promise((res, rej) => {
-			fs.stat(path, (err, stats) => {
-				if (err) return rej(err);
-				res(stats.size);
+): Promise<IDriveFile> {
+	// Calc md5 hash
+	const calcHash = new Promise<string>((res, rej) => {
+		const readable = fs.createReadStream(path);
+		const hash = crypto.createHash('md5');
+		const chunks = [];
+		readable
+			.on('error', rej)
+			.pipe(hash)
+			.on('error', rej)
+			.on('data', chunk => chunks.push(chunk))
+			.on('end', () => {
+				const buffer = Buffer.concat(chunks);
+				res(buffer.toString('hex'));
 			});
-		}))()
-	]);
+	});
+
+	// Detect content type
+	const detectMime = new Promise<[string, string]>((res, rej) => {
+		const readable = fs.createReadStream(path);
+		readable
+			.on('error', rej)
+			.once('data', (buffer: Buffer) => {
+				readable.destroy();
+				const type = fileType(buffer);
+				if (type) {
+					res([type.mime, type.ext]);
+				} else {
+					// 種類が同定できなかったら application/octet-stream にする
+					res(['application/octet-stream', null]);
+				}
+			});
+	});
+
+	// Get file size
+	const getFileSize = new Promise<number>((res, rej) => {
+		fs.stat(path, (err, stats) => {
+			if (err) return rej(err);
+			res(stats.size);
+		});
+	});
+
+	const [hash, [mime, ext], size] = await Promise.all([calcHash, detectMime, getFileSize]);
 
 	log(`hash: ${hash}, mime: ${mime}, ext: ${ext}, size: ${size}`);
 
 	// detect name
-	const detectedName: string = name || (ext ? `untitled.${ext}` : 'untitled');
+	const detectedName = name || (ext ? `untitled.${ext}` : 'untitled');
 
 	if (!force) {
 		// Check if there is a file with the same hash
@@ -125,26 +167,72 @@ const addFile = async (
 		if (much !== null) {
 			log('file with same hash is found');
 			return much;
-		} else {
-			log('file with same hash is not found');
 		}
 	}
 
-	const [wh, averageColor, folder] = await Promise.all([
-		// Width and height (when image)
-		(async () => {
-			// 画像かどうか
-			if (!/^image\/.*$/.test(mime)) {
-				return null;
+	//#region Check drive usage
+	if (!metaOnly) {
+		const usage = await DriveFile
+			.aggregate([{
+				$match: {
+					'metadata.userId': user._id,
+					'metadata.deletedAt': { $exists: false }
+				}
+			}, {
+				$project: {
+					length: true
+				}
+			}, {
+				$group: {
+					_id: null,
+					usage: { $sum: '$length' }
+				}
+			}])
+			.then((aggregates: any[]) => {
+				if (aggregates.length > 0) {
+					return aggregates[0].usage;
+				}
+				return 0;
+			});
+
+		log(`drive usage is ${usage}`);
+
+		// If usage limit exceeded
+		if (usage + size > user.driveCapacity) {
+			if (isLocalUser(user)) {
+				throw 'no-free-space';
+			} else {
+				// (アバターまたはバナーを含まず)最も古いファイルを削除する
+				deleteOldFile(user);
 			}
+		}
+	}
+	//#endregion
 
-			const imageType = mime.split('/')[1];
+	const fetchFolder = async () => {
+		if (!folderId) {
+			return null;
+		}
 
-			// 画像でもPNGかJPEGかGIFでないならスキップ
-			if (imageType != 'png' && imageType != 'jpeg' && imageType != 'gif') {
-				return null;
-			}
+		const driveFolder = await DriveFolder.findOne({
+			_id: folderId,
+			userId: user._id
+		});
 
+		if (driveFolder == null) throw 'folder-not-found';
+
+		return driveFolder;
+	};
+
+	const properties = {};
+
+	let propPromises = [];
+
+	const isImage = ['image/jpeg', 'image/gif', 'image/png'].includes(mime);
+
+	if (isImage) {
+		// Calc width and height
+		const calcWh = async () => {
 			log('calculate image width and height...');
 
 			// Calculate width and height
@@ -153,22 +241,12 @@ const addFile = async (
 
 			log(`image width and height is calculated: ${size.width}, ${size.height}`);
 
-			return [size.width, size.height];
-		})(),
-		// average color (when image)
-		(async () => {
-			// 画像かどうか
-			if (!/^image\/.*$/.test(mime)) {
-				return null;
-			}
-
-			const imageType = mime.split('/')[1];
-
-			// 画像でもPNGかJPEGでないならスキップ
-			if (imageType != 'png' && imageType != 'jpeg') {
-				return null;
-			}
+			properties['width'] = size.width;
+			properties['height'] = size.height;
+		};
 
+		// Calc average color
+		const calcAvg = async () => {
 			log('calculate average color...');
 
 			const info = await prominence(gm(fs.createReadStream(path), name)).identify();
@@ -185,111 +263,15 @@ const addFile = async (
 
 			log(`average color is calculated: ${r}, ${g}, ${b}`);
 
-			return isTransparent ? [r, g, b, 255] : [r, g, b];
-		})(),
-		// folder
-		(async () => {
-			if (!folderId) {
-				return null;
-			}
-			const driveFolder = await DriveFolder.findOne({
-				_id: folderId,
-				userId: user._id
-			});
-			if (!driveFolder) {
-				throw 'folder-not-found';
-			}
-			return driveFolder;
-		})(),
-		// usage checker
-		(async () => {
-			// Calculate drive usage
-			const usage = await DriveFile
-				.aggregate([{
-					$match: {
-						'metadata.userId': user._id,
-						'metadata.deletedAt': { $exists: false }
-					}
-				}, {
-					$project: {
-						length: true
-					}
-				}, {
-					$group: {
-						_id: null,
-						usage: { $sum: '$length' }
-					}
-				}])
-				.then((aggregates: any[]) => {
-					if (aggregates.length > 0) {
-						return aggregates[0].usage;
-					}
-					return 0;
-				});
+			const value = isTransparent ? [r, g, b, 255] : [r, g, b];
 
-			log(`drive usage is ${usage}`);
+			properties['avgColor'] = value;
+		};
 
-			// If usage limit exceeded
-			if (usage + size > user.driveCapacity) {
-				if (isLocalUser(user)) {
-					throw 'no-free-space';
-				} else {
-					//#region (アバターまたはバナーを含まず)最も古いファイルを削除する
-					const oldFile = await DriveFile.findOne({
-						_id: {
-							$nin: [user.avatarId, user.bannerId]
-						}
-					}, {
-						sort: {
-							_id: 1
-						}
-					});
-
-					if (oldFile) {
-						// チャンクをすべて削除
-						DriveFileChunk.remove({
-							files_id: oldFile._id
-						});
-
-						DriveFile.update({ _id: oldFile._id }, {
-							$set: {
-								'metadata.deletedAt': new Date(),
-								'metadata.isExpired': true
-							}
-						});
-
-						//#region サムネイルもあれば削除
-						const thumbnail = await DriveFileThumbnail.findOne({
-							'metadata.originalId': oldFile._id
-						});
-
-						if (thumbnail) {
-							DriveFileThumbnailChunk.remove({
-								files_id: thumbnail._id
-							});
-
-							DriveFileThumbnail.remove({ _id: thumbnail._id });
-						}
-						//#endregion
-					}
-					//#endregion
-				}
-			}
-		})()
-	]);
-
-	const readable = fs.createReadStream(path);
-
-	const properties = {};
-
-	if (wh) {
-		properties['width'] = wh[0];
-		properties['height'] = wh[1];
+		propPromises = [calcWh(), calcAvg()];
 	}
 
-	if (averageColor) {
-		properties['avgColor'] = averageColor;
-	}
+	const [folder] = await Promise.all([fetchFolder(), propPromises]);
 
 	const metadata = {
 		userId: user._id,
@@ -298,7 +280,8 @@ const addFile = async (
 		},
 		folderId: folder !== null ? folder._id : null,
 		comment: comment,
-		properties: properties
+		properties: properties,
+		isMetaOnly: metaOnly
 	} as IMetadata;
 
 	if (url !== null) {
@@ -309,74 +292,35 @@ const addFile = async (
 		metadata.uri = uri;
 	}
 
-	const file = await (writeChunks(detectedName, readable, mime, metadata) as Promise<IDriveFile>);
+	const driveFile = metaOnly
+		? await DriveFile.insert({
+			length: 0,
+			uploadDate: new Date(),
+			md5: hash,
+			filename: detectedName,
+			metadata: metadata,
+			contentType: mime
+		})
+		: await (writeChunks(detectedName, fs.createReadStream(path), mime, metadata) as Promise<IDriveFile>);
 
-	try {
-		const thumb = await genThumbnail(file);
-		if (thumb) {
-			await writeThumbnailChunks(detectedName, thumb, file._id);
+	log(`drive file has been created ${driveFile._id}`);
+
+	pack(driveFile).then(packedFile => {
+		// Publish drive_file_created event
+		event(user._id, 'drive_file_created', packedFile);
+		publishDriveStream(user._id, 'file_created', packedFile);
+	});
+
+	if (!metaOnly) {
+		try {
+			const thumb = await genThumbnail(driveFile);
+			if (thumb) {
+				await writeThumbnailChunks(detectedName, thumb, driveFile._id);
+			}
+		} catch (e) {
+			// noop
 		}
-	} catch (e) {
-		// noop
 	}
 
-	return file;
-};
-
-/**
- * Add file to drive
- *
- * @param user User who wish to add file
- * @param file File path or readableStream
- * @param comment Comment
- * @param type File type
- * @param folderId Folder ID
- * @param force If set to true, forcibly upload the file even if there is a file with the same hash.
- * @return Object that represents added file
- */
-export default (user: any, file: string | stream.Readable, ...args) => new Promise<any>((resolve, reject) => {
-	const isStream = typeof file === 'object' && typeof file.read === 'function';
-
-	// Get file path
-	new Promise<[string, any]>((res, rej) => {
-		if (typeof file === 'string') {
-			res([file, null]);
-		} else if (isStream) {
-			tmpFile()
-				.then(([path, cleanup]) => {
-					const readable: stream.Readable = file;
-					const writable = fs.createWriteStream(path);
-					readable
-						.on('error', rej)
-						.on('end', () => {
-							res([path, cleanup]);
-						})
-						.pipe(writable)
-						.on('error', rej);
-				})
-				.catch(rej);
-		} else {
-			rej(new Error('un-compatible file.'));
-		}
-	})
-	.then(([path, cleanup]) => new Promise<IDriveFile>((res, rej) => {
-		addFile(user, path, ...args)
-			.then(file => {
-				res(file);
-				if (cleanup) cleanup();
-			})
-			.catch(rej);
-	}))
-	.then(file => {
-		log(`drive file has been created ${file._id}`);
-
-		resolve(file);
-
-		pack(file).then(packedFile => {
-			// Publish drive_file_created event
-			event(user._id, 'drive_file_created', packedFile);
-			publishDriveStream(user._id, 'file_created', packedFile);
-		});
-	})
-	.catch(reject);
-});
+	return driveFile;
+}
diff --git a/src/services/drive/upload-from-url.ts b/src/services/drive/upload-from-url.ts
index ad2620c036..e216ca603d 100644
--- a/src/services/drive/upload-from-url.ts
+++ b/src/services/drive/upload-from-url.ts
@@ -1,14 +1,17 @@
+import * as fs from 'fs';
 import * as URL from 'url';
-import { IDriveFile, validateFileName } from '../../models/drive-file';
-import create from './add-file';
+
 import * as debug from 'debug';
 import * as tmp from 'tmp';
-import * as fs from 'fs';
 import * as request from 'request';
 
+import { IDriveFile, validateFileName } from '../../models/drive-file';
+import create from './add-file';
+import config from '../../config';
+
 const log = debug('misskey:drive:upload-from-url');
 
-export default async (url, user, folderId = null, uri = null): Promise<IDriveFile> => {
+export default async (url: string, user, folderId = null, uri: string = null): Promise<IDriveFile> => {
 	log(`REQUESTED: ${url}`);
 
 	let name = URL.parse(url).pathname.split('/').pop();
@@ -43,7 +46,7 @@ export default async (url, user, folderId = null, uri = null): Promise<IDriveFil
 	let error;
 
 	try {
-		driveFile = await create(user, path, name, null, folderId, false, url, uri);
+		driveFile = await create(user, path, name, null, folderId, false, config.preventCacheRemoteFiles, url, uri);
 		log(`created: ${driveFile._id}`);
 	} catch (e) {
 		error = e;
diff --git a/src/services/note/create.ts b/src/services/note/create.ts
index f049c34b65..b9ff1f679b 100644
--- a/src/services/note/create.ts
+++ b/src/services/note/create.ts
@@ -283,6 +283,8 @@ export default async (user: IUser, data: {
 		mentionedUsers = mentionedUsers.filter(x => x != null);
 
 		mentionedUsers.filter(u => isLocalUser(u)).forEach(async u => {
+			event(u, 'mention', noteObj);
+
 			// 既に言及されたユーザーに対する返信や引用renoteの場合も無視
 			if (data.reply && data.reply.userId.equals(u._id)) return;
 			if (data.renote && data.renote.userId.equals(u._id)) return;
diff --git a/webpack.config.ts b/webpack.config.ts
index 3aeecbd8a7..67fb929449 100644
--- a/webpack.config.ts
+++ b/webpack.config.ts
@@ -60,7 +60,7 @@ const entry = {
 
 const output = {
 	path: __dirname + '/built/client/assets',
-	filename: `[name].${version}.-.${isProduction ? 'min' : 'raw'}.js`
+	filename: `[name].${version}.-.js`
 };
 
 //#region Define consts
@@ -78,6 +78,7 @@ const consts = {
 	_WS_URL_: config.ws_url,
 	_DEV_URL_: config.dev_url,
 	_LANG_: '%lang%',
+	_LANGS_: Object.keys(locales).map(l => [l, locales[l].meta.lang]),
 	_HOST_: config.host,
 	_HOSTNAME_: config.hostname,
 	_URL_: config.url,
@@ -110,14 +111,14 @@ const plugins = [
 		//#region i18n
 		langs.forEach(lang => {
 			Object.keys(entry).forEach(file => {
-				let src = fs.readFileSync(`${__dirname}/built/client/assets/${file}.${version}.-.${isProduction ? 'min' : 'raw'}.js`, 'utf-8');
+				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);
 
-				fs.writeFileSync(`${__dirname}/built/client/assets/${file}.${version}.${lang}.${isProduction ? 'min' : 'raw'}.js`, src, 'utf-8');
+				fs.writeFileSync(`${__dirname}/built/client/assets/${file}.${version}.${lang}.js`, src, 'utf-8');
 			});
 		});
 		//#endregion
@@ -232,15 +233,14 @@ module.exports = {
 			}, {
 				loader: 'replace',
 				query: {
-					search: i18nPattern.toString(),
-					replace: 'i18nReplacement',
-					i18n: true
-				}
-			}, {
-				loader: 'replace',
-				query: {
-					search: faPattern.toString(),
-					replace: 'faReplacement'
+					qs: [{
+						search: i18nPattern.toString(),
+						replace: 'i18nReplacement',
+						i18n: true
+					}, {
+						search: faPattern.toString(),
+						replace: 'faReplacement'
+					}]
 				}
 			}]
 		}]
diff --git a/webpack/i18n.ts b/webpack/i18n.ts
index de4d02e9d9..e2cce060e8 100644
--- a/webpack/i18n.ts
+++ b/webpack/i18n.ts
@@ -2,17 +2,12 @@
  * Replace i18n texts
  */
 
-export const pattern = /%i18n:([a-z0-9_\-@\.\!]+?)%/g;
+export const pattern = /%i18n:([a-z0-9_\-@\.]+?)%/g;
 
 export const replacement = (ctx, match, key) => {
 	const client = '/src/client/app/';
 	let name = null;
 
-	const shouldEscape = key[0] == '!';
-	if (shouldEscape) {
-		key = key.substr(1);
-	}
-
 	if (key[0] == '@') {
 		name = ctx.src.substr(ctx.src.indexOf(client) + client.length);
 		key = key.substr(1);
@@ -20,5 +15,5 @@ export const replacement = (ctx, match, key) => {
 
 	const path = name ? `${name}|${key}` : key;
 
-	return shouldEscape ? `%i18n:!${path}%` : `%i18n:${path}%`;
+	return `%i18n:${path}%`;
 };