summaryrefslogtreecommitdiff
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
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>
-rw-r--r--.github/workflows/lint.yml7
-rw-r--r--.github/workflows/locale.yml29
-rw-r--r--Dockerfile2
-rw-r--r--locales/generateDTS.js232
-rw-r--r--locales/index.js93
-rw-r--r--locales/package.json3
-rw-r--r--locales/verify.js53
-rw-r--r--package.json17
-rw-r--r--packages/backend/src/core/NotificationService.ts4
-rw-r--r--packages/frontend-builder/locale-inliner.ts2
-rw-r--r--packages/frontend-builder/locale-inliner/apply-with-locale.ts2
-rw-r--r--packages/frontend-builder/package.json1
-rw-r--r--packages/frontend-embed/build.ts2
-rw-r--r--packages/frontend-embed/package.json1
-rw-r--r--packages/frontend-embed/src/components/I18n.vue4
-rw-r--r--packages/frontend-embed/src/i18n.ts2
-rw-r--r--packages/frontend-embed/vite.config.ts4
-rw-r--r--packages/frontend-shared/js/i18n.ts2
-rw-r--r--packages/frontend-shared/js/locale.ts2
-rw-r--r--packages/frontend-shared/js/store-boot-errors.ts2
-rw-r--r--packages/frontend-shared/package.json1
-rw-r--r--packages/frontend/.storybook/preload-locale.ts2
-rw-r--r--packages/frontend/build.ts2
-rw-r--r--packages/frontend/lib/vite-plugin-watch-locales.ts18
-rw-r--r--packages/frontend/package.json1
-rw-r--r--packages/frontend/src/components/global/I18n.vue4
-rw-r--r--packages/frontend/src/i18n.ts2
-rw-r--r--packages/frontend/test/i18n.test.ts2
-rw-r--r--packages/frontend/test/init.ts6
-rw-r--r--packages/frontend/vite.config.ts6
-rw-r--r--packages/i18n/README.md5
-rw-r--r--packages/i18n/build.ts163
-rw-r--r--packages/i18n/eslint.config.js35
-rw-r--r--packages/i18n/package.json42
-rw-r--r--packages/i18n/scripts/generateLocaleInterface.ts153
-rw-r--r--packages/i18n/scripts/verify.ts70
-rw-r--r--packages/i18n/src/autogen/locale.ts (renamed from locales/index.d.ts)15
-rw-r--r--packages/i18n/src/index.ts166
-rw-r--r--packages/i18n/src/types.ts14
-rw-r--r--packages/i18n/tsconfig.eslint.json8
-rw-r--r--packages/i18n/tsconfig.json14
-rw-r--r--packages/sw/build.js3
-rw-r--r--packages/sw/package.json1
-rw-r--r--packages/sw/src/scripts/lang.ts2
-rw-r--r--packages/sw/src/sw.ts2
-rw-r--r--pnpm-lock.yaml58
-rw-r--r--pnpm-workspace.yaml1
-rw-r--r--scripts/build-assets.mjs97
-rw-r--r--scripts/clean-all.js3
-rw-r--r--scripts/clean.js1
-rw-r--r--scripts/dev.mjs18
51 files changed, 860 insertions, 519 deletions
diff --git a/.github/workflows/lint.yml b/.github/workflows/lint.yml
index d80c56f032..33a1ccbc76 100644
--- a/.github/workflows/lint.yml
+++ b/.github/workflows/lint.yml
@@ -111,10 +111,5 @@ jobs:
node-version-file: '.node-version'
cache: 'pnpm'
- run: pnpm i --frozen-lockfile
- - run: pnpm --filter misskey-js run build
- if: ${{ matrix.workspace == 'backend' || matrix.workspace == 'frontend' || matrix.workspace == 'sw' }}
- - run: pnpm --filter misskey-reversi run build
- if: ${{ matrix.workspace == 'backend' || matrix.workspace == 'frontend' }}
- - run: pnpm --filter misskey-bubble-game run build
- if: ${{ matrix.workspace == 'frontend' }}
+ - run: pnpm --filter "${{ matrix.workspace }}^..." run build
- run: pnpm --filter ${{ matrix.workspace }} run typecheck
diff --git a/.github/workflows/locale.yml b/.github/workflows/locale.yml
index e63d83997b..d75335f38f 100644
--- a/.github/workflows/locale.yml
+++ b/.github/workflows/locale.yml
@@ -3,10 +3,12 @@ name: Lint
on:
push:
paths:
+ - packages/i18n/**
- locales/**
- .github/workflows/locale.yml
pull_request:
paths:
+ - packages/i18n/**
- locales/**
- .github/workflows/locale.yml
jobs:
@@ -14,15 +16,18 @@ jobs:
runs-on: ubuntu-latest
continue-on-error: true
steps:
- - uses: actions/checkout@v4.3.0
- with:
- fetch-depth: 0
- submodules: true
- - name: Setup pnpm
- uses: pnpm/action-setup@v4.2.0
- - uses: actions/setup-node@v4.4.0
- with:
- node-version-file: '.node-version'
- cache: 'pnpm'
- - run: pnpm i --frozen-lockfile
- - run: cd locales && node verify.js
+ - uses: actions/checkout@v4.3.0
+ with:
+ fetch-depth: 0
+ submodules: true
+ - name: Setup pnpm
+ uses: pnpm/action-setup@v4.2.0
+ - uses: actions/setup-node@v4.4.0
+ with:
+ node-version-file: ".node-version"
+ cache: "pnpm"
+ - run: pnpm i --frozen-lockfile
+ - run: pnpm --filter i18n build
+ - name: Verify Locales
+ working-directory: ./packages/i18n
+ run: pnpm run verify
diff --git a/Dockerfile b/Dockerfile
index 20e24d1dc2..a071970927 100644
--- a/Dockerfile
+++ b/Dockerfile
@@ -24,6 +24,7 @@ COPY --link ["packages/frontend-shared/package.json", "./packages/frontend-share
COPY --link ["packages/frontend/package.json", "./packages/frontend/"]
COPY --link ["packages/frontend-embed/package.json", "./packages/frontend-embed/"]
COPY --link ["packages/frontend-builder/package.json", "./packages/frontend-builder/"]
+COPY --link ["packages/i18n/package.json", "./packages/i18n/"]
COPY --link ["packages/icons-subsetter/package.json", "./packages/icons-subsetter/"]
COPY --link ["packages/sw/package.json", "./packages/sw/"]
COPY --link ["packages/misskey-js/package.json", "./packages/misskey-js/"]
@@ -101,6 +102,7 @@ COPY --chown=misskey:misskey --from=native-builder /misskey/packages/misskey-js/
COPY --chown=misskey:misskey --from=native-builder /misskey/packages/misskey-reversi/built ./packages/misskey-reversi/built
COPY --chown=misskey:misskey --from=native-builder /misskey/packages/misskey-bubble-game/built ./packages/misskey-bubble-game/built
COPY --chown=misskey:misskey --from=native-builder /misskey/packages/backend/built ./packages/backend/built
+COPY --chown=misskey:misskey --from=native-builder /misskey/packages/i18n/built ./packages/i18n/built
COPY --chown=misskey:misskey --from=native-builder /misskey/fluent-emojis /misskey/fluent-emojis
COPY --chown=misskey:misskey . ./
diff --git a/locales/generateDTS.js b/locales/generateDTS.js
deleted file mode 100644
index ab0613cc82..0000000000
--- a/locales/generateDTS.js
+++ /dev/null
@@ -1,232 +0,0 @@
-import * as fs from 'node:fs';
-import { fileURLToPath } from 'node:url';
-import { dirname } 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;
-
-function createMemberType(item) {
- 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.createStringLiteral(parameter),
- ),
- ),
- ],
- )
- : ts.factory.createKeywordTypeNode(ts.SyntaxKind.StringKeyword);
-}
-
-function createMembers(record) {
- 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 default function generateDTS() {
- const locale = yaml.load(fs.readFileSync(`${__dirname}/ja-JP.yml`, 'utf-8'));
- const members = createMembers(locale);
- const elements = [
- ts.factory.createVariableStatement(
- [ts.factory.createToken(ts.SyntaxKind.DeclareKeyword)],
- ts.factory.createVariableDeclarationList(
- [
- ts.factory.createVariableDeclaration(
- ts.factory.createIdentifier('kParameters'),
- undefined,
- ts.factory.createTypeOperatorNode(
- ts.SyntaxKind.UniqueKeyword,
- ts.factory.createKeywordTypeNode(ts.SyntaxKind.SymbolKeyword),
- ),
- undefined,
- ),
- ],
- ts.NodeFlags.Const,
- ),
- ),
- ts.factory.createTypeAliasDeclaration(
- [ts.factory.createToken(ts.SyntaxKind.ExportKeyword)],
- ts.factory.createIdentifier('ParameterizedString'),
- [
- ts.factory.createTypeParameterDeclaration(
- undefined,
- ts.factory.createIdentifier('T'),
- ts.factory.createKeywordTypeNode(ts.SyntaxKind.StringKeyword),
- ts.factory.createKeywordTypeNode(ts.SyntaxKind.StringKeyword),
- ),
- ],
- ts.factory.createIntersectionTypeNode([
- ts.factory.createKeywordTypeNode(ts.SyntaxKind.StringKeyword),
- ts.factory.createTypeLiteralNode([
- ts.factory.createPropertySignature(
- undefined,
- ts.factory.createComputedPropertyName(
- ts.factory.createIdentifier('kParameters'),
- ),
- undefined,
- ts.factory.createTypeReferenceNode(
- ts.factory.createIdentifier('T'),
- undefined,
- ),
- ),
- ])
- ]),
- ),
- ts.factory.createInterfaceDeclaration(
- [ts.factory.createToken(ts.SyntaxKind.ExportKeyword)],
- ts.factory.createIdentifier('ILocale'),
- undefined,
- undefined,
- [
- ts.factory.createIndexSignature(
- undefined,
- [
- ts.factory.createParameterDeclaration(
- undefined,
- undefined,
- ts.factory.createIdentifier('_'),
- undefined,
- ts.factory.createKeywordTypeNode(ts.SyntaxKind.StringKeyword),
- undefined,
- ),
- ],
- ts.factory.createUnionTypeNode([
- ts.factory.createKeywordTypeNode(ts.SyntaxKind.StringKeyword),
- ts.factory.createTypeReferenceNode(
- ts.factory.createIdentifier('ParameterizedString'),
- ),
- ts.factory.createTypeReferenceNode(
- ts.factory.createIdentifier('ILocale'),
- 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.factory.createVariableStatement(
- [ts.factory.createToken(ts.SyntaxKind.DeclareKeyword)],
- ts.factory.createVariableDeclarationList(
- [
- ts.factory.createVariableDeclaration(
- ts.factory.createIdentifier('locales'),
- undefined,
- ts.factory.createTypeLiteralNode([
- ts.factory.createIndexSignature(
- undefined,
- [
- ts.factory.createParameterDeclaration(
- undefined,
- undefined,
- ts.factory.createIdentifier('lang'),
- undefined,
- ts.factory.createKeywordTypeNode(
- ts.SyntaxKind.StringKeyword,
- ),
- undefined,
- ),
- ],
- ts.factory.createTypeReferenceNode(
- ts.factory.createIdentifier('Locale'),
- undefined,
- ),
- ),
- ]),
- undefined,
- ),
- ],
- ts.NodeFlags.Const,
- ),
- ),
- ts.factory.createFunctionDeclaration(
- [ts.factory.createModifier(ts.SyntaxKind.ExportKeyword)],
- undefined,
- ts.factory.createIdentifier('build'),
- undefined,
- [],
- ts.factory.createTypeReferenceNode(
- ts.factory.createIdentifier('Locale'),
- undefined,
- ),
- undefined,
- ),
- ts.factory.createExportDefault(ts.factory.createIdentifier('locales')),
- ];
- ts.addSyntheticLeadingComment(
- elements[0],
- ts.SyntaxKind.MultiLineCommentTrivia,
- ' eslint-disable ',
- true,
- );
- ts.addSyntheticLeadingComment(
- elements[0],
- ts.SyntaxKind.SingleLineCommentTrivia,
- ' This file is generated by locales/generateDTS.js',
- 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(
- 'index.d.ts',
- '',
- ts.ScriptTarget.ESNext,
- true,
- ts.ScriptKind.TS,
- ),
- );
-
- fs.writeFileSync(`${__dirname}/index.d.ts`, printed, 'utf-8');
-}
diff --git a/locales/index.js b/locales/index.js
deleted file mode 100644
index 6d9cf4796b..0000000000
--- a/locales/index.js
+++ /dev/null
@@ -1,93 +0,0 @@
-/**
- * Languages Loader
- */
-
-import * as fs from 'node:fs';
-import * as yaml from 'js-yaml';
-
-const merge = (...args) => args.reduce((a, c) => ({
- ...a,
- ...c,
- ...Object.entries(a)
- .filter(([k]) => c && typeof c[k] === 'object')
- .reduce((a, [k, v]) => (a[k] = merge(v, c[k]), a), {})
-}), {});
-
-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',
-];
-
-const primaries = {
- 'en': 'US',
- 'ja': 'JP',
- 'zh': 'CN',
-};
-
-// 何故か文字列にバックスペース文字が混入することがあり、YAMLが壊れるので取り除く
-const clean = (text) => text.replace(new RegExp(String.fromCodePoint(0x08), 'g'), '');
-
-export function build() {
- // 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((a, c) => (a[c] = yaml.load(clean(fs.readFileSync(new URL(`${c}.yml`, metaUrl), 'utf-8'))) || {}, a), {});
-
- // 空文字列が入ることがあり、フォールバックが動作しなくなるのでプロパティごと消す
- const removeEmpty = (obj) => {
- for (const [k, v] of Object.entries(obj)) {
- if (v === '') {
- delete obj[k];
- } else if (typeof v === 'object') {
- removeEmpty(v);
- }
- }
- return obj;
- };
- removeEmpty(locales);
-
- return Object.entries(locales)
- .reduce((a, [k, v]) => (a[k] = (() => {
- const [lang] = k.split('-');
- switch (k) {
- case 'ja-JP': return v;
- case 'ja-KS':
- case 'en-US': return merge(locales['ja-JP'], v);
- default: return merge(
- locales['ja-JP'],
- locales['en-US'],
- locales[`${lang}-${primaries[lang]}`] ?? {},
- v
- );
- }
- })(), a), {});
-}
-
-export default build();
diff --git a/locales/package.json b/locales/package.json
deleted file mode 100644
index bedb411a91..0000000000
--- a/locales/package.json
+++ /dev/null
@@ -1,3 +0,0 @@
-{
- "type": "module"
-}
diff --git a/locales/verify.js b/locales/verify.js
deleted file mode 100644
index a8e9875d6e..0000000000
--- a/locales/verify.js
+++ /dev/null
@@ -1,53 +0,0 @@
-import locales from './index.js';
-
-let valid = true;
-
-function writeError(type, lang, tree, data) {
- process.stderr.write(JSON.stringify({ type, lang, tree, data }));
- process.stderr.write('\n');
- valid = false;
-}
-
-function verify(expected, actual, lang, trace) {
- for (let 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], actual[key], 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].match(/\{[^}]+\}/g)?.map((s) => s.slice(1, -1)));
- const actualParameters = new Set(actual[key].match(/\{[^}]+\}/g)?.map((s) => s.slice(1, -1)));
- for (let parameter of expectedParameters) {
- if (!actualParameters.has(parameter)) {
- writeError('missing_parameter', lang, trace ? `${trace}.${key}` : key, { parameter });
- }
- }
- }
- }
- }
-}
-
-const { ['ja-JP']: original, ...verifiees } = locales;
-
-for (let lang in verifiees) {
- if (!Object.prototype.hasOwnProperty.call(locales, lang)) {
- continue;
- }
- verify(original, verifiees[lang], lang);
-}
-
-if (!valid) {
- process.exit(1);
-}
diff --git a/package.json b/package.json
index 664c5e9e71..f11b24d0ad 100644
--- a/package.json
+++ b/package.json
@@ -8,15 +8,17 @@
},
"packageManager": "pnpm@10.22.0",
"workspaces": [
- "packages/frontend-shared",
- "packages/frontend",
- "packages/frontend-embed",
- "packages/icons-subsetter",
- "packages/backend",
- "packages/sw",
"packages/misskey-js",
+ "packages/i18n",
"packages/misskey-reversi",
- "packages/misskey-bubble-game"
+ "packages/misskey-bubble-game",
+ "packages/icons-subsetter",
+ "packages/frontend-shared",
+ "packages/frontend-builder",
+ "packages/sw",
+ "packages/backend",
+ "packages/frontend",
+ "packages/frontend-embed"
],
"private": true,
"scripts": {
@@ -68,6 +70,7 @@
},
"devDependencies": {
"@eslint/js": "9.39.1",
+ "i18n": "workspace:*",
"@misskey-dev/eslint-plugin": "2.2.0",
"@types/js-yaml": "4.0.9",
"@types/node": "24.10.1",
diff --git a/packages/backend/src/core/NotificationService.ts b/packages/backend/src/core/NotificationService.ts
index eeade4569b..310ffec7ce 100644
--- a/packages/backend/src/core/NotificationService.ts
+++ b/packages/backend/src/core/NotificationService.ts
@@ -202,7 +202,7 @@ export class NotificationService implements OnApplicationShutdown {
}
// TODO
- //const locales = await import('../../../../locales/index.js');
+ //const locales = await import('i18n');
// TODO: locale ファイルをクライアント用とサーバー用で分けたい
@@ -271,7 +271,7 @@ export class NotificationService implements OnApplicationShutdown {
let untilTime = untilId ? this.toXListId(untilId) : null;
let notifications: MiNotification[];
- for (;;) {
+ for (; ;) {
let notificationsRes: [id: string, fields: string[]][];
// sinceidのみの場合は古い順、そうでない場合は新しい順。 QueryService.makePaginationQueryも参照
diff --git a/packages/frontend-builder/locale-inliner.ts b/packages/frontend-builder/locale-inliner.ts
index 9bef465eeb..191d7250a6 100644
--- a/packages/frontend-builder/locale-inliner.ts
+++ b/packages/frontend-builder/locale-inliner.ts
@@ -10,7 +10,7 @@ import { collectModifications } from './locale-inliner/collect-modifications.js'
import { applyWithLocale } from './locale-inliner/apply-with-locale.js';
import { blankLogger } from './logger.js';
import type { Logger } from './logger.js';
-import type { Locale } from '../../locales/index.js';
+import type { Locale } from 'i18n';
import type { Manifest as ViteManifest } from 'vite';
export class LocaleInliner {
diff --git a/packages/frontend-builder/locale-inliner/apply-with-locale.ts b/packages/frontend-builder/locale-inliner/apply-with-locale.ts
index 5e601cdf12..78851d3029 100644
--- a/packages/frontend-builder/locale-inliner/apply-with-locale.ts
+++ b/packages/frontend-builder/locale-inliner/apply-with-locale.ts
@@ -5,7 +5,7 @@
import MagicString from 'magic-string';
import { assertNever } from '../utils.js';
-import type { Locale, ILocale } from '../../../locales/index.js';
+import type { ILocale, Locale } from 'i18n';
import type { TextModification } from '../locale-inliner.js';
import type { Logger } from '../logger.js';
diff --git a/packages/frontend-builder/package.json b/packages/frontend-builder/package.json
index d01e4c86ed..37dd133fe6 100644
--- a/packages/frontend-builder/package.json
+++ b/packages/frontend-builder/package.json
@@ -18,6 +18,7 @@
"typescript": "5.9.3"
},
"dependencies": {
+ "i18n": "workspace:*",
"estree-walker": "3.0.3",
"magic-string": "0.30.21",
"vite": "7.2.4"
diff --git a/packages/frontend-embed/build.ts b/packages/frontend-embed/build.ts
index 737233a4d0..4e1f588802 100644
--- a/packages/frontend-embed/build.ts
+++ b/packages/frontend-embed/build.ts
@@ -2,7 +2,7 @@ import * as fs from 'fs/promises';
import url from 'node:url';
import path from 'node:path';
import { execa } from 'execa';
-import locales from '../../locales/index.js';
+import locales from 'i18n';
import { LocaleInliner } from '../frontend-builder/locale-inliner.js'
import { createLogger } from '../frontend-builder/logger';
diff --git a/packages/frontend-embed/package.json b/packages/frontend-embed/package.json
index 85e25c8faa..c27583cf86 100644
--- a/packages/frontend-embed/package.json
+++ b/packages/frontend-embed/package.json
@@ -11,6 +11,7 @@
},
"dependencies": {
"@discordapp/twemoji": "16.0.1",
+ "i18n": "workspace:*",
"@rollup/plugin-json": "6.1.0",
"@rollup/plugin-replace": "6.0.3",
"@rollup/pluginutils": "5.3.0",
diff --git a/packages/frontend-embed/src/components/I18n.vue b/packages/frontend-embed/src/components/I18n.vue
index b621110ec9..9866e50958 100644
--- a/packages/frontend-embed/src/components/I18n.vue
+++ b/packages/frontend-embed/src/components/I18n.vue
@@ -9,7 +9,7 @@ SPDX-License-Identifier: AGPL-3.0-only
<script setup lang="ts" generic="T extends string | ParameterizedString">
import { computed, h } from 'vue';
-import type { ParameterizedString } from '../../../../locales/index.js';
+import type { ParameterizedString } from 'i18n';
const props = withDefaults(defineProps<{
src: T;
@@ -25,7 +25,7 @@ const slots = defineSlots<T extends ParameterizedString<infer R> ? { [K in R]: (
const parsed = computed(() => {
let str = props.src as string;
const value: (string | { arg: string; })[] = [];
- for (;;) {
+ for (; ;) {
const nextBracketOpen = str.indexOf('{');
const nextBracketClose = str.indexOf('}');
diff --git a/packages/frontend-embed/src/i18n.ts b/packages/frontend-embed/src/i18n.ts
index 0b2b206b7e..6a3a18df17 100644
--- a/packages/frontend-embed/src/i18n.ts
+++ b/packages/frontend-embed/src/i18n.ts
@@ -6,7 +6,7 @@
import { markRaw } from 'vue';
import { I18n } from '@@/js/i18n.js';
import { locale } from '@@/js/locale.js';
-import type { Locale } from '../../../locales/index.js';
+import type { Locale } from 'i18n';
export const i18n = markRaw(new I18n<Locale>(locale, _DEV_));
diff --git a/packages/frontend-embed/vite.config.ts b/packages/frontend-embed/vite.config.ts
index db4afb43a7..9e5c24f9d4 100644
--- a/packages/frontend-embed/vite.config.ts
+++ b/packages/frontend-embed/vite.config.ts
@@ -1,10 +1,10 @@
import path from 'path';
import pluginVue from '@vitejs/plugin-vue';
-import { type UserConfig, defineConfig } from 'vite';
+import { defineConfig, type UserConfig } from 'vite';
import * as yaml from 'js-yaml';
import { promises as fsp } from 'fs';
-import locales from '../../locales/index.js';
+import locales from 'i18n';
import meta from '../../package.json';
import packageInfo from './package.json' with { type: 'json' };
import pluginJson5 from './vite.json5.js';
diff --git a/packages/frontend-shared/js/i18n.ts b/packages/frontend-shared/js/i18n.ts
index 3b103c4714..a42450ee88 100644
--- a/packages/frontend-shared/js/i18n.ts
+++ b/packages/frontend-shared/js/i18n.ts
@@ -3,7 +3,7 @@
* SPDX-License-Identifier: AGPL-3.0-only
*/
-import type { ILocale, ParameterizedString } from '../../../locales/index.js';
+import type { ILocale, ParameterizedString } from 'i18n';
// eslint-disable-next-line @typescript-eslint/no-explicit-any
type TODO = any;
diff --git a/packages/frontend-shared/js/locale.ts b/packages/frontend-shared/js/locale.ts
index 87e3922fd9..38584fbed6 100644
--- a/packages/frontend-shared/js/locale.ts
+++ b/packages/frontend-shared/js/locale.ts
@@ -4,7 +4,7 @@
*/
import { lang, version } from '@@/js/config.js';
-import type { Locale } from '../../../locales/index.js';
+import type { Locale } from 'i18n';
// ここはビルド時に const locale = JSON.parse("...") みたいな感じで置き換えられるので top-level await は消える
export let locale: Locale = await window.fetch(`/assets/locales/${lang}.${version}.json`).then(r => r.json(), () => null);
diff --git a/packages/frontend-shared/js/store-boot-errors.ts b/packages/frontend-shared/js/store-boot-errors.ts
index 31e6248445..2741339165 100644
--- a/packages/frontend-shared/js/store-boot-errors.ts
+++ b/packages/frontend-shared/js/store-boot-errors.ts
@@ -3,7 +3,7 @@
* SPDX-License-Identifier: AGPL-3.0-only
*/
-import type { Locale } from '../../../locales/index.js';
+import type { Locale } from 'i18n';
type BootLoaderLocaleBody = Locale['_bootErrors'] & { reload: Locale['reload'] };
diff --git a/packages/frontend-shared/package.json b/packages/frontend-shared/package.json
index a407df6bf8..5806414930 100644
--- a/packages/frontend-shared/package.json
+++ b/packages/frontend-shared/package.json
@@ -34,6 +34,7 @@
"js-built"
],
"dependencies": {
+ "i18n": "workspace:*",
"misskey-js": "workspace:*",
"vue": "3.5.24"
}
diff --git a/packages/frontend/.storybook/preload-locale.ts b/packages/frontend/.storybook/preload-locale.ts
index c823ff9bee..42918b44f3 100644
--- a/packages/frontend/.storybook/preload-locale.ts
+++ b/packages/frontend/.storybook/preload-locale.ts
@@ -4,7 +4,7 @@
*/
import { writeFile } from 'node:fs/promises';
-import locales from '../../../locales/index.js';
+import locales from 'i18n';
await writeFile(
new URL('locale.ts', import.meta.url),
diff --git a/packages/frontend/build.ts b/packages/frontend/build.ts
index 0401c2b9ba..0f605c745b 100644
--- a/packages/frontend/build.ts
+++ b/packages/frontend/build.ts
@@ -2,7 +2,7 @@ import * as fs from 'fs/promises';
import url from 'node:url';
import path from 'node:path';
import { execa } from 'execa';
-import locales from '../../locales/index.js';
+import locales from 'i18n';
import { LocaleInliner } from '../frontend-builder/locale-inliner.js'
import { createLogger } from '../frontend-builder/logger';
diff --git a/packages/frontend/lib/vite-plugin-watch-locales.ts b/packages/frontend/lib/vite-plugin-watch-locales.ts
index 8e209d27bd..372e9039d5 100644
--- a/packages/frontend/lib/vite-plugin-watch-locales.ts
+++ b/packages/frontend/lib/vite-plugin-watch-locales.ts
@@ -4,7 +4,7 @@
*/
import path from 'node:path'
-import locales from '../../../locales/index.js';
+import locales from 'i18n';
const localesDir = path.resolve(__dirname, '../../../locales')
@@ -13,14 +13,14 @@ const localesDir = path.resolve(__dirname, '../../../locales')
* @returns {import('vite').Plugin}
*/
export default function pluginWatchLocales() {
- return {
- name: 'watch-locales',
+ return {
+ name: 'watch-locales',
- configureServer(server) {
- const localeYmlPaths = Object.keys(locales).map(locale => path.join(localesDir, `${locale}.yml`));
+ configureServer(server) {
+ const localeYmlPaths = Object.keys(locales).map(locale => path.join(localesDir, `${locale}.yml`));
- // watcherにパスを追加
- server.watcher.add(localeYmlPaths);
+ // watcherにパスを追加
+ server.watcher.add(localeYmlPaths);
server.watcher.on('change', (filePath) => {
if (localeYmlPaths.includes(filePath)) {
@@ -31,6 +31,6 @@ export default function pluginWatchLocales() {
})
}
});
- },
- };
+ },
+ };
}
diff --git a/packages/frontend/package.json b/packages/frontend/package.json
index ec7b39312a..c3baf5ea74 100644
--- a/packages/frontend/package.json
+++ b/packages/frontend/package.json
@@ -20,6 +20,7 @@
"@discordapp/twemoji": "16.0.1",
"@github/webauthn-json": "2.1.1",
"@mcaptcha/vanilla-glue": "0.1.0-alpha-3",
+ "i18n": "workspace:*",
"@misskey-dev/browser-image-resizer": "2024.1.0",
"@rollup/plugin-json": "6.1.0",
"@rollup/plugin-replace": "6.0.3",
diff --git a/packages/frontend/src/components/global/I18n.vue b/packages/frontend/src/components/global/I18n.vue
index 6b7723e6ac..9866e50958 100644
--- a/packages/frontend/src/components/global/I18n.vue
+++ b/packages/frontend/src/components/global/I18n.vue
@@ -9,7 +9,7 @@ SPDX-License-Identifier: AGPL-3.0-only
<script setup lang="ts" generic="T extends string | ParameterizedString">
import { computed, h } from 'vue';
-import type { ParameterizedString } from '../../../../../locales/index.js';
+import type { ParameterizedString } from 'i18n';
const props = withDefaults(defineProps<{
src: T;
@@ -25,7 +25,7 @@ const slots = defineSlots<T extends ParameterizedString<infer R> ? { [K in R]: (
const parsed = computed(() => {
let str = props.src as string;
const value: (string | { arg: string; })[] = [];
- for (;;) {
+ for (; ;) {
const nextBracketOpen = str.indexOf('{');
const nextBracketClose = str.indexOf('}');
diff --git a/packages/frontend/src/i18n.ts b/packages/frontend/src/i18n.ts
index 0b2b206b7e..6a3a18df17 100644
--- a/packages/frontend/src/i18n.ts
+++ b/packages/frontend/src/i18n.ts
@@ -6,7 +6,7 @@
import { markRaw } from 'vue';
import { I18n } from '@@/js/i18n.js';
import { locale } from '@@/js/locale.js';
-import type { Locale } from '../../../locales/index.js';
+import type { Locale } from 'i18n';
export const i18n = markRaw(new I18n<Locale>(locale, _DEV_));
diff --git a/packages/frontend/test/i18n.test.ts b/packages/frontend/test/i18n.test.ts
index a51dfc6c4e..14b89f898d 100644
--- a/packages/frontend/test/i18n.test.ts
+++ b/packages/frontend/test/i18n.test.ts
@@ -5,7 +5,7 @@
import { describe, expect, it } from 'vitest';
import { I18n } from '../../frontend-shared/js/i18n.js'; // @@で参照できなかったので
-import type { ParameterizedString } from '../../../locales/index.js';
+import type { ParameterizedString } from 'i18n';
// TODO: このテストはfrontend-sharedに移動する
diff --git a/packages/frontend/test/init.ts b/packages/frontend/test/init.ts
index e38338cf95..28848f6c2f 100644
--- a/packages/frontend/test/init.ts
+++ b/packages/frontend/test/init.ts
@@ -7,13 +7,13 @@ import { vi } from 'vitest';
import createFetchMock from 'vitest-fetch-mock';
import type { Ref } from 'vue';
import { ref } from 'vue';
+// Set i18n
+import locales from 'i18n';
+import { updateI18n } from '@/i18n.js';
const fetchMocker = createFetchMock(vi);
fetchMocker.enableMocks();
-// Set i18n
-import locales from '../../../locales/index.js';
-import { updateI18n } from '@/i18n.js';
updateI18n(locales['en-US']);
// XXX: misskey-js panics if WebSocket is not defined
diff --git a/packages/frontend/vite.config.ts b/packages/frontend/vite.config.ts
index 6f320e99c9..c9c20b23ea 100644
--- a/packages/frontend/vite.config.ts
+++ b/packages/frontend/vite.config.ts
@@ -2,18 +2,18 @@ import path from 'path';
import pluginReplace from '@rollup/plugin-replace';
import pluginVue from '@vitejs/plugin-vue';
import pluginGlsl from 'vite-plugin-glsl';
-import { defineConfig } from 'vite';
import type { UserConfig } from 'vite';
+import { defineConfig } from 'vite';
import * as yaml from 'js-yaml';
import { promises as fsp } from 'fs';
-import locales from '../../locales/index.js';
+import locales from 'i18n';
import meta from '../../package.json';
import packageInfo from './package.json' with { type: 'json' };
import pluginUnwindCssModuleClassName from './lib/rollup-plugin-unwind-css-module-class-name.js';
import pluginJson5 from './vite.json5.js';
-import pluginCreateSearchIndex from './lib/vite-plugin-create-search-index.js';
import type { Options as SearchIndexOptions } from './lib/vite-plugin-create-search-index.js';
+import pluginCreateSearchIndex from './lib/vite-plugin-create-search-index.js';
import pluginWatchLocales from './lib/vite-plugin-watch-locales.js';
import { pluginRemoveUnrefI18n } from '../frontend-builder/rollup-plugin-remove-unref-i18n.js';
diff --git a/packages/i18n/README.md b/packages/i18n/README.md
new file mode 100644
index 0000000000..1013b84ea6
--- /dev/null
+++ b/packages/i18n/README.md
@@ -0,0 +1,5 @@
+# Misskey i18n
+
+Misskey の言語ファイル本体 (ja-JP.yml など) はリポジトリ直下の `/locales` に置かれており、そこから Crowdin 連携やビルド資産が生成されます。
+
+このパッケージは Misskey モノレポ内で、これらの言語ファイルを共通で扱うためのヘルパー群や型情報をまとめる位置づけです。バックエンド / フロントエンド / Service Worker など各パッケージが同じ翻訳データと型定義を利用できるようにすることを目的としており、npm での外部配布は想定していません。
diff --git a/packages/i18n/build.ts b/packages/i18n/build.ts
new file mode 100644
index 0000000000..a6bbf7dc63
--- /dev/null
+++ b/packages/i18n/build.ts
@@ -0,0 +1,163 @@
+/*
+ * SPDX-FileCopyrightText: syuilo and misskey-project
+ * SPDX-License-Identifier: AGPL-3.0-only
+ */
+
+import fs from 'node:fs';
+import { fileURLToPath } from 'node:url';
+import { dirname, resolve } from 'node:path';
+import { watch as chokidarWatch } from 'chokidar';
+import * as esbuild from 'esbuild';
+import { build } from 'esbuild';
+import { execa } from 'execa';
+import { globSync } from 'glob';
+import { generateLocaleInterface } from './scripts/generateLocaleInterface.js';
+import type { BuildOptions, BuildResult, Plugin, PluginBuild } from 'esbuild';
+
+const _filename = fileURLToPath(import.meta.url);
+const _dirname = dirname(_filename);
+const _package = JSON.parse(fs.readFileSync(_dirname + '/package.json', 'utf-8'));
+const _rootPackageDir = resolve(_dirname, '../../');
+const _rootPackage = JSON.parse(fs.readFileSync(resolve(_rootPackageDir, 'package.json'), 'utf-8'));
+const _frontendLocalesDir = resolve(_dirname, '../../built/_frontend_dist_/locales');
+const _localesDir = resolve(_rootPackageDir, 'locales');
+
+const entryPoints = globSync('./src/**/**.{ts,tsx}');
+
+const options: BuildOptions = {
+ entryPoints,
+ minify: process.env.NODE_ENV === 'production',
+ sourceRoot: 'src',
+ outdir: './built',
+ target: 'es2022',
+ platform: 'node',
+ format: 'esm',
+ sourcemap: 'linked',
+};
+
+// コマンドライン引数を取得
+const args = process.argv.slice(2).map(arg => arg.toLowerCase());
+
+// built配下をすべて削除する
+if (!args.includes('--no-clean')) {
+ fs.rmSync('./built', { recursive: true, force: true });
+}
+
+if (args.includes('--watch')) {
+ await watchSrc();
+} else {
+ await buildSrc();
+}
+
+function copyLocales(): void {
+ const srcDir = _localesDir;
+ const destDir = resolve(_dirname, 'built/locales');
+
+ fs.mkdirSync(destDir, { recursive: true });
+
+ const files = fs.readdirSync(srcDir).filter(f => f.endsWith('.yml'));
+ for (const file of files) {
+ fs.copyFileSync(resolve(srcDir, file), resolve(destDir, file));
+ }
+ console.log(`[${_package.name}] locales copied (${files.length} files).`);
+}
+
+/**
+ * フロントエンド用の locale JSON を書き出す
+ * Service Worker が HTTP 経由で取得するために必要
+ */
+async function writeFrontendLocalesJson(): Promise<void> {
+ // 動的 import でビルド済みモジュールから読み込み(循環参照回避)
+ const { writeFrontendLocalesJson: write } = await import('./built/index.js');
+ await write(_frontendLocalesDir, _rootPackage.version);
+ console.log(`[${_package.name}] frontend locales JSON written to ${_frontendLocalesDir}`);
+}
+
+async function buildSrc(): Promise<void> {
+ console.log(`[${_package.name}] start building...`);
+
+ await generateLocaleInterface(_localesDir);
+
+ await build(options)
+ .then(() => {
+ console.log(`[${_package.name}] build succeeded.`);
+ })
+ .catch((err) => {
+ process.stderr.write(err.stderr);
+ process.exit(1);
+ });
+
+ copyLocales();
+ await writeFrontendLocalesJson();
+
+ if (process.env.NODE_ENV === 'production') {
+ console.log(`[${_package.name}] skip building d.ts because NODE_ENV is production.`);
+ } else {
+ await buildDts();
+ }
+
+ console.log(`[${_package.name}] finish building.`);
+}
+
+function buildDts(): Promise<unknown> {
+ return execa(
+ 'tsc',
+ [
+ '--project', 'tsconfig.json',
+ '--rootDir', 'src',
+ '--outDir', 'built',
+ '--declaration', 'true',
+ '--emitDeclarationOnly', 'true',
+ ],
+ {
+ stdout: process.stdout,
+ stderr: process.stderr,
+ },
+ );
+}
+
+async function watchSrc(): Promise<void> {
+ const localesWatcher = chokidarWatch(_localesDir, {
+ ignoreInitial: true,
+ });
+ localesWatcher.on('all', async (event, path) => {
+ if (!path.endsWith('.yml')) return;
+ console.log(`[${_package.name}] locales changed: ${event} ${path}`);
+ copyLocales();
+ await writeFrontendLocalesJson();
+ await generateLocaleInterface(_localesDir);
+ });
+
+ const plugins: Plugin[] = [{
+ name: 'gen-dts',
+ setup(build: PluginBuild) {
+ build.onStart(() => {
+ console.log(`[${_package.name}] detect changed...`);
+ });
+ build.onEnd(async (result: BuildResult) => {
+ if (result.errors.length > 0) {
+ console.error(`[${_package.name}] watch build failed:`, result);
+ return;
+ }
+ await buildDts();
+ });
+ },
+ }];
+
+ console.log(`[${_package.name}] start watching...`);
+
+ const context = await esbuild.context({ ...options, plugins });
+ await context.watch();
+
+ await new Promise((resolve, reject) => {
+ process.on('SIGHUP', resolve);
+ process.on('SIGINT', resolve);
+ process.on('SIGTERM', resolve);
+ process.on('uncaughtException', reject);
+ process.on('exit', resolve);
+ }).finally(async () => {
+ await context.dispose();
+ await localesWatcher.close();
+ console.log(`[${_package.name}] finish watching.`);
+ });
+}
diff --git a/packages/i18n/eslint.config.js b/packages/i18n/eslint.config.js
new file mode 100644
index 0000000000..f18bc4a4be
--- /dev/null
+++ b/packages/i18n/eslint.config.js
@@ -0,0 +1,35 @@
+import tsParser from '@typescript-eslint/parser';
+import sharedConfig from '../shared/eslint.config.js';
+
+// eslint-disable-next-line import/no-default-export
+export default [
+ ...sharedConfig,
+ {
+ ignores: [
+ '**/node_modules',
+ 'built',
+ 'coverage',
+ 'vitest.config.ts',
+ 'test',
+ 'test-d',
+ 'generator',
+ ],
+ },
+ {
+ files: ['**/*.ts', '**/*.tsx'],
+ languageOptions: {
+ parserOptions: {
+ parser: tsParser,
+ project: ['./tsconfig.eslint.json'],
+ sourceType: 'module',
+ tsconfigRootDir: import.meta.dirname,
+ },
+ },
+ },
+ {
+ files: ['src/autogen/**/*.ts', 'src/autogen/**/*.tsx'],
+ rules: {
+ '@stylistic/indent': 'off',
+ },
+ },
+];
diff --git a/packages/i18n/package.json b/packages/i18n/package.json
new file mode 100644
index 0000000000..d06e485da0
--- /dev/null
+++ b/packages/i18n/package.json
@@ -0,0 +1,42 @@
+{
+ "name": "i18n",
+ "type": "module",
+ "private": true,
+ "main": "./built/index.js",
+ "types": "./built/index.d.ts",
+ "exports": {
+ ".": {
+ "types": "./built/index.d.ts",
+ "import": "./built/index.js"
+ }
+ },
+ "scripts": {
+ "generate": "tsx scripts/generateLocaleInterface.ts",
+ "verify": "tsx scripts/verify.ts",
+ "build": "tsx ./build.ts",
+ "watch": "nodemon -w package.json -e json --exec \"tsx ./build.ts --watch\"",
+ "tsd": "tsd",
+ "typecheck": "tsc --noEmit",
+ "lint": "pnpm typecheck && pnpm eslint",
+ "lint:fix": "pnpm eslint --fix"
+ },
+ "files": [
+ "built"
+ ],
+ "devDependencies": {
+ "@types/js-yaml": "4.0.9",
+ "@types/node": "24.10.1",
+ "@typescript-eslint/eslint-plugin": "8.47.0",
+ "@typescript-eslint/parser": "8.47.0",
+ "chokidar": "4.0.3",
+ "esbuild": "0.27.0",
+ "execa": "9.6.0",
+ "glob": "11.1.0",
+ "nodemon": "3.1.11",
+ "tsx": "4.20.6",
+ "typescript": "5.9.3"
+ },
+ "dependencies": {
+ "js-yaml": "4.1.1"
+ }
+}
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);
+}
diff --git a/locales/index.d.ts b/packages/i18n/src/autogen/locale.ts
index 7f0efd22e8..8f94aab555 100644
--- a/locales/index.d.ts
+++ b/packages/i18n/src/autogen/locale.ts
@@ -1,13 +1,7 @@
/* eslint-disable */
-// This file is generated by locales/generateDTS.js
+// This file is generated by scripts/generateLocaleInterface.ts
// Do not edit this file directly.
-declare const kParameters: unique symbol;
-export type ParameterizedString<T extends string = string> = string & {
- [kParameters]: T;
-};
-export interface ILocale {
- [_: string]: string | ParameterizedString | ILocale;
-}
+import { type ILocale, type ParameterizedString } from "../types.js";
export interface Locale extends ILocale {
/**
* 日本語
@@ -13089,8 +13083,3 @@ export interface Locale extends ILocale {
"mfm": string;
};
}
-declare const locales: {
- [lang: string]: Locale;
-};
-export function build(): Locale;
-export default locales;
diff --git a/packages/i18n/src/index.ts b/packages/i18n/src/index.ts
new file mode 100644
index 0000000000..e428267748
--- /dev/null
+++ b/packages/i18n/src/index.ts
@@ -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;
diff --git a/packages/i18n/src/types.ts b/packages/i18n/src/types.ts
new file mode 100644
index 0000000000..cf9568e792
--- /dev/null
+++ b/packages/i18n/src/types.ts
@@ -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;
+}
diff --git a/packages/i18n/tsconfig.eslint.json b/packages/i18n/tsconfig.eslint.json
new file mode 100644
index 0000000000..5aea206cd1
--- /dev/null
+++ b/packages/i18n/tsconfig.eslint.json
@@ -0,0 +1,8 @@
+{
+ "extends": "./tsconfig.json",
+ "include": [
+ "src/**/*.ts",
+ "scripts/**/*.ts",
+ "build.ts"
+ ]
+} \ No newline at end of file
diff --git a/packages/i18n/tsconfig.json b/packages/i18n/tsconfig.json
new file mode 100644
index 0000000000..31cd39b8ba
--- /dev/null
+++ b/packages/i18n/tsconfig.json
@@ -0,0 +1,14 @@
+{
+ "compilerOptions": {
+ "target": "ESNext",
+ "module": "NodeNext",
+ "moduleResolution": "NodeNext",
+ "esModuleInterop": true,
+ "forceConsistentCasingInFileNames": true,
+ "strict": true,
+ "skipLibCheck": true
+ },
+ "include": [
+ "src/**/*.ts"
+ ]
+} \ No newline at end of file
diff --git a/packages/sw/build.js b/packages/sw/build.js
index a9c2e428c0..d5ca028da5 100644
--- a/packages/sw/build.js
+++ b/packages/sw/build.js
@@ -7,8 +7,9 @@
import { fileURLToPath } from 'node:url';
import * as esbuild from 'esbuild';
-import locales from '../../locales/index.js';
+import locales from 'i18n';
import meta from '../../package.json' with { type: 'json' };
+
const watch = process.argv[2]?.includes('watch');
const __dirname = fileURLToPath(new URL('.', import.meta.url));
diff --git a/packages/sw/package.json b/packages/sw/package.json
index 51d78511c2..0fe10256ce 100644
--- a/packages/sw/package.json
+++ b/packages/sw/package.json
@@ -9,6 +9,7 @@
"lint": "pnpm typecheck && pnpm eslint"
},
"dependencies": {
+ "i18n": "workspace:*",
"esbuild": "0.27.0",
"idb-keyval": "6.2.2",
"misskey-js": "workspace:*"
diff --git a/packages/sw/src/scripts/lang.ts b/packages/sw/src/scripts/lang.ts
index 3000160e41..40b6aa4e7b 100644
--- a/packages/sw/src/scripts/lang.ts
+++ b/packages/sw/src/scripts/lang.ts
@@ -8,7 +8,7 @@
*/
import { get, set } from 'idb-keyval';
import { I18n } from '@@/js/i18n.js';
-import type { Locale } from '../../../../locales/index.js';
+import type { Locale } from 'i18n';
class SwLang {
public cacheName = `mk-cache-${_VERSION_}`;
diff --git a/packages/sw/src/sw.ts b/packages/sw/src/sw.ts
index 298af4b4b6..5cece73401 100644
--- a/packages/sw/src/sw.ts
+++ b/packages/sw/src/sw.ts
@@ -7,7 +7,7 @@ import { get } from 'idb-keyval';
import * as Misskey from 'misskey-js';
import type { PushNotificationDataMap } from '@/types.js';
import type { I18n } from '@@/js/i18n.js';
-import type { Locale } from '../../../locales/index.js';
+import type { Locale } from 'i18n';
import { createEmptyNotification, createNotification } from '@/scripts/create-notification.js';
import { swLang } from '@/scripts/lang.js';
import * as swos from '@/scripts/operations.js';
diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml
index 3793bb79c1..02a6124d7d 100644
--- a/pnpm-lock.yaml
+++ b/pnpm-lock.yaml
@@ -77,6 +77,9 @@ importers:
globals:
specifier: 16.5.0
version: 16.5.0
+ i18n:
+ specifier: workspace:*
+ version: link:packages/i18n
ncp:
specifier: 2.0.0
version: 2.0.0
@@ -785,6 +788,9 @@ importers:
frontend-shared:
specifier: workspace:*
version: link:../frontend-shared
+ i18n:
+ specifier: workspace:*
+ version: link:../i18n
icons-subsetter:
specifier: workspace:*
version: link:../icons-subsetter
@@ -1086,6 +1092,9 @@ importers:
estree-walker:
specifier: 3.0.3
version: 3.0.3
+ i18n:
+ specifier: workspace:*
+ version: link:../i18n
magic-string:
specifier: 0.30.21
version: 0.30.21
@@ -1147,6 +1156,9 @@ importers:
frontend-shared:
specifier: workspace:*
version: link:../frontend-shared
+ i18n:
+ specifier: workspace:*
+ version: link:../i18n
icons-subsetter:
specifier: workspace:*
version: link:../icons-subsetter
@@ -1286,6 +1298,9 @@ importers:
packages/frontend-shared:
dependencies:
+ i18n:
+ specifier: workspace:*
+ version: link:../i18n
misskey-js:
specifier: workspace:*
version: link:../misskey-js
@@ -1318,6 +1333,46 @@ importers:
specifier: 10.2.0
version: 10.2.0(eslint@9.39.1)
+ packages/i18n:
+ dependencies:
+ js-yaml:
+ specifier: 4.1.1
+ version: 4.1.1
+ devDependencies:
+ '@types/js-yaml':
+ specifier: 4.0.9
+ version: 4.0.9
+ '@types/node':
+ specifier: 24.10.1
+ version: 24.10.1
+ '@typescript-eslint/eslint-plugin':
+ specifier: 8.47.0
+ version: 8.47.0(@typescript-eslint/parser@8.47.0(eslint@9.39.1)(typescript@5.9.3))(eslint@9.39.1)(typescript@5.9.3)
+ '@typescript-eslint/parser':
+ specifier: 8.47.0
+ version: 8.47.0(eslint@9.39.1)(typescript@5.9.3)
+ chokidar:
+ specifier: 4.0.3
+ version: 4.0.3
+ esbuild:
+ specifier: 0.27.0
+ version: 0.27.0
+ execa:
+ specifier: 9.6.0
+ version: 9.6.0
+ glob:
+ specifier: 11.1.0
+ version: 11.1.0
+ nodemon:
+ specifier: 3.1.11
+ version: 3.1.11
+ tsx:
+ specifier: 4.20.6
+ version: 4.20.6
+ typescript:
+ specifier: 5.9.3
+ version: 5.9.3
+
packages/icons-subsetter:
dependencies:
'@tabler/icons-webfont':
@@ -1519,6 +1574,9 @@ importers:
esbuild:
specifier: 0.27.0
version: 0.27.0
+ i18n:
+ specifier: workspace:*
+ version: link:../i18n
idb-keyval:
specifier: 6.2.2
version: 6.2.2
diff --git a/pnpm-workspace.yaml b/pnpm-workspace.yaml
index c5607f7533..9d43518ff6 100644
--- a/pnpm-workspace.yaml
+++ b/pnpm-workspace.yaml
@@ -4,6 +4,7 @@ packages:
- packages/frontend
- packages/frontend-builder
- packages/frontend-embed
+ - packages/i18n
- packages/icons-subsetter
- packages/sw
- packages/misskey-js
diff --git a/scripts/build-assets.mjs b/scripts/build-assets.mjs
index e610a72380..34883e3513 100644
--- a/scripts/build-assets.mjs
+++ b/scripts/build-assets.mjs
@@ -11,9 +11,7 @@ import * as yaml from 'js-yaml';
import postcss from 'postcss';
import * as terser from 'terser';
-import { build as buildLocales } from '../locales/index.js';
-import generateDTS from '../locales/generateDTS.js';
-import meta from '../package.json' with { type: "json" };
+import { locales } from 'i18n';
import buildTarball from './tarball.mjs';
const configDir = fileURLToPath(new URL('../.config', import.meta.url));
@@ -23,86 +21,59 @@ const configPath = process.env.MISSKEY_CONFIG_YML
? path.resolve(configDir, 'test.yml')
: path.resolve(configDir, 'default.yml');
-let locales = buildLocales();
-
async function loadConfig() {
return fs.readFile(configPath, 'utf-8').then(data => yaml.load(data)).catch(() => null);
}
async function copyFrontendFonts() {
- await fs.cp('./packages/frontend/node_modules/three/examples/fonts', './built/_frontend_dist_/fonts', { dereference: true, recursive: true });
-}
-
-async function copyFrontendLocales() {
- generateDTS();
-
- await fs.mkdir('./built/_frontend_dist_/locales', { recursive: true });
-
- const v = { '_version_': meta.version };
-
- for (const [lang, locale] of Object.entries(locales)) {
- await fs.writeFile(`./built/_frontend_dist_/locales/${lang}.${meta.version}.json`, JSON.stringify({ ...locale, ...v }), 'utf-8');
- }
+ await fs.cp('./packages/frontend/node_modules/three/examples/fonts', './built/_frontend_dist_/fonts', { dereference: true, recursive: true });
}
async function copyBackendViews() {
- await fs.cp('./packages/backend/src/server/web/views', './packages/backend/built/server/web/views', { recursive: true });
+ await fs.cp('./packages/backend/src/server/web/views', './packages/backend/built/server/web/views', { recursive: true });
}
async function buildBackendScript() {
- await fs.mkdir('./packages/backend/built/server/web', { recursive: true });
+ await fs.mkdir('./packages/backend/built/server/web', { recursive: true });
- for (const file of [
- './packages/backend/src/server/web/boot.js',
- './packages/backend/src/server/web/boot.embed.js',
- './packages/backend/src/server/web/bios.js',
- './packages/backend/src/server/web/cli.js',
- './packages/backend/src/server/web/error.js',
- ]) {
- let source = await fs.readFile(file, { encoding: 'utf-8' });
- source = source.replaceAll('LANGS', JSON.stringify(Object.keys(locales)));
- const { code } = await terser.minify(source, { toplevel: true });
- await fs.writeFile(`./packages/backend/built/server/web/${path.basename(file)}`, code);
- }
+ for (const file of [
+ './packages/backend/src/server/web/boot.js',
+ './packages/backend/src/server/web/boot.embed.js',
+ './packages/backend/src/server/web/bios.js',
+ './packages/backend/src/server/web/cli.js',
+ './packages/backend/src/server/web/error.js',
+ ]) {
+ let source = await fs.readFile(file, { encoding: 'utf-8' });
+ source = source.replaceAll('LANGS', JSON.stringify(Object.keys(locales)));
+ const { code } = await terser.minify(source, { toplevel: true });
+ await fs.writeFile(`./packages/backend/built/server/web/${path.basename(file)}`, code);
+ }
}
async function buildBackendStyle() {
- await fs.mkdir('./packages/backend/built/server/web', { recursive: true });
+ await fs.mkdir('./packages/backend/built/server/web', { recursive: true });
- for (const file of [
- './packages/backend/src/server/web/style.css',
- './packages/backend/src/server/web/style.embed.css',
- './packages/backend/src/server/web/bios.css',
- './packages/backend/src/server/web/cli.css',
- './packages/backend/src/server/web/error.css'
- ]) {
- const source = await fs.readFile(file, { encoding: 'utf-8' });
- const { css } = await postcss([cssnano({ zindex: false })]).process(source, { from: undefined });
- await fs.writeFile(`./packages/backend/built/server/web/${path.basename(file)}`, css);
- }
+ for (const file of [
+ './packages/backend/src/server/web/style.css',
+ './packages/backend/src/server/web/style.embed.css',
+ './packages/backend/src/server/web/bios.css',
+ './packages/backend/src/server/web/cli.css',
+ './packages/backend/src/server/web/error.css'
+ ]) {
+ const source = await fs.readFile(file, { encoding: 'utf-8' });
+ const { css } = await postcss([cssnano({ zindex: false })]).process(source, { from: undefined });
+ await fs.writeFile(`./packages/backend/built/server/web/${path.basename(file)}`, css);
+ }
}
async function build() {
- await Promise.all([
- copyFrontendFonts(),
- copyFrontendLocales(),
- copyBackendViews(),
- buildBackendScript(),
- buildBackendStyle(),
+ await Promise.all([
+ copyFrontendFonts(),
+ copyBackendViews(),
+ buildBackendScript(),
+ buildBackendStyle(),
loadConfig().then(config => config?.publishTarballInsteadOfProvideRepositoryUrl && buildTarball()),
- ]);
+ ]);
}
await build();
-
-if (process.argv.includes('--watch')) {
- const watcher = fs.watch('./locales');
- for await (const event of watcher) {
- const filename = event.filename?.replaceAll('\\', '/');
- if (/^[a-z]+-[A-Z]+\.yml/.test(filename)) {
- console.log(`update ${filename} ...`)
- locales = buildLocales();
- await copyFrontendLocales()
- }
- }
-}
diff --git a/scripts/clean-all.js b/scripts/clean-all.js
index 5a8f9eba23..839ea3ba1c 100644
--- a/scripts/clean-all.js
+++ b/scripts/clean-all.js
@@ -24,6 +24,9 @@ const fs = require('fs');
fs.rmSync(__dirname + '/../packages/sw/built', { recursive: true, force: true });
fs.rmSync(__dirname + '/../packages/sw/node_modules', { recursive: true, force: true });
+ fs.rmSync(__dirname + '/../packages/i18n/built', { recursive: true, force: true });
+ fs.rmSync(__dirname + '/../packages/i18n/node_modules', { recursive: true, force: true });
+
fs.rmSync(__dirname + '/../packages/misskey-js/built', { recursive: true, force: true });
fs.rmSync(__dirname + '/../packages/misskey-js/node_modules', { recursive: true, force: true });
diff --git a/scripts/clean.js b/scripts/clean.js
index 69a8df76af..5cce8bacab 100644
--- a/scripts/clean.js
+++ b/scripts/clean.js
@@ -11,6 +11,7 @@ const fs = require('fs');
fs.rmSync(__dirname + '/../packages/frontend/built', { recursive: true, force: true });
fs.rmSync(__dirname + '/../packages/frontend-embed/built', { recursive: true, force: true });
fs.rmSync(__dirname + '/../packages/icons-subsetter/built', { recursive: true, force: true });
+ fs.rmSync(__dirname + '/../packages/i18n/built', { recursive: true, force: true });
fs.rmSync(__dirname + '/../packages/sw/built', { recursive: true, force: true });
fs.rmSync(__dirname + '/../packages/misskey-js/built', { recursive: true, force: true });
fs.rmSync(__dirname + '/../packages/misskey-reversi/built', { recursive: true, force: true });
diff --git a/scripts/dev.mjs b/scripts/dev.mjs
index e500510b9e..b54004132a 100644
--- a/scripts/dev.mjs
+++ b/scripts/dev.mjs
@@ -16,6 +16,13 @@ await execa('pnpm', ['clean'], {
stderr: process.stderr,
});
+// アセットのビルドで依存しているので一番最初に必要
+await execa('pnpm', ['--filter', 'i18n', 'build'], {
+ cwd: _dirname + '/../',
+ stdout: process.stdout,
+ stderr: process.stderr,
+});
+
await Promise.all([
execa('pnpm', ['build-pre'], {
cwd: _dirname + '/../',
@@ -38,6 +45,11 @@ await Promise.all([
stdout: process.stdout,
stderr: process.stderr,
}),
+ execa('pnpm', ['--filter', 'misskey-js', 'build'], {
+ cwd: _dirname + '/../',
+ stdout: process.stdout,
+ stderr: process.stderr,
+ }),
]);
execa('pnpm', ['build-pre', '--watch'], {
@@ -88,6 +100,12 @@ execa('pnpm', ['--filter', 'misskey-js', 'watch', '--no-clean'], {
stderr: process.stderr,
});
+execa('pnpm', ['--filter', 'i18n', 'watch', '--no-clean'], {
+ cwd: _dirname + '/../',
+ stdout: process.stdout,
+ stderr: process.stderr,
+});
+
execa('pnpm', ['--filter', 'misskey-reversi', 'watch', '--no-clean'], {
cwd: _dirname + '/../',
stdout: process.stdout,