@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.
@@ -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,11 @@ const requiredTokens = [
16
16
  'enableModuleFederationSSR',
17
17
  'enableBffRequestId',
18
18
  'enableTelemetryExporters',
19
+ 'i18nPlugin(',
20
+ 'localePathRedirect: true',
21
+ 'ULTRAMODERN_SITE_URL',
22
+ 'MODERN_PUBLIC_SITE_URL must be set for production builds',
23
+ 'globalVars',
19
24
  ];
20
25
  const missing = requiredTokens.filter((token) => !content.includes(token));
21
26
 
@@ -51,6 +56,11 @@ const requiredPaths = [
51
56
  'oxlint.config.ts',
52
57
  'oxfmt.config.ts',
53
58
  'scripts/bootstrap-agent-skills.mjs',
59
+ 'scripts/check-i18n-strings.mjs',
60
+ 'config/public/locales/en/translation.json',
61
+ 'config/public/locales/cs/translation.json',
62
+ 'src/modern-app-env.d.ts',
63
+ 'src/routes/[lang]/page.tsx',
54
64
  ];
55
65
  const manifestErrors = [];
56
66
 
@@ -61,6 +71,28 @@ for (const requiredPath of requiredPaths) {
61
71
  }
62
72
  }
63
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
+
64
96
  if (templateManifest.schemaVersion !== 1) {
65
97
  manifestErrors.push('schemaVersion');
66
98
  }
