@bleedingdev/modern-js-create 3.2.0-ultramodern.11 → 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 +172 -20
- 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/scripts/check-i18n-strings.mjs +83 -0
- package/template-workspace/scripts/validate-ultramodern-workspace.mjs.handlebars +28 -0
package/dist/index.js
CHANGED
|
@@ -561,8 +561,10 @@ const TYPESCRIPT_NATIVE_PREVIEW_VERSION = '7.0.0-dev.20260518.1';
|
|
|
561
561
|
const OXLINT_VERSION = '1.65.0';
|
|
562
562
|
const OXFMT_VERSION = '0.50.0';
|
|
563
563
|
const ULTRACITE_VERSION = '7.7.0';
|
|
564
|
+
const I18NEXT_VERSION = '26.2.0';
|
|
564
565
|
const REACT_VERSION = '^19.2.6';
|
|
565
566
|
const REACT_DOM_VERSION = '^19.2.6';
|
|
567
|
+
const REACT_I18NEXT_VERSION = '17.0.8';
|
|
566
568
|
const WORKSPACE_PACKAGE_VERSION = 'workspace:*';
|
|
567
569
|
const RSTACK_AGENT_SKILLS_COMMIT = '61c948b42512e223bad44b83af4080eba48b2677';
|
|
568
570
|
const baselineAgentSkills = [
|
|
@@ -585,6 +587,7 @@ const effectTsgoTypecheckCommand = "node -e \"const fs = require('node:fs'); con
|
|
|
585
587
|
const modernPackageNames = [
|
|
586
588
|
'@modern-js/app-tools',
|
|
587
589
|
'@modern-js/plugin-bff',
|
|
590
|
+
'@modern-js/plugin-i18n',
|
|
588
591
|
'@modern-js/plugin-tanstack',
|
|
589
592
|
'@modern-js/runtime'
|
|
590
593
|
];
|
|
@@ -917,6 +920,7 @@ function modernPackageSpecifier(packageName, packageSource) {
|
|
|
917
920
|
}
|
|
918
921
|
function appDependencies(scope, packageSource) {
|
|
919
922
|
return {
|
|
923
|
+
'@modern-js/plugin-i18n': modernPackageSpecifier('@modern-js/plugin-i18n', packageSource),
|
|
920
924
|
'@modern-js/plugin-tanstack': modernPackageSpecifier('@modern-js/plugin-tanstack', packageSource),
|
|
921
925
|
'@modern-js/runtime': modernPackageSpecifier('@modern-js/runtime', packageSource),
|
|
922
926
|
'@module-federation/modern-js-v3': MODULE_FEDERATION_VERSION,
|
|
@@ -924,8 +928,10 @@ function appDependencies(scope, packageSource) {
|
|
|
924
928
|
'@tanstack/react-router': TANSTACK_ROUTER_VERSION,
|
|
925
929
|
[ultramodern_workspace_packageName(scope, 'shared-contracts')]: WORKSPACE_PACKAGE_VERSION,
|
|
926
930
|
[ultramodern_workspace_packageName(scope, 'shared-design-tokens')]: WORKSPACE_PACKAGE_VERSION,
|
|
931
|
+
i18next: I18NEXT_VERSION,
|
|
927
932
|
react: REACT_VERSION,
|
|
928
|
-
'react-dom': REACT_DOM_VERSION
|
|
933
|
+
'react-dom': REACT_DOM_VERSION,
|
|
934
|
+
'react-i18next': REACT_I18NEXT_VERSION
|
|
929
935
|
};
|
|
930
936
|
}
|
|
931
937
|
function appDevDependencies(packageSource) {
|
|
@@ -955,13 +961,14 @@ function createRootPackageJson(scope, packageSource) {
|
|
|
955
961
|
build: 'pnpm -r --filter ./apps/** --filter ./services/** build',
|
|
956
962
|
format: 'oxfmt .',
|
|
957
963
|
'format:check': 'oxfmt --check .',
|
|
964
|
+
'i18n:check': "node ./scripts/check-i18n-strings.mjs",
|
|
958
965
|
lint: 'oxlint .',
|
|
959
966
|
'lint:fix': 'oxlint . --fix',
|
|
960
967
|
typecheck: `pnpm -r --filter "@${scope}/*" typecheck`,
|
|
961
968
|
'skills:install': "node ./scripts/bootstrap-agent-skills.mjs",
|
|
962
969
|
'skills:check': "node ./scripts/bootstrap-agent-skills.mjs --check",
|
|
963
970
|
'ultramodern:check': "node ./scripts/validate-ultramodern-workspace.mjs",
|
|
964
|
-
check: 'pnpm format:check && pnpm lint && pnpm typecheck && pnpm skills:check && pnpm ultramodern:check'
|
|
971
|
+
check: 'pnpm format:check && pnpm lint && pnpm typecheck && pnpm i18n:check && pnpm skills:check && pnpm ultramodern:check'
|
|
965
972
|
},
|
|
966
973
|
engines: {
|
|
967
974
|
node: '>=20',
|
|
@@ -1126,6 +1133,7 @@ function createSharedPackage(scope, id, description) {
|
|
|
1126
1133
|
function createAppModernConfig(app) {
|
|
1127
1134
|
return `// @effect-diagnostics processEnv:off
|
|
1128
1135
|
import { appTools, defineConfig, presetUltramodern } from '@modern-js/app-tools';
|
|
1136
|
+
import { i18nPlugin } from '@modern-js/plugin-i18n';
|
|
1129
1137
|
import { tanstackRouterPlugin } from '@modern-js/plugin-tanstack';
|
|
1130
1138
|
import { moduleFederationPlugin } from '@module-federation/modern-js-v3';
|
|
1131
1139
|
|
|
@@ -1140,7 +1148,17 @@ export default defineConfig(
|
|
|
1140
1148
|
polyfill: 'off',
|
|
1141
1149
|
splitRouteChunks: false,
|
|
1142
1150
|
},
|
|
1143
|
-
plugins: [
|
|
1151
|
+
plugins: [
|
|
1152
|
+
appTools(),
|
|
1153
|
+
i18nPlugin({
|
|
1154
|
+
localeDetection: {
|
|
1155
|
+
fallbackLanguage: 'en',
|
|
1156
|
+
languages: ['en', 'cs'],
|
|
1157
|
+
},
|
|
1158
|
+
}),
|
|
1159
|
+
tanstackRouterPlugin(),
|
|
1160
|
+
moduleFederationPlugin(),
|
|
1161
|
+
],
|
|
1144
1162
|
server: {
|
|
1145
1163
|
port,
|
|
1146
1164
|
ssr: {
|
|
@@ -1172,6 +1190,11 @@ function createSharedModuleFederationConfig() {
|
|
|
1172
1190
|
singleton: true,
|
|
1173
1191
|
treeShaking: false,
|
|
1174
1192
|
},
|
|
1193
|
+
i18next: {
|
|
1194
|
+
requiredVersion: dependencies.i18next,
|
|
1195
|
+
singleton: true,
|
|
1196
|
+
treeShaking: false,
|
|
1197
|
+
},
|
|
1175
1198
|
react: {
|
|
1176
1199
|
requiredVersion: reactVersion,
|
|
1177
1200
|
singleton: true,
|
|
@@ -1182,6 +1205,11 @@ function createSharedModuleFederationConfig() {
|
|
|
1182
1205
|
singleton: true,
|
|
1183
1206
|
treeShaking: false,
|
|
1184
1207
|
},
|
|
1208
|
+
'react-i18next': {
|
|
1209
|
+
requiredVersion: dependencies['react-i18next'],
|
|
1210
|
+
singleton: true,
|
|
1211
|
+
treeShaking: false,
|
|
1212
|
+
},
|
|
1185
1213
|
}`;
|
|
1186
1214
|
}
|
|
1187
1215
|
function formatTsObjectLiteral(value) {
|
|
@@ -1277,17 +1305,64 @@ export default defineConfig(
|
|
|
1277
1305
|
);
|
|
1278
1306
|
`;
|
|
1279
1307
|
}
|
|
1308
|
+
function createAppRuntimeConfig() {
|
|
1309
|
+
return `import { defineRuntimeConfig } from '@modern-js/runtime';
|
|
1310
|
+
import { createInstance } from 'i18next';
|
|
1311
|
+
|
|
1312
|
+
const i18nInstance = createInstance();
|
|
1313
|
+
|
|
1314
|
+
export default defineRuntimeConfig({
|
|
1315
|
+
i18n: {
|
|
1316
|
+
i18nInstance,
|
|
1317
|
+
initOptions: {
|
|
1318
|
+
defaultNS: 'translation',
|
|
1319
|
+
fallbackLng: 'en',
|
|
1320
|
+
interpolation: {
|
|
1321
|
+
escapeValue: false,
|
|
1322
|
+
},
|
|
1323
|
+
ns: ['translation'],
|
|
1324
|
+
supportedLngs: ['en', 'cs'],
|
|
1325
|
+
},
|
|
1326
|
+
},
|
|
1327
|
+
router: {
|
|
1328
|
+
framework: 'tanstack',
|
|
1329
|
+
},
|
|
1330
|
+
});
|
|
1331
|
+
`;
|
|
1332
|
+
}
|
|
1280
1333
|
function createShellPage() {
|
|
1281
|
-
return `
|
|
1334
|
+
return `import { useModernI18n } from '@modern-js/plugin-i18n/runtime';
|
|
1335
|
+
import { useTranslation } from 'react-i18next';
|
|
1336
|
+
|
|
1337
|
+
const remotes = ['remote-commerce', 'remote-identity', 'remote-design-system'];
|
|
1282
1338
|
|
|
1283
1339
|
export default function ShellHome() {
|
|
1340
|
+
const { t } = useTranslation();
|
|
1341
|
+
const { changeLanguage, language } = useModernI18n();
|
|
1342
|
+
const languageOptions = [
|
|
1343
|
+
{ code: 'en', label: t('language.en') },
|
|
1344
|
+
{ code: 'cs', label: t('language.cs') },
|
|
1345
|
+
];
|
|
1346
|
+
|
|
1284
1347
|
return (
|
|
1285
1348
|
<main>
|
|
1286
|
-
<
|
|
1287
|
-
|
|
1349
|
+
<nav aria-label={t('language.switcher')}>
|
|
1350
|
+
{languageOptions.map((option) => (
|
|
1351
|
+
<button
|
|
1352
|
+
disabled={language === option.code}
|
|
1353
|
+
key={option.code}
|
|
1354
|
+
onClick={() => void changeLanguage(option.code)}
|
|
1355
|
+
type="button"
|
|
1356
|
+
>
|
|
1357
|
+
{option.label}
|
|
1358
|
+
</button>
|
|
1359
|
+
))}
|
|
1360
|
+
</nav>
|
|
1361
|
+
<h1>{t('shell.title')}</h1>
|
|
1362
|
+
<p data-testid="ultramodern-preset">{t('shell.preset')}</p>
|
|
1288
1363
|
<ul>
|
|
1289
1364
|
{remotes.map((remote) => (
|
|
1290
|
-
<li key={remote}>{remote}</li>
|
|
1365
|
+
<li key={remote}>{t(\`shell.remotes.\${remote}\`)}</li>
|
|
1291
1366
|
))}
|
|
1292
1367
|
</ul>
|
|
1293
1368
|
</main>
|
|
@@ -1296,11 +1371,15 @@ export default function ShellHome() {
|
|
|
1296
1371
|
`;
|
|
1297
1372
|
}
|
|
1298
1373
|
function createRemotePage(app) {
|
|
1299
|
-
return `
|
|
1374
|
+
return `import { useTranslation } from 'react-i18next';
|
|
1375
|
+
|
|
1376
|
+
export default function ${toPascalCase(app.id)}Home() {
|
|
1377
|
+
const { t } = useTranslation();
|
|
1378
|
+
|
|
1300
1379
|
return (
|
|
1301
1380
|
<main>
|
|
1302
|
-
<h1
|
|
1303
|
-
<p data-mf-role="${app.kind}"
|
|
1381
|
+
<h1>{t('remote.title')}</h1>
|
|
1382
|
+
<p data-mf-role="${app.kind}">{t('remote.domain')}</p>
|
|
1304
1383
|
</main>
|
|
1305
1384
|
);
|
|
1306
1385
|
}
|
|
@@ -1321,11 +1400,15 @@ function createRemoteEntry(app) {
|
|
|
1321
1400
|
}
|
|
1322
1401
|
function createRemoteWidget(app) {
|
|
1323
1402
|
const componentName = 'remote-identity' === app.id ? 'IdentityWidget' : 'CommerceWidget';
|
|
1324
|
-
return `
|
|
1403
|
+
return `import { useTranslation } from 'react-i18next';
|
|
1404
|
+
|
|
1405
|
+
export default function ${componentName}() {
|
|
1406
|
+
const { t } = useTranslation();
|
|
1407
|
+
|
|
1325
1408
|
return (
|
|
1326
1409
|
<section data-mf-remote="${app.id}">
|
|
1327
|
-
<h2
|
|
1328
|
-
<p>
|
|
1410
|
+
<h2>{t('remote.widget.title')}</h2>
|
|
1411
|
+
<p>{t('remote.widget.body')}</p>
|
|
1329
1412
|
</section>
|
|
1330
1413
|
);
|
|
1331
1414
|
}
|
|
@@ -1334,7 +1417,7 @@ function createRemoteWidget(app) {
|
|
|
1334
1417
|
function createDesignButton() {
|
|
1335
1418
|
return `import { designTokens } from '../tokens';
|
|
1336
1419
|
|
|
1337
|
-
export default function Button({ label
|
|
1420
|
+
export default function Button({ label }: { label: string }) {
|
|
1338
1421
|
return (
|
|
1339
1422
|
<button
|
|
1340
1423
|
type="button"
|
|
@@ -1361,6 +1444,62 @@ function createDesignTokens() {
|
|
|
1361
1444
|
} as const;
|
|
1362
1445
|
`;
|
|
1363
1446
|
}
|
|
1447
|
+
function createEnglishTranslations(app) {
|
|
1448
|
+
if ('shell' === app.kind) return {
|
|
1449
|
+
language: {
|
|
1450
|
+
cs: 'Czech',
|
|
1451
|
+
en: 'English',
|
|
1452
|
+
switcher: 'Language'
|
|
1453
|
+
},
|
|
1454
|
+
shell: {
|
|
1455
|
+
preset: 'presetUltramodern workspace',
|
|
1456
|
+
remotes: {
|
|
1457
|
+
'remote-commerce': 'Commerce Remote',
|
|
1458
|
+
'remote-design-system': 'Design System Remote',
|
|
1459
|
+
'remote-identity': 'Identity Remote'
|
|
1460
|
+
},
|
|
1461
|
+
title: 'UltraModern SuperApp Shell'
|
|
1462
|
+
}
|
|
1463
|
+
};
|
|
1464
|
+
return {
|
|
1465
|
+
remote: {
|
|
1466
|
+
domain: app.domain ?? app.kind,
|
|
1467
|
+
title: app.displayName,
|
|
1468
|
+
widget: {
|
|
1469
|
+
body: 'vertical' === app.kind ? `Owns the ${app.domain} vertical route surface.` : 'Provides shared UI primitives for the workspace.',
|
|
1470
|
+
title: app.displayName
|
|
1471
|
+
}
|
|
1472
|
+
}
|
|
1473
|
+
};
|
|
1474
|
+
}
|
|
1475
|
+
function createCzechTranslations(app) {
|
|
1476
|
+
if ('shell' === app.kind) return {
|
|
1477
|
+
language: {
|
|
1478
|
+
cs: 'Cestina',
|
|
1479
|
+
en: 'Anglictina',
|
|
1480
|
+
switcher: 'Jazyk'
|
|
1481
|
+
},
|
|
1482
|
+
shell: {
|
|
1483
|
+
preset: 'presetUltramodern workspace',
|
|
1484
|
+
remotes: {
|
|
1485
|
+
'remote-commerce': 'Commerce remote',
|
|
1486
|
+
'remote-design-system': 'Design system remote',
|
|
1487
|
+
'remote-identity': 'Identity remote'
|
|
1488
|
+
},
|
|
1489
|
+
title: 'UltraModern SuperApp shell'
|
|
1490
|
+
}
|
|
1491
|
+
};
|
|
1492
|
+
return {
|
|
1493
|
+
remote: {
|
|
1494
|
+
domain: app.domain ?? app.kind,
|
|
1495
|
+
title: app.displayName,
|
|
1496
|
+
widget: {
|
|
1497
|
+
body: 'vertical' === app.kind ? `Vlastni ${app.domain} vertical route surface.` : 'Poskytuje sdilene UI prvky pro workspace.',
|
|
1498
|
+
title: app.displayName
|
|
1499
|
+
}
|
|
1500
|
+
}
|
|
1501
|
+
};
|
|
1502
|
+
}
|
|
1364
1503
|
function createEffectSharedApi() {
|
|
1365
1504
|
return `import {
|
|
1366
1505
|
HttpApi,
|
|
@@ -1700,6 +1839,9 @@ function writeApp(targetDir, scope, app, packageSource) {
|
|
|
1700
1839
|
writeJson(targetDir, `${app.directory}/tsconfig.json`, createPackageTsConfig(app.directory));
|
|
1701
1840
|
writeFile(targetDir, `${app.directory}/src/modern-app-env.d.ts`, "/// <reference types='@modern-js/app-tools/types' />\n");
|
|
1702
1841
|
writeFile(targetDir, `${app.directory}/modern.config.ts`, createAppModernConfig(app));
|
|
1842
|
+
writeFile(targetDir, `${app.directory}/src/modern.runtime.ts`, createAppRuntimeConfig());
|
|
1843
|
+
writeJson(targetDir, `${app.directory}/config/public/locales/en/translation.json`, createEnglishTranslations(app));
|
|
1844
|
+
writeJson(targetDir, `${app.directory}/config/public/locales/cs/translation.json`, createCzechTranslations(app));
|
|
1703
1845
|
writeFile(targetDir, `${app.directory}/module-federation.config.ts`, 'shell' === app.kind ? createShellModuleFederationConfig() : createRemoteModuleFederationConfig(app));
|
|
1704
1846
|
writeFile(targetDir, `${app.directory}/src/routes/layout.tsx`, createLayout(app.id));
|
|
1705
1847
|
writeFile(targetDir, `${app.directory}/src/routes/page.tsx`, 'shell' === app.kind ? createShellPage() : createRemotePage(app));
|
|
@@ -1855,7 +1997,7 @@ function detectBffRuntime() {
|
|
|
1855
1997
|
process.exit(1);
|
|
1856
1998
|
}
|
|
1857
1999
|
function src_renderTemplate(template, data) {
|
|
1858
|
-
const tagRegex = /\{\{(#if|#unless|\/if|\/unless)(?:\s+(\w+))
|
|
2000
|
+
const tagRegex = /\{\{(~?)(#if|#unless|\/if|\/unless)(?:\s+(\w+))?(~?)\}\}/g;
|
|
1859
2001
|
function renderConditionals(startIndex, expectedClose) {
|
|
1860
2002
|
let rendered = '';
|
|
1861
2003
|
let cursor = startIndex;
|
|
@@ -1866,7 +2008,7 @@ function src_renderTemplate(template, data) {
|
|
|
1866
2008
|
rendered: rendered + template.slice(cursor),
|
|
1867
2009
|
nextIndex: template.length
|
|
1868
2010
|
};
|
|
1869
|
-
const [raw, tag, condition] = match;
|
|
2011
|
+
const [raw, , tag, condition, rightTrim] = match;
|
|
1870
2012
|
const tagIndex = match.index;
|
|
1871
2013
|
rendered += template.slice(cursor, tagIndex);
|
|
1872
2014
|
cursor = tagIndex + raw.length;
|
|
@@ -1882,10 +2024,17 @@ function src_renderTemplate(template, data) {
|
|
|
1882
2024
|
}
|
|
1883
2025
|
if ('/if' === tag || '/unless' === tag) {
|
|
1884
2026
|
const kind = '/if' === tag ? 'if' : 'unless';
|
|
1885
|
-
if (expectedClose === kind)
|
|
1886
|
-
|
|
1887
|
-
|
|
1888
|
-
|
|
2027
|
+
if (expectedClose === kind) {
|
|
2028
|
+
let nextIndex = cursor;
|
|
2029
|
+
if ('~' === rightTrim) {
|
|
2030
|
+
const trailingWhitespace = /^\s*/u.exec(template.slice(nextIndex));
|
|
2031
|
+
nextIndex += trailingWhitespace?.[0].length ?? 0;
|
|
2032
|
+
}
|
|
2033
|
+
return {
|
|
2034
|
+
rendered,
|
|
2035
|
+
nextIndex
|
|
2036
|
+
};
|
|
2037
|
+
}
|
|
1889
2038
|
rendered += raw;
|
|
1890
2039
|
}
|
|
1891
2040
|
}
|
|
@@ -1974,6 +2123,7 @@ function createBuiltinTemplateManifest(version) {
|
|
|
1974
2123
|
'AGENTS.md',
|
|
1975
2124
|
'README.md',
|
|
1976
2125
|
'api/**',
|
|
2126
|
+
'config/**',
|
|
1977
2127
|
'modern.config.ts',
|
|
1978
2128
|
'oxfmt.config.ts',
|
|
1979
2129
|
'oxlint.config.ts',
|
|
@@ -2368,6 +2518,7 @@ async function main() {
|
|
|
2368
2518
|
tsconfigVersion: singleAppModernPackageSpecifier('@modern-js/tsconfig', packageSource, useWorkspaceProtocol),
|
|
2369
2519
|
pluginTanstackVersion: singleAppModernPackageSpecifier('@modern-js/plugin-tanstack', packageSource, useWorkspaceProtocol),
|
|
2370
2520
|
pluginBffVersion: singleAppModernPackageSpecifier('@modern-js/plugin-bff', packageSource, useWorkspaceProtocol),
|
|
2521
|
+
pluginI18nVersion: singleAppModernPackageSpecifier('@modern-js/plugin-i18n', packageSource, useWorkspaceProtocol),
|
|
2371
2522
|
isSubproject,
|
|
2372
2523
|
routerFramework,
|
|
2373
2524
|
bffRuntime,
|
|
@@ -2447,6 +2598,7 @@ function copyTemplate(src, dest, options) {
|
|
|
2447
2598
|
tsconfigVersion: options.tsconfigVersion,
|
|
2448
2599
|
pluginTanstackVersion: options.pluginTanstackVersion,
|
|
2449
2600
|
pluginBffVersion: options.pluginBffVersion,
|
|
2601
|
+
pluginI18nVersion: options.pluginI18nVersion,
|
|
2450
2602
|
isSubproject: options.isSubproject,
|
|
2451
2603
|
isTanstackRouter: 'tanstack' === options.routerFramework,
|
|
2452
2604
|
enableBff: 'none' !== options.bffRuntime,
|
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
|
|
|
@@ -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 = ['apps'].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.');
|
|
@@ -8,6 +8,7 @@ const rstackAgentSkillsCommit = '61c948b42512e223bad44b83af4080eba48b2677';
|
|
|
8
8
|
const modernPackages = [
|
|
9
9
|
'@modern-js/app-tools',
|
|
10
10
|
'@modern-js/plugin-bff',
|
|
11
|
+
'@modern-js/plugin-i18n',
|
|
11
12
|
'@modern-js/plugin-tanstack',
|
|
12
13
|
'@modern-js/runtime',
|
|
13
14
|
];
|
|
@@ -58,18 +59,31 @@ const requiredPaths = [
|
|
|
58
59
|
'.modernjs/ultramodern-workspace-template-manifest.json',
|
|
59
60
|
'.modernjs/ultramodern-package-source.json',
|
|
60
61
|
'scripts/bootstrap-agent-skills.mjs',
|
|
62
|
+
'scripts/check-i18n-strings.mjs',
|
|
61
63
|
'apps/shell-super-app/package.json',
|
|
64
|
+
'apps/shell-super-app/config/public/locales/en/translation.json',
|
|
65
|
+
'apps/shell-super-app/config/public/locales/cs/translation.json',
|
|
62
66
|
'apps/shell-super-app/modern.config.ts',
|
|
63
67
|
'apps/shell-super-app/module-federation.config.ts',
|
|
68
|
+
'apps/shell-super-app/src/modern.runtime.ts',
|
|
64
69
|
'apps/remotes/remote-commerce/package.json',
|
|
70
|
+
'apps/remotes/remote-commerce/config/public/locales/en/translation.json',
|
|
71
|
+
'apps/remotes/remote-commerce/config/public/locales/cs/translation.json',
|
|
65
72
|
'apps/remotes/remote-commerce/modern.config.ts',
|
|
66
73
|
'apps/remotes/remote-commerce/module-federation.config.ts',
|
|
74
|
+
'apps/remotes/remote-commerce/src/modern.runtime.ts',
|
|
67
75
|
'apps/remotes/remote-identity/package.json',
|
|
76
|
+
'apps/remotes/remote-identity/config/public/locales/en/translation.json',
|
|
77
|
+
'apps/remotes/remote-identity/config/public/locales/cs/translation.json',
|
|
68
78
|
'apps/remotes/remote-identity/modern.config.ts',
|
|
69
79
|
'apps/remotes/remote-identity/module-federation.config.ts',
|
|
80
|
+
'apps/remotes/remote-identity/src/modern.runtime.ts',
|
|
70
81
|
'apps/remotes/remote-design-system/package.json',
|
|
82
|
+
'apps/remotes/remote-design-system/config/public/locales/en/translation.json',
|
|
83
|
+
'apps/remotes/remote-design-system/config/public/locales/cs/translation.json',
|
|
71
84
|
'apps/remotes/remote-design-system/modern.config.ts',
|
|
72
85
|
'apps/remotes/remote-design-system/module-federation.config.ts',
|
|
86
|
+
'apps/remotes/remote-design-system/src/modern.runtime.ts',
|
|
73
87
|
'services/service-recommendations-effect/package.json',
|
|
74
88
|
'services/service-recommendations-effect/modern.config.ts',
|
|
75
89
|
'services/service-recommendations-effect/api/effect/index.ts',
|
|
@@ -134,6 +148,7 @@ assert(
|
|
|
134
148
|
const requiredRootScripts = {
|
|
135
149
|
format: 'oxfmt .',
|
|
136
150
|
'format:check': 'oxfmt --check .',
|
|
151
|
+
'i18n:check': 'node ./scripts/check-i18n-strings.mjs',
|
|
137
152
|
lint: 'oxlint .',
|
|
138
153
|
'lint:fix': 'oxlint . --fix',
|
|
139
154
|
'skills:check': 'node ./scripts/bootstrap-agent-skills.mjs --check',
|
|
@@ -207,6 +222,13 @@ const appPackagePaths = [
|
|
|
207
222
|
|
|
208
223
|
for (const packagePath of appPackagePaths) {
|
|
209
224
|
const packageJson = readJson(packagePath);
|
|
225
|
+
assert(
|
|
226
|
+
packageJson.dependencies?.['@modern-js/plugin-i18n'] ===
|
|
227
|
+
expectedModernDependency('@modern-js/plugin-i18n'),
|
|
228
|
+
`${packagePath} must use @modern-js/plugin-i18n through ${expectedModernDependency(
|
|
229
|
+
'@modern-js/plugin-i18n',
|
|
230
|
+
)}`,
|
|
231
|
+
);
|
|
210
232
|
assert(
|
|
211
233
|
packageJson.dependencies?.['@modern-js/plugin-tanstack'] ===
|
|
212
234
|
expectedModernDependency('@modern-js/plugin-tanstack'),
|
|
@@ -236,6 +258,11 @@ for (const packagePath of appPackagePaths) {
|
|
|
236
258
|
packageJson.dependencies?.[`@${packageScope}/shared-design-tokens`] === 'workspace:*',
|
|
237
259
|
`${packagePath} must link generated shared design tokens through workspace:*`,
|
|
238
260
|
);
|
|
261
|
+
assert(packageJson.dependencies?.i18next === '26.2.0', `${packagePath} must include i18next`);
|
|
262
|
+
assert(
|
|
263
|
+
packageJson.dependencies?.['react-i18next'] === '17.0.8',
|
|
264
|
+
`${packagePath} must include react-i18next`,
|
|
265
|
+
);
|
|
239
266
|
assert(
|
|
240
267
|
packageJson.dependencies?.['@tanstack/react-router'] === tanstackVersion,
|
|
241
268
|
`${packagePath} must use @tanstack/react-router ${tanstackVersion}`,
|
|
@@ -258,6 +285,7 @@ for (const configPath of [
|
|
|
258
285
|
]) {
|
|
259
286
|
const config = readText(configPath);
|
|
260
287
|
assert(config.includes('presetUltramodern('), `${configPath} must use presetUltramodern`);
|
|
288
|
+
assert(config.includes('i18nPlugin('), `${configPath} must enable plugin-i18n`);
|
|
261
289
|
assert(config.includes('tanstackRouterPlugin()'), `${configPath} must enable plugin-tanstack`);
|
|
262
290
|
assert(
|
|
263
291
|
config.includes('moduleFederationPlugin()'),
|