@bleedingdev/modern-js-plugin-i18n 3.9.0-ultramodern.6 → 3.9.0-ultramodern.7

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/package.json CHANGED
@@ -17,7 +17,7 @@
17
17
  "modern",
18
18
  "modern.js"
19
19
  ],
20
- "version": "3.9.0-ultramodern.6",
20
+ "version": "3.9.0-ultramodern.7",
21
21
  "engines": {
22
22
  "node": ">=20"
23
23
  },
@@ -122,12 +122,12 @@
122
122
  }
123
123
  },
124
124
  "dependencies": {
125
- "@modern-js/plugin": "npm:@bleedingdev/modern-js-plugin@3.9.0-ultramodern.6",
126
- "@modern-js/runtime-utils": "npm:@bleedingdev/modern-js-runtime-utils@3.9.0-ultramodern.6",
127
- "@modern-js/server-core": "npm:@bleedingdev/modern-js-server-core@3.9.0-ultramodern.6",
128
- "@modern-js/server-runtime": "npm:@bleedingdev/modern-js-server-runtime@3.9.0-ultramodern.6",
129
- "@modern-js/types": "npm:@bleedingdev/modern-js-types@3.9.0-ultramodern.6",
130
- "@modern-js/utils": "npm:@bleedingdev/modern-js-utils@3.9.0-ultramodern.6",
125
+ "@modern-js/plugin": "npm:@bleedingdev/modern-js-plugin@3.9.0-ultramodern.7",
126
+ "@modern-js/runtime-utils": "npm:@bleedingdev/modern-js-runtime-utils@3.9.0-ultramodern.7",
127
+ "@modern-js/server-core": "npm:@bleedingdev/modern-js-server-core@3.9.0-ultramodern.7",
128
+ "@modern-js/server-runtime": "npm:@bleedingdev/modern-js-server-runtime@3.9.0-ultramodern.7",
129
+ "@modern-js/types": "npm:@bleedingdev/modern-js-types@3.9.0-ultramodern.7",
130
+ "@modern-js/utils": "npm:@bleedingdev/modern-js-utils@3.9.0-ultramodern.7",
131
131
  "@swc/helpers": "^0.5.23",
132
132
  "i18next-browser-languagedetector": "^8.2.1",
133
133
  "i18next-chained-backend": "^5.0.6",
@@ -136,7 +136,7 @@
136
136
  "i18next-http-middleware": "^3.9.8"
137
137
  },
138
138
  "peerDependencies": {
139
- "@modern-js/runtime": "3.9.0-ultramodern.6",
139
+ "@modern-js/runtime": "3.9.0-ultramodern.7",
140
140
  "i18next": ">=25.7.4",
141
141
  "react": "^19.2.8",
142
142
  "react-dom": "^19.2.8",
@@ -151,8 +151,8 @@
151
151
  }
152
152
  },
