@bleedingdev/modern-js-create 3.2.0-ultramodern.11 → 3.2.0-ultramodern.13
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 +268 -22
- package/package.json +3 -3
- package/template/AGENTS.md +7 -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 +26 -1
- package/template/package.json.handlebars +8 -3
- package/template/scripts/check-i18n-strings.mjs +83 -0
- package/template/scripts/validate-ultramodern.mjs.handlebars +40 -0
- package/template/src/modern-app-env.d.ts +2 -0
- package/template/src/modern.runtime.ts.handlebars +17 -1
- package/template/src/routes/[lang]/page.tsx.handlebars +211 -0
- package/template-workspace/AGENTS.md +8 -1
- package/template-workspace/scripts/check-i18n-strings.mjs +83 -0
- package/template-workspace/scripts/validate-ultramodern-workspace.mjs.handlebars +67 -0
- package/template/src/routes/page.tsx.handlebars +0 -136
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,11 +1133,24 @@ 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
|
|
|
1132
1140
|
const appId = '${app.id}';
|
|
1133
1141
|
const port = Number(process.env['${app.portEnv}'] ?? ${app.port});
|
|
1142
|
+
const configuredSiteUrl = process.env['MODERN_PUBLIC_SITE_URL'];
|
|
1143
|
+
const hasConfiguredSiteUrl = typeof configuredSiteUrl === 'string' && configuredSiteUrl.length > 0;
|
|
1144
|
+
const isProductionBuild =
|
|
1145
|
+
process.env['NODE_ENV'] === 'production' || process.argv.includes('build');
|
|
1146
|
+
|
|
1147
|
+
if (isProductionBuild && !hasConfiguredSiteUrl) {
|
|
1148
|
+
throw new Error(
|
|
1149
|
+
'MODERN_PUBLIC_SITE_URL must be set for production builds so canonical and hreflang URLs use the deployed origin.',
|
|
1150
|
+
);
|
|
1151
|
+
}
|
|
1152
|
+
|
|
1153
|
+
const siteUrl = hasConfiguredSiteUrl ? configuredSiteUrl : \`http://localhost:\${port}\`;
|
|
1134
1154
|
|
|
1135
1155
|
export default defineConfig(
|
|
1136
1156
|
presetUltramodern(
|
|
@@ -1140,7 +1160,18 @@ export default defineConfig(
|
|
|
1140
1160
|
polyfill: 'off',
|
|
1141
1161
|
splitRouteChunks: false,
|
|
1142
1162
|
},
|
|
1143
|
-
plugins: [
|
|
1163
|
+
plugins: [
|
|
1164
|
+
appTools(),
|
|
1165
|
+
i18nPlugin({
|
|
1166
|
+
localeDetection: {
|
|
1167
|
+
fallbackLanguage: 'en',
|
|
1168
|
+
languages: ['en', 'cs'],
|
|
1169
|
+
localePathRedirect: true,
|
|
1170
|
+
},
|
|
1171
|
+
}),
|
|
1172
|
+
tanstackRouterPlugin(),
|
|
1173
|
+
moduleFederationPlugin(),
|
|
1174
|
+
],
|
|
1144
1175
|
server: {
|
|
1145
1176
|
port,
|
|
1146
1177
|
ssr: {
|
|
@@ -1148,6 +1179,11 @@ export default defineConfig(
|
|
|
1148
1179
|
moduleFederationAppSSR: true,
|
|
1149
1180
|
},
|
|
1150
1181
|
},
|
|
1182
|
+
source: {
|
|
1183
|
+
globalVars: {
|
|
1184
|
+
ULTRAMODERN_SITE_URL: siteUrl,
|
|
1185
|
+
},
|
|
1186
|
+
},
|
|
1151
1187
|
},
|
|
1152
1188
|
{
|
|
1153
1189
|
appId,
|
|
@@ -1172,6 +1208,11 @@ function createSharedModuleFederationConfig() {
|
|
|
1172
1208
|
singleton: true,
|
|
1173
1209
|
treeShaking: false,
|
|
1174
1210
|
},
|
|
1211
|
+
i18next: {
|
|
1212
|
+
requiredVersion: dependencies.i18next,
|
|
1213
|
+
singleton: true,
|
|
1214
|
+
treeShaking: false,
|
|
1215
|
+
},
|
|
1175
1216
|
react: {
|
|
1176
1217
|
requiredVersion: reactVersion,
|
|
1177
1218
|
singleton: true,
|
|
@@ -1182,6 +1223,11 @@ function createSharedModuleFederationConfig() {
|
|
|
1182
1223
|
singleton: true,
|
|
1183
1224
|
treeShaking: false,
|
|
1184
1225
|
},
|
|
1226
|
+
'react-i18next': {
|
|
1227
|
+
requiredVersion: dependencies['react-i18next'],
|
|
1228
|
+
singleton: true,
|
|
1229
|
+
treeShaking: false,
|
|
1230
|
+
},
|
|
1185
1231
|
}`;
|
|
1186
1232
|
}
|
|
1187
1233
|
function formatTsObjectLiteral(value) {
|
|
@@ -1277,17 +1323,135 @@ export default defineConfig(
|
|
|
1277
1323
|
);
|
|
1278
1324
|
`;
|
|
1279
1325
|
}
|
|
1326
|
+
function createAppRuntimeConfig() {
|
|
1327
|
+
return `import { defineRuntimeConfig } from '@modern-js/runtime';
|
|
1328
|
+
import { createInstance } from 'i18next';
|
|
1329
|
+
|
|
1330
|
+
const i18nInstance = createInstance();
|
|
1331
|
+
|
|
1332
|
+
export default defineRuntimeConfig({
|
|
1333
|
+
i18n: {
|
|
1334
|
+
i18nInstance,
|
|
1335
|
+
initOptions: {
|
|
1336
|
+
defaultNS: 'translation',
|
|
1337
|
+
fallbackLng: 'en',
|
|
1338
|
+
interpolation: {
|
|
1339
|
+
escapeValue: false,
|
|
1340
|
+
},
|
|
1341
|
+
ns: ['translation'],
|
|
1342
|
+
supportedLngs: ['en', 'cs'],
|
|
1343
|
+
},
|
|
1344
|
+
},
|
|
1345
|
+
router: {
|
|
1346
|
+
framework: 'tanstack',
|
|
1347
|
+
},
|
|
1348
|
+
});
|
|
1349
|
+
`;
|
|
1350
|
+
}
|
|
1351
|
+
function createLocalizedHeadComponent(includeLocationSuffix = false) {
|
|
1352
|
+
return `const fallbackLanguage = 'en';
|
|
1353
|
+
const supportedLanguages = ['en', 'cs'] as const;
|
|
1354
|
+
type SupportedLanguage = (typeof supportedLanguages)[number];
|
|
1355
|
+
|
|
1356
|
+
const isSupportedLanguage = (value: string): value is SupportedLanguage =>
|
|
1357
|
+
supportedLanguages.includes(value as SupportedLanguage);
|
|
1358
|
+
|
|
1359
|
+
const stripLanguagePrefix = (pathname: string) => {
|
|
1360
|
+
const segments = pathname.split('/').filter(Boolean);
|
|
1361
|
+
if (segments.length > 0 && isSupportedLanguage(segments[0] ?? '')) {
|
|
1362
|
+
segments.shift();
|
|
1363
|
+
}
|
|
1364
|
+
return \`/\${segments.join('/')}\`;
|
|
1365
|
+
};
|
|
1366
|
+
|
|
1367
|
+
const localizedPath = (pathname: string, language: SupportedLanguage) => {
|
|
1368
|
+
const pathWithoutLanguage = stripLanguagePrefix(pathname);
|
|
1369
|
+
return pathWithoutLanguage === '/' ? \`/\${language}\` : \`/\${language}\${pathWithoutLanguage}\`;
|
|
1370
|
+
};
|
|
1371
|
+
|
|
1372
|
+
const absoluteUrl = (pathname: string) => {
|
|
1373
|
+
const origin = ULTRAMODERN_SITE_URL.replace(/\\/+$/u, '');
|
|
1374
|
+
return \`\${origin}\${pathname}\`;
|
|
1375
|
+
};
|
|
1376
|
+
${includeLocationSuffix ? `
|
|
1377
|
+
const locationSuffix = (location: { hash?: unknown; search?: unknown; searchStr?: unknown }) => {
|
|
1378
|
+
const { hash, search, searchStr } = location;
|
|
1379
|
+
let locationSearch = '';
|
|
1380
|
+
if (typeof searchStr === 'string') {
|
|
1381
|
+
locationSearch = searchStr;
|
|
1382
|
+
} else if (typeof search === 'string') {
|
|
1383
|
+
locationSearch = search;
|
|
1384
|
+
}
|
|
1385
|
+
const locationHash = typeof hash === 'string' ? hash : '';
|
|
1386
|
+
return \`\${locationSearch}\${locationHash}\`;
|
|
1387
|
+
};
|
|
1388
|
+
` : ''}
|
|
1389
|
+
const LocalizedHead = () => {
|
|
1390
|
+
const { language } = useModernI18n();
|
|
1391
|
+
const location = useLocation();
|
|
1392
|
+
const currentLanguage = isSupportedLanguage(language) ? language : fallbackLanguage;
|
|
1393
|
+
const canonicalPath = localizedPath(location.pathname, currentLanguage);
|
|
1394
|
+
|
|
1395
|
+
return (
|
|
1396
|
+
<Helmet>
|
|
1397
|
+
<link rel="canonical" href={absoluteUrl(canonicalPath)} />
|
|
1398
|
+
{supportedLanguages.map((code) => (
|
|
1399
|
+
<link
|
|
1400
|
+
href={absoluteUrl(localizedPath(location.pathname, code))}
|
|
1401
|
+
hrefLang={code}
|
|
1402
|
+
key={code}
|
|
1403
|
+
rel="alternate"
|
|
1404
|
+
/>
|
|
1405
|
+
))}
|
|
1406
|
+
<link
|
|
1407
|
+
href={absoluteUrl(localizedPath(location.pathname, fallbackLanguage))}
|
|
1408
|
+
hrefLang="x-default"
|
|
1409
|
+
rel="alternate"
|
|
1410
|
+
/>
|
|
1411
|
+
</Helmet>
|
|
1412
|
+
);
|
|
1413
|
+
};
|
|
1414
|
+
`;
|
|
1415
|
+
}
|
|
1280
1416
|
function createShellPage() {
|
|
1281
|
-
return `
|
|
1417
|
+
return `import { Helmet } from '@modern-js/runtime/head';
|
|
1418
|
+
import { useModernI18n } from '@modern-js/plugin-i18n/runtime';
|
|
1419
|
+
import { useLocation } from '@modern-js/runtime/tanstack-router';
|
|
1420
|
+
import { useTranslation } from 'react-i18next';
|
|
1282
1421
|
|
|
1422
|
+
const remotes = ['remote-commerce', 'remote-identity', 'remote-design-system'];
|
|
1423
|
+
|
|
1424
|
+
${createLocalizedHeadComponent(true)}
|
|
1283
1425
|
export default function ShellHome() {
|
|
1426
|
+
const { t } = useTranslation();
|
|
1427
|
+
const { language } = useModernI18n();
|
|
1428
|
+
const location = useLocation();
|
|
1429
|
+
const currentLanguage = isSupportedLanguage(language) ? language : fallbackLanguage;
|
|
1430
|
+
const suffix = locationSuffix(location);
|
|
1431
|
+
const languageOptions = supportedLanguages.map((code) => ({
|
|
1432
|
+
code,
|
|
1433
|
+
href: \`\${localizedPath(location.pathname, code)}\${suffix}\`,
|
|
1434
|
+
label: t(\`language.\${code}\`),
|
|
1435
|
+
}));
|
|
1284
1436
|
return (
|
|
1285
1437
|
<main>
|
|
1286
|
-
<
|
|
1287
|
-
<
|
|
1438
|
+
<LocalizedHead />
|
|
1439
|
+
<nav aria-label={t('language.switcher')}>
|
|
1440
|
+
{languageOptions.map((option) => (
|
|
1441
|
+
<a
|
|
1442
|
+
aria-current={currentLanguage === option.code ? 'page' : undefined}
|
|
1443
|
+
href={option.href}
|
|
1444
|
+
key={option.code}
|
|
1445
|
+
>
|
|
1446
|
+
{option.label}
|
|
1447
|
+
</a>
|
|
1448
|
+
))}
|
|
1449
|
+
</nav>
|
|
1450
|
+
<h1>{t('shell.title')}</h1>
|
|
1451
|
+
<p data-testid="ultramodern-preset">{t('shell.preset')}</p>
|
|
1288
1452
|
<ul>
|
|
1289
1453
|
{remotes.map((remote) => (
|
|
1290
|
-
<li key={remote}>{remote}</li>
|
|
1454
|
+
<li key={remote}>{t(\`shell.remotes.\${remote}\`)}</li>
|
|
1291
1455
|
))}
|
|
1292
1456
|
</ul>
|
|
1293
1457
|
</main>
|
|
@@ -1296,11 +1460,20 @@ export default function ShellHome() {
|
|
|
1296
1460
|
`;
|
|
1297
1461
|
}
|
|
1298
1462
|
function createRemotePage(app) {
|
|
1299
|
-
return `
|
|
1463
|
+
return `import { Helmet } from '@modern-js/runtime/head';
|
|
1464
|
+
import { useModernI18n } from '@modern-js/plugin-i18n/runtime';
|
|
1465
|
+
import { useLocation } from '@modern-js/runtime/tanstack-router';
|
|
1466
|
+
import { useTranslation } from 'react-i18next';
|
|
1467
|
+
|
|
1468
|
+
${createLocalizedHeadComponent()}
|
|
1469
|
+
export default function ${toPascalCase(app.id)}Home() {
|
|
1470
|
+
const { t } = useTranslation();
|
|
1471
|
+
|
|
1300
1472
|
return (
|
|
1301
1473
|
<main>
|
|
1302
|
-
<
|
|
1303
|
-
<
|
|
1474
|
+
<LocalizedHead />
|
|
1475
|
+
<h1>{t('remote.title')}</h1>
|
|
1476
|
+
<p data-mf-role="${app.kind}">{t('remote.domain')}</p>
|
|
1304
1477
|
</main>
|
|
1305
1478
|
);
|
|
1306
1479
|
}
|
|
@@ -1321,11 +1494,15 @@ function createRemoteEntry(app) {
|
|
|
1321
1494
|
}
|
|
1322
1495
|
function createRemoteWidget(app) {
|
|
1323
1496
|
const componentName = 'remote-identity' === app.id ? 'IdentityWidget' : 'CommerceWidget';
|
|
1324
|
-
return `
|
|
1497
|
+
return `import { useTranslation } from 'react-i18next';
|
|
1498
|
+
|
|
1499
|
+
export default function ${componentName}() {
|
|
1500
|
+
const { t } = useTranslation();
|
|
1501
|
+
|
|
1325
1502
|
return (
|
|
1326
1503
|
<section data-mf-remote="${app.id}">
|
|
1327
|
-
<h2
|
|
1328
|
-
<p>
|
|
1504
|
+
<h2>{t('remote.widget.title')}</h2>
|
|
1505
|
+
<p>{t('remote.widget.body')}</p>
|
|
1329
1506
|
</section>
|
|
1330
1507
|
);
|
|
1331
1508
|
}
|
|
@@ -1334,7 +1511,7 @@ function createRemoteWidget(app) {
|
|
|
1334
1511
|
function createDesignButton() {
|
|
1335
1512
|
return `import { designTokens } from '../tokens';
|
|
1336
1513
|
|
|
1337
|
-
export default function Button({ label
|
|
1514
|
+
export default function Button({ label }: { label: string }) {
|
|
1338
1515
|
return (
|
|
1339
1516
|
<button
|
|
1340
1517
|
type="button"
|
|
@@ -1361,6 +1538,62 @@ function createDesignTokens() {
|
|
|
1361
1538
|
} as const;
|
|
1362
1539
|
`;
|
|
1363
1540
|
}
|
|
1541
|
+
function createEnglishTranslations(app) {
|
|
1542
|
+
if ('shell' === app.kind) return {
|
|
1543
|
+
language: {
|
|
1544
|
+
cs: 'Czech',
|
|
1545
|
+
en: 'English',
|
|
1546
|
+
switcher: 'Language'
|
|
1547
|
+
},
|
|
1548
|
+
shell: {
|
|
1549
|
+
preset: 'presetUltramodern workspace',
|
|
1550
|
+
remotes: {
|
|
1551
|
+
'remote-commerce': 'Commerce Remote',
|
|
1552
|
+
'remote-design-system': 'Design System Remote',
|
|
1553
|
+
'remote-identity': 'Identity Remote'
|
|
1554
|
+
},
|
|
1555
|
+
title: 'UltraModern SuperApp Shell'
|
|
1556
|
+
}
|
|
1557
|
+
};
|
|
1558
|
+
return {
|
|
1559
|
+
remote: {
|
|
1560
|
+
domain: app.domain ?? app.kind,
|
|
1561
|
+
title: app.displayName,
|
|
1562
|
+
widget: {
|
|
1563
|
+
body: 'vertical' === app.kind ? `Owns the ${app.domain} vertical route surface.` : 'Provides shared UI primitives for the workspace.',
|
|
1564
|
+
title: app.displayName
|
|
1565
|
+
}
|
|
1566
|
+
}
|
|
1567
|
+
};
|
|
1568
|
+
}
|
|
1569
|
+
function createCzechTranslations(app) {
|
|
1570
|
+
if ('shell' === app.kind) return {
|
|
1571
|
+
language: {
|
|
1572
|
+
cs: 'Cestina',
|
|
1573
|
+
en: 'Anglictina',
|
|
1574
|
+
switcher: 'Jazyk'
|
|
1575
|
+
},
|
|
1576
|
+
shell: {
|
|
1577
|
+
preset: 'presetUltramodern workspace',
|
|
1578
|
+
remotes: {
|
|
1579
|
+
'remote-commerce': 'Commerce remote',
|
|
1580
|
+
'remote-design-system': 'Design system remote',
|
|
1581
|
+
'remote-identity': 'Identity remote'
|
|
1582
|
+
},
|
|
1583
|
+
title: 'UltraModern SuperApp shell'
|
|
1584
|
+
}
|
|
1585
|
+
};
|
|
1586
|
+
return {
|
|
1587
|
+
remote: {
|
|
1588
|
+
domain: app.domain ?? app.kind,
|
|
1589
|
+
title: app.displayName,
|
|
1590
|
+
widget: {
|
|
1591
|
+
body: 'vertical' === app.kind ? `Vlastni ${app.domain} vertical route surface.` : 'Poskytuje sdilene UI prvky pro workspace.',
|
|
1592
|
+
title: app.displayName
|
|
1593
|
+
}
|
|
1594
|
+
}
|
|
1595
|
+
};
|
|
1596
|
+
}
|
|
1364
1597
|
function createEffectSharedApi() {
|
|
1365
1598
|
return `import {
|
|
1366
1599
|
HttpApi,
|
|
@@ -1698,11 +1931,14 @@ function createTemplateManifest(modernVersion, packageSource) {
|
|
|
1698
1931
|
function writeApp(targetDir, scope, app, packageSource) {
|
|
1699
1932
|
writeJson(targetDir, `${app.directory}/package.json`, createAppPackage(scope, app, packageSource));
|
|
1700
1933
|
writeJson(targetDir, `${app.directory}/tsconfig.json`, createPackageTsConfig(app.directory));
|
|
1701
|
-
writeFile(targetDir, `${app.directory}/src/modern-app-env.d.ts`, "/// <reference types='@modern-js/app-tools/types' />\n");
|
|
1934
|
+
writeFile(targetDir, `${app.directory}/src/modern-app-env.d.ts`, "/// <reference types='@modern-js/app-tools/types' />\n\ndeclare const ULTRAMODERN_SITE_URL: string;\n");
|
|
1702
1935
|
writeFile(targetDir, `${app.directory}/modern.config.ts`, createAppModernConfig(app));
|
|
1936
|
+
writeFile(targetDir, `${app.directory}/src/modern.runtime.ts`, createAppRuntimeConfig());
|
|
1937
|
+
writeJson(targetDir, `${app.directory}/config/public/locales/en/translation.json`, createEnglishTranslations(app));
|
|
1938
|
+
writeJson(targetDir, `${app.directory}/config/public/locales/cs/translation.json`, createCzechTranslations(app));
|
|
1703
1939
|
writeFile(targetDir, `${app.directory}/module-federation.config.ts`, 'shell' === app.kind ? createShellModuleFederationConfig() : createRemoteModuleFederationConfig(app));
|
|
1704
1940
|
writeFile(targetDir, `${app.directory}/src/routes/layout.tsx`, createLayout(app.id));
|
|
1705
|
-
writeFile(targetDir, `${app.directory}/src/routes/page.tsx`, 'shell' === app.kind ? createShellPage() : createRemotePage(app));
|
|
1941
|
+
writeFile(targetDir, `${app.directory}/src/routes/[lang]/page.tsx`, 'shell' === app.kind ? createShellPage() : createRemotePage(app));
|
|
1706
1942
|
if ('vertical' === app.kind) {
|
|
1707
1943
|
writeFile(targetDir, `${app.directory}/src/remote-entry.tsx`, createRemoteEntry(app));
|
|
1708
1944
|
const widgetFile = 'remote-identity' === app.id ? 'identity-widget.tsx' : 'commerce-widget.tsx';
|
|
@@ -1855,7 +2091,7 @@ function detectBffRuntime() {
|
|
|
1855
2091
|
process.exit(1);
|
|
1856
2092
|
}
|
|
1857
2093
|
function src_renderTemplate(template, data) {
|
|
1858
|
-
const tagRegex = /\{\{(#if|#unless|\/if|\/unless)(?:\s+(\w+))
|
|
2094
|
+
const tagRegex = /\{\{(~?)(#if|#unless|\/if|\/unless)(?:\s+(\w+))?(~?)\}\}/g;
|
|
1859
2095
|
function renderConditionals(startIndex, expectedClose) {
|
|
1860
2096
|
let rendered = '';
|
|
1861
2097
|
let cursor = startIndex;
|
|
@@ -1866,7 +2102,7 @@ function src_renderTemplate(template, data) {
|
|
|
1866
2102
|
rendered: rendered + template.slice(cursor),
|
|
1867
2103
|
nextIndex: template.length
|
|
1868
2104
|
};
|
|
1869
|
-
const [raw, tag, condition] = match;
|
|
2105
|
+
const [raw, , tag, condition, rightTrim] = match;
|
|
1870
2106
|
const tagIndex = match.index;
|
|
1871
2107
|
rendered += template.slice(cursor, tagIndex);
|
|
1872
2108
|
cursor = tagIndex + raw.length;
|
|
@@ -1882,10 +2118,17 @@ function src_renderTemplate(template, data) {
|
|
|
1882
2118
|
}
|
|
1883
2119
|
if ('/if' === tag || '/unless' === tag) {
|
|
1884
2120
|
const kind = '/if' === tag ? 'if' : 'unless';
|
|
1885
|
-
if (expectedClose === kind)
|
|
1886
|
-
|
|
1887
|
-
|
|
1888
|
-
|
|
2121
|
+
if (expectedClose === kind) {
|
|
2122
|
+
let nextIndex = cursor;
|
|
2123
|
+
if ('~' === rightTrim) {
|
|
2124
|
+
const trailingWhitespace = /^\s*/u.exec(template.slice(nextIndex));
|
|
2125
|
+
nextIndex += trailingWhitespace?.[0].length ?? 0;
|
|
2126
|
+
}
|
|
2127
|
+
return {
|
|
2128
|
+
rendered,
|
|
2129
|
+
nextIndex
|
|
2130
|
+
};
|
|
2131
|
+
}
|
|
1889
2132
|
rendered += raw;
|
|
1890
2133
|
}
|
|
1891
2134
|
}
|
|
@@ -1974,6 +2217,7 @@ function createBuiltinTemplateManifest(version) {
|
|
|
1974
2217
|
'AGENTS.md',
|
|
1975
2218
|
'README.md',
|
|
1976
2219
|
'api/**',
|
|
2220
|
+
'config/**',
|
|
1977
2221
|
'modern.config.ts',
|
|
1978
2222
|
'oxfmt.config.ts',
|
|
1979
2223
|
'oxlint.config.ts',
|
|
@@ -2368,6 +2612,7 @@ async function main() {
|
|
|
2368
2612
|
tsconfigVersion: singleAppModernPackageSpecifier('@modern-js/tsconfig', packageSource, useWorkspaceProtocol),
|
|
2369
2613
|
pluginTanstackVersion: singleAppModernPackageSpecifier('@modern-js/plugin-tanstack', packageSource, useWorkspaceProtocol),
|
|
2370
2614
|
pluginBffVersion: singleAppModernPackageSpecifier('@modern-js/plugin-bff', packageSource, useWorkspaceProtocol),
|
|
2615
|
+
pluginI18nVersion: singleAppModernPackageSpecifier('@modern-js/plugin-i18n', packageSource, useWorkspaceProtocol),
|
|
2371
2616
|
isSubproject,
|
|
2372
2617
|
routerFramework,
|
|
2373
2618
|
bffRuntime,
|
|
@@ -2447,6 +2692,7 @@ function copyTemplate(src, dest, options) {
|
|
|
2447
2692
|
tsconfigVersion: options.tsconfigVersion,
|
|
2448
2693
|
pluginTanstackVersion: options.pluginTanstackVersion,
|
|
2449
2694
|
pluginBffVersion: options.pluginBffVersion,
|
|
2695
|
+
pluginI18nVersion: options.pluginI18nVersion,
|
|
2450
2696
|
isSubproject: options.isSubproject,
|
|
2451
2697
|
isTanstackRouter: 'tanstack' === options.routerFramework,
|
|
2452
2698
|
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.13",
|
|
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.13"
|
|
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.13"
|
|
58
58
|
}
|
|
59
59
|
}
|
package/template/AGENTS.md
CHANGED
|
@@ -7,8 +7,15 @@ 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
|
+
|
|
17
|
+
Routes are locale-prefixed by default through `localePathRedirect: true`. Keep localized pages under `src/routes/[lang]`, use links for language switching, and preserve canonical plus `hreflang` metadata. Production builds fail unless `MODERN_PUBLIC_SITE_URL` is set, so deployed canonical URLs always use the production origin.
|
|
18
|
+
|
|
12
19
|
## Private Skills
|
|
13
20
|
|
|
14
21
|
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';
|
|
@@ -11,6 +12,18 @@ const enableTelemetryExporters =
|
|
|
11
12
|
process.env['MODERN_BASELINE_ENABLE_TELEMETRY_EXPORTERS'] !== 'false';
|
|
12
13
|
const telemetryFailLoudStartup = process.env['MODERN_TELEMETRY_FAIL_LOUD_STARTUP'] !== 'false';
|
|
13
14
|
const otlpEndpoint = process.env['MODERN_TELEMETRY_OTLP_ENDPOINT'];
|
|
15
|
+
const configuredSiteUrl = process.env['MODERN_PUBLIC_SITE_URL'];
|
|
16
|
+
const hasConfiguredSiteUrl = typeof configuredSiteUrl === 'string' && configuredSiteUrl.length > 0;
|
|
17
|
+
const isProductionBuild =
|
|
18
|
+
process.env['NODE_ENV'] === 'production' || process.argv.includes('build');
|
|
19
|
+
|
|
20
|
+
if (isProductionBuild && !hasConfiguredSiteUrl) {
|
|
21
|
+
throw new Error(
|
|
22
|
+
'MODERN_PUBLIC_SITE_URL must be set for production builds so canonical and hreflang URLs use the deployed origin.',
|
|
23
|
+
);
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
const siteUrl = hasConfiguredSiteUrl ? configuredSiteUrl : 'http://localhost:8080';
|
|
14
27
|
const victoriaMetricsEndpoint = process.env['MODERN_TELEMETRY_VICTORIA_ENDPOINT'];
|
|
15
28
|
|
|
16
29
|
// https://bleedingdev.github.io/ultramodern.js/configure/app/usage.html
|
|
@@ -27,11 +40,23 @@ export default defineConfig(
|
|
|
27
40
|
|
|
28
41
|
{{/if}} plugins: [
|
|
29
42
|
appTools(),
|
|
43
|
+
i18nPlugin({
|
|
44
|
+
localeDetection: {
|
|
45
|
+
fallbackLanguage: 'en',
|
|
46
|
+
languages: ['en', 'cs'],
|
|
47
|
+
localePathRedirect: true,
|
|
48
|
+
},
|
|
49
|
+
}),
|
|
30
50
|
{{#if isTanstackRouter}}
|
|
31
51
|
tanstackRouterPlugin(),
|
|
32
52
|
{{/if}}{{#if enableBff}}
|
|
33
53
|
bffPlugin(),
|
|
34
54
|
{{/if}} ],
|
|
55
|
+
source: {
|
|
56
|
+
globalVars: {
|
|
57
|
+
ULTRAMODERN_SITE_URL: siteUrl,
|
|
58
|
+
},
|
|
59
|
+
},
|
|
35
60
|
},
|
|
36
61
|
{
|
|
37
62
|
appId,
|
|
@@ -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",
|