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:
parent
32b5583432
commit
fe01a5a28f
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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:
|
||||
|
|
@ -22,7 +24,10 @@ jobs:
|
|||
uses: pnpm/action-setup@v4.2.0
|
||||
- uses: actions/setup-node@v4.4.0
|
||||
with:
|
||||
node-version-file: '.node-version'
|
||||
cache: 'pnpm'
|
||||
node-version-file: ".node-version"
|
||||
cache: "pnpm"
|
||||
- run: pnpm i --frozen-lockfile
|
||||
- run: cd locales && node verify.js
|
||||
- run: pnpm --filter i18n build
|
||||
- name: Verify Locales
|
||||
working-directory: ./packages/i18n
|
||||
run: pnpm run verify
|
||||
|
|
|
|||
|
|
@ -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 . ./
|
||||
|
||||
|
|
|
|||
|
|
@ -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');
|
||||
}
|
||||
|
|
@ -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();
|
||||
|
|
@ -1,3 +0,0 @@
|
|||
{
|
||||
"type": "module"
|
||||
}
|
||||
|
|
@ -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);
|
||||
}
|
||||
17
package.json
17
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",
|
||||
|
|
|
|||
|
|
@ -202,7 +202,7 @@ export class NotificationService implements OnApplicationShutdown {
|
|||
}
|
||||
|
||||
// TODO
|
||||
//const locales = await import('../../../../locales/index.js');
|
||||
//const locales = await import('i18n');
|
||||
|
||||
// TODO: locale ファイルをクライアント用とサーバー用で分けたい
|
||||
|
||||
|
|
|
|||
|
|
@ -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 {
|
||||
|
|
|
|||
|
|
@ -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';
|
||||
|
||||
|
|
|
|||
|
|
@ -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"
|
||||
|
|
|
|||
|
|
@ -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';
|
||||
|
||||
|
|
|
|||
|
|
@ -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",
|
||||
|
|
|
|||
|
|
@ -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;
|
||||
|
|
|
|||
|
|
@ -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_));
|
||||
|
||||
|
|
|
|||
|
|
@ -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';
|
||||
|
|
|
|||
|
|
@ -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;
|
||||
|
|
|
|||
|
|
@ -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);
|
||||
|
|
|
|||
|
|
@ -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'] };
|
||||
|
||||
|
|
|
|||
|
|
@ -34,6 +34,7 @@
|
|||
"js-built"
|
||||
],
|
||||
"dependencies": {
|
||||
"i18n": "workspace:*",
|
||||
"misskey-js": "workspace:*",
|
||||
"vue": "3.5.24"
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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),
|
||||
|
|
|
|||
|
|
@ -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';
|
||||
|
||||
|
|
|
|||
|
|
@ -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')
|
||||
|
||||
|
|
|
|||
|
|
@ -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",
|
||||
|
|
|
|||
|
|
@ -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;
|
||||
|
|
|
|||
|
|
@ -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_));
|
||||
|
||||
|
|
|
|||
|
|
@ -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に移動する
|
||||
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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';
|
||||
|
||||
|
|
|
|||
|
|
@ -0,0 +1,5 @@
|
|||
# Misskey i18n
|
||||
|
||||
Misskey の言語ファイル本体 (ja-JP.yml など) はリポジトリ直下の `/locales` に置かれており、そこから Crowdin 連携やビルド資産が生成されます。
|
||||
|
||||
このパッケージは Misskey モノレポ内で、これらの言語ファイルを共通で扱うためのヘルパー群や型情報をまとめる位置づけです。バックエンド / フロントエンド / Service Worker など各パッケージが同じ翻訳データと型定義を利用できるようにすることを目的としており、npm での外部配布は想定していません。
|
||||
|
|
@ -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.`);
|
||||
});
|
||||
}
|
||||
|
|
@ -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',
|
||||
},
|
||||
},
|
||||
];
|
||||
|
|
@ -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"
|
||||
}
|
||||
}
|
||||
|
|
@ -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'));
|
||||
}
|
||||
|
|
@ -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);
|
||||
}
|
||||
|
|
@ -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;
|
||||
|
|
@ -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;
|
||||
|
|
@ -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;
|
||||
}
|
||||
|
|
@ -0,0 +1,8 @@
|
|||
{
|
||||
"extends": "./tsconfig.json",
|
||||
"include": [
|
||||
"src/**/*.ts",
|
||||
"scripts/**/*.ts",
|
||||
"build.ts"
|
||||
]
|
||||
}
|
||||
|
|
@ -0,0 +1,14 @@
|
|||
{
|
||||
"compilerOptions": {
|
||||
"target": "ESNext",
|
||||
"module": "NodeNext",
|
||||
"moduleResolution": "NodeNext",
|
||||
"esModuleInterop": true,
|
||||
"forceConsistentCasingInFileNames": true,
|
||||
"strict": true,
|
||||
"skipLibCheck": true
|
||||
},
|
||||
"include": [
|
||||
"src/**/*.ts"
|
||||
]
|
||||
}
|
||||
|
|
@ -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));
|
||||
|
|
|
|||
|
|
@ -9,6 +9,7 @@
|
|||
"lint": "pnpm typecheck && pnpm eslint"
|
||||
},
|
||||
"dependencies": {
|
||||
"i18n": "workspace:*",
|
||||
"esbuild": "0.27.0",
|
||||
"idb-keyval": "6.2.2",
|
||||
"misskey-js": "workspace:*"
|
||||
|
|
|
|||
|
|
@ -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_}`;
|
||||
|
|
|
|||
|
|
@ -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';
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -4,6 +4,7 @@ packages:
|
|||
- packages/frontend
|
||||
- packages/frontend-builder
|
||||
- packages/frontend-embed
|
||||
- packages/i18n
|
||||
- packages/icons-subsetter
|
||||
- packages/sw
|
||||
- packages/misskey-js
|
||||
|
|
|
|||
|
|
@ -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,8 +21,6 @@ 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);
|
||||
}
|
||||
|
|
@ -33,18 +29,6 @@ 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');
|
||||
}
|
||||
}
|
||||
|
||||
async function copyBackendViews() {
|
||||
await fs.cp('./packages/backend/src/server/web/views', './packages/backend/built/server/web/views', { recursive: true });
|
||||
}
|
||||
|
|
@ -85,7 +69,6 @@ async function buildBackendStyle() {
|
|||
async function build() {
|
||||
await Promise.all([
|
||||
copyFrontendFonts(),
|
||||
copyFrontendLocales(),
|
||||
copyBackendViews(),
|
||||
buildBackendScript(),
|
||||
buildBackendStyle(),
|
||||
|
|
@ -94,15 +77,3 @@ async function build() {
|
|||
}
|
||||
|
||||
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()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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 });
|
||||
|
||||
|
|
|
|||
|
|
@ -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 });
|
||||
|
|
|
|||
|
|
@ -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,
|
||||
|
|
|
|||
Loading…
Reference in New Issue