/* * SPDX-FileCopyrightText: syuilo and misskey-project * SPDX-License-Identifier: AGPL-3.0-only */ /** * Languages Loader */ import * as fs from 'node:fs'; import * as yaml from 'js-yaml'; import { languages, primaries } from './const.js'; import type { Locale } from './autogen/locale.js'; import type { ILocale, ParameterizedString } from './types.js'; type Language = typeof languages[number]; type PrimaryLang = keyof typeof primaries; type Locales = Record; /** * オブジェクトを再帰的にマージする */ function merge(...args: (T | ILocale | undefined)[]): T { return args.reduce((a, c) => ({ ...a, ...c, ...Object.entries(a) .filter(([k]) => c && typeof c[k] === 'object') .reduce>((acc, [k, v]) => { acc[k] = merge(v as ILocale, (c as ILocale)[k] as ILocale); return acc; }, {}), }), {} as ILocale) as T; } /** * 何故か文字列にバックスペース文字が混入することがあり、YAMLが壊れるので取り除く */ function clean (text: string) { return text.replace(new RegExp(String.fromCodePoint(0x08), 'g'), ''); } /** * 空文字列が入ることがあり、フォールバックが動作しなくなるのでプロパティごと消す */ function removeEmpty(obj: T): T { for (const [k, v] of Object.entries(obj)) { if (v === '') { delete obj[k]; } else if (typeof v === 'object') { removeEmpty(v as ILocale); } } return obj; } function build(): Record { // vitestの挙動を調整するため、一度ローカル変数化する必要がある // https://github.com/vitest-dev/vitest/issues/3988#issuecomment-1686599577 // https://github.com/misskey-dev/misskey/pull/14057#issuecomment-2192833785 const metaUrl = import.meta.url; const locales = languages.reduce((a, lang) => { a[lang] = (yaml.load(clean(fs.readFileSync(new URL(`./locales/${lang}.yml`, metaUrl), 'utf-8'))) ?? {}) as ILocale; return a; }, {} as Locales); removeEmpty(locales); return Object.entries(locales).reduce>((a, [k, v]) => { const lang = k.split('-')[0]; const key = k as Language; switch (key) { case 'ja-JP': a[key] = v as Locale; break; case 'ja-KS': case 'en-US': a[key] = merge(locales['ja-JP'] as Locale, v); break; default: { const primaryLang = lang as PrimaryLang; const primaryKey = (lang in primaries ? `${lang}-${primaries[primaryLang]}` : undefined) as Language | undefined; a[key] = merge( locales['ja-JP'] as Locale, locales['en-US'], primaryKey ? locales[primaryKey] : {}, v, ); break; } } return a; }, {} as Record); } const locales = build() as { [lang: string]: Locale; }; /** * フロントエンド用の locale JSON を書き出す * Service Worker が HTTP 経由で取得するために必要 * @param destDir 出力先ディレクトリ(例: built/_frontend_dist_/locales) * @param version バージョン文字列(ファイル名とJSON内に埋め込まれる) */ async function writeFrontendLocalesJson(destDir: string, version: string): Promise { const { mkdir, writeFile } = await import('node:fs/promises'); const { resolve } = await import('node:path'); await mkdir(destDir, { recursive: true }); const builtLocales = build(); const v = { '_version_': version }; for (const [lang, locale] of Object.entries(builtLocales)) { await writeFile( resolve(destDir, `${lang}.${version}.json`), JSON.stringify({ ...locale, ...v }), 'utf-8', ); } } export { locales, languages, build, writeFrontendLocalesJson }; export type { Language, Locale, ILocale, ParameterizedString }; export default locales;