@i18n-micro/core 1.0.27 → 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 +91 -0
- package/dist/index.cjs +1 -1
- package/dist/index.d.ts +3 -2
- package/dist/index.mjs +329 -268
- package/dist/route-service.d.ts +6 -5
- package/dist/translation.d.ts +16 -17
- package/package.json +4 -4
- package/src/base.ts +250 -0
- package/src/helpers.ts +7 -6
- package/src/index.ts +5 -2
- package/src/route-service.ts +29 -33
- package/src/translation.ts +71 -181
- package/tests/base.test.ts +485 -0
- package/tests/route-service.test.ts +31 -61
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
|
}
|