@bleedingdev/modern-js-create 3.2.0-ultramodern.10 → 3.2.0-ultramodern.12
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/index.js +249 -146
- package/package.json +3 -3
- package/template/AGENTS.md +5 -0
- package/template/config/public/locales/cs/translation.json +39 -0
- package/template/config/public/locales/en/translation.json +39 -0
- package/template/modern.config.ts.handlebars +8 -1
- package/template/package.json.handlebars +8 -3
- package/template/scripts/check-i18n-strings.mjs +83 -0
- package/template/scripts/validate-ultramodern.mjs.handlebars +12 -0
- package/template/src/modern.runtime.ts.handlebars +17 -1
- package/template/src/routes/page.tsx.handlebars +45 -26
- package/template-workspace/AGENTS.md +6 -1
- package/template-workspace/oxfmt.config.ts +12 -3
- package/template-workspace/oxlint.config.ts +11 -4
- package/template-workspace/scripts/bootstrap-agent-skills.mjs +17 -28
- package/template-workspace/scripts/check-i18n-strings.mjs +83 -0
- package/template-workspace/scripts/validate-ultramodern-workspace.mjs.handlebars +121 -91
package/package.json
CHANGED
|
@@ -21,7 +21,7 @@
|
|
|
21
21
|
"engines": {
|
|
22
22
|
"node": ">=20"
|
|
23
23
|
},
|
|
24
|
-
"version": "3.2.0-ultramodern.
|
|
24
|
+
"version": "3.2.0-ultramodern.12",
|
|
25
25
|
"types": "./dist/types/index.d.ts",
|
|
26
26
|
"main": "./dist/index.js",
|
|
27
27
|
"bin": {
|
|
@@ -41,7 +41,7 @@
|
|
|
41
41
|
"@types/node": "^25.8.0",
|
|
42
42
|
"@typescript/native-preview": "7.0.0-dev.20260516.1",
|
|
43
43
|
"tsx": "^4.22.0",
|
|
44
|
-
"@modern-js/i18n-utils": "npm:@bleedingdev/modern-js-i18n-utils@3.2.0-ultramodern.
|
|
44
|
+
"@modern-js/i18n-utils": "npm:@bleedingdev/modern-js-i18n-utils@3.2.0-ultramodern.12"
|
|
45
45
|
},
|
|
46
46
|
"publishConfig": {
|
|
47
47
|
"registry": "https://registry.npmjs.org/",
|
|
@@ -54,6 +54,6 @@
|
|
|
54
54
|
"start": "node ./dist/index.js"
|
|
55
55
|
},
|
|
56
56
|
"ultramodern": {
|
|
57
|
-
"frameworkVersion": "3.2.0-ultramodern.
|
|
57
|
+
"frameworkVersion": "3.2.0-ultramodern.12"
|
|
58
58
|
}
|
|
59
59
|
}
|
package/template/AGENTS.md
CHANGED
|
@@ -7,8 +7,13 @@ This project is generated for Codex-first UltraModern.js work.
|
|
|
7
7
|
- `pnpm lint` runs Oxlint with the Ultracite preset.
|
|
8
8
|
- `pnpm format` runs oxfmt.
|
|
9
9
|
- `pnpm typecheck` runs effect-tsgo as the TypeScript checker.
|
|
10
|
+
- `pnpm i18n:check` rejects hardcoded user-visible JSX text.
|
|
10
11
|
- `pnpm ultramodern:check` verifies the generated contract.
|
|
11
12
|
|
|
13
|
+
## Internationalization
|
|
14
|
+
|
|
15
|
+
Runtime i18n is enabled by default. Agents must put user-visible UI copy in `config/public/locales/<lang>/translation.json` and render it through `react-i18next` or `@modern-js/plugin-i18n/runtime`. Do not add hardcoded JSX text, `aria-label`, `title`, `alt`, or `placeholder` strings unless the value is a non-translatable technical token.
|
|
16
|
+
|
|
12
17
|
## Private Skills
|
|
13
18
|
|
|
14
19
|
Private orchestration skills are not vendored into this template. If you are authorized for `TechsioCZ/skills`, run:
|
|
@@ -0,0 +1,39 @@
|
|
|
1
|
+
{
|
|
2
|
+
"home": {
|
|
3
|
+
"bff": {
|
|
4
|
+
"response": "Odpoved Effect HttpApi:"
|
|
5
|
+
},
|
|
6
|
+
"cards": {
|
|
7
|
+
"bff": {
|
|
8
|
+
"body": "Pouzivej Effect jako hlavni BFF cestu, Hono nech jako explicitni zalozni volbu.",
|
|
9
|
+
"title": "BFF + Effect"
|
|
10
|
+
},
|
|
11
|
+
"config": {
|
|
12
|
+
"body": "Upravuj vygenerovane vychozi hodnoty v modern.config.ts.",
|
|
13
|
+
"title": "Konfigurace presetUltramodern"
|
|
14
|
+
},
|
|
15
|
+
"gates": {
|
|
16
|
+
"body": "Starter obsahuje PR workflow pro ultramodern:check a build.",
|
|
17
|
+
"title": "Ultramodern kontroly"
|
|
18
|
+
},
|
|
19
|
+
"guide": {
|
|
20
|
+
"body": "Projdi si verejny preset pripraveny pro MV, TanStack a Effect.",
|
|
21
|
+
"title": "UltraModern.js pruvodce"
|
|
22
|
+
}
|
|
23
|
+
},
|
|
24
|
+
"description": {
|
|
25
|
+
"afterConfig": ", udrzuj",
|
|
26
|
+
"afterPreset": "profil. Zacni v",
|
|
27
|
+
"end": "zelene a lad vygenerovany preset jen tam, kde aplikace potrebuje mekci cestu.",
|
|
28
|
+
"intro": "Tento starter prinasi verejny"
|
|
29
|
+
},
|
|
30
|
+
"language": {
|
|
31
|
+
"cs": "Cestina",
|
|
32
|
+
"en": "Anglictina",
|
|
33
|
+
"switcher": "Jazyk"
|
|
34
|
+
},
|
|
35
|
+
"logoAlt": "Logo UltraModern.js",
|
|
36
|
+
"name": "presetUltramodern",
|
|
37
|
+
"title": "UltraModern.js 3.0"
|
|
38
|
+
}
|
|
39
|
+
}
|
|
@@ -0,0 +1,39 @@
|
|
|
1
|
+
{
|
|
2
|
+
"home": {
|
|
3
|
+
"bff": {
|
|
4
|
+
"response": "Effect HttpApi response:"
|
|
5
|
+
},
|
|
6
|
+
"cards": {
|
|
7
|
+
"bff": {
|
|
8
|
+
"body": "Keep Effect as the preferred BFF lane while Hono stays an explicit fallback.",
|
|
9
|
+
"title": "BFF + Effect"
|
|
10
|
+
},
|
|
11
|
+
"config": {
|
|
12
|
+
"body": "Tune the generated defaults in modern.config.ts.",
|
|
13
|
+
"title": "Configure presetUltramodern"
|
|
14
|
+
},
|
|
15
|
+
"gates": {
|
|
16
|
+
"body": "The starter includes a PR workflow for ultramodern:check and build.",
|
|
17
|
+
"title": "Ultramodern Gates"
|
|
18
|
+
},
|
|
19
|
+
"guide": {
|
|
20
|
+
"body": "Review the MV-first, TanStack-ready, Effect-ready public preset.",
|
|
21
|
+
"title": "UltraModern.js Guide"
|
|
22
|
+
}
|
|
23
|
+
},
|
|
24
|
+
"description": {
|
|
25
|
+
"afterConfig": ", keep",
|
|
26
|
+
"afterPreset": "profile. Start in",
|
|
27
|
+
"end": "green, and tune the generated preset only where your app needs a softer lane.",
|
|
28
|
+
"intro": "This starter ships the public"
|
|
29
|
+
},
|
|
30
|
+
"language": {
|
|
31
|
+
"cs": "Czech",
|
|
32
|
+
"en": "English",
|
|
33
|
+
"switcher": "Language"
|
|
34
|
+
},
|
|
35
|
+
"logoAlt": "UltraModern.js Logo",
|
|
36
|
+
"name": "presetUltramodern",
|
|
37
|
+
"title": "UltraModern.js 3.0"
|
|
38
|
+
}
|
|
39
|
+
}
|
|
@@ -2,7 +2,8 @@
|
|
|
2
2
|
import { appTools, defineConfig, presetUltramodern } from '@modern-js/app-tools';
|
|
3
3
|
import path from 'node:path';
|
|
4
4
|
{{#if enableBff}}import { bffPlugin } from '@modern-js/plugin-bff';
|
|
5
|
-
{{/if}}
|
|
5
|
+
{{/if}}import { i18nPlugin } from '@modern-js/plugin-i18n';
|
|
6
|
+
{{#if isTanstackRouter}}import { tanstackRouterPlugin } from '@modern-js/plugin-tanstack';
|
|
6
7
|
{{/if}}
|
|
7
8
|
const appId = process.env['MODERN_BASELINE_APP_ID'] || path.basename(process.cwd());
|
|
8
9
|
const enableModuleFederationSSR = process.env['MODERN_BASELINE_ENABLE_MF_SSR'] !== 'false';
|
|
@@ -27,6 +28,12 @@ export default defineConfig(
|
|
|
27
28
|
|
|
28
29
|
{{/if}} plugins: [
|
|
29
30
|
appTools(),
|
|
31
|
+
i18nPlugin({
|
|
32
|
+
localeDetection: {
|
|
33
|
+
fallbackLanguage: 'en',
|
|
34
|
+
languages: ['en', 'cs'],
|
|
35
|
+
},
|
|
36
|
+
}),
|
|
30
37
|
{{#if isTanstackRouter}}
|
|
31
38
|
tanstackRouterPlugin(),
|
|
32
39
|
{{/if}}{{#if enableBff}}
|
|
@@ -8,9 +8,10 @@
|
|
|
8
8
|
"build": "modern build",
|
|
9
9
|
"serve": "modern serve",
|
|
10
10
|
"typecheck": "node -e \"const fs = require('node:fs'); const { execFileSync, spawnSync } = require('node:child_process'); const bin = execFileSync('effect-tsgo', ['get-exe-path'], { encoding: 'utf8' }).trim(); if (process.platform !== 'win32') fs.chmodSync(bin, 0o755); const result = spawnSync(bin, ['--noEmit', '-p', 'tsconfig.json'], { stdio: 'inherit' }); process.exit(result.status ?? 1);\"",
|
|
11
|
+
"i18n:check": "node ./scripts/check-i18n-strings.mjs",
|
|
11
12
|
"skills:install": "node ./scripts/bootstrap-agent-skills.mjs",
|
|
12
13
|
"skills:check": "node ./scripts/bootstrap-agent-skills.mjs --check",
|
|
13
|
-
"ultramodern:check": "{{#unless isSubproject}}pnpm format:check && pnpm lint && {{/unless}}pnpm typecheck{{#unless isSubproject}} && pnpm skills:check{{/unless}} && node ./scripts/validate-ultramodern.mjs"{{#unless isSubproject}},
|
|
14
|
+
"ultramodern:check": "{{#unless isSubproject}}pnpm format:check && pnpm lint && {{/unless}}pnpm typecheck && pnpm i18n:check{{#unless isSubproject}} && pnpm skills:check{{/unless}} && node ./scripts/validate-ultramodern.mjs"{{#unless isSubproject}},
|
|
14
15
|
"format": "oxfmt .",
|
|
15
16
|
"format:check": "oxfmt --check .",
|
|
16
17
|
"lint": "oxlint .",
|
|
@@ -18,12 +19,16 @@
|
|
|
18
19
|
"prepare": "simple-git-hooks"{{/unless}}
|
|
19
20
|
},
|
|
20
21
|
"dependencies": {
|
|
22
|
+
"@modern-js/plugin-i18n": "{{pluginI18nVersion}}",
|
|
21
23
|
{{#if isTanstackRouter}} "@modern-js/plugin-tanstack": "{{pluginTanstackVersion}}",
|
|
22
|
-
{{/if}}
|
|
24
|
+
{{/if}}
|
|
25
|
+
"@modern-js/runtime": "{{runtimeVersion}}",
|
|
23
26
|
{{#if isTanstackRouter}} "@tanstack/react-router": "1.170.1",
|
|
24
27
|
{{/if}}
|
|
28
|
+
"i18next": "26.2.0",
|
|
25
29
|
"react": "^19.2.3",
|
|
26
|
-
"react-dom": "^19.2.0"
|
|
30
|
+
"react-dom": "^19.2.0",
|
|
31
|
+
"react-i18next": "17.0.8"
|
|
27
32
|
},
|
|
28
33
|
"devDependencies": {
|
|
29
34
|
"@effect/tsgo": "0.7.3",
|
|
@@ -0,0 +1,83 @@
|
|
|
1
|
+
import fs from 'node:fs';
|
|
2
|
+
import path from 'node:path';
|
|
3
|
+
|
|
4
|
+
const root = process.cwd();
|
|
5
|
+
const scanRoots = ['src'].map((scanRoot) => path.join(root, scanRoot));
|
|
6
|
+
const ignoredDirectories = new Set(['.modern', '.modernjs', 'dist', 'node_modules']);
|
|
7
|
+
const visibleAttributePattern =
|
|
8
|
+
/\s(?:aria-label|alt|placeholder|title)=["']([^"']*[A-Za-z][^"']*)["']/gu;
|
|
9
|
+
const jsxTextPattern = />([^<>{}]*[A-Za-z][^<>{}]*)</gu;
|
|
10
|
+
|
|
11
|
+
const collectFiles = (directory) => {
|
|
12
|
+
if (!fs.existsSync(directory)) {
|
|
13
|
+
return [];
|
|
14
|
+
}
|
|
15
|
+
|
|
16
|
+
const files = [];
|
|
17
|
+
for (const entry of fs.readdirSync(directory, { withFileTypes: true })) {
|
|
18
|
+
if (entry.isDirectory()) {
|
|
19
|
+
if (!ignoredDirectories.has(entry.name)) {
|
|
20
|
+
files.push(...collectFiles(path.join(directory, entry.name)));
|
|
21
|
+
}
|
|
22
|
+
continue;
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
if (entry.isFile() && /\.(jsx|tsx)$/u.test(entry.name) && !entry.name.endsWith('.d.ts')) {
|
|
26
|
+
files.push(path.join(directory, entry.name));
|
|
27
|
+
}
|
|
28
|
+
}
|
|
29
|
+
return files;
|
|
30
|
+
};
|
|
31
|
+
|
|
32
|
+
const lineNumberForIndex = (content, index) => content.slice(0, index).split('\n').length;
|
|
33
|
+
const isIgnoredLine = (content, index) => {
|
|
34
|
+
const lineStart = content.lastIndexOf('\n', index) + 1;
|
|
35
|
+
const lineEnd = content.indexOf('\n', index);
|
|
36
|
+
const currentLineEnd = lineEnd === -1 ? content.length : lineEnd;
|
|
37
|
+
const previousLineStart = content.lastIndexOf('\n', Math.max(0, lineStart - 2)) + 1;
|
|
38
|
+
const nextLineEnd = content.indexOf('\n', currentLineEnd + 1);
|
|
39
|
+
const context = content.slice(
|
|
40
|
+
previousLineStart,
|
|
41
|
+
nextLineEnd === -1 ? content.length : nextLineEnd,
|
|
42
|
+
);
|
|
43
|
+
return /i18n-ignore/u.test(context);
|
|
44
|
+
};
|
|
45
|
+
|
|
46
|
+
const violations = [];
|
|
47
|
+
for (const filePath of scanRoots.flatMap(collectFiles)) {
|
|
48
|
+
const content = fs.readFileSync(filePath, 'utf-8');
|
|
49
|
+
for (const match of content.matchAll(visibleAttributePattern)) {
|
|
50
|
+
if (!isIgnoredLine(content, match.index ?? 0)) {
|
|
51
|
+
violations.push({
|
|
52
|
+
filePath,
|
|
53
|
+
line: lineNumberForIndex(content, match.index ?? 0),
|
|
54
|
+
text: match[1].trim(),
|
|
55
|
+
});
|
|
56
|
+
}
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
for (const match of content.matchAll(jsxTextPattern)) {
|
|
60
|
+
const text = match[1].replaceAll(/\s+/gu, ' ').trim();
|
|
61
|
+
if (text && !isIgnoredLine(content, match.index ?? 0)) {
|
|
62
|
+
violations.push({
|
|
63
|
+
filePath,
|
|
64
|
+
line: lineNumberForIndex(content, match.index ?? 0),
|
|
65
|
+
text,
|
|
66
|
+
});
|
|
67
|
+
}
|
|
68
|
+
}
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
if (violations.length > 0) {
|
|
72
|
+
console.error('Hardcoded user-visible JSX strings found. Move copy to locale JSON files.');
|
|
73
|
+
for (const violation of violations) {
|
|
74
|
+
console.error(
|
|
75
|
+
`${path.relative(root, violation.filePath)}:${violation.line} ${JSON.stringify(
|
|
76
|
+
violation.text,
|
|
77
|
+
)}`,
|
|
78
|
+
);
|
|
79
|
+
}
|
|
80
|
+
process.exit(1);
|
|
81
|
+
}
|
|
82
|
+
|
|
83
|
+
console.log('No hardcoded user-visible JSX strings found.');
|
|
@@ -16,6 +16,7 @@ const requiredTokens = [
|
|
|
16
16
|
'enableModuleFederationSSR',
|
|
17
17
|
'enableBffRequestId',
|
|
18
18
|
'enableTelemetryExporters',
|
|
19
|
+
'i18nPlugin(',
|
|
19
20
|
];
|
|
20
21
|
const missing = requiredTokens.filter((token) => !content.includes(token));
|
|
21
22
|
|
|
@@ -51,6 +52,9 @@ const requiredPaths = [
|
|
|
51
52
|
'oxlint.config.ts',
|
|
52
53
|
'oxfmt.config.ts',
|
|
53
54
|
'scripts/bootstrap-agent-skills.mjs',
|
|
55
|
+
'scripts/check-i18n-strings.mjs',
|
|
56
|
+
'config/public/locales/en/translation.json',
|
|
57
|
+
'config/public/locales/cs/translation.json',
|
|
54
58
|
];
|
|
55
59
|
const manifestErrors = [];
|
|
56
60
|
|
|
@@ -118,6 +122,7 @@ const skillsLock = JSON.parse(
|
|
|
118
122
|
const requiredScripts = {
|
|
119
123
|
format: 'oxfmt .',
|
|
120
124
|
'format:check': 'oxfmt --check .',
|
|
125
|
+
'i18n:check': 'node ./scripts/check-i18n-strings.mjs',
|
|
121
126
|
lint: 'oxlint .',
|
|
122
127
|
'lint:fix': 'oxlint . --fix',
|
|
123
128
|
'skills:check': 'node ./scripts/bootstrap-agent-skills.mjs --check',
|
|
@@ -139,6 +144,13 @@ if (
|
|
|
139
144
|
process.exit(1);
|
|
140
145
|
}
|
|
141
146
|
|
|
147
|
+
for (const dependency of ['@modern-js/plugin-i18n', 'i18next', 'react-i18next']) {
|
|
148
|
+
if (!packageJson.dependencies?.[dependency]) {
|
|
149
|
+
console.error(`Missing dependency: ${dependency}`);
|
|
150
|
+
process.exit(1);
|
|
151
|
+
}
|
|
152
|
+
}
|
|
153
|
+
|
|
142
154
|
for (const dependency of [
|
|
143
155
|
'@effect/tsgo',
|
|
144
156
|
'@typescript/native-preview',
|
|
@@ -1,7 +1,23 @@
|
|
|
1
1
|
import { defineRuntimeConfig } from '@modern-js/runtime';
|
|
2
|
+
import { createInstance } from 'i18next';
|
|
3
|
+
|
|
4
|
+
const i18nInstance = createInstance();
|
|
2
5
|
|
|
3
6
|
export default defineRuntimeConfig({
|
|
7
|
+
i18n: {
|
|
8
|
+
i18nInstance,
|
|
9
|
+
initOptions: {
|
|
10
|
+
defaultNS: 'translation',
|
|
11
|
+
fallbackLng: 'en',
|
|
12
|
+
interpolation: {
|
|
13
|
+
escapeValue: false,
|
|
14
|
+
},
|
|
15
|
+
ns: ['translation'],
|
|
16
|
+
supportedLngs: ['en', 'cs'],
|
|
17
|
+
},
|
|
18
|
+
},
|
|
4
19
|
{{#if isTanstackRouter}} router: {
|
|
5
20
|
framework: 'tanstack',
|
|
6
|
-
},
|
|
21
|
+
},
|
|
22
|
+
{{/if~}}
|
|
7
23
|
});
|
|
@@ -1,11 +1,19 @@
|
|
|
1
1
|
import { Helmet } from '@modern-js/runtime/head';
|
|
2
|
+
import { useModernI18n } from '@modern-js/plugin-i18n/runtime';
|
|
2
3
|
{{#if useEffectBff}}import effectBff from '@api/effect/index';
|
|
3
4
|
import { Effect } from '@modern-js/plugin-bff/effect-client';
|
|
4
5
|
import { useEffect, useState } from 'react';
|
|
5
6
|
{{/if}}
|
|
7
|
+
import { useTranslation } from 'react-i18next';
|
|
6
8
|
import './index.css';
|
|
7
9
|
|
|
8
10
|
const Index = () => {
|
|
11
|
+
const { t } = useTranslation();
|
|
12
|
+
const { changeLanguage, language } = useModernI18n();
|
|
13
|
+
const languageOptions = [
|
|
14
|
+
{ code: 'en', label: t('home.language.en') },
|
|
15
|
+
{ code: 'cs', label: t('home.language.cs') },
|
|
16
|
+
];
|
|
9
17
|
{{#if useEffectBff}} const [effectMessage, setEffectMessage] = useState('loading...');
|
|
10
18
|
|
|
11
19
|
useEffect(() => {
|
|
@@ -36,25 +44,41 @@ const Index = () => {
|
|
|
36
44
|
/>
|
|
37
45
|
</Helmet>
|
|
38
46
|
<main>
|
|
47
|
+
<nav className="language-switcher" aria-label={t('home.language.switcher')}>
|
|
48
|
+
{languageOptions.map((option) => (
|
|
49
|
+
<button
|
|
50
|
+
disabled={language === option.code}
|
|
51
|
+
key={option.code}
|
|
52
|
+
onClick={() => void changeLanguage(option.code)}
|
|
53
|
+
type="button"
|
|
54
|
+
>
|
|
55
|
+
{option.label}
|
|
56
|
+
</button>
|
|
57
|
+
))}
|
|
58
|
+
</nav>
|
|
39
59
|
<div className="title">
|
|
40
|
-
|
|
60
|
+
{t('home.title')}
|
|
41
61
|
<img
|
|
62
|
+
alt={t('home.logoAlt')}
|
|
42
63
|
className="logo"
|
|
43
64
|
src="https://lf3-static.bytednsdoc.com/obj/eden-cn/zq-uylkvT/ljhwZthlaukjlkulzlp/modern-js-logo.svg"
|
|
44
|
-
alt="UltraModern.js Logo"
|
|
45
65
|
/>
|
|
46
|
-
<p className="name">
|
|
66
|
+
<p className="name">{t('home.name')}</p>
|
|
47
67
|
</div>
|
|
48
68
|
<p className="description{{#if enableTailwind}} text-emerald-700 font-semibold{{/if}}">
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
<code className="code">
|
|
53
|
-
|
|
69
|
+
{t('home.description.intro')} <code className="code">presetUltramodern(...)</code>{' '}
|
|
70
|
+
{/* i18n-ignore technical token */}
|
|
71
|
+
{t('home.description.afterPreset')}
|
|
72
|
+
<code className="code">modern.config.ts</code>
|
|
73
|
+
{/* i18n-ignore technical token */}
|
|
74
|
+
{t('home.description.afterConfig')}
|
|
75
|
+
<code className="code">pnpm run ultramodern:check</code>
|
|
76
|
+
{/* i18n-ignore technical token */}
|
|
77
|
+
{t('home.description.end')}
|
|
54
78
|
</p>
|
|
55
79
|
{{#if useEffectBff}}
|
|
56
80
|
<p className="description effect-message{{#if enableTailwind}} text-emerald-700 font-semibold{{/if}}">
|
|
57
|
-
|
|
81
|
+
{t('home.bff.response')} <code className="code">{effectMessage}</code>
|
|
58
82
|
</p>
|
|
59
83
|
{{/if}}
|
|
60
84
|
<div className="grid">
|
|
@@ -65,14 +89,14 @@ const Index = () => {
|
|
|
65
89
|
className="card"
|
|
66
90
|
>
|
|
67
91
|
<h2>
|
|
68
|
-
|
|
92
|
+
{t('home.cards.guide.title')}
|
|
69
93
|
<img
|
|
94
|
+
alt=""
|
|
70
95
|
className="arrow-right"
|
|
71
96
|
src="https://lf3-static.bytednsdoc.com/obj/eden-cn/zq-uylkvT/ljhwZthlaukjlkulzlp/arrow-right.svg"
|
|
72
|
-
alt="Guide"
|
|
73
97
|
/>
|
|
74
98
|
</h2>
|
|
75
|
-
<p>
|
|
99
|
+
<p>{t('home.cards.guide.body')}</p>
|
|
76
100
|
</a>
|
|
77
101
|
<a
|
|
78
102
|
href="https://bleedingdev.github.io/ultramodern.js/configure/app/usage.html"
|
|
@@ -81,16 +105,14 @@ const Index = () => {
|
|
|
81
105
|
rel="noreferrer"
|
|
82
106
|
>
|
|
83
107
|
<h2>
|
|
84
|
-
|
|
108
|
+
{t('home.cards.config.title')}
|
|
85
109
|
<img
|
|
110
|
+
alt=""
|
|
86
111
|
className="arrow-right"
|
|
87
112
|
src="https://lf3-static.bytednsdoc.com/obj/eden-cn/zq-uylkvT/ljhwZthlaukjlkulzlp/arrow-right.svg"
|
|
88
|
-
alt="Tutorials"
|
|
89
113
|
/>
|
|
90
114
|
</h2>
|
|
91
|
-
<p>
|
|
92
|
-
Tune the generated defaults in <code className="code">modern.config.ts</code>.
|
|
93
|
-
</p>
|
|
115
|
+
<p>{t('home.cards.config.body')}</p>
|
|
94
116
|
</a>
|
|
95
117
|
<a
|
|
96
118
|
href="https://github.com/BleedingDev/ultramodern.js/blob/main-ultramodern/packages/toolkit/create/template/.github/workflows/ultramodern-gates.yml.handlebars"
|
|
@@ -99,17 +121,14 @@ const Index = () => {
|
|
|
99
121
|
rel="noreferrer"
|
|
100
122
|
>
|
|
101
123
|
<h2>
|
|
102
|
-
|
|
124
|
+
{t('home.cards.gates.title')}
|
|
103
125
|
<img
|
|
126
|
+
alt=""
|
|
104
127
|
className="arrow-right"
|
|
105
128
|
src="https://lf3-static.bytednsdoc.com/obj/eden-cn/zq-uylkvT/ljhwZthlaukjlkulzlp/arrow-right.svg"
|
|
106
|
-
alt="Config"
|
|
107
129
|
/>
|
|
108
130
|
</h2>
|
|
109
|
-
<p>
|
|
110
|
-
The starter includes a PR workflow for <code className="code">ultramodern:check</code>{' '}
|
|
111
|
-
and build.
|
|
112
|
-
</p>
|
|
131
|
+
<p>{t('home.cards.gates.body')}</p>
|
|
113
132
|
</a>
|
|
114
133
|
<a
|
|
115
134
|
href="https://bleedingdev.github.io/ultramodern.js/configure/app/bff/effect.html"
|
|
@@ -118,14 +137,14 @@ const Index = () => {
|
|
|
118
137
|
className="card"
|
|
119
138
|
>
|
|
120
139
|
<h2>
|
|
121
|
-
|
|
140
|
+
{t('home.cards.bff.title')}
|
|
122
141
|
<img
|
|
142
|
+
alt=""
|
|
123
143
|
className="arrow-right"
|
|
124
144
|
src="https://lf3-static.bytednsdoc.com/obj/eden-cn/zq-uylkvT/ljhwZthlaukjlkulzlp/arrow-right.svg"
|
|
125
|
-
alt="Github"
|
|
126
145
|
/>
|
|
127
146
|
</h2>
|
|
128
|
-
<p>
|
|
147
|
+
<p>{t('home.cards.bff.body')}</p>
|
|
129
148
|
</a>
|
|
130
149
|
</div>
|
|
131
150
|
</main>
|
|
@@ -7,7 +7,12 @@ This workspace is generated as an agent-ready UltraModern.js SuperApp. Agents sh
|
|
|
7
7
|
- `pnpm lint` runs Oxlint with the Ultracite preset.
|
|
8
8
|
- `pnpm format` runs oxfmt.
|
|
9
9
|
- `pnpm typecheck` runs effect-tsgo as the TypeScript checker.
|
|
10
|
-
- `pnpm check`
|
|
10
|
+
- `pnpm i18n:check` rejects hardcoded user-visible JSX text in generated apps.
|
|
11
|
+
- `pnpm check` runs formatting, linting, effect-tsgo, i18n checks, private-skill availability checks, and the generated workspace contract.
|
|
12
|
+
|
|
13
|
+
## Internationalization
|
|
14
|
+
|
|
15
|
+
Runtime i18n is enabled by default for generated apps. Agents must put user-visible UI copy in each app's `config/public/locales/<lang>/translation.json` and render it through `react-i18next` or `@modern-js/plugin-i18n/runtime`. Do not add hardcoded JSX text, `aria-label`, `title`, `alt`, or `placeholder` strings unless the value is a non-translatable technical token.
|
|
11
16
|
|
|
12
17
|
## Required Skill Baseline
|
|
13
18
|
|
|
@@ -1,7 +1,16 @@
|
|
|
1
|
-
import { defineConfig } from
|
|
2
|
-
import ultracite from
|
|
1
|
+
import { defineConfig } from 'oxfmt';
|
|
2
|
+
import ultracite from 'ultracite/oxfmt';
|
|
3
3
|
|
|
4
4
|
export default defineConfig({
|
|
5
5
|
extends: [ultracite],
|
|
6
|
-
ignorePatterns: [
|
|
6
|
+
ignorePatterns: [
|
|
7
|
+
'.agents',
|
|
8
|
+
'**/*.json',
|
|
9
|
+
'dist',
|
|
10
|
+
'node_modules',
|
|
11
|
+
'.modern',
|
|
12
|
+
'.modernjs',
|
|
13
|
+
'**/routeTree.gen.ts',
|
|
14
|
+
],
|
|
15
|
+
singleQuote: true,
|
|
7
16
|
});
|
|
@@ -1,6 +1,6 @@
|
|
|
1
|
-
import { defineConfig } from
|
|
2
|
-
import core from
|
|
3
|
-
import react from
|
|
1
|
+
import { defineConfig } from 'oxlint';
|
|
2
|
+
import core from 'ultracite/oxlint/core';
|
|
3
|
+
import react from 'ultracite/oxlint/react';
|
|
4
4
|
|
|
5
5
|
export default defineConfig({
|
|
6
6
|
env: {
|
|
@@ -8,5 +8,12 @@ export default defineConfig({
|
|
|
8
8
|
node: true,
|
|
9
9
|
},
|
|
10
10
|
extends: [core, react],
|
|
11
|
-
ignorePatterns: [
|
|
11
|
+
ignorePatterns: [
|
|
12
|
+
'.agents',
|
|
13
|
+
'dist',
|
|
14
|
+
'node_modules',
|
|
15
|
+
'.modern',
|
|
16
|
+
'.modernjs',
|
|
17
|
+
'**/routeTree.gen.ts',
|
|
18
|
+
],
|
|
12
19
|
});
|
|
@@ -8,43 +8,37 @@ const lockPath = path.join(root, '.agents/skills-lock.json');
|
|
|
8
8
|
const checkOnly = process.argv.includes('--check');
|
|
9
9
|
const force = process.argv.includes('--force');
|
|
10
10
|
|
|
11
|
-
|
|
12
|
-
return JSON.parse(fs.readFileSync(filePath, 'utf8'));
|
|
13
|
-
}
|
|
11
|
+
const readJson = (filePath) => JSON.parse(fs.readFileSync(filePath, 'utf-8'));
|
|
14
12
|
|
|
15
|
-
|
|
16
|
-
|
|
13
|
+
const run = (command, args, options = {}) =>
|
|
14
|
+
execFileSync(command, args, {
|
|
17
15
|
cwd: options.cwd ?? root,
|
|
18
|
-
encoding: '
|
|
16
|
+
encoding: 'utf-8',
|
|
19
17
|
stdio: options.stdio ?? ['ignore', 'pipe', 'pipe'],
|
|
20
18
|
});
|
|
21
|
-
}
|
|
22
19
|
|
|
23
|
-
|
|
24
|
-
const repo = source.repository.replace(/^https:\/\/github.com
|
|
20
|
+
const cloneSource = (source, targetDir) => {
|
|
21
|
+
const repo = source.repository.replace(/^https:\/\/github.com\//u, '');
|
|
25
22
|
try {
|
|
26
23
|
run('gh', ['repo', 'clone', repo, targetDir, '--', '--depth', '1'], {
|
|
27
24
|
stdio: 'inherit',
|
|
28
25
|
});
|
|
29
|
-
return;
|
|
30
26
|
} catch {
|
|
31
27
|
run('git', ['clone', '--depth', '1', source.repository, targetDir], {
|
|
32
28
|
stdio: 'inherit',
|
|
33
29
|
});
|
|
34
30
|
}
|
|
35
|
-
}
|
|
31
|
+
};
|
|
36
32
|
|
|
37
|
-
|
|
33
|
+
const resolveSkillDir = (sourceRoot, skillName) => {
|
|
38
34
|
const candidates = [
|
|
39
35
|
path.join(sourceRoot, skillName),
|
|
40
36
|
path.join(sourceRoot, 'skills', skillName),
|
|
41
37
|
path.join(sourceRoot, 'skills', 'engineering', skillName),
|
|
42
38
|
path.join(sourceRoot, 'skills', 'productivity', skillName),
|
|
43
39
|
];
|
|
44
|
-
return candidates.find(candidate =>
|
|
45
|
-
|
|
46
|
-
);
|
|
47
|
-
}
|
|
40
|
+
return candidates.find((candidate) => fs.existsSync(path.join(candidate, 'SKILL.md')));
|
|
41
|
+
};
|
|
48
42
|
|
|
49
43
|
if (!fs.existsSync(lockPath)) {
|
|
50
44
|
console.error('Missing .agents/skills-lock.json');
|
|
@@ -54,17 +48,14 @@ if (!fs.existsSync(lockPath)) {
|
|
|
54
48
|
const lock = readJson(lockPath);
|
|
55
49
|
const installDir = path.join(root, lock.installDir ?? '.agents/skills');
|
|
56
50
|
const privateSources = (lock.sources ?? []).filter(
|
|
57
|
-
source => source.install === 'clone-if-authorized',
|
|
51
|
+
(source) => source.install === 'clone-if-authorized',
|
|
58
52
|
);
|
|
59
53
|
|
|
60
54
|
if (checkOnly) {
|
|
61
|
-
const missing = privateSources.flatMap(source =>
|
|
55
|
+
const missing = privateSources.flatMap((source) =>
|
|
62
56
|
(source.baseline ?? [])
|
|
63
|
-
.map(skill => skill.name)
|
|
64
|
-
.filter(
|
|
65
|
-
skillName =>
|
|
66
|
-
!fs.existsSync(path.join(installDir, skillName, 'SKILL.md')),
|
|
67
|
-
),
|
|
57
|
+
.map((skill) => skill.name)
|
|
58
|
+
.filter((skillName) => !fs.existsSync(path.join(installDir, skillName, 'SKILL.md'))),
|
|
68
59
|
);
|
|
69
60
|
if (missing.length > 0) {
|
|
70
61
|
console.warn(
|
|
@@ -85,9 +76,7 @@ for (const source of privateSources) {
|
|
|
85
76
|
for (const skill of source.baseline ?? []) {
|
|
86
77
|
const sourceSkillDir = resolveSkillDir(tempDir, skill.name);
|
|
87
78
|
if (!sourceSkillDir) {
|
|
88
|
-
throw new Error(
|
|
89
|
-
`Skill ${skill.name} not found in ${source.repository}`,
|
|
90
|
-
);
|
|
79
|
+
throw new Error(`Skill ${skill.name} not found in ${source.repository}`);
|
|
91
80
|
}
|
|
92
81
|
const targetSkillDir = path.join(installDir, skill.name);
|
|
93
82
|
if (fs.existsSync(targetSkillDir)) {
|
|
@@ -95,12 +84,12 @@ for (const source of privateSources) {
|
|
|
95
84
|
console.log(`Skipping existing ${skill.name}`);
|
|
96
85
|
continue;
|
|
97
86
|
}
|
|
98
|
-
fs.rmSync(targetSkillDir, {
|
|
87
|
+
fs.rmSync(targetSkillDir, { force: true, recursive: true });
|
|
99
88
|
}
|
|
100
89
|
fs.cpSync(sourceSkillDir, targetSkillDir, { recursive: true });
|
|
101
90
|
console.log(`Installed ${skill.name}`);
|
|
102
91
|
}
|
|
103
92
|
} finally {
|
|
104
|
-
fs.rmSync(tempDir, {
|
|
93
|
+
fs.rmSync(tempDir, { force: true, recursive: true });
|
|
105
94
|
}
|
|
106
95
|
}
|