mirror of
https://github.com/misskey-dev/misskey.git
synced 2026-03-21 03:30:42 +00: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:
153
packages/i18n/scripts/generateLocaleInterface.ts
Normal file
153
packages/i18n/scripts/generateLocaleInterface.ts
Normal file
@@ -0,0 +1,153 @@
|
||||
/*
|
||||
* SPDX-FileCopyrightText: syuilo and misskey-project
|
||||
* SPDX-License-Identifier: AGPL-3.0-only
|
||||
*/
|
||||
|
||||
import * as fs from 'node:fs';
|
||||
import { fileURLToPath } from 'node:url';
|
||||
import { dirname, resolve } from 'node:path';
|
||||
import * as yaml from 'js-yaml';
|
||||
import ts from 'typescript';
|
||||
|
||||
const __filename = fileURLToPath(import.meta.url);
|
||||
const __dirname = dirname(__filename);
|
||||
const parameterRegExp = /\{(\w+)\}/g;
|
||||
|
||||
interface LocaleRecord {
|
||||
[key: string]: string | LocaleRecord;
|
||||
}
|
||||
|
||||
function createMemberType(item: string | LocaleRecord): ts.TypeNode {
|
||||
if (typeof item !== 'string') {
|
||||
return ts.factory.createTypeLiteralNode(createMembers(item));
|
||||
}
|
||||
const parameters = Array.from(
|
||||
item.matchAll(parameterRegExp),
|
||||
([, parameter]) => parameter,
|
||||
);
|
||||
return parameters.length
|
||||
? ts.factory.createTypeReferenceNode(
|
||||
ts.factory.createIdentifier('ParameterizedString'),
|
||||
[
|
||||
ts.factory.createUnionTypeNode(
|
||||
parameters.map((parameter) =>
|
||||
ts.factory.createLiteralTypeNode(ts.factory.createStringLiteral(parameter)),
|
||||
),
|
||||
),
|
||||
],
|
||||
)
|
||||
: ts.factory.createKeywordTypeNode(ts.SyntaxKind.StringKeyword);
|
||||
}
|
||||
|
||||
function createMembers(record: LocaleRecord): ts.TypeElement[] {
|
||||
return Object.entries(record).map(([k, v]) => {
|
||||
const node = ts.factory.createPropertySignature(
|
||||
undefined,
|
||||
ts.factory.createStringLiteral(k),
|
||||
undefined,
|
||||
createMemberType(v),
|
||||
);
|
||||
if (typeof v === 'string') {
|
||||
ts.addSyntheticLeadingComment(
|
||||
node,
|
||||
ts.SyntaxKind.MultiLineCommentTrivia,
|
||||
`*
|
||||
* ${v.replace(/\n/g, '\n * ')}
|
||||
`,
|
||||
true,
|
||||
);
|
||||
}
|
||||
return node;
|
||||
});
|
||||
}
|
||||
|
||||
export async function generateLocaleInterface(localesDir: string): Promise<void> {
|
||||
const locale = yaml.load(fs.readFileSync(`${localesDir}/ja-JP.yml`, 'utf-8').toString()) as LocaleRecord;
|
||||
const members = createMembers(locale);
|
||||
|
||||
const elements: ts.Statement[] = [
|
||||
ts.factory.createImportDeclaration(
|
||||
undefined,
|
||||
ts.factory.createImportClause(
|
||||
false,
|
||||
undefined,
|
||||
ts.factory.createNamedImports([
|
||||
ts.factory.createImportSpecifier(
|
||||
true,
|
||||
undefined,
|
||||
ts.factory.createIdentifier('ILocale'),
|
||||
),
|
||||
ts.factory.createImportSpecifier(
|
||||
true,
|
||||
undefined,
|
||||
ts.factory.createIdentifier('ParameterizedString'),
|
||||
),
|
||||
]),
|
||||
),
|
||||
ts.factory.createStringLiteral('../types.js'),
|
||||
undefined,
|
||||
),
|
||||
ts.factory.createInterfaceDeclaration(
|
||||
[ts.factory.createToken(ts.SyntaxKind.ExportKeyword)],
|
||||
ts.factory.createIdentifier('Locale'),
|
||||
undefined,
|
||||
[
|
||||
ts.factory.createHeritageClause(ts.SyntaxKind.ExtendsKeyword, [
|
||||
ts.factory.createExpressionWithTypeArguments(
|
||||
ts.factory.createIdentifier('ILocale'),
|
||||
undefined,
|
||||
),
|
||||
]),
|
||||
],
|
||||
members,
|
||||
),
|
||||
];
|
||||
|
||||
ts.addSyntheticLeadingComment(
|
||||
elements[0],
|
||||
ts.SyntaxKind.MultiLineCommentTrivia,
|
||||
' eslint-disable ',
|
||||
true,
|
||||
);
|
||||
ts.addSyntheticLeadingComment(
|
||||
elements[0],
|
||||
ts.SyntaxKind.SingleLineCommentTrivia,
|
||||
' This file is generated by scripts/generateLocaleInterface.ts',
|
||||
true,
|
||||
);
|
||||
ts.addSyntheticLeadingComment(
|
||||
elements[0],
|
||||
ts.SyntaxKind.SingleLineCommentTrivia,
|
||||
' Do not edit this file directly.',
|
||||
true,
|
||||
);
|
||||
|
||||
const printed = ts
|
||||
.createPrinter({
|
||||
newLine: ts.NewLineKind.LineFeed,
|
||||
})
|
||||
.printList(
|
||||
ts.ListFormat.MultiLine,
|
||||
ts.factory.createNodeArray(elements),
|
||||
ts.createSourceFile(
|
||||
'locale.ts',
|
||||
'',
|
||||
ts.ScriptTarget.ESNext,
|
||||
true,
|
||||
ts.ScriptKind.TS,
|
||||
),
|
||||
);
|
||||
|
||||
const autogenDir = `${__dirname}/../src/autogen`;
|
||||
fs.mkdirSync(autogenDir, { recursive: true });
|
||||
|
||||
// 一瞬ファイルが存在しなくなって途切れる→不安定になるらしいので、リネームで対処
|
||||
fs.writeFileSync(`${autogenDir}/_locale.ts`, printed, 'utf-8');
|
||||
fs.renameSync(`${autogenDir}/_locale.ts`, `${autogenDir}/locale.ts`);
|
||||
}
|
||||
|
||||
// スクリプトとして直接実行された場合
|
||||
const isMain = import.meta.url === `file://${process.argv[1]}`;
|
||||
if (isMain) {
|
||||
await generateLocaleInterface(resolve(__dirname, '../../../locales'));
|
||||
}
|
||||
70
packages/i18n/scripts/verify.ts
Normal file
70
packages/i18n/scripts/verify.ts
Normal file
@@ -0,0 +1,70 @@
|
||||
/*
|
||||
* SPDX-FileCopyrightText: syuilo and misskey-project
|
||||
* SPDX-License-Identifier: AGPL-3.0-only
|
||||
*/
|
||||
|
||||
let valid = true;
|
||||
|
||||
interface LocaleRecord {
|
||||
[key: string]: string | LocaleRecord;
|
||||
}
|
||||
|
||||
interface ErrorData {
|
||||
expected?: string;
|
||||
actual?: string;
|
||||
parameter?: string;
|
||||
}
|
||||
|
||||
function writeError(type: string, lang: string, tree: string, data: ErrorData): void {
|
||||
process.stderr.write(JSON.stringify({ type, lang, tree, data }));
|
||||
process.stderr.write('\n');
|
||||
valid = false;
|
||||
}
|
||||
|
||||
function verify(expected: LocaleRecord, actual: LocaleRecord, lang: string, trace?: string): void {
|
||||
for (const key in expected) {
|
||||
if (!Object.prototype.hasOwnProperty.call(actual, key)) {
|
||||
continue;
|
||||
}
|
||||
if (typeof expected[key] === 'object') {
|
||||
if (typeof actual[key] !== 'object') {
|
||||
writeError('mismatched_type', lang, trace ? `${trace}.${key}` : key, { expected: 'object', actual: typeof actual[key] });
|
||||
continue;
|
||||
}
|
||||
verify(expected[key] as LocaleRecord, actual[key] as LocaleRecord, lang, trace ? `${trace}.${key}` : key);
|
||||
} else if (typeof expected[key] === 'string') {
|
||||
switch (typeof actual[key]) {
|
||||
case 'object':
|
||||
writeError('mismatched_type', lang, trace ? `${trace}.${key}` : key, { expected: 'string', actual: 'object' });
|
||||
break;
|
||||
case 'undefined':
|
||||
continue;
|
||||
case 'string': {
|
||||
const expectedParameters = new Set((expected[key] as string).match(/\{[^}]+\}/g)?.map((s) => s.slice(1, -1)));
|
||||
const actualParameters = new Set((actual[key] as string).match(/\{[^}]+\}/g)?.map((s) => s.slice(1, -1)));
|
||||
for (const parameter of expectedParameters) {
|
||||
if (!actualParameters.has(parameter)) {
|
||||
writeError('missing_parameter', lang, trace ? `${trace}.${key}` : key, { parameter });
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// index.tsはtsのまま動かすことを想定していない(ビルド成果物を外部に公開する).
|
||||
// よってビルド後のものを検証する
|
||||
const locales = await import('../built/index.js');
|
||||
const { 'ja-JP': original, ...verifiees } = locales as unknown as Record<string, LocaleRecord>;
|
||||
|
||||
for (const lang in verifiees) {
|
||||
if (!Object.prototype.hasOwnProperty.call(locales, lang)) {
|
||||
continue;
|
||||
}
|
||||
verify(original, verifiees[lang], lang);
|
||||
}
|
||||
|
||||
if (!valid) {
|
||||
process.exit(1);
|
||||
}
|
||||
Reference in New Issue
Block a user