misskey/packages/i18n/scripts/generateLocaleInterface.ts

154 lines
3.9 KiB
TypeScript

/*
* 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'));
}