@i18n-micro/core 1.0.28 → 1.1.0
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/base.d.ts +2 -2
- package/dist/index.cjs +1 -1
- package/dist/index.d.ts +2 -2
- package/dist/index.mjs +200 -236
- package/dist/route-service.d.ts +6 -5
- package/dist/translation.d.ts +16 -17
- package/package.json +3 -3
- package/src/base.ts +4 -4
- package/src/helpers.ts +7 -6
- package/src/index.ts +2 -2
- package/src/route-service.ts +29 -34
- package/src/translation.ts +71 -181
- package/tests/base.test.ts +11 -36
- package/tests/route-service.test.ts +31 -81
package/dist/route-service.d.ts
CHANGED
|
@@ -5,16 +5,18 @@ interface NavigateToInterface {
|
|
|
5
5
|
redirectCode?: number;
|
|
6
6
|
external?: boolean;
|
|
7
7
|
}
|
|
8
|
+
export type GetDefaultLocale = () => string | null | undefined;
|
|
8
9
|
export declare class RouteService {
|
|
9
10
|
private i18nConfig;
|
|
10
11
|
private router;
|
|
11
12
|
private hashLocaleDefault;
|
|
12
13
|
private noPrefixDefault;
|
|
13
14
|
private navigateTo;
|
|
14
|
-
|
|
15
|
-
private
|
|
16
|
-
|
|
17
|
-
|
|
15
|
+
/** Optional getter for current locale (used by hash/noPrefix when state changes). Plugin provides () => localeState.value */
|
|
16
|
+
private getDefaultLocale?;
|
|
17
|
+
constructor(i18nConfig: ModuleOptionsExtend, router: Router, hashLocaleDefault: string | null | undefined, noPrefixDefault: string | null | undefined, navigateTo: (to: RouteLocationRaw | undefined | null, options?: NavigateToInterface) => Promise<void | NavigationFailure | false> | false | void | RouteLocationRaw,
|
|
18
|
+
/** Optional getter for current locale (used by hash/noPrefix when state changes). Plugin provides () => localeState.value */
|
|
19
|
+
getDefaultLocale?: GetDefaultLocale | undefined);
|
|
18
20
|
/**
|
|
19
21
|
* Extracts locale from URL path by checking the first path segment
|
|
20
22
|
* @param path - URL path (e.g., '/ru/sdfsdf' or '/en/about')
|
|
@@ -31,7 +33,6 @@ export declare class RouteService {
|
|
|
31
33
|
private handlePrefixStrategy;
|
|
32
34
|
private createLocalizedRoute;
|
|
33
35
|
getLocalizedRoute(to: RouteLocationResolvedGeneric | RouteLocationAsPathGeneric | RouteLocationNamedRaw | string, route: RouteLocationNormalizedLoaded, locale?: string): RouteLocationResolved;
|
|
34
|
-
updateCookies(toLocale: string): void;
|
|
35
36
|
getCurrentRoute(): RouteLocationNormalizedLoaded;
|
|
36
37
|
private resolveRouteWithStrategy;
|
|
37
38
|
switchLocaleLogic(toLocale: string, i18nRouteParams: I18nRouteParams, to?: RouteLocationNamedRaw | RouteLocationResolvedGeneric | string): string | false | void | import('vue-router').RouteLocationAsRelativeGeneric | RouteLocationAsPathGeneric | Promise<false | void | NavigationFailure>;
|
package/dist/translation.d.ts
CHANGED
|
@@ -1,24 +1,23 @@
|
|
|
1
1
|
import { Translations } from '@i18n-micro/types';
|
|
2
|
-
|
|
3
|
-
|
|
2
|
+
/**
|
|
3
|
+
* Bare Metal: Простое хранилище переводов без Ref, useState, devalue.
|
|
4
|
+
* Ключ Map: locale (general) или locale:routeName (page-specific).
|
|
5
|
+
*/
|
|
6
|
+
export interface TranslationStorage {
|
|
7
|
+
translations: Map<string, Translations>;
|
|
4
8
|
}
|
|
5
|
-
export
|
|
6
|
-
generalLocaleCache: RefLike<Record<string, Translations>> | Record<string, Translations>;
|
|
7
|
-
routeLocaleCache: RefLike<Record<string, Translations>> | Record<string, Translations>;
|
|
8
|
-
dynamicTranslationsCaches: RefLike<Record<string, Translations>[]> | Record<string, Translations>[];
|
|
9
|
-
serverTranslationCache: RefLike<Record<string, Map<string, Translations | unknown>>> | Record<string, Map<string, Translations | unknown>>;
|
|
10
|
-
}
|
|
11
|
-
export declare function useTranslationHelper(caches?: TranslationCache): {
|
|
9
|
+
export declare function useTranslationHelper(storage?: TranslationStorage): {
|
|
12
10
|
hasCache(locale: string, page: string): boolean;
|
|
13
|
-
getCache(locale: string, routeName: string):
|
|
14
|
-
setCache(
|
|
15
|
-
|
|
16
|
-
mergeGlobalTranslation(locale: string, newTranslations: Translations, force?: boolean): void;
|
|
11
|
+
getCache(locale: string, routeName: string): Translations | undefined;
|
|
12
|
+
setCache(_locale: string, _routeName: string, _cache: Map<string, unknown>): void;
|
|
13
|
+
hasTranslation(locale: string, key: string): boolean;
|
|
17
14
|
hasGeneralTranslation(locale: string): boolean;
|
|
18
15
|
hasPageTranslation(locale: string, routeName: string): boolean;
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
16
|
+
getTranslation<T = unknown>(locale: string, routeName: string, key: string): T | null;
|
|
17
|
+
loadTranslations(locale: string, data: Translations): void;
|
|
18
|
+
setTranslations(locale: string, data: Translations): void;
|
|
19
|
+
loadPageTranslations(locale: string, routeName: string, data: Translations): void;
|
|
20
|
+
mergeTranslation(locale: string, routeName: string, newTranslations: Translations, _force?: boolean): void;
|
|
21
|
+
mergeGlobalTranslation(locale: string, newTranslations: Translations, _force?: boolean): void;
|
|
23
22
|
clearCache(): void;
|
|
24
23
|
};
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@i18n-micro/core",
|
|
3
|
-
"version": "1.0
|
|
3
|
+
"version": "1.1.0",
|
|
4
4
|
"description": "",
|
|
5
5
|
"repository": "s00d/nuxt-i18n-micro",
|
|
6
6
|
"license": "MIT",
|
|
@@ -22,10 +22,10 @@
|
|
|
22
22
|
},
|
|
23
23
|
"keywords": [],
|
|
24
24
|
"dependencies": {
|
|
25
|
-
"@i18n-micro/types": "1.0
|
|
25
|
+
"@i18n-micro/types": "1.1.0"
|
|
26
26
|
},
|
|
27
27
|
"devDependencies": {
|
|
28
|
-
"vite": "^7.
|
|
28
|
+
"vite": "^7.3.1",
|
|
29
29
|
"vite-plugin-dts": "^4.5.4"
|
|
30
30
|
},
|
|
31
31
|
"scripts": {
|
package/src/base.ts
CHANGED
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import { useTranslationHelper, type
|
|
1
|
+
import { useTranslationHelper, type TranslationStorage } from './translation'
|
|
2
2
|
import { FormatService } from './format-service'
|
|
3
3
|
import { interpolate, defaultPlural } from './helpers'
|
|
4
4
|
import type {
|
|
@@ -12,7 +12,7 @@ import type {
|
|
|
12
12
|
} from '@i18n-micro/types'
|
|
13
13
|
|
|
14
14
|
export interface BaseI18nOptions {
|
|
15
|
-
|
|
15
|
+
storage?: TranslationStorage
|
|
16
16
|
plural?: PluralFunc
|
|
17
17
|
missingWarn?: boolean
|
|
18
18
|
missingHandler?: (locale: string, key: string, routeName: string) => void
|
|
@@ -41,7 +41,7 @@ export abstract class BaseI18n {
|
|
|
41
41
|
public enablePreviousPageFallback: boolean
|
|
42
42
|
|
|
43
43
|
constructor(options: BaseI18nOptions = {}) {
|
|
44
|
-
this.helper = useTranslationHelper(options.
|
|
44
|
+
this.helper = useTranslationHelper(options.storage)
|
|
45
45
|
this.formatter = new FormatService()
|
|
46
46
|
this.pluralFunc = options.plural || defaultPlural
|
|
47
47
|
this.missingWarn = options.missingWarn ?? true
|
|
@@ -226,7 +226,7 @@ export abstract class BaseI18n {
|
|
|
226
226
|
this.helper.mergeGlobalTranslation(locale, translations, true)
|
|
227
227
|
}
|
|
228
228
|
else {
|
|
229
|
-
this.helper.
|
|
229
|
+
this.helper.setTranslations(locale, translations)
|
|
230
230
|
}
|
|
231
231
|
}
|
|
232
232
|
|
package/src/helpers.ts
CHANGED
|
@@ -1,13 +1,14 @@
|
|
|
1
1
|
import type { Params, Strategies, PluralFunc, Getter, TranslationKey } from '@i18n-micro/types'
|
|
2
2
|
|
|
3
|
-
|
|
4
|
-
let result = template
|
|
3
|
+
const RE_TOKEN = /\{(\w+)\}/g
|
|
5
4
|
|
|
6
|
-
|
|
7
|
-
|
|
8
|
-
}
|
|
5
|
+
export function interpolate(template: string, params: Params): string {
|
|
6
|
+
if (!params) return template
|
|
9
7
|
|
|
10
|
-
return
|
|
8
|
+
return template.replace(RE_TOKEN, (_, key) => {
|
|
9
|
+
const value = params[key]
|
|
10
|
+
return value !== undefined ? String(value) : `{${key}}`
|
|
11
|
+
})
|
|
11
12
|
}
|
|
12
13
|
|
|
13
14
|
export function withPrefixStrategy(strategy: Strategies) {
|
package/src/index.ts
CHANGED
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import { useTranslationHelper, type
|
|
1
|
+
import { useTranslationHelper, type TranslationStorage } from './translation'
|
|
2
2
|
import { RouteService } from './route-service'
|
|
3
3
|
import { FormatService } from './format-service'
|
|
4
4
|
import { interpolate, withPrefixStrategy, isNoPrefixStrategy, isPrefixStrategy, isPrefixExceptDefaultStrategy, isPrefixAndDefaultStrategy, defaultPlural } from './helpers'
|
|
@@ -16,6 +16,6 @@ export {
|
|
|
16
16
|
RouteService,
|
|
17
17
|
FormatService,
|
|
18
18
|
BaseI18n,
|
|
19
|
-
type
|
|
19
|
+
type TranslationStorage,
|
|
20
20
|
type BaseI18nOptions,
|
|
21
21
|
}
|
package/src/route-service.ts
CHANGED
|
@@ -13,7 +13,7 @@ import type {
|
|
|
13
13
|
Router,
|
|
14
14
|
} from 'vue-router'
|
|
15
15
|
import type { I18nRouteParams, Locale, ModuleOptionsExtend } from '@i18n-micro/types'
|
|
16
|
-
import { isNoPrefixStrategy, withPrefixStrategy } from './helpers'
|
|
16
|
+
import { isNoPrefixStrategy, withPrefixStrategy, isPrefixAndDefaultStrategy, isPrefixExceptDefaultStrategy } from './helpers'
|
|
17
17
|
|
|
18
18
|
interface NavigateToInterface {
|
|
19
19
|
replace?: boolean
|
|
@@ -21,6 +21,8 @@ interface NavigateToInterface {
|
|
|
21
21
|
external?: boolean
|
|
22
22
|
}
|
|
23
23
|
|
|
24
|
+
export type GetDefaultLocale = () => string | null | undefined
|
|
25
|
+
|
|
24
26
|
export class RouteService {
|
|
25
27
|
constructor(
|
|
26
28
|
private i18nConfig: ModuleOptionsExtend,
|
|
@@ -28,9 +30,8 @@ export class RouteService {
|
|
|
28
30
|
private hashLocaleDefault: string | null | undefined,
|
|
29
31
|
private noPrefixDefault: string | null | undefined,
|
|
30
32
|
private navigateTo: (to: RouteLocationRaw | undefined | null, options?: NavigateToInterface) => Promise<void | NavigationFailure | false> | false | void | RouteLocationRaw,
|
|
31
|
-
|
|
32
|
-
private
|
|
33
|
-
private cookieLocaleName: string | null | undefined = null,
|
|
33
|
+
/** Optional getter for current locale (used by hash/noPrefix when state changes). Plugin provides () => localeState.value */
|
|
34
|
+
private getDefaultLocale?: GetDefaultLocale,
|
|
34
35
|
) {}
|
|
35
36
|
|
|
36
37
|
/**
|
|
@@ -75,14 +76,25 @@ export class RouteService {
|
|
|
75
76
|
getCurrentLocale(route?: RouteLocationNormalizedLoaded | RouteLocationResolvedGeneric): string {
|
|
76
77
|
route = route ?? this.router.currentRoute.value
|
|
77
78
|
|
|
78
|
-
// 1. Check hashMode
|
|
79
|
-
if (this.i18nConfig.hashMode
|
|
80
|
-
|
|
79
|
+
// 1. Check hashMode (getter overrides initial value when state changes)
|
|
80
|
+
if (this.i18nConfig.hashMode) {
|
|
81
|
+
const fromGetter = this.getDefaultLocale?.()
|
|
82
|
+
if (fromGetter) return fromGetter
|
|
83
|
+
if (this.hashLocaleDefault) return this.hashLocaleDefault
|
|
81
84
|
}
|
|
82
85
|
|
|
83
86
|
// 2. Check noPrefix strategy
|
|
84
|
-
if (isNoPrefixStrategy(this.i18nConfig.strategy!)
|
|
85
|
-
|
|
87
|
+
if (isNoPrefixStrategy(this.i18nConfig.strategy!)) {
|
|
88
|
+
const fromGetter = this.getDefaultLocale?.()
|
|
89
|
+
if (fromGetter) return fromGetter
|
|
90
|
+
if (this.noPrefixDefault) return this.noPrefixDefault
|
|
91
|
+
}
|
|
92
|
+
|
|
93
|
+
const path = route.path || route.fullPath || ''
|
|
94
|
+
// 2b. prefix_and_default at /: useState/cookie can override — / is valid for any locale
|
|
95
|
+
if (isPrefixAndDefaultStrategy(this.i18nConfig.strategy!) && (path === '/' || path === '')) {
|
|
96
|
+
const fromGetter = this.getDefaultLocale?.()
|
|
97
|
+
if (fromGetter) return fromGetter
|
|
86
98
|
}
|
|
87
99
|
|
|
88
100
|
// 3. Check route.params.locale (for existing routes)
|
|
@@ -91,18 +103,21 @@ export class RouteService {
|
|
|
91
103
|
}
|
|
92
104
|
|
|
93
105
|
// 4. Extract locale from URL path (for non-existent routes)
|
|
94
|
-
const path = route.path || route.fullPath || ''
|
|
95
106
|
const localeFromPath = this.extractLocaleFromPath(path)
|
|
96
107
|
if (localeFromPath) {
|
|
97
108
|
return localeFromPath
|
|
98
109
|
}
|
|
99
110
|
|
|
100
|
-
// 5.
|
|
101
|
-
if (this.
|
|
102
|
-
return this.
|
|
111
|
+
// 5. For prefix_except_default: URL without locale prefix = defaultLocale
|
|
112
|
+
if (isPrefixExceptDefaultStrategy(this.i18nConfig.strategy!)) {
|
|
113
|
+
return (this.i18nConfig.defaultLocale || 'en').toString()
|
|
103
114
|
}
|
|
104
115
|
|
|
105
|
-
// 6.
|
|
116
|
+
// 6. Check getter (for no_prefix and other strategies)
|
|
117
|
+
const fromGetter = this.getDefaultLocale?.()
|
|
118
|
+
if (fromGetter) return fromGetter
|
|
119
|
+
|
|
120
|
+
// 7. Return defaultLocale as fallback
|
|
106
121
|
return (this.i18nConfig.defaultLocale || 'en').toString()
|
|
107
122
|
}
|
|
108
123
|
|
|
@@ -373,25 +388,6 @@ export class RouteService {
|
|
|
373
388
|
return this.createLocalizedRoute(processedTo, route, currentLocale)
|
|
374
389
|
}
|
|
375
390
|
|
|
376
|
-
updateCookies(toLocale: string): void {
|
|
377
|
-
const cookieLocaleName = this.cookieLocaleName || this.i18nConfig.localeCookie || 'user-locale'
|
|
378
|
-
if (this.i18nConfig.hashMode) {
|
|
379
|
-
this.setCookie('hash-locale', toLocale)
|
|
380
|
-
// useCookie('hash-locale').value = toLocale
|
|
381
|
-
this.hashLocaleDefault = toLocale
|
|
382
|
-
}
|
|
383
|
-
if (isNoPrefixStrategy(this.i18nConfig.strategy!)) {
|
|
384
|
-
this.setCookie(cookieLocaleName, toLocale)
|
|
385
|
-
// useCookie(cookieLocaleName).value = toLocale
|
|
386
|
-
this.noPrefixDefault = toLocale
|
|
387
|
-
}
|
|
388
|
-
// Update cookie for regular strategy (prefix or prefix_except_default)
|
|
389
|
-
if (!this.i18nConfig.hashMode && !isNoPrefixStrategy(this.i18nConfig.strategy!) && cookieLocaleName) {
|
|
390
|
-
this.setCookie(cookieLocaleName, toLocale)
|
|
391
|
-
this.cookieLocaleDefault = toLocale
|
|
392
|
-
}
|
|
393
|
-
}
|
|
394
|
-
|
|
395
391
|
getCurrentRoute(): RouteLocationNormalizedLoaded {
|
|
396
392
|
return this.router.currentRoute.value
|
|
397
393
|
}
|
|
@@ -423,7 +419,6 @@ export class RouteService {
|
|
|
423
419
|
current = to ?? this.getCurrentRoute() as RouteLocationResolved
|
|
424
420
|
}
|
|
425
421
|
|
|
426
|
-
this.updateCookies(toLocale)
|
|
427
422
|
const switchedRoute = this.switchLocaleRoute(fromLocale, toLocale, current, i18nRouteParams)
|
|
428
423
|
|
|
429
424
|
if (typeof switchedRoute === 'string' && switchedRoute.startsWith('http')) {
|
package/src/translation.ts
CHANGED
|
@@ -1,216 +1,106 @@
|
|
|
1
1
|
import type { Translations } from '@i18n-micro/types'
|
|
2
2
|
|
|
3
|
-
|
|
4
|
-
|
|
5
|
-
|
|
3
|
+
/**
|
|
4
|
+
* Bare Metal: Простое хранилище переводов без Ref, useState, devalue.
|
|
5
|
+
* Ключ Map: locale (general) или locale:routeName (page-specific).
|
|
6
|
+
*/
|
|
7
|
+
export interface TranslationStorage {
|
|
8
|
+
translations: Map<string, Translations>
|
|
6
9
|
}
|
|
7
10
|
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
routeLocaleCache: RefLike<Record<string, Translations>> | Record<string, Translations>
|
|
11
|
-
dynamicTranslationsCaches: RefLike<Record<string, Translations>[]> | Record<string, Translations>[]
|
|
12
|
-
serverTranslationCache: RefLike<Record<string, Map<string, Translations | unknown>>> | Record<string, Map<string, Translations | unknown>>
|
|
13
|
-
}
|
|
14
|
-
|
|
15
|
-
// Глобальные кэши для fallback (только для unit-тестов и обратной совместимости)
|
|
16
|
-
// НЕ используются в SSR продакшене
|
|
17
|
-
const globalGeneralLocaleCache: Record<string, Translations> = {}
|
|
18
|
-
const globalRouteLocaleCache: Record<string, Translations> = {}
|
|
19
|
-
const globalDynamicTranslationsCaches: Record<string, Translations>[] = []
|
|
20
|
-
const globalServerTranslationCache: Record<string, Map<string, Translations | unknown>> = {}
|
|
21
|
-
|
|
22
|
-
function deepClone<T>(value: T): T {
|
|
23
|
-
if (Array.isArray(value)) {
|
|
24
|
-
return JSON.parse(JSON.stringify(value)) as T
|
|
25
|
-
}
|
|
26
|
-
else if (typeof value === 'object' && value !== null) {
|
|
27
|
-
return JSON.parse(JSON.stringify(value)) as T
|
|
28
|
-
}
|
|
29
|
-
return value
|
|
30
|
-
}
|
|
31
|
-
|
|
32
|
-
function findTranslation<T = unknown>(translations: Translations | null, key: string): T | null {
|
|
33
|
-
let value: string | number | boolean | Translations | unknown | null = translations
|
|
11
|
+
function findValue<T = unknown>(data: Translations | null | undefined, key: string): T | null {
|
|
12
|
+
if (!data || typeof key !== 'string') return null
|
|
34
13
|
|
|
35
|
-
if (
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
if (translations[key]) {
|
|
40
|
-
value = translations[key]
|
|
41
|
-
}
|
|
42
|
-
else {
|
|
43
|
-
const parts = key.toString().split('.')
|
|
44
|
-
for (const part of parts) {
|
|
45
|
-
if (value && typeof value === 'object' && part in value) {
|
|
46
|
-
value = (value as Translations)[part]
|
|
47
|
-
}
|
|
48
|
-
else {
|
|
49
|
-
return null
|
|
50
|
-
}
|
|
14
|
+
if (key in data) {
|
|
15
|
+
const value = data[key]
|
|
16
|
+
if (typeof value === 'object' && value !== null) {
|
|
17
|
+
return value as T
|
|
51
18
|
}
|
|
19
|
+
return value as T
|
|
52
20
|
}
|
|
53
21
|
|
|
54
|
-
|
|
55
|
-
|
|
22
|
+
const parts = key.split('.')
|
|
23
|
+
let value: unknown = data
|
|
24
|
+
for (const part of parts) {
|
|
25
|
+
if (value && typeof value === 'object' && part in (value as object)) {
|
|
26
|
+
value = (value as Translations)[part]
|
|
27
|
+
}
|
|
28
|
+
else {
|
|
29
|
+
return null
|
|
30
|
+
}
|
|
56
31
|
}
|
|
57
|
-
|
|
58
32
|
return (value as T) ?? null
|
|
59
33
|
}
|
|
60
34
|
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
return typeof refOrValue === 'object' && refOrValue !== null && 'value' in refOrValue
|
|
64
|
-
? (refOrValue as RefLike<T>).value
|
|
65
|
-
: refOrValue as T
|
|
66
|
-
}
|
|
67
|
-
|
|
68
|
-
// Вспомогательная функция для установки значения в Ref или обычный объект
|
|
69
|
-
function setValue<T extends Record<string, unknown>>(
|
|
70
|
-
refOrValue: RefLike<T> | T,
|
|
71
|
-
key: string,
|
|
72
|
-
value: T[keyof T],
|
|
73
|
-
): void {
|
|
74
|
-
const target = getValue(refOrValue)
|
|
75
|
-
;(target as Record<string, unknown>)[key] = value
|
|
76
|
-
}
|
|
77
|
-
|
|
78
|
-
// Вспомогательная функция для получения значения из Ref или обычного объекта по ключу
|
|
79
|
-
function getValueByKey<T extends Record<string, unknown>>(refOrValue: RefLike<T> | T, key: string): T[keyof T] | undefined {
|
|
80
|
-
const target = getValue(refOrValue)
|
|
81
|
-
return (target as Record<string, unknown>)[key] as T[keyof T] | undefined
|
|
82
|
-
}
|
|
83
|
-
|
|
84
|
-
export function useTranslationHelper(caches?: TranslationCache) {
|
|
85
|
-
// Используем переданные кэши или глобальные (fallback для тестов)
|
|
86
|
-
const generalLocaleCache = caches?.generalLocaleCache ?? globalGeneralLocaleCache
|
|
87
|
-
const routeLocaleCache = caches?.routeLocaleCache ?? globalRouteLocaleCache
|
|
88
|
-
const dynamicTranslationsCaches = caches?.dynamicTranslationsCaches ?? globalDynamicTranslationsCaches
|
|
89
|
-
const serverTranslationCache = caches?.serverTranslationCache ?? globalServerTranslationCache
|
|
35
|
+
export function useTranslationHelper(storage?: TranslationStorage) {
|
|
36
|
+
const translations = storage?.translations ?? new Map<string, Translations>()
|
|
90
37
|
|
|
91
38
|
return {
|
|
92
39
|
hasCache(locale: string, page: string) {
|
|
93
40
|
const cacheKey = `${locale}:${page}`
|
|
94
|
-
|
|
95
|
-
return (cache ?? new Map<string, Translations | unknown>()).size > 0
|
|
41
|
+
return translations.has(cacheKey) || translations.has(locale)
|
|
96
42
|
},
|
|
97
43
|
getCache(locale: string, routeName: string) {
|
|
98
44
|
const cacheKey = `${locale}:${routeName}`
|
|
99
|
-
return
|
|
45
|
+
return translations.get(cacheKey)
|
|
100
46
|
},
|
|
101
|
-
setCache(
|
|
102
|
-
|
|
103
|
-
setValue(serverTranslationCache, cacheKey, cache)
|
|
47
|
+
setCache(_locale: string, _routeName: string, _cache: Map<string, unknown>) {
|
|
48
|
+
// No-op for bare metal
|
|
104
49
|
},
|
|
105
|
-
|
|
106
|
-
const
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
|
|
110
|
-
const existing = currentCache ?? {}
|
|
111
|
-
setValue(routeLocaleCache, cacheKey, {
|
|
112
|
-
...existing,
|
|
113
|
-
...newTranslations,
|
|
114
|
-
})
|
|
115
|
-
}
|
|
116
|
-
|
|
117
|
-
const isDev = process.env.NODE_ENV !== 'production'
|
|
118
|
-
if (!currentCache && isDev) {
|
|
119
|
-
// Если кэша нет, выводим предупреждение в dev-режиме и ничего не делаем.
|
|
120
|
-
console.warn(`[i18n] mergeTranslation called for '${cacheKey}' which was not pre-loaded. Skipping merge. Use force: true if this is intentional.`)
|
|
50
|
+
hasTranslation(locale: string, key: string): boolean {
|
|
51
|
+
for (const [k, v] of translations) {
|
|
52
|
+
if ((k === locale || k.startsWith(`${locale}:`)) && findValue(v, key) !== null) {
|
|
53
|
+
return true
|
|
54
|
+
}
|
|
121
55
|
}
|
|
56
|
+
return false
|
|
122
57
|
},
|
|
123
|
-
|
|
124
|
-
|
|
125
|
-
if (!force && !currentCache) {
|
|
126
|
-
console.error(`marge: route ${locale} not loaded`)
|
|
127
|
-
}
|
|
128
|
-
const existing = currentCache ?? {}
|
|
129
|
-
setValue(generalLocaleCache, locale, {
|
|
130
|
-
...existing,
|
|
131
|
-
...newTranslations,
|
|
132
|
-
})
|
|
58
|
+
hasGeneralTranslation(locale: string): boolean {
|
|
59
|
+
return translations.has(locale)
|
|
133
60
|
},
|
|
134
|
-
|
|
135
|
-
return
|
|
61
|
+
hasPageTranslation(locale: string, routeName: string): boolean {
|
|
62
|
+
return translations.has(`${locale}:${routeName}`)
|
|
136
63
|
},
|
|
137
|
-
|
|
138
|
-
const
|
|
139
|
-
|
|
140
|
-
|
|
64
|
+
getTranslation<T = unknown>(locale: string, routeName: string, key: string): T | null {
|
|
65
|
+
const routeKey = `${locale}:${routeName}`
|
|
66
|
+
const routeData = translations.get(routeKey)
|
|
67
|
+
const val = findValue<T>(routeData, key)
|
|
68
|
+
if (val !== null) return val
|
|
69
|
+
|
|
70
|
+
const generalData = translations.get(locale)
|
|
71
|
+
return findValue<T>(generalData, key)
|
|
141
72
|
},
|
|
142
|
-
|
|
143
|
-
|
|
144
|
-
|
|
145
|
-
|
|
146
|
-
return true
|
|
147
|
-
}
|
|
148
|
-
}
|
|
149
|
-
|
|
150
|
-
const generalCache = getValueByKey(generalLocaleCache, locale)
|
|
151
|
-
return findTranslation(generalCache || null, key) !== null
|
|
73
|
+
loadTranslations(locale: string, data: Translations): void {
|
|
74
|
+
// Merge with existing, replacing duplicate keys
|
|
75
|
+
const existing = translations.get(locale) ?? {}
|
|
76
|
+
translations.set(locale, { ...existing, ...data })
|
|
152
77
|
},
|
|
153
|
-
|
|
154
|
-
|
|
155
|
-
|
|
156
|
-
|
|
157
|
-
|
|
158
|
-
|
|
159
|
-
|
|
160
|
-
|
|
161
|
-
|
|
162
|
-
|
|
163
|
-
const dynamicCaches = getValue(dynamicTranslationsCaches)
|
|
164
|
-
for (const dynamicCache of dynamicCaches) {
|
|
165
|
-
result = findTranslation<T>(dynamicCache[locale] || null, key)
|
|
166
|
-
if (result !== null) break
|
|
167
|
-
}
|
|
168
|
-
|
|
169
|
-
if (!result) {
|
|
170
|
-
const routeCache = getValueByKey(routeLocaleCache, cacheKey)
|
|
171
|
-
const generalCache = getValueByKey(generalLocaleCache, locale)
|
|
172
|
-
result = findTranslation<T>(routeCache || null, key)
|
|
173
|
-
?? findTranslation<T>(generalCache || null, key)
|
|
78
|
+
setTranslations(locale: string, data: Translations): void {
|
|
79
|
+
// Replace all translations for locale (no merge)
|
|
80
|
+
translations.set(locale, data)
|
|
81
|
+
},
|
|
82
|
+
loadPageTranslations(locale: string, routeName: string, data: Translations): void {
|
|
83
|
+
const key = `${locale}:${routeName}`
|
|
84
|
+
const existing = translations.get(key)
|
|
85
|
+
// Perf: при пустом existing — сохраняем ссылку, избегаем O(n) копирования больших объектов
|
|
86
|
+
if (!existing || Object.keys(existing).length === 0) {
|
|
87
|
+
translations.set(key, data)
|
|
174
88
|
}
|
|
175
|
-
|
|
176
|
-
|
|
177
|
-
const currentServerCache = serverCache ?? new Map<string, Translations | unknown>()
|
|
178
|
-
currentServerCache.set(key, result)
|
|
179
|
-
setValue(serverTranslationCache, cacheKey, currentServerCache)
|
|
89
|
+
else {
|
|
90
|
+
translations.set(key, { ...existing, ...data })
|
|
180
91
|
}
|
|
181
|
-
|
|
182
|
-
return result
|
|
183
92
|
},
|
|
184
|
-
|
|
185
|
-
const
|
|
186
|
-
|
|
93
|
+
mergeTranslation(locale: string, routeName: string, newTranslations: Translations, _force = false): void {
|
|
94
|
+
const key = `${locale}:${routeName}`
|
|
95
|
+
const existing = translations.get(key) ?? {}
|
|
96
|
+
translations.set(key, { ...existing, ...newTranslations })
|
|
187
97
|
},
|
|
188
|
-
|
|
189
|
-
|
|
98
|
+
mergeGlobalTranslation(locale: string, newTranslations: Translations, _force = false): void {
|
|
99
|
+
const existing = translations.get(locale) ?? {}
|
|
100
|
+
translations.set(locale, { ...existing, ...newTranslations })
|
|
190
101
|
},
|
|
191
|
-
clearCache() {
|
|
192
|
-
|
|
193
|
-
const generalCache = getValue(generalLocaleCache)
|
|
194
|
-
Object.keys(generalCache).forEach((key) => {
|
|
195
|
-
setValue(generalLocaleCache, key, {})
|
|
196
|
-
})
|
|
197
|
-
|
|
198
|
-
// Clear route-specific cache
|
|
199
|
-
const routeCache = getValue(routeLocaleCache)
|
|
200
|
-
Object.keys(routeCache).forEach((key) => {
|
|
201
|
-
setValue(routeLocaleCache, key, {})
|
|
202
|
-
})
|
|
203
|
-
|
|
204
|
-
// Clear dynamic caches
|
|
205
|
-
const dynamicCaches = getValue(dynamicTranslationsCaches)
|
|
206
|
-
dynamicCaches.length = 0
|
|
207
|
-
|
|
208
|
-
// Clear server translation cache
|
|
209
|
-
const serverCache = getValue(serverTranslationCache)
|
|
210
|
-
Object.keys(serverCache).forEach((key) => {
|
|
211
|
-
const cacheMap = getValueByKey(serverTranslationCache, key)
|
|
212
|
-
cacheMap?.clear()
|
|
213
|
-
})
|
|
102
|
+
clearCache(): void {
|
|
103
|
+
translations.clear()
|
|
214
104
|
},
|
|
215
105
|
}
|
|
216
106
|
}
|
package/tests/base.test.ts
CHANGED
|
@@ -49,14 +49,9 @@ describe('BaseI18n', () => {
|
|
|
49
49
|
expect(i18n.getRoute()).toBe('general')
|
|
50
50
|
})
|
|
51
51
|
|
|
52
|
-
test('should initialize with custom
|
|
53
|
-
const
|
|
54
|
-
|
|
55
|
-
routeLocaleCache: {},
|
|
56
|
-
dynamicTranslationsCaches: [],
|
|
57
|
-
serverTranslationCache: {},
|
|
58
|
-
}
|
|
59
|
-
const i18n = new TestI18n('en', 'en', 'general', { cache })
|
|
52
|
+
test('should initialize with custom storage', () => {
|
|
53
|
+
const storage = { translations: new Map<string, Translations>() }
|
|
54
|
+
const i18n = new TestI18n('en', 'en', 'general', { storage })
|
|
60
55
|
expect(i18n).toBeDefined()
|
|
61
56
|
})
|
|
62
57
|
|
|
@@ -93,13 +88,8 @@ describe('BaseI18n', () => {
|
|
|
93
88
|
})
|
|
94
89
|
|
|
95
90
|
test('should interpolate params in translation', async () => {
|
|
96
|
-
const
|
|
97
|
-
|
|
98
|
-
routeLocaleCache: {},
|
|
99
|
-
dynamicTranslationsCaches: [],
|
|
100
|
-
serverTranslationCache: {},
|
|
101
|
-
}
|
|
102
|
-
const i18n = new TestI18n('en', 'en', 'general', { cache })
|
|
91
|
+
const storage = { translations: new Map<string, Translations>() }
|
|
92
|
+
const i18n = new TestI18n('en', 'en', 'general', { storage })
|
|
103
93
|
const translations: Translations = { greeting: 'Hello, {name}!' }
|
|
104
94
|
await i18n['helper'].loadTranslations('en', translations)
|
|
105
95
|
|
|
@@ -117,13 +107,8 @@ describe('BaseI18n', () => {
|
|
|
117
107
|
})
|
|
118
108
|
|
|
119
109
|
test('should fallback to fallbackLocale when translation is missing', async () => {
|
|
120
|
-
const
|
|
121
|
-
|
|
122
|
-
routeLocaleCache: {},
|
|
123
|
-
dynamicTranslationsCaches: [],
|
|
124
|
-
serverTranslationCache: {},
|
|
125
|
-
}
|
|
126
|
-
const i18n = new TestI18n('en', 'fr', 'general', { cache })
|
|
110
|
+
const storage = { translations: new Map<string, Translations>() }
|
|
111
|
+
const i18n = new TestI18n('en', 'fr', 'general', { storage })
|
|
127
112
|
const translations: Translations = { greeting: 'Bonjour' }
|
|
128
113
|
await i18n['helper'].loadTranslations('fr', translations)
|
|
129
114
|
|
|
@@ -139,15 +124,10 @@ describe('BaseI18n', () => {
|
|
|
139
124
|
})
|
|
140
125
|
|
|
141
126
|
test('should use previousPageInfo fallback when enabled', async () => {
|
|
142
|
-
const
|
|
143
|
-
generalLocaleCache: {},
|
|
144
|
-
routeLocaleCache: {},
|
|
145
|
-
dynamicTranslationsCaches: [],
|
|
146
|
-
serverTranslationCache: {},
|
|
147
|
-
}
|
|
127
|
+
const storage = { translations: new Map<string, Translations>() }
|
|
148
128
|
const prevInfo = { locale: 'fr', routeName: 'previous' }
|
|
149
129
|
const i18n = new TestI18n('en', 'en', 'general', {
|
|
150
|
-
|
|
130
|
+
storage,
|
|
151
131
|
getPreviousPageInfo: () => prevInfo,
|
|
152
132
|
enablePreviousPageFallback: true,
|
|
153
133
|
})
|
|
@@ -479,13 +459,8 @@ describe('BaseI18n', () => {
|
|
|
479
459
|
})
|
|
480
460
|
|
|
481
461
|
test('should handle route change', async () => {
|
|
482
|
-
const
|
|
483
|
-
|
|
484
|
-
routeLocaleCache: {},
|
|
485
|
-
dynamicTranslationsCaches: [],
|
|
486
|
-
serverTranslationCache: {},
|
|
487
|
-
}
|
|
488
|
-
const i18n = new TestI18n('en', 'en', 'general', { cache })
|
|
462
|
+
const storage = { translations: new Map<string, Translations>() }
|
|
463
|
+
const i18n = new TestI18n('en', 'en', 'general', { storage })
|
|
489
464
|
const generalTranslations: Translations = { greeting: 'Hello' }
|
|
490
465
|
const routeTranslations: Translations = { title: 'About Page' }
|
|
491
466
|
|