Files
misskey/packages/frontend-builder/locale-inliner/apply-with-locale.ts
anatawa12 8598f3912e 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})
2025-08-08 11:26:18 +09:00

98 lines
3.6 KiB
TypeScript

import MagicString from 'magic-string';
import type { Locale } from '../../../locales/index.js';
import { assertNever } from '../utils.js';
import type { TextModification } from '../locale-inliner.js';
import type { Logger } from '../logger.js';
export function applyWithLocale(
sourceCode: MagicString,
modifications: TextModification[],
localeName: string,
localeJson: Locale,
fileLogger: Logger,
) {
for (const modification of modifications) {
switch (modification.type) {
case "delete":
sourceCode.remove(modification.begin, modification.end);
break;
case "insert":
sourceCode.appendRight(modification.begin, modification.text);
break;
case "replace":
sourceCode.update(modification.begin, modification.end, modification.text);
break;
case "localized": {
const accessed = getPropertyByPath(localeJson, modification.localizationKey);
if (accessed == null) {
fileLogger.warn(`Cannot find localization key ${modification.localizationKey.join('.')}`);
}
sourceCode.update(modification.begin, modification.end, JSON.stringify(accessed));
break;
}
case "parameterized-function": {
const accessed = getPropertyByPath(localeJson, modification.localizationKey);
let replacement: string;
if (typeof accessed === 'string') {
replacement = formatFunction(accessed);
} else if (typeof accessed === 'object' && accessed !== null) {
replacement = `({${Object.entries(accessed).map(([key, value]) => `${JSON.stringify(key)}:${formatFunction(value)}`).join(',')}})`;
} else {
fileLogger.warn(`Cannot find localization key ${modification.localizationKey.join('.')}`);
replacement = '(() => "")'; // placeholder for missing locale
}
sourceCode.update(modification.begin, modification.end, replacement);
break;
function formatFunction(accessed: string): string {
const params = new Set<string>();
const components: string[] = [];
let lastIndex = 0;
for (const match of accessed.matchAll(/\{(.+?)}/g)) {
const [fullMatch, paramName] = match;
if (lastIndex < match.index) {
components.push(JSON.stringify(accessed.slice(lastIndex, match.index)));
}
params.add(paramName);
components.push(paramName);
lastIndex = match.index + fullMatch.length;
}
components.push(JSON.stringify(accessed.slice(lastIndex)));
// we replace with `(({name,count})=>(name+count+"some"))`
const paramList = Array.from(params).join(',');
let body = components.filter(x => x != '""').join('+');
if (body == '') body = '""'; // if the body is empty, we return empty string
return `(({${paramList}})=>(${body}))`;
}
}
case "locale-name": {
sourceCode.update(modification.begin, modification.end, modification.literal ? JSON.stringify(localeName) : localeName);
break;
}
case "locale-json": {
// locale-json is inlined to place where initialize module-level variable which is executed only once.
// In such case we can use JSON.parse to speed up the parsing script.
// https://v8.dev/blog/cost-of-javascript-2019#json
sourceCode.update(modification.begin, modification.end, `JSON.parse(${JSON.stringify(JSON.stringify(localeJson))})`);
break;
}
default: {
assertNever(modification);
}
}
}
}
function getPropertyByPath(localeJson: any, localizationKey: string[]): string | object | null {
if (localizationKey.length === 0) return localeJson;
let current: any = localeJson;
for (const key of localizationKey) {
if (typeof current !== 'object' || current === null || !(key in current)) {
return null; // Key not found
}
current = current[key];
}
return current ?? null;
}