@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 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: [appTools(), tanstackRouterPlugin(), moduleFederationPlugin()],
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 `const remotes = ['remote-commerce', 'remote-identity', 'remote-design-system'];
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
- <h1>UltraModern SuperApp Shell</h1>
1287
- <p data-testid="ultramodern-preset">presetUltramodern workspace</p>
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 `export default function ${toPascalCase(app.id)}Home() {
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
- <h1>${app.displayName}</h1>
1303
- <p data-mf-role="${app.kind}">${app.domain ?? app.kind}</p>
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 `export default function ${componentName}() {
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>${app.displayName}</h2>
1328
- <p>Owns the ${app.domain} vertical route surface.</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 = 'Design System Button' }: { label?: string }) {
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+))?\}\}/g;
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) return {
1886
- rendered,
1887
- nextIndex: cursor
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.11",
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.11"
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.11"
57
+ "frameworkVersion": "3.2.0-ultramodern.13"
58
58
  }
59
59
  }
@@ -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}}{{#if isTanstackRouter}}import { tanstackRouterPlugin } from '@modern-js/plugin-tanstack';
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}} "@modern-js/runtime": "{{runtimeVersion}}",
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",