summaryrefslogtreecommitdiff
path: root/packages/i18n/scripts
diff options
context:
space:
mode:
authorおさむのひと <46447427+samunohito@users.noreply.github.com>2025-11-30 13:27:44 +0900
committerGitHub <noreply@github.com>2025-11-30 13:27:44 +0900
commitfe01a5a28f34c873019ae3c34086acd6bd791a1d (patch)
treef4d8e8c4a0e97c120b272c3d253ea62789cc4afd /packages/i18n/scripts
parentfix(deps): update [frontend] update dependencies [ci skip] (#16901) (diff)
downloadmisskey-fe01a5a28f34c873019ae3c34086acd6bd791a1d.tar.gz
misskey-fe01a5a28f34c873019ae3c34086acd6bd791a1d.tar.bz2
misskey-fe01a5a28f34c873019ae3c34086acd6bd791a1d.zip
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>
Diffstat (limited to 'packages/i18n/scripts')
-rw-r--r--packages/i18n/scripts/generateLocaleInterface.ts153
-rw-r--r--packages/i18n/scripts/verify.ts70
2 files changed, 223 insertions, 0 deletions
diff --git a/packages/i18n/scripts/generateLocaleInterface.ts b/packages/i18n/scripts/generateLocaleInterface.ts
new file mode 100644
index 0000000000..1c0f5c6a79
--- /dev/null
+++ b/packages/i18n/scripts/generateLocaleInterface.ts
@@ -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'));
+}
diff --git a/packages/i18n/scripts/verify.ts b/packages/i18n/scripts/verify.ts
new file mode 100644
index 0000000000..877966e49f
--- /dev/null
+++ b/packages/i18n/scripts/verify.ts
@@ -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);
+}