@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 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: [appTools(), tanstackRouterPlugin(), moduleFederationPlugin()],
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 `const remotes = ['remote-commerce', 'remote-identity', 'remote-design-system'];
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
- <h1>UltraModern SuperApp Shell</h1>
1287
- <p data-testid="ultramodern-preset">presetUltramodern workspace</p>
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 `export default function ${toPascalCase(app.id)}Home() {
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>${app.displayName}</h1>
1303
- <p data-mf-role="${app.kind}">${app.domain ?? app.kind}</p>
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 `export default function ${componentName}() {
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>${app.displayName}</h2>
1328
- <p>Owns the ${app.domain} vertical route surface.</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 = 'Design System Button' }: { label?: string }) {
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+))?\}\}/g;
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) return {
1886
- rendered,
1887
- nextIndex: cursor
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.11",
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.11"
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.11"
57
+ "frameworkVersion": "3.2.0-ultramodern.12"
58
58
  }
59
59
  }
@@ -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}}{{#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';
@@ -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}} "@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",
@@ -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
- },{{/if}}
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
- UltraModern.js 3.0
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">presetUltramodern</p>
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
- This starter ships the public <code className="code">presetUltramodern(...)</code>{' '}
50
- profile. Start in
51
- <code className="code">modern.config.ts</code>, keep
52
- <code className="code">pnpm run ultramodern:check</code> green, and tune the generated
53
- preset only where your app needs a softer lane.
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
- Effect HttpApi response: <code className="code">{effectMessage}</code>
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
- UltraModern.js Guide
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>Review the MV-first, TanStack-ready, Effect-ready public preset.</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
- Configure presetUltramodern
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
- Ultramodern Gates
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
- BFF + Effect
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>Keep Effect as the preferred BFF lane while Hono stays an explicit fallback.</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` runs formatting, linting, effect-tsgo, private-skill availability checks, and the generated workspace contract.
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()'),