@@ -118,6 +150,7 @@ const skillsLock = JSON.parse(
118
150
  const requiredScripts = {
119
151
  format: 'oxfmt .',
120
152
  'format:check': 'oxfmt --check .',
153
+ 'i18n:check': 'node ./scripts/check-i18n-strings.mjs',
121
154
  lint: 'oxlint .',
122
155
  'lint:fix': 'oxlint . --fix',
123
156
  'skills:check': 'node ./scripts/bootstrap-agent-skills.mjs --check',
@@ -139,6 +172,13 @@ if (
139
172
  process.exit(1);
140
173
  }
141
174
 
175
+ for (const dependency of ['@modern-js/plugin-i18n', 'i18next', 'react-i18next']) {
176
+ if (!packageJson.dependencies?.[dependency]) {
177
+ console.error(`Missing dependency: ${dependency}`);
178
+ process.exit(1);
179
+ }
180
+ }
181
+
142
182
  for (const dependency of [
143
183
  '@effect/tsgo',
144
184
  '@typescript/native-preview',
@@ -1 +1,3 @@
1
1
  /// <reference types='@modern-js/app-tools/types' />
2
+
3
+ declare const ULTRAMODERN_SITE_URL: string;
@@ -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
  });
@@ -0,0 +1,211 @@
1
+ import { Helmet } from '@modern-js/runtime/head';
2
+ import { useModernI18n } from '@modern-js/plugin-i18n/runtime';
3
+ import { useLocation } from '@modern-js/runtime/{{routerImportPath}}';
4
+ {{#if useEffectBff}}import effectBff from '@api/effect/index';
5
+ import { Effect } from '@modern-js/plugin-bff/effect-client';
6
+ import { useEffect, useState } from 'react';
7
+ {{/if}}
8
+ import { useTranslation } from 'react-i18next';
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
+ };
47
+
48
+ const Index = () => {
49
+ const { t } = useTranslation();
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
+ }));
60
+ {{#if useEffectBff}} const [effectMessage, setEffectMessage] = useState('loading...');
61
+
62
+ useEffect(() => {
63
+ let mounted = true;
64
+ Effect.runFork(
65
+ Effect.promise(() => effectBff.client.greetings.hello({})).pipe(
66
+ Effect.tap((data) =>
67
+ Effect.sync(() => {
68
+ if (mounted) {
69
+ setEffectMessage(data.message);
70
+ }
71
+ }),
72
+ ),
73
+ ),
74
+ );
75
+ return () => {
76
+ mounted = false;
77
+ };
78
+ }, []);
79
+ {{/if}}
80
+ return (
81
+ <div className="container-box">
82
+ <Helmet>
83
+ <link
84
+ rel="icon"
85
+ type="image/x-icon"
86
+ href="https://lf3-static.bytednsdoc.com/obj/eden-cn/uhbfnupenuhf/favicon.ico"
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
+ />
102
+ </Helmet>
103
+ <main>
104
+ <nav className="language-switcher" aria-label={t('home.language.switcher')}>
105
+ {languageOptions.map((option) => (
106
+ <a
107
+ aria-current={currentLanguage === option.code ? 'page' : undefined}
108
+ href={option.href}
109
+ key={option.code}
110
+ >
111
+ {option.label}
112
+ </a>
113
+ ))}
114
+ </nav>
115
+ <div className="title">
116
+ {t('home.title')}
117
+ <img
118
+ alt={t('home.logoAlt')}
119
+ className="logo"
120
+ src="https://lf3-static.bytednsdoc.com/obj/eden-cn/zq-uylkvT/ljhwZthlaukjlkulzlp/modern-js-logo.svg"
121
+ />
122
+ <p className="name">{t('home.name')}</p>
123
+ </div>
124
+ <p className="description{{#if enableTailwind}} text-emerald-700 font-semibold{{/if}}">
125
+ {t('home.description.intro')} <code className="code">presetUltramodern(...)</code>{' '}
126
+ {/* i18n-ignore technical token */}
127
+ {t('home.description.afterPreset')}
128
+ <code className="code">modern.config.ts</code>
129
+ {/* i18n-ignore technical token */}
130
+ {t('home.description.afterConfig')}
131
+ <code className="code">pnpm run ultramodern:check</code>
132
+ {/* i18n-ignore technical token */}
133
+ {t('home.description.end')}
134
+ </p>
135
+ {{#if useEffectBff}}
136
+ <p className="description effect-message{{#if enableTailwind}} text-emerald-700 font-semibold{{/if}}">
137
+ {t('home.bff.response')} <code className="code">{effectMessage}</code>
138
+ </p>
139
+ {{/if}}
140
+ <div className="grid">
141
+ <a
142
+ href="https://bleedingdev.github.io/ultramodern.js/guides/get-started/ultramodern.html"
143
+ target="_blank"
144
+ rel="noopener noreferrer"
145
+ className="card"
146
+ >
147
+ <h2>
148
+ {t('home.cards.guide.title')}
149
+ <img
150
+ alt=""
151
+ className="arrow-right"
152
+ src="https://lf3-static.bytednsdoc.com/obj/eden-cn/zq-uylkvT/ljhwZthlaukjlkulzlp/arrow-right.svg"
153
+ />
154
+ </h2>
155
+ <p>{t('home.cards.guide.body')}</p>
156
+ </a>
157
+ <a
158
+ href="https://bleedingdev.github.io/ultramodern.js/configure/app/usage.html"
159
+ target="_blank"
160
+ className="card"
161
+ rel="noreferrer"
162
+ >
163
+ <h2>
164
+ {t('home.cards.config.title')}
165
+ <img
166
+ alt=""
167
+ className="arrow-right"
168
+ src="https://lf3-static.bytednsdoc.com/obj/eden-cn/zq-uylkvT/ljhwZthlaukjlkulzlp/arrow-right.svg"
169
+ />
170
+ </h2>
171
+ <p>{t('home.cards.config.body')}</p>
172
+ </a>
173
+ <a
174
+ href="https://github.com/BleedingDev/ultramodern.js/blob/main-ultramodern/packages/toolkit/create/template/.github/workflows/ultramodern-gates.yml.handlebars"
175
+ target="_blank"
176
+ className="card"
177
+ rel="noreferrer"
178
+ >
179
+ <h2>
180
+ {t('home.cards.gates.title')}
181
+ <img
182
+ alt=""
183
+ className="arrow-right"
184
+ src="https://lf3-static.bytednsdoc.com/obj/eden-cn/zq-uylkvT/ljhwZthlaukjlkulzlp/arrow-right.svg"
185
+ />
186
+ </h2>
187
+ <p>{t('home.cards.gates.body')}</p>
188
+ </a>
189
+ <a
190
+ href="https://bleedingdev.github.io/ultramodern.js/configure/app/bff/effect.html"
191
+ target="_blank"
192
+ rel="noopener noreferrer"
193
+ className="card"
194
+ >
195
+ <h2>
196
+ {t('home.cards.bff.title')}
197
+ <img
198
+ alt=""
199
+ className="arrow-right"
200
+ src="https://lf3-static.bytednsdoc.com/obj/eden-cn/zq-uylkvT/ljhwZthlaukjlkulzlp/arrow-right.svg"
201
+ />
202
+ </h2>
203
+ <p>{t('home.cards.bff.body')}</p>
204
+ </a>
205
+ </div>
206
+ </main>
207
+ </div>
208
+ );
209
+ };
210
+
211
+ export default Index;
@@ -7,7 +7,14 @@ 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.
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.
11
18
 
12
19
  ## Required Skill Baseline
13
20
 
@@ -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,39 @@ 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-app-env.d.ts',
69
+ 'apps/shell-super-app/src/modern.runtime.ts',
70
+ 'apps/shell-super-app/src/routes/[lang]/page.tsx',
64
71
  'apps/remotes/remote-commerce/package.json',
72
+ 'apps/remotes/remote-commerce/config/public/locales/en/translation.json',
73
+ 'apps/remotes/remote-commerce/config/public/locales/cs/translation.json',
65
74
  'apps/remotes/remote-commerce/modern.config.ts',
66
75
  'apps/remotes/remote-commerce/module-federation.config.ts',
76
+ 'apps/remotes/remote-commerce/src/modern-app-env.d.ts',
77
+ 'apps/remotes/remote-commerce/src/modern.runtime.ts',
78
+ 'apps/remotes/remote-commerce/src/routes/[lang]/page.tsx',
67
79
  'apps/remotes/remote-identity/package.json',
80
+ 'apps/remotes/remote-identity/config/public/locales/en/translation.json',
81
+ 'apps/remotes/remote-identity/config/public/locales/cs/translation.json',
68
82
  'apps/remotes/remote-identity/modern.config.ts',
69
83
  'apps/remotes/remote-identity/module-federation.config.ts',
84
+ 'apps/remotes/remote-identity/src/modern-app-env.d.ts',
85
+ 'apps/remotes/remote-identity/src/modern.runtime.ts',
86
+ 'apps/remotes/remote-identity/src/routes/[lang]/page.tsx',
70
87
  'apps/remotes/remote-design-system/package.json',
88
+ 'apps/remotes/remote-design-system/config/public/locales/en/translation.json',
89
+ 'apps/remotes/remote-design-system/config/public/locales/cs/translation.json',
71
90
  'apps/remotes/remote-design-system/modern.config.ts',
72
91
  'apps/remotes/remote-design-system/module-federation.config.ts',
92
+ 'apps/remotes/remote-design-system/src/modern-app-env.d.ts',
93
+ 'apps/remotes/remote-design-system/src/modern.runtime.ts',
94
+ 'apps/remotes/remote-design-system/src/routes/[lang]/page.tsx',
73
95
  'services/service-recommendations-effect/package.json',
74
96
  'services/service-recommendations-effect/modern.config.ts',
75
97
  'services/service-recommendations-effect/api/effect/index.ts',
@@ -83,6 +105,18 @@ for (const requiredPath of requiredPaths) {
83
105
  assertExists(requiredPath);
84
106
  }
85
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
+
86
120
  const rootPackage = readJson('package.json');
87
121
  const packageSource = readJson('.modernjs/ultramodern-package-source.json');
88
122
  const skillsLock = readJson('.agents/skills-lock.json');
@@ -134,6 +168,7 @@ assert(
134
168
  const requiredRootScripts = {
135
169
  format: 'oxfmt .',
136
170
  'format:check': 'oxfmt --check .',
171
+ 'i18n:check': 'node ./scripts/check-i18n-strings.mjs',
137
172
  lint: 'oxlint .',
138
173
  'lint:fix': 'oxlint . --fix',
139
174
  'skills:check': 'node ./scripts/bootstrap-agent-skills.mjs --check',
@@ -207,6 +242,13 @@ const appPackagePaths = [
207
242
 
208
243
  for (const packagePath of appPackagePaths) {
209
244
  const packageJson = readJson(packagePath);
245
+ assert(
246
+ packageJson.dependencies?.['@modern-js/plugin-i18n'] ===
247
+ expectedModernDependency('@modern-js/plugin-i18n'),
248
+ `${packagePath} must use @modern-js/plugin-i18n through ${expectedModernDependency(
249
+ '@modern-js/plugin-i18n',
250
+ )}`,
251
+ );
210
252
  assert(
211
253
  packageJson.dependencies?.['@modern-js/plugin-tanstack'] ===
212
254
  expectedModernDependency('@modern-js/plugin-tanstack'),
@@ -236,6 +278,11 @@ for (const packagePath of appPackagePaths) {
236
278
  packageJson.dependencies?.[`@${packageScope}/shared-design-tokens`] === 'workspace:*',
237
279
  `${packagePath} must link generated shared design tokens through workspace:*`,
238
280
  );
281
+ assert(packageJson.dependencies?.i18next === '26.2.0', `${packagePath} must include i18next`);
282
+ assert(
283
+ packageJson.dependencies?.['react-i18next'] === '17.0.8',
284
+ `${packagePath} must include react-i18next`,
285
+ );
239
286
  assert(
240
287
  packageJson.dependencies?.['@tanstack/react-router'] === tanstackVersion,
241
288
  `${packagePath} must use @tanstack/react-router ${tanstackVersion}`,
@@ -258,6 +305,13 @@ for (const configPath of [
258
305
  ]) {
259
306
  const config = readText(configPath);
260
307
  assert(config.includes('presetUltramodern('), `${configPath} must use presetUltramodern`);
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
+ );
261
315
  assert(config.includes('tanstackRouterPlugin()'), `${configPath} must enable plugin-tanstack`);
262
316
  assert(
263
317
  config.includes('moduleFederationPlugin()'),
@@ -265,6 +319,19 @@ for (const configPath of [
265
319
  );
266
320
  }
267
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
+
268
335
  const shellMf = readText('apps/shell-super-app/module-federation.config.ts');
269
336
  assert(shellMf.includes("name: 'shellSuperApp'"), 'Shell MF config must name the shell');
270
337
  assert(