From 8598f3912ecc16f9b7c3f502e09d9ea96f7e507d Mon Sep 17 00:00:00 2001 From: anatawa12 Date: Fri, 8 Aug 2025 11:26:18 +0900 Subject: per-locale bundle & inline locale (#16369) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit * 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}) --- packages/frontend-embed/build.ts | 51 ++++++++++++++++++++++++++++++++++ packages/frontend-embed/package.json | 5 ++-- packages/frontend-embed/src/boot.ts | 19 +++---------- packages/frontend-embed/src/i18n.ts | 3 +- packages/frontend-embed/vite.config.ts | 13 +++++++-- 5 files changed, 70 insertions(+), 21 deletions(-) create mode 100644 packages/frontend-embed/build.ts (limited to 'packages/frontend-embed') diff --git a/packages/frontend-embed/build.ts b/packages/frontend-embed/build.ts new file mode 100644 index 0000000000..737233a4d0 --- /dev/null +++ b/packages/frontend-embed/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_embed_vite_'; + +/** + * @return {Promise} + */ +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-embed/package.json b/packages/frontend-embed/package.json index e76046add3..f9d1330ae5 100644 --- a/packages/frontend-embed/package.json +++ b/packages/frontend-embed/package.json @@ -4,7 +4,7 @@ "type": "module", "scripts": { "watch": "vite", - "build": "vite build", + "build": "tsx build.ts", "typecheck": "vue-tsc --noEmit", "eslint": "eslint --quiet \"src/**/*.{ts,vue}\"", "lint": "pnpm typecheck && pnpm eslint" @@ -20,8 +20,8 @@ "astring": "1.9.0", "buraha": "0.0.1", "estree-walker": "3.0.3", - "icons-subsetter": "workspace:*", "frontend-shared": "workspace:*", + "icons-subsetter": "workspace:*", "json5": "2.2.3", "mfm-js": "0.25.0", "misskey-js": "workspace:*", @@ -63,6 +63,7 @@ "nodemon": "3.1.10", "prettier": "3.6.2", "start-server-and-test": "2.0.12", + "tsx": "4.20.3", "vite-plugin-turbosnap": "1.0.3", "vue-component-type-helpers": "3.0.5", "vue-eslint-parser": "10.2.0", diff --git a/packages/frontend-embed/src/boot.ts b/packages/frontend-embed/src/boot.ts index 459b283e23..9d69437c30 100644 --- a/packages/frontend-embed/src/boot.ts +++ b/packages/frontend-embed/src/boot.ts @@ -17,15 +17,16 @@ import { createApp, defineAsyncComponent } from 'vue'; import defaultLightTheme from '@@/themes/l-light.json5'; import defaultDarkTheme from '@@/themes/d-dark.json5'; import { MediaProxy } from '@@/js/media-proxy.js'; +import { storeBootloaderErrors } from '@@/js/store-boot-errors'; import { applyTheme, assertIsTheme } from '@/theme.js'; import { fetchCustomEmojis } from '@/custom-emojis.js'; import { DI } from '@/di.js'; import { serverMetadata } from '@/server-metadata.js'; -import { url, version, locale, lang, updateLocale } from '@@/js/config.js'; +import { url, version, lang } from '@@/js/config.js'; import { parseEmbedParams } from '@@/js/embed-page.js'; import { postMessageToParentWindow, setIframeId } from '@/post-message.js'; import { serverContext } from '@/server-context.js'; -import { i18n, updateI18n } from '@/i18n.js'; +import { i18n } from '@/i18n.js'; import type { Theme } from '@/theme.js'; @@ -76,19 +77,7 @@ if (embedParams.colorMode === 'dark') { //#endregion //#region Detect language & fetch translations -const localeVersion = localStorage.getItem('localeVersion'); -const localeOutdated = (localeVersion == null || localeVersion !== version || locale == null); -if (localeOutdated) { - const res = await window.fetch(`/assets/locales/${lang}.${version}.json`); - if (res.status === 200) { - const newLocale = await res.text(); - const parsedNewLocale = JSON.parse(newLocale); - localStorage.setItem('locale', newLocale); - localStorage.setItem('localeVersion', version); - updateLocale(parsedNewLocale); - updateI18n(parsedNewLocale); - } -} +storeBootloaderErrors({ ...i18n.ts._bootErrors, reload: i18n.ts.reload }); //#endregion // サイズの制限 diff --git a/packages/frontend-embed/src/i18n.ts b/packages/frontend-embed/src/i18n.ts index 6ad503b089..0b2b206b7e 100644 --- a/packages/frontend-embed/src/i18n.ts +++ b/packages/frontend-embed/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, _DEV_)); +// test 以外では使わないこと。インライン化されてるのでだいたい意味がない export function updateI18n(newLocale: Locale) { i18n.locale = newLocale; } diff --git a/packages/frontend-embed/vite.config.ts b/packages/frontend-embed/vite.config.ts index a057581b3a..eb57db9774 100644 --- a/packages/frontend-embed/vite.config.ts +++ b/packages/frontend-embed/vite.config.ts @@ -8,6 +8,7 @@ import locales from '../../locales/index.js'; import meta from '../../package.json'; import packageInfo from './package.json' with { type: 'json' }; import pluginJson5 from './vite.json5.js'; +import pluginRemoveUnrefI18n from '../frontend-builder/rollup-plugin-remove-unref-i18n'; 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; @@ -85,6 +86,7 @@ export function getConfig(): UserConfig { plugins: [ pluginVue(), + pluginRemoveUnrefI18n(), pluginJson5(), ], @@ -135,15 +137,20 @@ 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'], + // 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)) { -- cgit v1.2.3-freya