Skip to content

Commit 1fe93df

Browse files
committed
feat: i18n use i18next detection plugin
1 parent 2e20545 commit 1fe93df

File tree

5 files changed

+105
-1
lines changed

5 files changed

+105
-1
lines changed
Lines changed: 68 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,68 @@
1+
import type { LanguageDetectorOptions } from '../instance';
2+
3+
export const DEFAULT_I18NEXT_DETECTION_OPTIONS = {
4+
caches: ['cookie', 'localStorage'],
5+
order: [
6+
'querystring',
7+
'cookie',
8+
'localStorage',
9+
'header',
10+
'navigator',
11+
'htmlTag',
12+
'path',
13+
'subdomain',
14+
],
15+
cookieMinutes: 60 * 24 * 365,
16+
lookupQuerystring: 'lng',
17+
lookupCookie: 'i18next',
18+
lookupLocalStorage: 'i18nextLng',
19+
lookupHeader: 'accept-language',
20+
};
21+
22+
/**
23+
* Deep merge detection options, merging user options with default options
24+
* @param defaultOptions - Default detection options
25+
* @param userOptions - User-provided detection options (optional)
26+
* @returns Merged detection options
27+
*/
28+
export function mergeDetectionOptions(
29+
userOptions?: LanguageDetectorOptions,
30+
defaultOptions: LanguageDetectorOptions = DEFAULT_I18NEXT_DETECTION_OPTIONS,
31+
): LanguageDetectorOptions {
32+
if (!userOptions) {
33+
return defaultOptions;
34+
}
35+
36+
const merged: Record<string, any> = { ...defaultOptions };
37+
const userOptionsRecord = userOptions as Record<string, any>;
38+
39+
// Deep merge nested objects
40+
for (const key in userOptions) {
41+
if (userOptionsRecord[key] !== undefined) {
42+
const userValue = userOptionsRecord[key];
43+
const defaultValue = merged[key];
44+
45+
// If both are objects (but not arrays or Date), deep merge them
46+
if (
47+
userValue &&
48+
typeof userValue === 'object' &&
49+
!Array.isArray(userValue) &&
50+
!(userValue instanceof Date) &&
51+
defaultValue &&
52+
typeof defaultValue === 'object' &&
53+
!Array.isArray(defaultValue) &&
54+
!(defaultValue instanceof Date)
55+
) {
56+
merged[key] = mergeDetectionOptions(
57+
defaultValue as LanguageDetectorOptions,
58+
userValue as LanguageDetectorOptions,
59+
);
60+
} else {
61+
// Otherwise, use user value (which overrides default)
62+
merged[key] = userValue;
63+
}
64+
}
65+
}
66+
67+
return merged as LanguageDetectorOptions;
68+
}
Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,6 @@
1+
import { LanguageDetector } from 'i18next-http-middleware';
2+
import type { I18nInstance } from '../instance';
3+
4+
export const useI18nextLanguageDetector = (i18nInstance: I18nInstance) => {
5+
return i18nInstance.use(LanguageDetector);
6+
};
Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,6 @@
1+
import LanguageDetector from 'i18next-browser-languagedetector';
2+
import type { I18nInstance } from '../instance';
3+
4+
export const useI18nextLanguageDetector = (i18nInstance: I18nInstance) => {
5+
return i18nInstance.use(LanguageDetector);
6+
};

packages/runtime/plugin-i18n/src/runtime/i18n/instance.ts

Lines changed: 14 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -9,11 +9,25 @@ export interface I18nInstance {
99
cloneInstance?: () => I18nInstance; // ssr need
1010
}
1111

12+
type LanguageDetectorOrder = string[];
13+
type LanguageDetectorCaches = boolean | string[];
14+
export interface LanguageDetectorOptions {
15+
order?: LanguageDetectorOrder;
16+
lookupQuerystring?: string;
17+
lookupCookie?: string;
18+
lookupSession?: string;
19+
lookupFromPathIndex?: number;
20+
caches?: LanguageDetectorCaches;
21+
cookieExpirationDate?: Date;
22+
cookieDomain?: string;
23+
}
24+
1225
export type I18nInitOptions = {
1326
lng?: string;
1427
fallbackLng?: string;
1528
supportedLngs?: string[];
1629
initImmediate?: boolean;
30+
detection?: LanguageDetectorOptions;
1731
};
1832

1933
export function isI18nInstance(obj: any): obj is I18nInstance {

packages/runtime/plugin-i18n/src/runtime/index.tsx

Lines changed: 11 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -9,6 +9,8 @@ import type { BaseLocaleDetectionOptions } from '../utils/config';
99
import { ModernI18nProvider } from './context';
1010
import type { I18nInitOptions, I18nInstance } from './i18n';
1111
import { getI18nInstance } from './i18n';
12+
import { useI18nextLanguageDetector } from './i18n/detection';
13+
import { mergeDetectionOptions } from './i18n/detection/config';
1214
import { getI18nextProvider, getInitReactI18next } from './i18n/instance';
1315
import { getEntryPath, getLanguageFromPath } from './utils';
1416

@@ -26,7 +28,6 @@ export const i18nPlugin = (options: I18nPluginOptions): RuntimePlugin => ({
2628
const {
2729
entryName,
2830
i18nInstance: userI18nInstance,
29-
changeLanguage,
3031
initOptions: userInitOptions,
3132
localeDetection,
3233
} = options;
@@ -60,6 +61,9 @@ export const i18nPlugin = (options: I18nPluginOptions): RuntimePlugin => ({
6061
if (initReactI18next) {
6162
i18nInstance.use(initReactI18next);
6263
}
64+
if (i18nextDetector) {
65+
useI18nextLanguageDetector(i18nInstance);
66+
}
6367
// Always detect language from path for consistency between SSR and client
6468
let initialLanguage = fallbackLanguage;
6569
if (localePathRedirect) {
@@ -73,11 +77,17 @@ export const i18nPlugin = (options: I18nPluginOptions): RuntimePlugin => ({
7377
}
7478
}
7579
if (!i18nInstance.isInitialized) {
80+
// Merge detection options: default options + user options
81+
const mergedDetection = i18nextDetector
82+
? mergeDetectionOptions(userInitOptions?.detection)
83+
: userInitOptions?.detection;
84+
7685
const initOptions: I18nInitOptions = {
7786
lng: initialLanguage,
7887
fallbackLng: fallbackLanguage,
7988
supportedLngs: languages,
8089
...(userInitOptions || {}),
90+
detection: mergedDetection,
8191
};
8292
await i18nInstance.init(initOptions);
8393
}

0 commit comments

Comments
 (0)