diff --git a/locales/ja.yml b/locales/ja.yml index 6f8b1eaf20..953556eedb 100644 --- a/locales/ja.yml +++ b/locales/ja.yml @@ -495,9 +495,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: "ツール" diff --git a/package.json b/package.json index 119d2dac53..14f7956f52 100644 --- a/package.json +++ b/package.json @@ -211,6 +211,7 @@ "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/client/app/boot.js b/src/client/app/boot.js index 9338bc501e..e09a5d12e9 100644 --- a/src/client/app/boot.js +++ b/src/client/app/boot.js @@ -29,11 +29,21 @@ 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'); + + const vuex = localStorage.getItem('vuex'); + if (vuex) { + const data = JSON.parse(vuex); + if (data.device.lang) lang = data.device.lang; + } + + const storedLang = localStorage.getItem('lang'); + if (storedLang) lang = storedLang; + //#endregion // Detect the user agent const ua = navigator.userAgent.toLowerCase(); @@ -68,13 +78,6 @@ // 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 +87,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/views/components/avatar.vue b/src/client/app/common/views/components/avatar.vue index 8cc72fb518..6b407ccc80 100644 --- a/src/client/app/common/views/components/avatar.vue +++ b/src/client/app/common/views/components/avatar.vue @@ -22,7 +22,7 @@ export default Vue.extend({ }, computed: { lightmode(): boolean { - return localStorage.getItem('lightmode') == 'true'; + return this.$store.state.device.lightmode; }, style(): any { return { diff --git a/src/client/app/common/views/components/messaging-room.vue b/src/client/app/common/views/components/messaging-room.vue index 330834233a..7832a331df 100644 --- a/src/client/app/common/views/components/messaging-room.vue +++ b/src/client/app/common/views/components/messaging-room.vue @@ -149,9 +149,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(); } 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/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/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/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/settings.vue b/src/client/app/desktop/views/components/settings.vue index 24af64a59c..3fe09b9acc 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,60 @@ 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' + darkmode: localStorage.getItem('darkmode') == 'true' }; }, computed: { licenseUrl(): string { return `${docsUrl}/${lang}/license`; + }, + + apiViaStream: { + get() { return this.$store.state.device.apiViaStream; }, + set(value) { this.$store.commit('device/set', { key: 'apiViaStream', value }); } + }, + + autoPopout: { + get() { return this.$store.state.device.autoPopout; }, + set(value) { this.$store.commit('device/set', { key: 'autoPopout', value }); } + }, + + enableSounds: { + get() { return this.$store.state.device.enableSounds; }, + set(value) { this.$store.commit('device/set', { key: 'enableSounds', value }); } + }, + + soundVolume: { + get() { return this.$store.state.device.soundVolume; }, + set(value) { this.$store.commit('device/set', { key: 'soundVolume', value }); } + }, + + lang: { + get() { return this.$store.state.device.lang; }, + set(value) { this.$store.commit('device/set', { key: 'lang', value }); } + }, + + preventUpdate: { + get() { return this.$store.state.device.preventUpdate; }, + set(value) { this.$store.commit('device/set', { key: 'preventUpdate', value }); } + }, + + debug: { + get() { return this.$store.state.device.debug; }, + set(value) { this.$store.commit('device/set', { key: 'debug', value }); } + }, + + enableExperimentalFeatures: { + get() { return this.$store.state.device.enableExperimentalFeatures; }, + set(value) { this.$store.commit('device/set', { key: 'enableExperimentalFeatures', value }); } } }, watch: { - autoPopout() { - localStorage.setItem('autoPopout', this.autoPopout ? 'true' : 'false'); - }, - apiViaStream() { - localStorage.setItem('apiViaStream', this.apiViaStream ? 'true' : 'false'); - }, darkmode() { (this as any)._updateDarkmode_(this.darkmode); - }, - enableSounds() { - localStorage.setItem('enableSounds', this.enableSounds ? 'true' : 'false'); - }, - soundVolume() { - localStorage.setItem('soundVolume', this.soundVolume.toString()); - }, - lang() { - localStorage.setItem('lang', this.lang); - }, - preventUpdate() { - localStorage.setItem('preventUpdate', this.preventUpdate ? 'true' : 'false'); - }, - debug() { - localStorage.setItem('debug', this.debug ? 'true' : 'false'); - }, - useRawScript() { - localStorage.setItem('useRawScript', this.useRawScript ? 'true' : 'false'); - }, - enableExperimental() { - localStorage.setItem('enableExperimental', this.enableExperimental ? 'true' : 'false'); } }, created() { @@ -391,7 +386,7 @@ export default Vue.extend({ }, 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/window.vue b/src/client/app/desktop/views/components/window.vue index ac84c2bd62..7ee1da12ba 100644 --- a/src/client/app/desktop/views/components/window.vue +++ b/src/client/app/desktop/views/components/window.vue @@ -95,7 +95,7 @@ export default Vue.extend({ }, created() { - if (localStorage.getItem('autoPopout') == 'true' && this.popoutUrl) { + if (this.$store.state.device.autoPopout && this.popoutUrl) { this.popout(); this.preventMount = true; } else { diff --git a/src/client/app/init.ts b/src/client/app/init.ts index fd04f9bcc3..34bc6a2785 100644 --- a/src/client/app/init.ts +++ b/src/client/app/init.ts @@ -147,7 +147,7 @@ export default (callback: (launch: (router: VueRouter, api?: (os: MiOS) => API) os, api: os.api, apis: os.apis, - clientSettings: os.store.state.settings.data + clientSettings: os.store.state.settings }; } }); @@ -173,7 +173,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/views/components/media-image.vue b/src/client/app/mobile/views/components/media-image.vue index d4ee0c66a9..c4622b01a7 100644 --- a/src/client/app/mobile/views/components/media-image.vue +++ b/src/client/app/mobile/views/components/media-image.vue @@ -17,7 +17,7 @@ export default Vue.extend({ }, computed: { lightmode(): boolean { - return localStorage.getItem('lightmode') == 'true'; + return this.$store.state.device.lightmode; }, style(): any { return { diff --git a/src/client/app/mobile/views/pages/settings.vue b/src/client/app/mobile/views/pages/settings.vue index 55c7895a90..3b0df87549 100644 --- a/src/client/app/mobile/views/pages/settings.vue +++ b/src/client/app/mobile/views/pages/settings.vue @@ -71,11 +71,7 @@ </md-optgroup> <md-optgroup label="%i18n:@specify-language%"> - <md-option value="ja">日本語</md-option> - <md-option value="en">English</md-option> - <md-option value="fr">Français</md-option> - <md-option value="pl">Polski</md-option> - <md-option value="de">Deutsch</md-option> + <md-option v-for="x in langs" :value="x[0]" :key="x[0]">{{ x[1] }}</md-option> </md-optgroup> </md-select> </md-field> @@ -122,7 +118,7 @@ <script lang="ts"> import Vue from 'vue'; -import { apiUrl, 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'; @@ -137,9 +133,8 @@ export default Vue.extend({ apiUrl, version, codename, + langs, darkmode: localStorage.getItem('darkmode') == 'true', - lightmode: localStorage.getItem('lightmode') == 'true', - lang: localStorage.getItem('lang') || '', latestVersion: undefined, checkingForUpdate: false }; @@ -148,20 +143,22 @@ export default Vue.extend({ computed: { name(): string { return Vue.filter('userName')((this as any).os.i); - } + }, + + lightmode: { + get() { return this.$store.state.device.lightmode; }, + set(value) { this.$store.commit('device/set', { key: 'lightmode', value }); } + }, + + lang: { + get() { return this.$store.state.device.lang; }, + set(value) { this.$store.commit('device/set', { key: 'lang', value }); } + }, }, watch: { darkmode() { (this as any)._updateDarkmode_(this.darkmode); - }, - - lightmode() { - localStorage.setItem('lightmode', this.lightmode); - }, - - lang() { - localStorage.setItem('lang', this.lang); } }, 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/store.ts b/src/client/app/store.ts index 1f1189054d..dceb51f2f3 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 = { @@ -14,14 +16,28 @@ const defaultSettings = { showRenotedMyNotes: true }; +const defaultDeviceSettings = { + apiViaStream: true, + autoPopout: false, + enableSounds: true, + soundVolume: 0.5, + lang: null, + preventUpdate: false, + debug: false, + lightmode: false, +}; + 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 +55,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 +134,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 +142,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 +150,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/webpack.config.ts b/webpack.config.ts index 3aeecbd8a7..cd3a144b26 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