@i18n-micro/core 1.0.27 → 1.0.28
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 +2 -1
- package/dist/index.mjs +343 -246
- package/package.json +4 -4
- package/src/base.ts +250 -0
- package/src/index.ts +3 -0
- package/src/route-service.ts +5 -4
- package/tests/base.test.ts +510 -0
- package/tests/route-service.test.ts +23 -3
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@i18n-micro/core",
|
|
3
|
-
"version": "1.0.
|
|
3
|
+
"version": "1.0.28",
|
|
4
4
|
"description": "",
|
|
5
5
|
"repository": "s00d/nuxt-i18n-micro",
|
|
6
6
|
"license": "MIT",
|
|
@@ -22,11 +22,11 @@
|
|
|
22
22
|
},
|
|
23
23
|
"keywords": [],
|
|
24
24
|
"dependencies": {
|
|
25
|
-
"@i18n-micro/types": "1.0.
|
|
25
|
+
"@i18n-micro/types": "1.0.16"
|
|
26
26
|
},
|
|
27
27
|
"devDependencies": {
|
|
28
|
-
"vite": "^
|
|
29
|
-
"vite-plugin-dts": "^4.
|
|
28
|
+
"vite": "^7.2.7",
|
|
29
|
+
"vite-plugin-dts": "^4.5.4"
|
|
30
30
|
},
|
|
31
31
|
"scripts": {
|
|
32
32
|
"build": "vite build",
|
package/src/base.ts
ADDED
|
@@ -0,0 +1,250 @@
|
|
|
1
|
+
import { useTranslationHelper, type TranslationCache } from './translation'
|
|
2
|
+
import { FormatService } from './format-service'
|
|
3
|
+
import { interpolate, defaultPlural } from './helpers'
|
|
4
|
+
import type {
|
|
5
|
+
Translations,
|
|
6
|
+
Params,
|
|
7
|
+
PluralFunc,
|
|
8
|
+
Getter,
|
|
9
|
+
CleanTranslation,
|
|
10
|
+
TranslationKey,
|
|
11
|
+
MissingHandler,
|
|
12
|
+
} from '@i18n-micro/types'
|
|
13
|
+
|
|
14
|
+
export interface BaseI18nOptions {
|
|
15
|
+
cache?: TranslationCache
|
|
16
|
+
plural?: PluralFunc
|
|
17
|
+
missingWarn?: boolean
|
|
18
|
+
missingHandler?: (locale: string, key: string, routeName: string) => void
|
|
19
|
+
// Optional hooks for Nuxt runtime specific features
|
|
20
|
+
getPreviousPageInfo?: () => { locale: string, routeName: string } | null
|
|
21
|
+
getCustomMissingHandler?: () => MissingHandler | null
|
|
22
|
+
enablePreviousPageFallback?: boolean
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
/**
|
|
26
|
+
* Abstract base class for i18n adapters
|
|
27
|
+
*
|
|
28
|
+
* Contains all common translation logic (t, ts, tc, tn, td, tdr, has).
|
|
29
|
+
* Adapters must implement abstract methods to provide current state (locale, fallbackLocale, route).
|
|
30
|
+
*/
|
|
31
|
+
export abstract class BaseI18n {
|
|
32
|
+
// Public fields (made public to allow type export in Nuxt plugins)
|
|
33
|
+
public helper: ReturnType<typeof useTranslationHelper>
|
|
34
|
+
public formatter = new FormatService()
|
|
35
|
+
public pluralFunc: PluralFunc
|
|
36
|
+
public missingWarn: boolean
|
|
37
|
+
public missingHandler?: (locale: string, key: string, routeName: string) => void
|
|
38
|
+
// Optional hooks for Nuxt runtime specific features
|
|
39
|
+
public getPreviousPageInfo?: () => { locale: string, routeName: string } | null
|
|
40
|
+
public getCustomMissingHandler?: () => MissingHandler | null
|
|
41
|
+
public enablePreviousPageFallback: boolean
|
|
42
|
+
|
|
43
|
+
constructor(options: BaseI18nOptions = {}) {
|
|
44
|
+
this.helper = useTranslationHelper(options.cache)
|
|
45
|
+
this.formatter = new FormatService()
|
|
46
|
+
this.pluralFunc = options.plural || defaultPlural
|
|
47
|
+
this.missingWarn = options.missingWarn ?? true
|
|
48
|
+
this.missingHandler = options.missingHandler
|
|
49
|
+
this.getPreviousPageInfo = options.getPreviousPageInfo
|
|
50
|
+
this.getCustomMissingHandler = options.getCustomMissingHandler
|
|
51
|
+
this.enablePreviousPageFallback = options.enablePreviousPageFallback ?? false
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
// --- Abstract methods (must be implemented by subclasses) ---
|
|
55
|
+
|
|
56
|
+
/**
|
|
57
|
+
* Get current locale
|
|
58
|
+
*/
|
|
59
|
+
public abstract getLocale(): string
|
|
60
|
+
|
|
61
|
+
/**
|
|
62
|
+
* Get fallback locale
|
|
63
|
+
*/
|
|
64
|
+
public abstract getFallbackLocale(): string
|
|
65
|
+
|
|
66
|
+
/**
|
|
67
|
+
* Get current route name
|
|
68
|
+
*/
|
|
69
|
+
public abstract getRoute(): string
|
|
70
|
+
|
|
71
|
+
// --- Public methods (implemented in base class) ---
|
|
72
|
+
|
|
73
|
+
/**
|
|
74
|
+
* Get translation for a key
|
|
75
|
+
* Based on logic from src/runtime/plugins/01.plugin.ts
|
|
76
|
+
*/
|
|
77
|
+
public t(
|
|
78
|
+
key: TranslationKey,
|
|
79
|
+
params?: Params,
|
|
80
|
+
defaultValue?: string | null,
|
|
81
|
+
routeName?: string,
|
|
82
|
+
): CleanTranslation {
|
|
83
|
+
if (!key) return ''
|
|
84
|
+
|
|
85
|
+
// Use abstract getters to get current state
|
|
86
|
+
const locale = this.getLocale()
|
|
87
|
+
const route = routeName || this.getRoute()
|
|
88
|
+
|
|
89
|
+
// 1. Try to find translation in current locale
|
|
90
|
+
// Note: In Nuxt runtime, server already merges global translations, so we don't need explicit fallback
|
|
91
|
+
let value = this.helper.getTranslation<string>(locale, route, key)
|
|
92
|
+
|
|
93
|
+
// 2. If translation not found and there are saved previous translations, use them (only if enabled)
|
|
94
|
+
if (!value && this.enablePreviousPageFallback && this.getPreviousPageInfo) {
|
|
95
|
+
const prev = this.getPreviousPageInfo()
|
|
96
|
+
if (prev) {
|
|
97
|
+
const prevValue = this.helper.getTranslation<string>(prev.locale, prev.routeName, key)
|
|
98
|
+
if (prevValue) {
|
|
99
|
+
value = prevValue
|
|
100
|
+
if (process.env.NODE_ENV !== 'production') {
|
|
101
|
+
console.log(`Using fallback translation from previous route: ${prev.routeName} -> ${key}`)
|
|
102
|
+
}
|
|
103
|
+
}
|
|
104
|
+
}
|
|
105
|
+
}
|
|
106
|
+
|
|
107
|
+
// 3. Fallback to fallbackLocale if not found and different (for non-Nuxt adapters)
|
|
108
|
+
if (!value) {
|
|
109
|
+
const fallbackLocale = this.getFallbackLocale()
|
|
110
|
+
if (locale !== fallbackLocale) {
|
|
111
|
+
value = this.helper.getTranslation<string>(fallbackLocale, route, key)
|
|
112
|
+
}
|
|
113
|
+
}
|
|
114
|
+
|
|
115
|
+
// 4. Handle missing
|
|
116
|
+
if (!value) {
|
|
117
|
+
// Call custom handler if set (Nuxt runtime), otherwise use instance handler
|
|
118
|
+
const customHandler = this.getCustomMissingHandler?.()
|
|
119
|
+
if (customHandler) {
|
|
120
|
+
customHandler(locale, key, route)
|
|
121
|
+
}
|
|
122
|
+
else if (this.missingHandler) {
|
|
123
|
+
this.missingHandler(locale, key as string, route)
|
|
124
|
+
}
|
|
125
|
+
else if (this.missingWarn) {
|
|
126
|
+
const isDev = process.env.NODE_ENV !== 'production'
|
|
127
|
+
const isClient = typeof window !== 'undefined'
|
|
128
|
+
if (isDev && isClient) {
|
|
129
|
+
console.warn(`Not found '${key}' key in '${locale}' locale messages for route '${route}'.`)
|
|
130
|
+
}
|
|
131
|
+
}
|
|
132
|
+
value = defaultValue === undefined ? key : (defaultValue || key)
|
|
133
|
+
}
|
|
134
|
+
|
|
135
|
+
// 5. Interpolate
|
|
136
|
+
return typeof value === 'string' && params ? interpolate(value, params) : value as CleanTranslation
|
|
137
|
+
}
|
|
138
|
+
|
|
139
|
+
/**
|
|
140
|
+
* Get translation as string
|
|
141
|
+
*/
|
|
142
|
+
public ts(
|
|
143
|
+
key: TranslationKey,
|
|
144
|
+
params?: Params,
|
|
145
|
+
defaultValue?: string,
|
|
146
|
+
routeName?: string,
|
|
147
|
+
): string {
|
|
148
|
+
const value = this.t(key, params, defaultValue, routeName)
|
|
149
|
+
return value?.toString() ?? defaultValue ?? key
|
|
150
|
+
}
|
|
151
|
+
|
|
152
|
+
/**
|
|
153
|
+
* Plural translation
|
|
154
|
+
*/
|
|
155
|
+
public tc(key: TranslationKey, count: number | Params, defaultValue?: string): string {
|
|
156
|
+
const { count: countValue, ...params } = typeof count === 'number' ? { count } : count
|
|
157
|
+
|
|
158
|
+
if (countValue === undefined) {
|
|
159
|
+
return defaultValue ?? key
|
|
160
|
+
}
|
|
161
|
+
|
|
162
|
+
// Getter passed to plural function
|
|
163
|
+
const getter: Getter = (k: TranslationKey, p?: Params, dv?: string) => {
|
|
164
|
+
return this.t(k, p, dv)
|
|
165
|
+
}
|
|
166
|
+
|
|
167
|
+
const result = this.pluralFunc(
|
|
168
|
+
key,
|
|
169
|
+
Number.parseInt(countValue.toString()),
|
|
170
|
+
params,
|
|
171
|
+
this.getLocale(),
|
|
172
|
+
getter,
|
|
173
|
+
)
|
|
174
|
+
|
|
175
|
+
return result ?? defaultValue ?? key
|
|
176
|
+
}
|
|
177
|
+
|
|
178
|
+
/**
|
|
179
|
+
* Format number
|
|
180
|
+
*/
|
|
181
|
+
public tn(value: number, options?: Intl.NumberFormatOptions): string {
|
|
182
|
+
return this.formatter.formatNumber(value, this.getLocale(), options)
|
|
183
|
+
}
|
|
184
|
+
|
|
185
|
+
/**
|
|
186
|
+
* Format date
|
|
187
|
+
*/
|
|
188
|
+
public td(value: Date | number | string, options?: Intl.DateTimeFormatOptions): string {
|
|
189
|
+
return this.formatter.formatDate(value, this.getLocale(), options)
|
|
190
|
+
}
|
|
191
|
+
|
|
192
|
+
/**
|
|
193
|
+
* Format relative time
|
|
194
|
+
*/
|
|
195
|
+
public tdr(value: Date | number | string, options?: Intl.RelativeTimeFormatOptions): string {
|
|
196
|
+
return this.formatter.formatRelativeTime(value, this.getLocale(), options)
|
|
197
|
+
}
|
|
198
|
+
|
|
199
|
+
/**
|
|
200
|
+
* Check if translation exists
|
|
201
|
+
* Based on logic from src/runtime/plugins/01.plugin.ts
|
|
202
|
+
*/
|
|
203
|
+
public has(key: TranslationKey, routeName?: string): boolean {
|
|
204
|
+
const route = routeName || this.getRoute()
|
|
205
|
+
const locale = this.getLocale()
|
|
206
|
+
|
|
207
|
+
// Check only through getTranslation (as in plugin)
|
|
208
|
+
return !!this.helper.getTranslation(locale, route, key)
|
|
209
|
+
}
|
|
210
|
+
|
|
211
|
+
/**
|
|
212
|
+
* Clear cache
|
|
213
|
+
*/
|
|
214
|
+
public clearCache(): void {
|
|
215
|
+
this.helper.clearCache()
|
|
216
|
+
}
|
|
217
|
+
|
|
218
|
+
// --- Public methods (for subclasses to use) ---
|
|
219
|
+
|
|
220
|
+
/**
|
|
221
|
+
* Core translation loading logic (without reactivity)
|
|
222
|
+
* Subclasses can override addTranslations/addRouteTranslations to add reactivity
|
|
223
|
+
*/
|
|
224
|
+
public loadTranslationsCore(locale: string, translations: Translations, merge: boolean): void {
|
|
225
|
+
if (merge) {
|
|
226
|
+
this.helper.mergeGlobalTranslation(locale, translations, true)
|
|
227
|
+
}
|
|
228
|
+
else {
|
|
229
|
+
this.helper.loadTranslations(locale, translations)
|
|
230
|
+
}
|
|
231
|
+
}
|
|
232
|
+
|
|
233
|
+
/**
|
|
234
|
+
* Core route translation loading logic (without reactivity)
|
|
235
|
+
* Subclasses can override addRouteTranslations to add reactivity
|
|
236
|
+
*/
|
|
237
|
+
public loadRouteTranslationsCore(
|
|
238
|
+
locale: string,
|
|
239
|
+
routeName: string,
|
|
240
|
+
translations: Translations,
|
|
241
|
+
merge: boolean,
|
|
242
|
+
): void {
|
|
243
|
+
if (merge) {
|
|
244
|
+
this.helper.mergeTranslation(locale, routeName, translations, true)
|
|
245
|
+
}
|
|
246
|
+
else {
|
|
247
|
+
this.helper.loadPageTranslations(locale, routeName, translations)
|
|
248
|
+
}
|
|
249
|
+
}
|
|
250
|
+
}
|
package/src/index.ts
CHANGED
|
@@ -2,6 +2,7 @@ import { useTranslationHelper, type TranslationCache } 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'
|
|
5
|
+
import { BaseI18n, type BaseI18nOptions } from './base'
|
|
5
6
|
|
|
6
7
|
export {
|
|
7
8
|
useTranslationHelper,
|
|
@@ -14,5 +15,7 @@ export {
|
|
|
14
15
|
defaultPlural,
|
|
15
16
|
RouteService,
|
|
16
17
|
FormatService,
|
|
18
|
+
BaseI18n,
|
|
17
19
|
type TranslationCache,
|
|
20
|
+
type BaseI18nOptions,
|
|
18
21
|
}
|
package/src/route-service.ts
CHANGED
|
@@ -374,19 +374,20 @@ export class RouteService {
|
|
|
374
374
|
}
|
|
375
375
|
|
|
376
376
|
updateCookies(toLocale: string): void {
|
|
377
|
+
const cookieLocaleName = this.cookieLocaleName || this.i18nConfig.localeCookie || 'user-locale'
|
|
377
378
|
if (this.i18nConfig.hashMode) {
|
|
378
379
|
this.setCookie('hash-locale', toLocale)
|
|
379
380
|
// useCookie('hash-locale').value = toLocale
|
|
380
381
|
this.hashLocaleDefault = toLocale
|
|
381
382
|
}
|
|
382
383
|
if (isNoPrefixStrategy(this.i18nConfig.strategy!)) {
|
|
383
|
-
this.setCookie(
|
|
384
|
-
// useCookie(
|
|
384
|
+
this.setCookie(cookieLocaleName, toLocale)
|
|
385
|
+
// useCookie(cookieLocaleName).value = toLocale
|
|
385
386
|
this.noPrefixDefault = toLocale
|
|
386
387
|
}
|
|
387
388
|
// Update cookie for regular strategy (prefix or prefix_except_default)
|
|
388
|
-
if (!this.i18nConfig.hashMode && !isNoPrefixStrategy(this.i18nConfig.strategy!) &&
|
|
389
|
-
this.setCookie(
|
|
389
|
+
if (!this.i18nConfig.hashMode && !isNoPrefixStrategy(this.i18nConfig.strategy!) && cookieLocaleName) {
|
|
390
|
+
this.setCookie(cookieLocaleName, toLocale)
|
|
390
391
|
this.cookieLocaleDefault = toLocale
|
|
391
392
|
}
|
|
392
393
|
}
|