1
0
mirror of https://github.com/misskey-dev/misskey.git synced 2026-05-20 07:05:34 +02:00

refactor: localesをworkspace管理下のパッケージに (#16895)

* refactor: localesをworkspace管理下のパッケージに

* fix copilot review

* move

* move

* rename

* fix ci

* revert unwanted indent changes

* fix

* fix

* fix

* fix

* 間違えてコミットしていたのを戻す

* 不要

* 追加漏れ

* ymlの場所だけ戻す

* localesの位置を戻したのでこの差分は不要

* 内容的にlocalesにある方が正しい

* i18nパッケージ用のREADME.mdを用意

* fix locale.yml

* fix locale.yml

---------

Co-authored-by: kakkokari-gtyih <67428053+kakkokari-gtyih@users.noreply.github.com>
This commit is contained in:
おさむのひと
2025-11-30 13:27:44 +09:00
committed by GitHub
parent 32b5583432
commit fe01a5a28f
51 changed files with 860 additions and 519 deletions

File diff suppressed because it is too large Load Diff

166
packages/i18n/src/index.ts Normal file
View File

@@ -0,0 +1,166 @@
/*
* 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 type { Locale } from './autogen/locale.js';
import type { ILocale, ParameterizedString } from './types.js';
const languages = [
'ar-SA',
'ca-ES',
'cs-CZ',
'da-DK',
'de-DE',
'en-US',
'es-ES',
'fr-FR',
'id-ID',
'it-IT',
'ja-JP',
'ja-KS',
'kab-KAB',
'kn-IN',
'ko-KR',
'nl-NL',
'no-NO',
'pl-PL',
'pt-PT',
'ru-RU',
'sk-SK',
'th-TH',
'tr-TR',
'ug-CN',
'uk-UA',
'vi-VN',
'zh-CN',
'zh-TW',
] as const;
type Language = typeof languages[number];
const primaries = {
'en': 'US',
'ja': 'JP',
'zh': 'CN',
} as const satisfies Record<string, string>;
type PrimaryLang = keyof typeof primaries;
type Locales = Record<Language, ILocale>;
/**
* オブジェクトを再帰的にマージする
*/
function merge<T extends ILocale>(...args: (T | ILocale | undefined)[]): T {
return args.reduce<ILocale>((a, c) => ({
...a,
...c,
...Object.entries(a)
.filter(([k]) => c && typeof c[k] === 'object')
.reduce<Record<string, ILocale[string]>>((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<T extends ILocale>(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<Language, Locale> {
// 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<Locales>((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<Record<Language, Locale>>((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<Locale>(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<Locale>(
locales['ja-JP'] as Locale,
locales['en-US'],
primaryKey ? locales[primaryKey] : {},
v,
);
break;
}
}
return a;
}, {} as Record<Language, Locale>);
}
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<void> {
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, build, writeFrontendLocalesJson };
export type { Language, Locale, ILocale, ParameterizedString };
export default locales;

View File

@@ -0,0 +1,14 @@
/*
* SPDX-FileCopyrightText: syuilo and misskey-project
* SPDX-License-Identifier: AGPL-3.0-only
*/
declare const kParameters: unique symbol;
export type ParameterizedString<T extends string = string> = string & {
[kParameters]: T;
};
export interface ILocale {
[_: string]: string | ParameterizedString | ILocale;
}