diff options
| author | anatawa12 <anatawa12@icloud.com> | 2025-08-08 11:26:18 +0900 |
|---|---|---|
| committer | GitHub <noreply@github.com> | 2025-08-08 11:26:18 +0900 |
| commit | 8598f3912ecc16f9b7c3f502e09d9ea96f7e507d (patch) | |
| tree | 73b24c7ab473189a679ae6aa476b6822f812cbed /packages/frontend | |
| parent | Update CONTRIBUTING.md (diff) | |
| download | misskey-8598f3912ecc16f9b7c3f502e09d9ea96f7e507d.tar.gz misskey-8598f3912ecc16f9b7c3f502e09d9ea96f7e507d.tar.bz2 misskey-8598f3912ecc16f9b7c3f502e09d9ea96f7e507d.zip | |
per-locale bundle & inline locale (#16369)
* feat: split entry file by locale name
* chore: とりあえず transform hook で雑に分割
* chore: とりあえず transform 結果をいい感じに
* chore: concurrent buildで高速化
* chore: vite ではローケルのないものをビルドして後処理でどうにかするように
* chore: 後処理のためにi18n.jを単体になるように切り出す
* chore: use typescript
* chore: remove unref(i18n) in vite build process
* chore: inline variable
* fix: build error
* fix: i18n.ts.something.replaceAll() become error
* chore: ignore export specifier from error
* chore: support i18n.tsx as object
* chore: process literal for all files
* chore: split config and locale
* chore: inline locale name
* chore: remove updating locale in boot common
* chore: use top-level await to load locales
* chore: inline locale
* chore: remove loading locale from boot.js
* chore: remove loading locale from boot.js
* コメント追加
* fix test; fetchに失敗する
* import削除ログをdebugレベルに
* fix: watch pug
* chore: use hash for entry files
* chore: remove es-module-lexer from dependencies
* chore: move to frontend-builder
* chore: use inline locale in embed
* chore: refetch json on hot reload
* feat: store localization related to boot.js in backend in bootloaderLocales localstorage
* 応急処置を戻す
* fix spex
* fix `Using i18n identifier "e" directly. Skipping inlining.` warning
* refactor: use scriptsDir parameter
* chore: remove i18n from depmap
* chore: make build crash if errors
* error -> warn few conditions
* use inline object
* update localstorage keys
* remove accessing locale localstorage
* fix: failed to process i18n.tsx.aaa({x:i18n.bbb})
Diffstat (limited to 'packages/frontend')
| -rw-r--r-- | packages/frontend/.gitignore | 1 | ||||
| -rw-r--r-- | packages/frontend/.storybook/preview.ts | 2 | ||||
| -rw-r--r-- | packages/frontend/build.ts | 51 | ||||
| -rw-r--r-- | packages/frontend/package.json | 4 | ||||
| -rw-r--r-- | packages/frontend/src/boot/common.ts | 26 | ||||
| -rw-r--r-- | packages/frontend/src/components/MkExtensionInstaller.vue | 6 | ||||
| -rw-r--r-- | packages/frontend/src/i18n.ts | 3 | ||||
| -rw-r--r-- | packages/frontend/src/local-storage.ts | 3 | ||||
| -rw-r--r-- | packages/frontend/src/pages/settings/preferences.vue | 2 | ||||
| -rw-r--r-- | packages/frontend/src/utility/clear-cache.ts | 2 | ||||
| -rw-r--r-- | packages/frontend/tsconfig.json | 1 | ||||
| -rw-r--r-- | packages/frontend/vite.config.ts | 13 |
12 files changed, 77 insertions, 37 deletions
diff --git a/packages/frontend/.gitignore b/packages/frontend/.gitignore index 1aa0ac14e8..4f8684c18e 100644 --- a/packages/frontend/.gitignore +++ b/packages/frontend/.gitignore @@ -1 +1,2 @@ /storybook-static +/build/ diff --git a/packages/frontend/.storybook/preview.ts b/packages/frontend/.storybook/preview.ts index fb855c1410..2aac8af400 100644 --- a/packages/frontend/.storybook/preview.ts +++ b/packages/frontend/.storybook/preview.ts @@ -9,7 +9,6 @@ import { type Preview, setup } from '@storybook/vue3'; import isChromatic from 'chromatic/isChromatic'; import { initialize, mswLoader } from 'msw-storybook-addon'; import { userDetailed } from './fakes.js'; -import locale from './locale.js'; import { commonHandlers, onUnhandledRequest } from './mocks.js'; import themes from './themes.js'; import '../src/style.scss'; @@ -55,7 +54,6 @@ function initLocalStorage() { ...userDetailed(), policies: {}, })); - localStorage.setItem('locale', JSON.stringify(locale)); } initialize({ diff --git a/packages/frontend/build.ts b/packages/frontend/build.ts new file mode 100644 index 0000000000..0401c2b9ba --- /dev/null +++ b/packages/frontend/build.ts @@ -0,0 +1,51 @@ +import * as fs from 'fs/promises'; +import url from 'node:url'; +import path from 'node:path'; +import { execa } from 'execa'; +import locales from '../../locales/index.js'; +import { LocaleInliner } from '../frontend-builder/locale-inliner.js' +import { createLogger } from '../frontend-builder/logger'; + +// requires node 21 or later +const __dirname = path.dirname(url.fileURLToPath(import.meta.url)); +const outputDir = __dirname + '/../../built/_frontend_vite_'; + +/** + * @return {Promise<void>} + */ +async function viteBuild() { + await execa('vite', ['build'], { + cwd: __dirname, + stdout: process.stdout, + stderr: process.stderr, + }); +} + + +async function buildAllLocale() { + const logger = createLogger() + const inliner = await LocaleInliner.create({ + outputDir, + logger, + scriptsDir: 'scripts', + i18nFile: 'src/i18n.ts', + }) + + await inliner.loadFiles(); + + inliner.collectsModifications(); + + await inliner.saveAllLocales(locales); + + if (logger.errorCount > 0) { + throw new Error(`Build failed with ${logger.errorCount} errors and ${logger.warningCount} warnings.`); + } +} + +async function build() { + await fs.rm(outputDir, { recursive: true, force: true }); + await viteBuild(); + await buildAllLocale(); +} + +await build(); diff --git a/packages/frontend/package.json b/packages/frontend/package.json index f4f72d944a..eaefe92612 100644 --- a/packages/frontend/package.json +++ b/packages/frontend/package.json @@ -4,7 +4,7 @@ "type": "module", "scripts": { "watch": "vite", - "build": "vite build", + "build": "tsx build.ts", "storybook-dev": "nodemon --verbose --watch src --ext \"mdx,ts,vue\" --ignore \"*.stories.ts\" --exec \"pnpm build-storybook-pre && pnpm exec storybook dev -p 6006 --ci\"", "build-storybook-pre": "(tsc -p .storybook || echo done.) && node .storybook/generate.js && node .storybook/preload-locale.js && node .storybook/preload-theme.js", "build-storybook": "pnpm build-storybook-pre && storybook build --webpack-stats-json storybook-static", @@ -47,6 +47,7 @@ "date-fns": "4.1.0", "estree-walker": "3.0.3", "eventemitter3": "5.0.1", + "execa": "9.6.0", "frontend-shared": "workspace:*", "icons-subsetter": "workspace:*", "idb-keyval": "6.2.2", @@ -137,6 +138,7 @@ "start-server-and-test": "2.0.12", "storybook": "9.1.0", "storybook-addon-misskey-theme": "github:misskey-dev/storybook-addon-misskey-theme", + "tsx": "4.20.3", "vite-plugin-turbosnap": "1.0.3", "vitest": "3.2.4", "vitest-fetch-mock": "0.4.5", diff --git a/packages/frontend/src/boot/common.ts b/packages/frontend/src/boot/common.ts index ea41155ab0..395d1e5e7e 100644 --- a/packages/frontend/src/boot/common.ts +++ b/packages/frontend/src/boot/common.ts @@ -5,9 +5,10 @@ import { computed, watch, version as vueVersion } from 'vue'; import { compareVersions } from 'compare-versions'; -import { version, lang, updateLocale, locale, apiUrl, isSafeMode } from '@@/js/config.js'; +import { version, lang, apiUrl, isSafeMode } from '@@/js/config.js'; import defaultLightTheme from '@@/themes/l-light.json5'; import defaultDarkTheme from '@@/themes/d-green-lime.json5'; +import { storeBootloaderErrors } from '@@/js/store-boot-errors'; import type { App } from 'vue'; import widgets from '@/widgets/index.js'; import directives from '@/directives/index.js'; @@ -79,25 +80,7 @@ export async function common(createVue: () => Promise<App<Element>>) { //#endregion //#region Detect language & fetch translations - const localeVersion = miLocalStorage.getItem('localeVersion'); - const localeOutdated = (localeVersion == null || localeVersion !== version || locale == null); - - async function fetchAndUpdateLocale({ useCache } = { useCache: true }) { - const fetchOptions: RequestInit | undefined = useCache ? undefined : { cache: 'no-store' }; - const res = await window.fetch(`/assets/locales/${lang}.${version}.json`, fetchOptions); - if (res.status === 200) { - const newLocale = await res.text(); - const parsedNewLocale = JSON.parse(newLocale); - miLocalStorage.setItem('locale', newLocale); - miLocalStorage.setItem('localeVersion', version); - updateLocale(parsedNewLocale); - updateI18n(parsedNewLocale); - } - } - - if (localeOutdated) { - fetchAndUpdateLocale(); - } + storeBootloaderErrors({ ...i18n.ts._bootErrors, reload: i18n.ts.reload }); if (import.meta.hot) { import.meta.hot.on('locale-update', async (updatedLang: string) => { @@ -106,7 +89,8 @@ export async function common(createVue: () => Promise<App<Element>>) { await new Promise(resolve => { window.setTimeout(resolve, 500); }); - await fetchAndUpdateLocale({ useCache: false }); + // fetch with cache: 'no-store' to ensure the latest locale is fetched + await window.fetch(`/assets/locales/${lang}.${version}.json`, { cache: 'no-store' }).then(async res => res.status === 200 && await res.text()); window.location.reload(); } }); diff --git a/packages/frontend/src/components/MkExtensionInstaller.vue b/packages/frontend/src/components/MkExtensionInstaller.vue index a2247d844b..c9d18ee731 100644 --- a/packages/frontend/src/components/MkExtensionInstaller.vue +++ b/packages/frontend/src/components/MkExtensionInstaller.vue @@ -17,7 +17,7 @@ SPDX-License-Identifier: AGPL-3.0-only <MkInfo :warn="true">{{ i18n.ts._externalResourceInstaller.checkVendorBeforeInstall }}</MkInfo> - <div v-if="isPlugin" class="_gaps_s"> + <div v-if="extension.type === 'plugin'" class="_gaps_s"> <MkFolder :defaultOpen="true"> <template #icon><i class="ti ti-info-circle"></i></template> <template #label>{{ i18n.ts.metadata }}</template> @@ -60,7 +60,7 @@ SPDX-License-Identifier: AGPL-3.0-only <MkCode :code="extension.raw"/> </MkFolder> </div> - <div v-else-if="isTheme" class="_gaps_s"> + <div v-else-if="extension.type === 'theme'" class="_gaps_s"> <MkFolder :defaultOpen="true"> <template #icon><i class="ti ti-info-circle"></i></template> <template #label>{{ i18n.ts.metadata }}</template> @@ -78,7 +78,7 @@ SPDX-License-Identifier: AGPL-3.0-only </FormSplit> <MkKeyValue> <template #key>{{ i18n.ts._externalResourceInstaller._meta.base }}</template> - <template #value>{{ i18n.ts[extension.meta.base ?? 'none'] }}</template> + <template #value>{{ { light: i18n.ts.light, dark: i18n.ts.dark, none: i18n.ts.none }[extension.meta.base ?? 'none'] }}</template> </MkKeyValue> </div> </MkFolder> diff --git a/packages/frontend/src/i18n.ts b/packages/frontend/src/i18n.ts index 6ad503b089..0b2b206b7e 100644 --- a/packages/frontend/src/i18n.ts +++ b/packages/frontend/src/i18n.ts @@ -5,11 +5,12 @@ import { markRaw } from 'vue'; import { I18n } from '@@/js/i18n.js'; +import { locale } from '@@/js/locale.js'; import type { Locale } from '../../../locales/index.js'; -import { locale } from '@@/js/config.js'; export const i18n = markRaw(new I18n<Locale>(locale, _DEV_)); +// test 以外では使わないこと。インライン化されてるのでだいたい意味がない export function updateI18n(newLocale: Locale) { i18n.locale = newLocale; } diff --git a/packages/frontend/src/local-storage.ts b/packages/frontend/src/local-storage.ts index b64a8c5dd5..687983bcdb 100644 --- a/packages/frontend/src/local-storage.ts +++ b/packages/frontend/src/local-storage.ts @@ -22,8 +22,7 @@ export type Keys = ( 'fontSize' | 'ui' | 'ui_temp' | - 'locale' | - 'localeVersion' | + 'bootloaderLocales' | 'theme' | 'themeId' | 'customCss' | diff --git a/packages/frontend/src/pages/settings/preferences.vue b/packages/frontend/src/pages/settings/preferences.vue index ed4f36c0e5..fb1a409180 100644 --- a/packages/frontend/src/pages/settings/preferences.vue +++ b/packages/frontend/src/pages/settings/preferences.vue @@ -886,8 +886,6 @@ const useSystemFont = ref(miLocalStorage.getItem('useSystemFont') != null); watch(lang, () => { miLocalStorage.setItem('lang', lang.value as string); - miLocalStorage.removeItem('locale'); - miLocalStorage.removeItem('localeVersion'); }); watch(fontSize, () => { diff --git a/packages/frontend/src/utility/clear-cache.ts b/packages/frontend/src/utility/clear-cache.ts index b6ae254727..8a62265438 100644 --- a/packages/frontend/src/utility/clear-cache.ts +++ b/packages/frontend/src/utility/clear-cache.ts @@ -13,8 +13,6 @@ export async function clearCache() { os.waiting(); miLocalStorage.removeItem('instance'); miLocalStorage.removeItem('instanceCachedAt'); - miLocalStorage.removeItem('locale'); - miLocalStorage.removeItem('localeVersion'); miLocalStorage.removeItem('theme'); miLocalStorage.removeItem('emojis'); miLocalStorage.removeItem('lastEmojisFetchedAt'); diff --git a/packages/frontend/tsconfig.json b/packages/frontend/tsconfig.json index 9c7c611e5b..c445ada034 100644 --- a/packages/frontend/tsconfig.json +++ b/packages/frontend/tsconfig.json @@ -46,6 +46,7 @@ }, "compileOnSave": false, "include": [ + "./build.ts", "./lib/**/*.ts", "./src/**/*.ts", "./src/**/*.vue", diff --git a/packages/frontend/vite.config.ts b/packages/frontend/vite.config.ts index 7b6bf436b1..e9707d13d1 100644 --- a/packages/frontend/vite.config.ts +++ b/packages/frontend/vite.config.ts @@ -14,6 +14,7 @@ import pluginJson5 from './vite.json5.js'; import pluginCreateSearchIndex from './lib/vite-plugin-create-search-index.js'; import type { Options as SearchIndexOptions } from './lib/vite-plugin-create-search-index.js'; import pluginWatchLocales from './lib/vite-plugin-watch-locales.js'; +import pluginRemoveUnrefI18n from '../frontend-builder/rollup-plugin-remove-unref-i18n.js'; const url = process.env.NODE_ENV === 'development' ? yaml.load(await fsp.readFile('../../.config/default.yml', 'utf-8')).url : null; const host = url ? (new URL(url)).hostname : undefined; @@ -111,6 +112,7 @@ export function getConfig(): UserConfig { pluginWatchLocales(), ...searchIndexes.map(options => pluginCreateSearchIndex(options)), pluginVue(), + pluginRemoveUnrefI18n(), pluginUnwindCssModuleClassName(), pluginJson5(), ...process.env.NODE_ENV === 'production' @@ -174,16 +176,21 @@ export function getConfig(): UserConfig { manifest: 'manifest.json', rollupOptions: { input: { - app: './src/_boot_.ts', + i18n: './src/i18n.ts', + entry: './src/_boot_.ts', }, external: externalPackages.map(p => p.match), + preserveEntrySignatures: 'allow-extension', output: { manualChunks: { vue: ['vue'], photoswipe: ['photoswipe', 'photoswipe/lightbox', 'photoswipe/style.css'], + // dependencies of i18n.ts + 'config': ['@@/js/config.js'], }, - chunkFileNames: process.env.NODE_ENV === 'production' ? '[hash:8].js' : '[name]-[hash:8].js', - assetFileNames: process.env.NODE_ENV === 'production' ? '[hash:8][extname]' : '[name]-[hash:8][extname]', + entryFileNames: 'scripts/[hash:8].js', + chunkFileNames: 'scripts/[hash:8].js', + assetFileNames: 'assets/[hash:8][extname]', paths(id) { for (const p of externalPackages) { if (p.match.test(id)) { |