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:
13085
packages/i18n/src/autogen/locale.ts
Normal file
13085
packages/i18n/src/autogen/locale.ts
Normal file
File diff suppressed because it is too large
Load Diff
166
packages/i18n/src/index.ts
Normal file
166
packages/i18n/src/index.ts
Normal 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;
|
||||
14
packages/i18n/src/types.ts
Normal file
14
packages/i18n/src/types.ts
Normal 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;
|
||||
}
|
||||
Reference in New Issue
Block a user