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