153
153
  "devDependencies": {
154
- "@modern-js/app-tools": "npm:@bleedingdev/modern-js-app-tools@3.9.0-ultramodern.6",
155
- "@modern-js/runtime": "npm:@bleedingdev/modern-js-runtime@3.9.0-ultramodern.6",
154
+ "@modern-js/app-tools": "npm:@bleedingdev/modern-js-app-tools@3.9.0-ultramodern.7",
155
+ "@modern-js/runtime": "npm:@bleedingdev/modern-js-runtime@3.9.0-ultramodern.7",
156
156
  "@rslib/core": "1.0.0",
157
157
  "@types/node": "^26.4.1",
158
158
  "@typescript/native-preview": "7.0.0-dev.20260707.2",
package/rstest.config.mts CHANGED
@@ -29,7 +29,6 @@ export default {
29
29
  include: [
30
30
  'tests/i18nUtils.test.ts',
31
31
  'tests/localisedUrls.test.ts',
32
- 'tests/linkTypes.test.ts',
33
32
  'tests/backendDefaults.test.ts',
34
33
  'tests/redirectPolicy.test.ts',
35
34
  'tests/reactI18nextRuntimeBoundary.test.ts',
@@ -74,8 +74,7 @@ export type TranslateFn = (
74
74
  // declares a top-level `[key: string]: any` and overloaded call-signature
75
75
  // properties; both make i18next's `i18n` structurally unassignable to this
76
76
  // type, so the documented `i18nInstance: i18next` usage does not typecheck.
77
- // Do NOT restore upstream's shape when resolving a sync merge — the guard is
78
- // tests/type-fixture/i18nInstanceTypes.fixture.ts.
77
+ // Do NOT restore upstream's shape when resolving a sync merge.
79
78
  export interface I18nInstance {
80
79
  language: string;
81
80
  isInitialized?: boolean;
@@ -1,11 +1,8 @@
1
1
  import fs from 'node:fs';
2
2
  import os from 'node:os';
3
3
  import path from 'node:path';
4
- import { afterEach, describe, expect, test } from '@rstest/core';
5
- import {
6
- DEFAULT_I18NEXT_BACKEND_OPTIONS,
7
- resolveDefaultLocalesDir,
8
- } from '../src/runtime/i18n/backend/defaults.node';
4
+ import { describe, expect, test } from '@rstest/core';
5
+ import { resolveDefaultLocalesDir } from '../src/runtime/i18n/backend/defaults.node';
9
6
 
10
7
  const makeTempDir = (...dirs: string[]): string => {
11
8
  const root = fs.mkdtempSync(path.join(os.tmpdir(), 'i18n-defaults-'));
@@ -16,12 +13,6 @@ const makeTempDir = (...dirs: string[]): string => {
16
13
  };
17
14
 
18
15
  describe('node backend default loadPath', () => {
19
- const originalCwd = process.cwd();
20
-
21
- afterEach(() => {
22
- process.chdir(originalCwd);
23
- });
24
-
25
16
  test('prefers the project-root ./locales convention (the detection root)', () => {
26
17
  const root = makeTempDir('locales', 'config/public/locales');
27
18
  expect(resolveDefaultLocalesDir(root)).toBe('./locales');
@@ -36,16 +27,4 @@ describe('node backend default loadPath', () => {
36
27
  const root = makeTempDir();
37
28
  expect(resolveDefaultLocalesDir(root)).toBe('./locales');
38
29
  });
39
-
40
- test('DEFAULT_I18NEXT_BACKEND_OPTIONS resolves against the working directory', () => {
41
- const root = makeTempDir('locales');
42
- process.chdir(root);
43
-
44
- expect(DEFAULT_I18NEXT_BACKEND_OPTIONS.loadPath).toBe(
45
- './locales/{{lng}}/{{ns}}.json',
46
- );
47
- expect(DEFAULT_I18NEXT_BACKEND_OPTIONS.addPath).toBe(
48
- './locales/{{lng}}/{{ns}}.json',
49
- );
50
- });
51
30
  });
@@ -14,12 +14,6 @@ const props: FederatedI18nBoundaryProps = {
14
14
  };
15
15
 
16
16
  describe('native federation resolver wrapper', () => {
17
- test('reports missing integration when no resolver is installed', () => {
18
- expect(() =>
19
- renderToStaticMarkup(<FederatedI18nBoundary {...props} />),
20
- ).toThrow(/requires the @modern-js\/i18n-integration runtime plugin/);
21
- });
22
-
23
17
  test('retains the actionable fallback when a resolver does not handle i18n', () => {
24
18
  expect(() =>
25
19
  renderToStaticMarkup(
@@ -3,13 +3,11 @@ import {
3
3
  RuntimeContext,
4
4
  } from '@modern-js/runtime/context';
5
5
  import type React from 'react';
6
- import type { ComponentType, PropsWithChildren } from 'react';
7
- import { act, useEffect, useState } from 'react';
6
+ import type { PropsWithChildren } from 'react';
7
+ import { act, useEffect } from 'react';
8
8
  import { createRoot, type Root } from 'react-dom/client';
9
- import { i18nPlugin } from '../src/runtime';
10
9
  import { ModernI18nProvider, useModernI18n } from '../src/runtime/context';
11
10
  import type { I18nInstance } from '../src/runtime/i18n';
12
- import { getReactI18nextIntegration } from '../src/runtime/i18n/react-i18next';
13
11
  import { Link } from '../src/runtime/Link';
14
12
  import {
15
13
  createI18nRootWrapper,
@@ -44,29 +42,6 @@ function createI18nInstance(language = 'en'): I18nInstance {
44
42
  };
45
43
  }
46
44
 
47
- function collectI18nWrapRoot() {
48
- let wrapRoot: ((App: ComponentType<any>) => ComponentType<any>) | undefined;
49
-
50
- i18nPlugin({
51
- reactI18next: false,
52
- localeDetection: {
53
- fallbackLanguage: 'en',
54
- },
55
- }).setup?.({
56
- getRuntimeConfig: () => ({}),
57
- onBeforeRender: () => undefined,
58
- wrapRoot: (callback: (App: ComponentType<any>) => ComponentType<any>) => {
59
- wrapRoot = callback;
60
- },
61
- } as any);
62
-
63
- if (!wrapRoot) {
64
- throw new Error('Expected i18n runtime plugin to register wrapRoot');
65
- }
66
-
67
- return wrapRoot;
68
- }
69
-
70
45
  async function renderI18nRoot(node: React.ReactNode) {
71
46
  const container = document.createElement('div');
72
47
  document.body.appendChild(container);
@@ -113,38 +88,6 @@ describe('i18n runtime wrapRoot', () => {
113
88
  window.history.replaceState(null, '', '/');
114
89
  });
115
90
 
116
- test('renders children when no root App exists yet', async () => {
117
- const wrapRoot = collectI18nWrapRoot();
118
- const I18nRoot = wrapRoot(undefined as unknown as ComponentType<any>);
119
-
120
- rendered = await renderI18nRoot(
121
- <I18nRoot>
122
- <main>router content</main>
123
- </I18nRoot>,
124
- );
125
-
126
- expect(rendered.container.textContent).toContain('router content');
127
- });
128
-
129
- test('preserves App props and children', async () => {
130
- const wrapRoot = collectI18nWrapRoot();
131
- const App = ({ children, label }: PropsWithChildren<{ label: string }>) => (
132
- <main data-label={label}>{children}</main>
133
- );
134
- const I18nRoot = wrapRoot(App);
135
-
136
- rendered = await renderI18nRoot(
137
- <I18nRoot label="root">
138
- <span>router content</span>
139
- </I18nRoot>,
140
- );
141
-
142
- expect(
143
- rendered.container.querySelector('main')?.getAttribute('data-label'),
144
- ).toBe('root');
145
- expect(rendered.container.textContent).toContain('router content');
146
- });
147
-
148
91
  test('keeps the optional i18next provider inside Modern i18n context', async () => {
149
92
  const i18nInstance = createI18nInstance('cs');
150
93
  const observedLanguages: string[] = [];
@@ -172,62 +115,6 @@ describe('i18n runtime wrapRoot', () => {
172
115
  expect(observedLanguages).toEqual(['cs:cs']);
173
116
  expect(rendered.container.textContent).toContain('router content');
174
117
  });
175
-
176
- test('keeps the i18next provider instance stable across unrelated parent renders', async () => {
177
- const i18nInstance = createI18nInstance('cs');
178
- const providerInstances: I18nInstance[] = [];
179
- const I18nextProvider = ({
180
- children,
181
- i18n,
182
- }: PropsWithChildren<{ i18n: I18nInstance }>) => {
183
- providerInstances.push(i18n);
184
- return <>{children}</>;
185
- };
186
- const App = () => <main>router content</main>;
187
- const I18nRoot = createI18nRootWrapper({
188
- htmlLangAttr: false,
189
- localePathRedirect: false,
190
- languages: ['en', 'cs'],
191
- fallbackLanguage: 'en',
192
- getLatestI18nInstance: () => i18nInstance,
193
- getI18nextProvider: () => I18nextProvider,
194
- })(App);
195
- const Parent = () => {
196
- const [renderVersion, setRenderVersion] = useState(0);
197
- return (
198
- <>
199
- <button
200
- type="button"
201
- onClick={() => setRenderVersion(version => version + 1)}
202
- >
203
- Render {renderVersion}
204
- </button>
205
- <I18nRoot renderVersion={renderVersion} />
206
- </>
207
- );
208
- };
209
-
210
- rendered = await renderI18nRoot(<Parent />);
211
- const initialProviderInstance = providerInstances.at(-1);
212
-
213
- await act(async () => {
214
- rendered?.container
215
- .querySelector('button')
216
- ?.dispatchEvent(new MouseEvent('click', { bubbles: true }));
217
- });
218
-
219
- expect(providerInstances.length).toBeGreaterThan(1);
220
- expect(providerInstances.at(-1)).toBe(initialProviderInstance);
221
- });
222
- });
223
-
224
- describe('i18n react-i18next integration', () => {
225
- test('loads the bundled react-i18next integration', async () => {
226
- const integration = await getReactI18nextIntegration();
227
-
228
- expect(integration.I18nextProvider).toEqual(expect.any(Function));
229
- expect(integration.initReactI18next).toBeDefined();
230
- });
231
118
  });
232
119
 
233
120
  describe('native navigation extension contract', () => {
@@ -1,68 +0,0 @@
1
- import { execFileSync } from 'node:child_process';
2
- import fs from 'node:fs';
3
- import path from 'node:path';
4
-
5
- const fixtureDir = path.resolve(__dirname, 'type-fixture');
6
-
7
- function resolveTsgoBin() {
8
- const pkgPath = require.resolve('@typescript/native-preview/package.json');
9
- const pkgDir = path.dirname(pkgPath);
10
- const pkg = require(pkgPath) as {
11
- bin?:
12
- | string
13
- | {
14
- tsgo?: string;
15
- };
16
- };
17
- const binEntry = typeof pkg.bin === 'string' ? pkg.bin : pkg.bin?.tsgo;
18
- return path.resolve(pkgDir, binEntry ?? 'bin/tsgo.js');
19
- }
20
-
21
- const tsgoBin = resolveTsgoBin();
22
- for (const artifact of [
23
- 'dist/types/runtime/context.d.ts',
24
- 'dist/cjs/cli/index.js',
25
- 'dist/cjs/shared/utils.js',
26
- ]) {
27
- if (!fs.existsSync(path.resolve(__dirname, '..', artifact))) {
28
- throw new Error(
29
- `Missing plugin-i18n build artifact ${artifact}. Run pnpm build:required before unit tests.`,
30
- );
31
- }
32
- }
33
-
34
- describe('Link type-level tests', () => {
35
- test('fixture type-checks correctly: valid uses compile, invalid uses are rejected', () => {
36
- try {
37
- execFileSync(
38
- process.execPath,
39
- [tsgoBin, '--noEmit', '-p', 'tsconfig.json'],
40
- {
41
- cwd: fixtureDir,
42
- stdio: 'pipe',
43
- },
44
- );
45
- } catch (e: any) {
46
- const stdout = e?.stdout ? String(e.stdout) : '';
47
- const stderr = e?.stderr ? String(e.stderr) : '';
48
- throw new Error(`TypeScript type-check failed:\n${stdout}\n${stderr}`);
49
- }
50
- }, 60_000);
51
-
52
- test('consumer declarations avoid toolchain internals', () => {
53
- const output = execFileSync(process.execPath, [
54
- tsgoBin,
55
- '--ignoreConfig',
56
- '--listFilesOnly',
57
- '--module',
58
- 'preserve',
59
- '--moduleResolution',
60
- 'bundler',
61
- path.resolve(__dirname, '../dist/types/runtime/context.d.ts'),
62
- ]);
63
- const files = output.toString().replaceAll('\\', '/');
64
- expect(files).not.toMatch(
65
- /\/packages\/(cli|runtime\/plugin-runtime\/dist\/types|solutions\/app-tools|toolkit\/(types|utils))\//u,
66
- );
67
- }, 60_000);
68
- });
@@ -1,55 +0,0 @@
1
- import { describe, expect, test } from '@rstest/core';
2
- import {
3
- interpolateRouteParams,
4
- normalizeSearch,
5
- } from '../src/runtime/linkHelpers';
6
-
7
- describe('native link target normalization', () => {
8
- test('splat params preserve separators and percent-encode each segment', () => {
9
- expect(
10
- interpolateRouteParams('/files/*', { '*': 'resume drafts/Q1 deck.pdf' }),
11
- ).toBe('/files/resume%20drafts/Q1%20deck.pdf');
12
- });
13
- const searchScenarios: Array<{
14
- name: string;
15
- search: Parameters<typeof normalizeSearch>[0];
16
- searchFromTo: string;
17
- expected: ReturnType<typeof normalizeSearch>;
18
- }> = [
19
- {
20
- name: 'object array values are preserved',
21
- search: { tag: ['boots', 'sale'], page: 2 },
22
- searchFromTo: '?ignored=1',
23
- expected: {
24
- searchString: '?tag=boots&tag=sale&page=2',
25
- searchObject: { tag: ['boots', 'sale'], page: '2' },
26
- },
27
- },
28
- {
29
- name: 'target query arrays are preserved',
30
- search: undefined,
31
- searchFromTo: '?tag=boots&tag=sale&page=2',
32
- expected: {
33
- searchString: '?tag=boots&tag=sale&page=2',
34
- searchObject: { tag: ['boots', 'sale'], page: '2' },
35
- },
36
- },
37
- {
38
- name: 'empty search clears the target query',
39
- search: '',
40
- searchFromTo: '?tag=boots&tag=sale',
41
- expected: {
42
- searchString: '',
43
- searchObject: undefined,
44
- },
45
- },
46
- ];
47
-
48
- for (const scenario of searchScenarios) {
49
- test(`normalizes search: ${scenario.name}`, () => {
50
- expect(normalizeSearch(scenario.search, scenario.searchFromTo)).toEqual(
51
- scenario.expected,
52
- );
53
- });
54
- }
55
- });
@@ -1,11 +0,0 @@
1
- // The Window._SSR_DATA augmentation lives in @modern-js/runtime's internal
2
- // core/types module, which this fixture's isolated program never pulls in.
3
- interface Window {
4
- _SSR_DATA?: {
5
- data?: {
6
- i18nData?: {
7
- lng?: string;
8
- };
9
- };
10
- };
11
- }
@@ -1,29 +0,0 @@
1
- import {
2
- type I18nInstance,
3
- useModernI18n,
4
- } from '@modern-js/plugin-i18n/runtime';
5
- import i18next, { type i18n } from 'i18next';
6
-
7
- // WRITE direction — this is what had zero coverage and was broken before the
8
- // top-level index signature was removed from `I18nInstance`. Eight integration
9
- // fixtures (tests/integration/i18n/*/src/modern.runtime.tsx) write exactly
10
- // this and none of them is ever typechecked; this file is the guard.
11
- export const w1: I18nInstance = i18next.createInstance();
12
- export const w2: I18nInstance = i18next;
13
-
14
- // READ direction.
15
- declare const inst: I18nInstance;
16
- export const s: string = inst.t('k');
17
- export const b: boolean = inst.exists!('k');
18
- export const langs: readonly string[] = inst.languages!;
19
- export const fixed: string = inst.getFixedT!('en')('k');
20
-
21
- // W2 generic, both with and without an explicit type argument.
22
- type StarlingLike = i18n & { setLang?: (l: string) => void | Promise<void> };
23
- export function useTyped() {
24
- const r = useModernI18n<StarlingLike>();
25
- return r.i18nInstance.t('k') satisfies string;
26
- }
27
- export function useDefault() {
28
- return useModernI18n().i18nInstance.t('k') satisfies string;
29
- }
@@ -1,51 +0,0 @@
1
- import { Link } from '@modern-js/plugin-i18n/runtime';
2
-
3
- declare module '@modern-js/plugin-i18n/runtime' {
4
- interface UltramodernCanonicalRoutes {
5
- '/': Record<string, never>;
6
- '/talks': Record<string, never>;
7
- '/talks/$slug': { slug: string };
8
- }
9
- }
10
-
11
- // --- valid uses ---
12
-
13
- // Known route with required params: must compile.
14
- const _a = <Link to="/talks/$slug" params={{ slug: 'x' }} />;
15
-
16
- // Known route without params: must compile.
17
- const _b = <Link to="/talks" />;
18
-
19
- // Root route: must compile.
20
- const _c = <Link to="/" />;
21
-
22
- // Canonical path with hash suffix: must compile.
23
- const _d = <Link to="/#work-with-me" />;
24
-
25
- // Canonical path with query and hash: must compile.
26
- const _e = <Link to="/talks?tag=x#abstract" />;
27
-
28
- // Bare hash: must compile.
29
- const _f = <Link to="#hash" />;
30
-
31
- // External HTTPS URL: must compile.
32
- const _g = <Link to="https://example.com" />;
33
-
34
- // Dynamic (computed) value — escape hatch: must compile.
35
- declare function compute(): string;
36
- const dynamic: string = compute();
37
- const _h = <Link to={dynamic} />;
38
-
39
- // --- invalid uses (each preceded by @ts-expect-error) ---
40
-
41
- // Unknown route.
42
- // @ts-expect-error
43
- const _i = <Link to="/talkz" />;
44
-
45
- // Known param route but params prop is missing.
46
- // @ts-expect-error
47
- const _j = <Link to="/talks/$slug" />;
48
-
49
- // Known route without params but params are provided (forbidden).
50
- // @ts-expect-error
51
- const _k = <Link to="/talks" params={{ slug: 'x' }} />;
@@ -1,16 +0,0 @@
1
- {
2
- "compilerOptions": {
3
- "jsx": "react-jsx",
4
- "strict": true,
5
- "moduleResolution": "Bundler",
6
- "module": "Preserve",
7
- "noEmit": true,
8
- "skipLibCheck": true,
9
- "lib": ["ESNext", "DOM"],
10
- "types": ["node"],
11
- "paths": {
12
- "@modern-js/plugin-i18n/runtime": ["../../src/runtime/index.tsx"]
13
- }
14
- },
15
- "include": ["*.tsx", "*.ts"]
16
- }