@bleedingdev/modern-js-create 3.2.0-ultramodern.12 → 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 +109 -15
- package/package.json +3 -3
- package/template/AGENTS.md +2 -0
- package/template/modern.config.ts.handlebars +18 -0
- package/template/scripts/validate-ultramodern.mjs.handlebars +28 -0
- package/template/src/modern-app-env.d.ts +2 -0
- package/template/src/routes/{page.tsx.handlebars → [lang]/page.tsx.handlebars} +67 -11
- package/template-workspace/AGENTS.md +2 -0
- package/template-workspace/scripts/validate-ultramodern-workspace.mjs.handlebars +39 -0
package/dist/index.js
CHANGED
|
@@ -1139,6 +1139,18 @@ import { moduleFederationPlugin } from '@module-federation/modern-js-v3';
|
|
|
1139
1139
|
|
|
1140
1140
|
const appId = '${app.id}';
|
|
1141
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}\`;
|
|
1142
1154
|
|
|
1143
1155
|
export default defineConfig(
|
|
1144
1156
|
presetUltramodern(
|
|
@@ -1154,6 +1166,7 @@ export default defineConfig(
|
|
|
1154
1166
|
localeDetection: {
|
|
1155
1167
|
fallbackLanguage: 'en',
|
|
1156
1168
|
languages: ['en', 'cs'],
|
|
1169
|
+
localePathRedirect: true,
|
|
1157
1170
|
},
|
|
1158
1171
|
}),
|
|
1159
1172
|
tanstackRouterPlugin(),
|
|
@@ -1166,6 +1179,11 @@ export default defineConfig(
|
|
|
1166
1179
|
moduleFederationAppSSR: true,
|
|
1167
1180
|
},
|
|
1168
1181
|
},
|
|
1182
|
+
source: {
|
|
1183
|
+
globalVars: {
|
|
1184
|
+
ULTRAMODERN_SITE_URL: siteUrl,
|
|
1185
|
+
},
|
|
1186
|
+
},
|
|
1169
1187
|
},
|
|
1170
1188
|
{
|
|
1171
1189
|
appId,
|
|
@@ -1330,32 +1348,103 @@ export default defineRuntimeConfig({
|
|
|
1330
1348
|
});
|
|
1331
1349
|
`;
|
|
1332
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
|
+
}
|
|
1333
1416
|
function createShellPage() {
|
|
1334
|
-
return `import {
|
|
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';
|
|
1335
1420
|
import { useTranslation } from 'react-i18next';
|
|
1336
1421
|
|
|
1337
1422
|
const remotes = ['remote-commerce', 'remote-identity', 'remote-design-system'];
|
|
1338
1423
|
|
|
1424
|
+
${createLocalizedHeadComponent(true)}
|
|
1339
1425
|
export default function ShellHome() {
|
|
1340
1426
|
const { t } = useTranslation();
|
|
1341
|
-
const {
|
|
1342
|
-
const
|
|
1343
|
-
|
|
1344
|
-
|
|
1345
|
-
|
|
1346
|
-
|
|
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
|
+
}));
|
|
1347
1436
|
return (
|
|
1348
1437
|
<main>
|
|
1438
|
+
<LocalizedHead />
|
|
1349
1439
|
<nav aria-label={t('language.switcher')}>
|
|
1350
1440
|
{languageOptions.map((option) => (
|
|
1351
|
-
<
|
|
1352
|
-
|
|
1441
|
+
<a
|
|
1442
|
+
aria-current={currentLanguage === option.code ? 'page' : undefined}
|
|
1443
|
+
href={option.href}
|
|
1353
1444
|
key={option.code}
|
|
1354
|
-
onClick={() => void changeLanguage(option.code)}
|
|
1355
|
-
type="button"
|
|
1356
1445
|
>
|
|
1357
1446
|
{option.label}
|
|
1358
|
-
</
|
|
1447
|
+
</a>
|
|
1359
1448
|
))}
|
|
1360
1449
|
</nav>
|
|
1361
1450
|
<h1>{t('shell.title')}</h1>
|
|
@@ -1371,13 +1460,18 @@ export default function ShellHome() {
|
|
|
1371
1460
|
`;
|
|
1372
1461
|
}
|
|
1373
1462
|
function createRemotePage(app) {
|
|
1374
|
-
return `import {
|
|
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';
|
|
1375
1467
|
|
|
1468
|
+
${createLocalizedHeadComponent()}
|
|
1376
1469
|
export default function ${toPascalCase(app.id)}Home() {
|
|
1377
1470
|
const { t } = useTranslation();
|
|
1378
1471
|
|
|
1379
1472
|
return (
|
|
1380
1473
|
<main>
|
|
1474
|
+
<LocalizedHead />
|
|
1381
1475
|
<h1>{t('remote.title')}</h1>
|
|
1382
1476
|
<p data-mf-role="${app.kind}">{t('remote.domain')}</p>
|
|
1383
1477
|
</main>
|
|
@@ -1837,14 +1931,14 @@ function createTemplateManifest(modernVersion, packageSource) {
|
|
|
1837
1931
|
function writeApp(targetDir, scope, app, packageSource) {
|
|
1838
1932
|
writeJson(targetDir, `${app.directory}/package.json`, createAppPackage(scope, app, packageSource));
|
|
1839
1933
|
writeJson(targetDir, `${app.directory}/tsconfig.json`, createPackageTsConfig(app.directory));
|
|
1840
|
-
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");
|
|
1841
1935
|
writeFile(targetDir, `${app.directory}/modern.config.ts`, createAppModernConfig(app));
|
|
1842
1936
|
writeFile(targetDir, `${app.directory}/src/modern.runtime.ts`, createAppRuntimeConfig());
|
|
1843
1937
|
writeJson(targetDir, `${app.directory}/config/public/locales/en/translation.json`, createEnglishTranslations(app));
|
|
1844
1938
|
writeJson(targetDir, `${app.directory}/config/public/locales/cs/translation.json`, createCzechTranslations(app));
|
|
1845
1939
|
writeFile(targetDir, `${app.directory}/module-federation.config.ts`, 'shell' === app.kind ? createShellModuleFederationConfig() : createRemoteModuleFederationConfig(app));
|
|
1846
1940
|
writeFile(targetDir, `${app.directory}/src/routes/layout.tsx`, createLayout(app.id));
|
|
1847
|
-
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));
|
|
1848
1942
|
if ('vertical' === app.kind) {
|
|
1849
1943
|
writeFile(targetDir, `${app.directory}/src/remote-entry.tsx`, createRemoteEntry(app));
|
|
1850
1944
|
const widgetFile = 'remote-identity' === app.id ? 'identity-widget.tsx' : 'commerce-widget.tsx';
|
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
|
@@ -14,6 +14,8 @@ This project is generated for Codex-first UltraModern.js work.
|
|
|
14
14
|
|
|
15
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
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
|
+
|
|
17
19
|
## Private Skills
|
|
18
20
|
|
|
19
21
|
Private orchestration skills are not vendored into this template. If you are authorized for `TechsioCZ/skills`, run:
|
|
@@ -12,6 +12,18 @@ const enableTelemetryExporters =
|
|
|
12
12
|
process.env['MODERN_BASELINE_ENABLE_TELEMETRY_EXPORTERS'] !== 'false';
|
|
13
13
|
const telemetryFailLoudStartup = process.env['MODERN_TELEMETRY_FAIL_LOUD_STARTUP'] !== 'false';
|
|
14
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';
|
|
15
27
|
const victoriaMetricsEndpoint = process.env['MODERN_TELEMETRY_VICTORIA_ENDPOINT'];
|
|
16
28
|
|
|
17
29
|
// https://bleedingdev.github.io/ultramodern.js/configure/app/usage.html
|
|
@@ -32,6 +44,7 @@ export default defineConfig(
|
|
|
32
44
|
localeDetection: {
|
|
33
45
|
fallbackLanguage: 'en',
|
|
34
46
|
languages: ['en', 'cs'],
|
|
47
|
+
localePathRedirect: true,
|
|
35
48
|
},
|
|
36
49
|
}),
|
|
37
50
|
{{#if isTanstackRouter}}
|
|
@@ -39,6 +52,11 @@ export default defineConfig(
|
|
|
39
52
|
{{/if}}{{#if enableBff}}
|
|
40
53
|
bffPlugin(),
|
|
41
54
|
{{/if}} ],
|
|
55
|
+
source: {
|
|
56
|
+
globalVars: {
|
|
57
|
+
ULTRAMODERN_SITE_URL: siteUrl,
|
|
58
|
+
},
|
|
59
|
+
},
|
|
42
60
|
},
|
|
43
61
|
{
|
|
44
62
|
appId,
|
|
@@ -17,6 +17,10 @@ const requiredTokens = [
|
|
|
17
17
|
'enableBffRequestId',
|
|
18
18
|
'enableTelemetryExporters',
|
|
19
19
|
'i18nPlugin(',
|
|
20
|
+
'localePathRedirect: true',
|
|
21
|
+
'ULTRAMODERN_SITE_URL',
|
|
22
|
+
'MODERN_PUBLIC_SITE_URL must be set for production builds',
|
|
23
|
+
'globalVars',
|
|
20
24
|
];
|
|
21
25
|
const missing = requiredTokens.filter((token) => !content.includes(token));
|
|
22
26
|
|
|
@@ -55,6 +59,8 @@ const requiredPaths = [
|
|
|
55
59
|
'scripts/check-i18n-strings.mjs',
|
|
56
60
|
'config/public/locales/en/translation.json',
|
|
57
61
|
'config/public/locales/cs/translation.json',
|
|
62
|
+
'src/modern-app-env.d.ts',
|
|
63
|
+
'src/routes/[lang]/page.tsx',
|
|
58
64
|
];
|
|
59
65
|
const manifestErrors = [];
|
|
60
66
|
|
|
@@ -65,6 +71,28 @@ for (const requiredPath of requiredPaths) {
|
|
|
65
71
|
}
|
|
66
72
|
}
|
|
67
73
|
|
|
74
|
+
if (fs.existsSync(path.resolve(process.cwd(), 'src/routes/page.tsx'))) {
|
|
75
|
+
console.error('src/routes/page.tsx must move under src/routes/[lang]/page.tsx');
|
|
76
|
+
process.exit(1);
|
|
77
|
+
}
|
|
78
|
+
|
|
79
|
+
const pageContent = fs.readFileSync(
|
|
80
|
+
path.resolve(process.cwd(), 'src/routes/[lang]/page.tsx'),
|
|
81
|
+
'utf-8',
|
|
82
|
+
);
|
|
83
|
+
for (const token of [
|
|
84
|
+
'rel="canonical"',
|
|
85
|
+
'rel="alternate"',
|
|
86
|
+
'hrefLang="x-default"',
|
|
87
|
+
'localizedPath(',
|
|
88
|
+
'<a',
|
|
89
|
+
]) {
|
|
90
|
+
if (!pageContent.includes(token)) {
|
|
91
|
+
console.error(`Localized route is missing ${token}`);
|
|
92
|
+
process.exit(1);
|
|
93
|
+
}
|
|
94
|
+
}
|
|
95
|
+
|
|
68
96
|
if (templateManifest.schemaVersion !== 1) {
|
|
69
97
|
manifestErrors.push('schemaVersion');
|
|
70
98
|
}
|
|
@@ -1,19 +1,62 @@
|
|
|
1
1
|
import { Helmet } from '@modern-js/runtime/head';
|
|
2
2
|
import { useModernI18n } from '@modern-js/plugin-i18n/runtime';
|
|
3
|
+
import { useLocation } from '@modern-js/runtime/{{routerImportPath}}';
|
|
3
4
|
{{#if useEffectBff}}import effectBff from '@api/effect/index';
|
|
4
5
|
import { Effect } from '@modern-js/plugin-bff/effect-client';
|
|
5
6
|
import { useEffect, useState } from 'react';
|
|
6
7
|
{{/if}}
|
|
7
8
|
import { useTranslation } from 'react-i18next';
|
|
8
|
-
import '
|
|
9
|
+
import '../index.css';
|
|
10
|
+
|
|
11
|
+
const fallbackLanguage = 'en';
|
|
12
|
+
const supportedLanguages = ['en', 'cs'] as const;
|
|
13
|
+
type SupportedLanguage = (typeof supportedLanguages)[number];
|
|
14
|
+
|
|
15
|
+
const isSupportedLanguage = (value: string): value is SupportedLanguage =>
|
|
16
|
+
supportedLanguages.includes(value as SupportedLanguage);
|
|
17
|
+
|
|
18
|
+
const stripLanguagePrefix = (pathname: string) => {
|
|
19
|
+
const segments = pathname.split('/').filter(Boolean);
|
|
20
|
+
if (segments.length > 0 && isSupportedLanguage(segments[0] ?? '')) {
|
|
21
|
+
segments.shift();
|
|
22
|
+
}
|
|
23
|
+
return `/${segments.join('/')}`;
|
|
24
|
+
};
|
|
25
|
+
|
|
26
|
+
const localizedPath = (pathname: string, language: SupportedLanguage) => {
|
|
27
|
+
const pathWithoutLanguage = stripLanguagePrefix(pathname);
|
|
28
|
+
return pathWithoutLanguage === '/' ? `/${language}` : `/${language}${pathWithoutLanguage}`;
|
|
29
|
+
};
|
|
30
|
+
|
|
31
|
+
const absoluteUrl = (pathname: string) => {
|
|
32
|
+
const origin = ULTRAMODERN_SITE_URL.replace(/\/+$/u, '');
|
|
33
|
+
return `${origin}${pathname}`;
|
|
34
|
+
};
|
|
35
|
+
|
|
36
|
+
const locationSuffix = (location: { hash?: unknown; search?: unknown; searchStr?: unknown }) => {
|
|
37
|
+
const { hash, search, searchStr } = location;
|
|
38
|
+
let locationSearch = '';
|
|
39
|
+
if (typeof searchStr === 'string') {
|
|
40
|
+
locationSearch = searchStr;
|
|
41
|
+
} else if (typeof search === 'string') {
|
|
42
|
+
locationSearch = search;
|
|
43
|
+
}
|
|
44
|
+
const locationHash = typeof hash === 'string' ? hash : '';
|
|
45
|
+
return `${locationSearch}${locationHash}`;
|
|
46
|
+
};
|
|
9
47
|
|
|
10
48
|
const Index = () => {
|
|
11
49
|
const { t } = useTranslation();
|
|
12
|
-
const {
|
|
13
|
-
const
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
50
|
+
const { language } = useModernI18n();
|
|
51
|
+
const location = useLocation();
|
|
52
|
+
const currentLanguage = isSupportedLanguage(language) ? language : fallbackLanguage;
|
|
53
|
+
const canonicalPath = localizedPath(location.pathname, currentLanguage);
|
|
54
|
+
const suffix = locationSuffix(location);
|
|
55
|
+
const languageOptions = supportedLanguages.map((code) => ({
|
|
56
|
+
code,
|
|
57
|
+
href: `${localizedPath(location.pathname, code)}${suffix}`,
|
|
58
|
+
label: t(`home.language.${code}`),
|
|
59
|
+
}));
|
|
17
60
|
{{#if useEffectBff}} const [effectMessage, setEffectMessage] = useState('loading...');
|
|
18
61
|
|
|
19
62
|
useEffect(() => {
|
|
@@ -42,18 +85,31 @@ const Index = () => {
|
|
|
42
85
|
type="image/x-icon"
|
|
43
86
|
href="https://lf3-static.bytednsdoc.com/obj/eden-cn/uhbfnupenuhf/favicon.ico"
|
|
44
87
|
/>
|
|
88
|
+
<link rel="canonical" href={absoluteUrl(canonicalPath)} />
|
|
89
|
+
{supportedLanguages.map((code) => (
|
|
90
|
+
<link
|
|
91
|
+
href={absoluteUrl(localizedPath(location.pathname, code))}
|
|
92
|
+
hrefLang={code}
|
|
93
|
+
key={code}
|
|
94
|
+
rel="alternate"
|
|
95
|
+
/>
|
|
96
|
+
))}
|
|
97
|
+
<link
|
|
98
|
+
href={absoluteUrl(localizedPath(location.pathname, fallbackLanguage))}
|
|
99
|
+
hrefLang="x-default"
|
|
100
|
+
rel="alternate"
|
|
101
|
+
/>
|
|
45
102
|
</Helmet>
|
|
46
103
|
<main>
|
|
47
104
|
<nav className="language-switcher" aria-label={t('home.language.switcher')}>
|
|
48
105
|
{languageOptions.map((option) => (
|
|
49
|
-
<
|
|
50
|
-
|
|
106
|
+
<a
|
|
107
|
+
aria-current={currentLanguage === option.code ? 'page' : undefined}
|
|
108
|
+
href={option.href}
|
|
51
109
|
key={option.code}
|
|
52
|
-
onClick={() => void changeLanguage(option.code)}
|
|
53
|
-
type="button"
|
|
54
110
|
>
|
|
55
111
|
{option.label}
|
|
56
|
-
</
|
|
112
|
+
</a>
|
|
57
113
|
))}
|
|
58
114
|
</nav>
|
|
59
115
|
<div className="title">
|
|
@@ -14,6 +14,8 @@ This workspace is generated as an agent-ready UltraModern.js SuperApp. Agents sh
|
|
|
14
14
|
|
|
15
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.
|
|
16
16
|
|
|
17
|
+
Routes are locale-prefixed by default through `localePathRedirect: true`. Keep localized app 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 per deployed app, so canonical URLs always use the production origin.
|
|
18
|
+
|
|
17
19
|
## Required Skill Baseline
|
|
18
20
|
|
|
19
21
|
Use these skills when the task touches the matching subsystem:
|
|
@@ -65,25 +65,33 @@ const requiredPaths = [
|
|
|
65
65
|
'apps/shell-super-app/config/public/locales/cs/translation.json',
|
|
66
66
|
'apps/shell-super-app/modern.config.ts',
|
|
67
67
|
'apps/shell-super-app/module-federation.config.ts',
|
|
68
|
+
'apps/shell-super-app/src/modern-app-env.d.ts',
|
|
68
69
|
'apps/shell-super-app/src/modern.runtime.ts',
|
|
70
|
+
'apps/shell-super-app/src/routes/[lang]/page.tsx',
|
|
69
71
|
'apps/remotes/remote-commerce/package.json',
|
|
70
72
|
'apps/remotes/remote-commerce/config/public/locales/en/translation.json',
|
|
71
73
|
'apps/remotes/remote-commerce/config/public/locales/cs/translation.json',
|
|
72
74
|
'apps/remotes/remote-commerce/modern.config.ts',
|
|
73
75
|
'apps/remotes/remote-commerce/module-federation.config.ts',
|
|
76
|
+
'apps/remotes/remote-commerce/src/modern-app-env.d.ts',
|
|
74
77
|
'apps/remotes/remote-commerce/src/modern.runtime.ts',
|
|
78
|
+
'apps/remotes/remote-commerce/src/routes/[lang]/page.tsx',
|
|
75
79
|
'apps/remotes/remote-identity/package.json',
|
|
76
80
|
'apps/remotes/remote-identity/config/public/locales/en/translation.json',
|
|
77
81
|
'apps/remotes/remote-identity/config/public/locales/cs/translation.json',
|
|
78
82
|
'apps/remotes/remote-identity/modern.config.ts',
|
|
79
83
|
'apps/remotes/remote-identity/module-federation.config.ts',
|
|
84
|
+
'apps/remotes/remote-identity/src/modern-app-env.d.ts',
|
|
80
85
|
'apps/remotes/remote-identity/src/modern.runtime.ts',
|
|
86
|
+
'apps/remotes/remote-identity/src/routes/[lang]/page.tsx',
|
|
81
87
|
'apps/remotes/remote-design-system/package.json',
|
|
82
88
|
'apps/remotes/remote-design-system/config/public/locales/en/translation.json',
|
|
83
89
|
'apps/remotes/remote-design-system/config/public/locales/cs/translation.json',
|
|
84
90
|
'apps/remotes/remote-design-system/modern.config.ts',
|
|
85
91
|
'apps/remotes/remote-design-system/module-federation.config.ts',
|
|
92
|
+
'apps/remotes/remote-design-system/src/modern-app-env.d.ts',
|
|
86
93
|
'apps/remotes/remote-design-system/src/modern.runtime.ts',
|
|
94
|
+
'apps/remotes/remote-design-system/src/routes/[lang]/page.tsx',
|
|
87
95
|
'services/service-recommendations-effect/package.json',
|
|
88
96
|
'services/service-recommendations-effect/modern.config.ts',
|
|
89
97
|
'services/service-recommendations-effect/api/effect/index.ts',
|
|
@@ -97,6 +105,18 @@ for (const requiredPath of requiredPaths) {
|
|
|
97
105
|
assertExists(requiredPath);
|
|
98
106
|
}
|
|
99
107
|
|
|
108
|
+
for (const appDirectory of [
|
|
109
|
+
'apps/shell-super-app',
|
|
110
|
+
'apps/remotes/remote-commerce',
|
|
111
|
+
'apps/remotes/remote-identity',
|
|
112
|
+
'apps/remotes/remote-design-system',
|
|
113
|
+
]) {
|
|
114
|
+
assert(
|
|
115
|
+
!fs.existsSync(path.join(root, appDirectory, 'src/routes/page.tsx')),
|
|
116
|
+
`${appDirectory} must use src/routes/[lang]/page.tsx`,
|
|
117
|
+
);
|
|
118
|
+
}
|
|
119
|
+
|
|
100
120
|
const rootPackage = readJson('package.json');
|
|
101
121
|
const packageSource = readJson('.modernjs/ultramodern-package-source.json');
|
|
102
122
|
const skillsLock = readJson('.agents/skills-lock.json');
|
|
@@ -286,6 +306,12 @@ for (const configPath of [
|
|
|
286
306
|
const config = readText(configPath);
|
|
287
307
|
assert(config.includes('presetUltramodern('), `${configPath} must use presetUltramodern`);
|
|
288
308
|
assert(config.includes('i18nPlugin('), `${configPath} must enable plugin-i18n`);
|
|
309
|
+
assert(config.includes('localePathRedirect: true'), `${configPath} must prefix localized URLs`);
|
|
310
|
+
assert(config.includes('ULTRAMODERN_SITE_URL'), `${configPath} must expose site URL metadata`);
|
|
311
|
+
assert(
|
|
312
|
+
config.includes('MODERN_PUBLIC_SITE_URL must be set for production builds'),
|
|
313
|
+
`${configPath} must require MODERN_PUBLIC_SITE_URL for production builds`,
|
|
314
|
+
);
|
|
289
315
|
assert(config.includes('tanstackRouterPlugin()'), `${configPath} must enable plugin-tanstack`);
|
|
290
316
|
assert(
|
|
291
317
|
config.includes('moduleFederationPlugin()'),
|
|
@@ -293,6 +319,19 @@ for (const configPath of [
|
|
|
293
319
|
);
|
|
294
320
|
}
|
|
295
321
|
|
|
322
|
+
for (const routePath of [
|
|
323
|
+
'apps/shell-super-app/src/routes/[lang]/page.tsx',
|
|
324
|
+
'apps/remotes/remote-commerce/src/routes/[lang]/page.tsx',
|
|
325
|
+
'apps/remotes/remote-identity/src/routes/[lang]/page.tsx',
|
|
326
|
+
'apps/remotes/remote-design-system/src/routes/[lang]/page.tsx',
|
|
327
|
+
]) {
|
|
328
|
+
const route = readText(routePath);
|
|
329
|
+
assert(route.includes('rel="canonical"'), `${routePath} must emit canonical metadata`);
|
|
330
|
+
assert(route.includes('rel="alternate"'), `${routePath} must emit alternate locale metadata`);
|
|
331
|
+
assert(route.includes('hrefLang="x-default"'), `${routePath} must emit x-default metadata`);
|
|
332
|
+
assert(route.includes('localizedPath('), `${routePath} must build localized URLs`);
|
|
333
|
+
}
|
|
334
|
+
|
|
296
335
|
const shellMf = readText('apps/shell-super-app/module-federation.config.ts');
|
|
297
336
|
assert(shellMf.includes("name: 'shellSuperApp'"), 'Shell MF config must name the shell');
|
|
298
337
|
assert(
|