forked from mirrors/misskey
* 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})
146 lines
4.2 KiB
TypeScript
146 lines
4.2 KiB
TypeScript
import * as fs from 'fs/promises';
|
|
import * as path from 'node:path';
|
|
import { type Locale } from '../../locales/index.js';
|
|
import type { Manifest as ViteManifest } from 'vite';
|
|
import MagicString from 'magic-string';
|
|
import { collectModifications } from './locale-inliner/collect-modifications.js';
|
|
import { applyWithLocale } from './locale-inliner/apply-with-locale.js';
|
|
import { blankLogger, type Logger } from './logger.js';
|
|
|
|
export class LocaleInliner {
|
|
outputDir: string;
|
|
scriptsDir: string;
|
|
i18nFile: string;
|
|
i18nFileName: string;
|
|
logger: Logger;
|
|
chunks: ScriptChunk[];
|
|
|
|
static async create(options: {
|
|
outputDir: string,
|
|
scriptsDir: string,
|
|
i18nFile: string,
|
|
logger: Logger,
|
|
}): Promise<LocaleInliner> {
|
|
const manifest: ViteManifest = JSON.parse(await fs.readFile(`${options.outputDir}/manifest.json`, 'utf-8'));
|
|
return new LocaleInliner({ ...options, manifest });
|
|
}
|
|
|
|
constructor(options: {
|
|
outputDir: string,
|
|
scriptsDir: string,
|
|
i18nFile: string,
|
|
manifest: ViteManifest,
|
|
logger: Logger,
|
|
}) {
|
|
this.outputDir = options.outputDir;
|
|
this.scriptsDir = options.scriptsDir;
|
|
this.i18nFile = options.i18nFile;
|
|
this.i18nFileName = this.stripScriptDir(options.manifest[this.i18nFile].file);
|
|
this.logger = options.logger;
|
|
this.chunks = Object.values(options.manifest).filter(chunk => this.isScriptFile(chunk.file)).map(chunk => ({
|
|
fileName: this.stripScriptDir(chunk.file),
|
|
chunkName: chunk.name,
|
|
}));
|
|
}
|
|
|
|
async loadFiles() {
|
|
await Promise.all(this.chunks.map(async chunk => {
|
|
const filePath = path.join(this.outputDir, this.scriptsDir, chunk.fileName);
|
|
chunk.sourceCode = await fs.readFile(filePath, 'utf-8');
|
|
}));
|
|
}
|
|
|
|
collectsModifications() {
|
|
for (const chunk of this.chunks) {
|
|
if (!chunk.sourceCode) {
|
|
throw new Error(`Source code for ${chunk.fileName} is not loaded.`);
|
|
}
|
|
const fileLogger = this.logger.prefixed(`${chunk.fileName} (${chunk.chunkName}): `);
|
|
chunk.modifications = collectModifications(chunk.sourceCode, chunk.fileName, fileLogger, this);
|
|
}
|
|
}
|
|
|
|
async saveAllLocales(locales: Record<string, Locale>) {
|
|
const localeNames = Object.keys(locales);
|
|
for (const localeName of localeNames) {
|
|
await this.saveLocale(localeName, locales[localeName]);
|
|
}
|
|
}
|
|
|
|
async saveLocale(localeName: string, localeJson: Locale) {
|
|
// create directory
|
|
await fs.mkdir(path.join(this.outputDir, localeName), { recursive: true });
|
|
const localeLogger = localeName == 'ja-JP' ? this.logger : blankLogger; // we want to log for single locale only
|
|
for (const chunk of this.chunks) {
|
|
if (!chunk.sourceCode || !chunk.modifications) {
|
|
throw new Error(`Source code or modifications for ${chunk.fileName} is not available.`);
|
|
}
|
|
const fileLogger = localeLogger.prefixed(`${chunk.fileName} (${chunk.chunkName}): `);
|
|
const magicString = new MagicString(chunk.sourceCode);
|
|
applyWithLocale(magicString, chunk.modifications, localeName, localeJson, fileLogger);
|
|
|
|
await fs.writeFile(path.join(this.outputDir, localeName, chunk.fileName), magicString.toString());
|
|
}
|
|
}
|
|
|
|
isScriptFile(fileName: string) {
|
|
return fileName.startsWith(this.scriptsDir + '/') && fileName.endsWith('.js');
|
|
}
|
|
|
|
stripScriptDir(fileName: string) {
|
|
if (!fileName.startsWith(this.scriptsDir + '/')) {
|
|
throw new Error(`${fileName} does not start with ${this.scriptsDir}/`);
|
|
}
|
|
return fileName.slice(this.scriptsDir.length + 1);
|
|
}
|
|
}
|
|
|
|
interface ScriptChunk {
|
|
fileName: string;
|
|
chunkName?: string;
|
|
sourceCode?: string;
|
|
modifications?: TextModification[];
|
|
}
|
|
|
|
export type TextModification = {
|
|
type: 'delete';
|
|
begin: number;
|
|
end: number;
|
|
localizedOnly: boolean;
|
|
} | {
|
|
// can be used later to insert '../scripts' for common files
|
|
type: 'insert';
|
|
begin: number;
|
|
text: string;
|
|
localizedOnly: boolean;
|
|
} | {
|
|
type: 'replace';
|
|
begin: number;
|
|
end: number;
|
|
text: string;
|
|
localizedOnly: boolean;
|
|
} | {
|
|
type: 'localized';
|
|
begin: number;
|
|
end: number;
|
|
localizationKey: string[];
|
|
localizedOnly: true;
|
|
} | {
|
|
type: 'parameterized-function';
|
|
begin: number;
|
|
end: number;
|
|
localizationKey: string[];
|
|
localizedOnly: true;
|
|
} | {
|
|
type: 'locale-name';
|
|
begin: number;
|
|
end: number;
|
|
literal: boolean;
|
|
localizedOnly: true;
|
|
} | {
|
|
type: 'locale-json';
|
|
begin: number;
|
|
end: number;
|
|
localizedOnly: true;
|
|
};
|