@bleedingdev/modern-js-plugin-i18n 3.4.0-ultramodern.1 → 3.4.0-ultramodern.3
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/cjs/cli/index.js +2 -1
- package/dist/cjs/runtime/context.js +8 -0
- package/dist/cjs/runtime/core.js +204 -0
- package/dist/cjs/runtime/i18n/backend/middleware.node.js +22 -4
- package/dist/cjs/runtime/index.js +44 -173
- package/dist/cjs/runtime/no-react-i18next.js +76 -0
- package/dist/esm/cli/index.mjs +2 -1
- package/dist/esm/runtime/context.mjs +8 -0
- package/dist/esm/runtime/core.mjs +139 -0
- package/dist/esm/runtime/i18n/backend/middleware.node.mjs +19 -4
- package/dist/esm/runtime/index.mjs +5 -140
- package/dist/esm/runtime/no-react-i18next.mjs +6 -0
- package/dist/esm-node/cli/index.mjs +2 -1
- package/dist/esm-node/runtime/context.mjs +8 -0
- package/dist/esm-node/runtime/core.mjs +140 -0
- package/dist/esm-node/runtime/i18n/backend/middleware.node.mjs +19 -4
- package/dist/esm-node/runtime/index.mjs +5 -140
- package/dist/esm-node/runtime/no-react-i18next.mjs +7 -0
- package/dist/types/runtime/context.d.ts +1 -0
- package/dist/types/runtime/core.d.ts +30 -0
- package/dist/types/runtime/i18n/backend/middleware.node.d.ts +4 -1
- package/dist/types/runtime/index.d.ts +2 -24
- package/dist/types/runtime/no-react-i18next.d.ts +3 -0
- package/package.json +21 -11
- package/rstest.config.mts +1 -0
- package/src/cli/index.ts +6 -1
- package/src/runtime/context.tsx +13 -0
- package/src/runtime/core.tsx +335 -0
- package/src/runtime/i18n/backend/middleware.node.ts +31 -1
- package/src/runtime/index.tsx +4 -316
- package/src/runtime/no-react-i18next.tsx +7 -0
- package/tests/i18nUtils.test.ts +34 -0
- package/tests/localisedUrls.test.ts +35 -0
- package/tests/reactI18nextRuntimeBoundary.test.ts +27 -0
- package/tests/routerAdapter.test.tsx +58 -1
package/tests/i18nUtils.test.ts
CHANGED
|
@@ -4,6 +4,10 @@ import {
|
|
|
4
4
|
DEFAULT_I18NEXT_BACKEND_OPTIONS as NODE_DEFAULT_I18NEXT_BACKEND_OPTIONS,
|
|
5
5
|
resolveDefaultLocalesDir,
|
|
6
6
|
} from '../src/runtime/i18n/backend/defaults.node';
|
|
7
|
+
import {
|
|
8
|
+
FsBackendWithSave,
|
|
9
|
+
resolveFsBackendConstructor,
|
|
10
|
+
} from '../src/runtime/i18n/backend/middleware.node';
|
|
7
11
|
import { initializeI18nInstance } from '../src/runtime/i18n/utils';
|
|
8
12
|
|
|
9
13
|
function createBackendI18nInstance(): I18nInstance {
|
|
@@ -20,6 +24,36 @@ function createBackendI18nInstance(): I18nInstance {
|
|
|
20
24
|
}
|
|
21
25
|
|
|
22
26
|
describe('i18n runtime utils', () => {
|
|
27
|
+
test('normalizes node fs backend CJS and ESM namespace shapes', () => {
|
|
28
|
+
class FakeBackend {}
|
|
29
|
+
|
|
30
|
+
expect(resolveFsBackendConstructor(FakeBackend)).toBe(FakeBackend);
|
|
31
|
+
expect(resolveFsBackendConstructor({ default: FakeBackend })).toBe(
|
|
32
|
+
FakeBackend,
|
|
33
|
+
);
|
|
34
|
+
expect(resolveFsBackendConstructor({ 'module.exports': FakeBackend })).toBe(
|
|
35
|
+
FakeBackend,
|
|
36
|
+
);
|
|
37
|
+
expect(
|
|
38
|
+
resolveFsBackendConstructor({ default: { default: FakeBackend } }),
|
|
39
|
+
).toBe(FakeBackend);
|
|
40
|
+
expect(
|
|
41
|
+
resolveFsBackendConstructor({
|
|
42
|
+
default: { 'module.exports': FakeBackend },
|
|
43
|
+
}),
|
|
44
|
+
).toBe(FakeBackend);
|
|
45
|
+
});
|
|
46
|
+
|
|
47
|
+
test('node fs backend wrapper extends a resolved constructor', () => {
|
|
48
|
+
const backend = new FsBackendWithSave({}, {}, {}) as {
|
|
49
|
+
type?: string;
|
|
50
|
+
save: (language: string, namespace: string, data: unknown) => void;
|
|
51
|
+
};
|
|
52
|
+
|
|
53
|
+
expect(backend.type).toBe('backend');
|
|
54
|
+
expect(() => backend.save('en', 'translation', {})).not.toThrow();
|
|
55
|
+
});
|
|
56
|
+
|
|
23
57
|
test('node fs backend defaults follow the detected locales directory', () => {
|
|
24
58
|
// The default must match whichever conventional root exists at runtime
|
|
25
59
|
// (./locales first, then ./config/public/locales), mirroring the CLI
|
|
@@ -53,6 +53,41 @@ describe('resolveLocalisedUrlsConfig', () => {
|
|
|
53
53
|
});
|
|
54
54
|
|
|
55
55
|
describe('cli modifyFileSystemRoutes', () => {
|
|
56
|
+
test('uses the no-react runtime entry when reactI18next is disabled', () => {
|
|
57
|
+
let runtimePlugin:
|
|
58
|
+
| {
|
|
59
|
+
path: string;
|
|
60
|
+
config: Record<string, unknown>;
|
|
61
|
+
}
|
|
62
|
+
| undefined;
|
|
63
|
+
|
|
64
|
+
i18nCliPlugin({ reactI18next: false }).setup({
|
|
65
|
+
_internalRuntimePlugins: (fn: any) => {
|
|
66
|
+
const plugins: Array<{
|
|
67
|
+
path: string;
|
|
68
|
+
config: Record<string, unknown>;
|
|
69
|
+
}> = [];
|
|
70
|
+
fn({
|
|
71
|
+
entrypoint: { entryName: 'main' },
|
|
72
|
+
plugins,
|
|
73
|
+
});
|
|
74
|
+
runtimePlugin = plugins[0];
|
|
75
|
+
},
|
|
76
|
+
modifyFileSystemRoutes: () => {},
|
|
77
|
+
_internalServerPlugins: () => {},
|
|
78
|
+
getAppContext: () => ({
|
|
79
|
+
appDirectory: process.cwd(),
|
|
80
|
+
metaName: 'modern-js',
|
|
81
|
+
}),
|
|
82
|
+
getNormalizedConfig: () => ({}),
|
|
83
|
+
} as any);
|
|
84
|
+
|
|
85
|
+
expect(runtimePlugin?.path).toBe(
|
|
86
|
+
'@modern-js/plugin-i18n/runtime/no-react-i18next',
|
|
87
|
+
);
|
|
88
|
+
expect(runtimePlugin?.config.reactI18next).toBe(false);
|
|
89
|
+
});
|
|
90
|
+
|
|
56
91
|
const setupModifyRoutes = (localeDetection: Record<string, unknown>) => {
|
|
57
92
|
let modifyRoutes:
|
|
58
93
|
| ((args: { entrypoint: any; routes: any[] }) => {
|
|
@@ -0,0 +1,27 @@
|
|
|
1
|
+
import { readFileSync } from 'node:fs';
|
|
2
|
+
import { dirname, resolve } from 'node:path';
|
|
3
|
+
import { fileURLToPath } from 'node:url';
|
|
4
|
+
import { describe, expect, test } from '@rstest/core';
|
|
5
|
+
|
|
6
|
+
const __dirname = dirname(fileURLToPath(import.meta.url));
|
|
7
|
+
const runtimeRoot = resolve(__dirname, '../src/runtime');
|
|
8
|
+
|
|
9
|
+
const readRuntimeSource = (file: string) =>
|
|
10
|
+
readFileSync(resolve(runtimeRoot, file), 'utf8');
|
|
11
|
+
|
|
12
|
+
describe('react-i18next runtime boundary', () => {
|
|
13
|
+
test('keeps the disabled runtime entry free of the optional adapter edge', () => {
|
|
14
|
+
const noReactEntry = readRuntimeSource('no-react-i18next.tsx');
|
|
15
|
+
const core = readRuntimeSource('core.tsx');
|
|
16
|
+
|
|
17
|
+
expect(noReactEntry).not.toContain('./i18n/react-i18next');
|
|
18
|
+
expect(core).not.toContain('./i18n/react-i18next');
|
|
19
|
+
expect(core).not.toContain("import('react-i18next')");
|
|
20
|
+
});
|
|
21
|
+
|
|
22
|
+
test('keeps the default runtime entry wired to react-i18next integration', () => {
|
|
23
|
+
const defaultEntry = readRuntimeSource('index.tsx');
|
|
24
|
+
|
|
25
|
+
expect(defaultEntry).toContain('./i18n/react-i18next');
|
|
26
|
+
});
|
|
27
|
+
});
|
|
@@ -5,7 +5,7 @@ import {
|
|
|
5
5
|
} from '@modern-js/runtime/context';
|
|
6
6
|
import type React from 'react';
|
|
7
7
|
import type { ComponentType, PropsWithChildren } from 'react';
|
|
8
|
-
import { act } from 'react';
|
|
8
|
+
import { act, useState } from 'react';
|
|
9
9
|
import { createRoot, type Root } from 'react-dom/client';
|
|
10
10
|
import { i18nPlugin } from '../src/runtime';
|
|
11
11
|
import { ModernI18nProvider, useModernI18n } from '../src/runtime/context';
|
|
@@ -453,4 +453,61 @@ describe('i18n router adapter', () => {
|
|
|
453
453
|
replace: true,
|
|
454
454
|
});
|
|
455
455
|
});
|
|
456
|
+
|
|
457
|
+
test('exposes a language-scoped t function for rendered copy', async () => {
|
|
458
|
+
const i18nInstance = createI18nInstance('en');
|
|
459
|
+
i18nInstance.t = (key: string) => `${i18nInstance.language}:${key}`;
|
|
460
|
+
const renderTranslations: Array<(key: string) => string> = [];
|
|
461
|
+
let setProviderLanguage: ((language: string) => void) | undefined;
|
|
462
|
+
|
|
463
|
+
const StatefulI18nProvider = ({ children }: PropsWithChildren) => {
|
|
464
|
+
const [language, setLanguage] = useState('en');
|
|
465
|
+
setProviderLanguage = setLanguage;
|
|
466
|
+
|
|
467
|
+
return (
|
|
468
|
+
<ModernI18nProvider
|
|
469
|
+
value={{
|
|
470
|
+
language,
|
|
471
|
+
i18nInstance,
|
|
472
|
+
languages: ['en', 'cs'],
|
|
473
|
+
localePathRedirect: true,
|
|
474
|
+
localisedUrls,
|
|
475
|
+
updateLanguage: setLanguage,
|
|
476
|
+
}}
|
|
477
|
+
>
|
|
478
|
+
{children}
|
|
479
|
+
</ModernI18nProvider>
|
|
480
|
+
);
|
|
481
|
+
};
|
|
482
|
+
|
|
483
|
+
const Harness = () => {
|
|
484
|
+
const { t } = useModernI18n();
|
|
485
|
+
renderTranslations.push(t);
|
|
486
|
+
return <span data-testid="translation">{t('key')}</span>;
|
|
487
|
+
};
|
|
488
|
+
|
|
489
|
+
rendered = await renderWithRuntime(
|
|
490
|
+
<StatefulI18nProvider>
|
|
491
|
+
<Harness />
|
|
492
|
+
</StatefulI18nProvider>,
|
|
493
|
+
createReactRouterRuntimeContext({ navigate: rstest.fn() }),
|
|
494
|
+
);
|
|
495
|
+
|
|
496
|
+
expect(
|
|
497
|
+
rendered.container.querySelector('[data-testid="translation"]')
|
|
498
|
+
?.textContent,
|
|
499
|
+
).toBe('en:key');
|
|
500
|
+
const initialT = renderTranslations.at(-1);
|
|
501
|
+
|
|
502
|
+
await act(async () => {
|
|
503
|
+
i18nInstance.language = 'cs';
|
|
504
|
+
setProviderLanguage?.('cs');
|
|
505
|
+
});
|
|
506
|
+
|
|
507
|
+
expect(
|
|
508
|
+
rendered.container.querySelector('[data-testid="translation"]')
|
|
509
|
+
?.textContent,
|
|
510
|
+
).toBe('cs:key');
|
|
511
|
+
expect(renderTranslations.at(-1)).not.toBe(initialT);
|
|
512
|
+
});
|
|
456
513
|
});
|