@i18n-micro/core 1.0.27
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/LICENSE +21 -0
- package/README.md +176 -0
- package/dist/format-service.d.ts +5 -0
- package/dist/helpers.d.ts +18 -0
- package/dist/index.cjs +1 -0
- package/dist/index.d.ts +5 -0
- package/dist/index.mjs +366 -0
- package/dist/route-service.d.ts +40 -0
- package/dist/translation.d.ts +24 -0
- package/fix.ts +1 -0
- package/jest.config.cjs +7 -0
- package/package.json +35 -0
- package/src/format-service.ts +41 -0
- package/src/helpers.ts +53 -0
- package/src/index.ts +18 -0
- package/src/route-service.ts +473 -0
- package/src/translation.ts +216 -0
- package/tests/core.test.ts +81 -0
- package/tests/format-service.test.ts +101 -0
- package/tests/helpers.test.ts +103 -0
- package/tests/route-service.test.ts +377 -0
- package/tsconfig.json +24 -0
- package/vite.config.mts +25 -0
|
@@ -0,0 +1,24 @@
|
|
|
1
|
+
import { Translations } from '@i18n-micro/types';
|
|
2
|
+
export interface RefLike<T> {
|
|
3
|
+
value: T;
|
|
4
|
+
}
|
|
5
|
+
export interface TranslationCache {
|
|
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): {
|
|
12
|
+
hasCache(locale: string, page: string): boolean;
|
|
13
|
+
getCache(locale: string, routeName: string): Map<string, unknown> | undefined;
|
|
14
|
+
setCache(locale: string, routeName: string, cache: Map<string, Translations | unknown>): void;
|
|
15
|
+
mergeTranslation(locale: string, routeName: string, newTranslations: Translations, force?: boolean): void;
|
|
16
|
+
mergeGlobalTranslation(locale: string, newTranslations: Translations, force?: boolean): void;
|
|
17
|
+
hasGeneralTranslation(locale: string): boolean;
|
|
18
|
+
hasPageTranslation(locale: string, routeName: string): boolean;
|
|
19
|
+
hasTranslation: (locale: string, key: import('@i18n-micro/types').TranslationKey) => boolean;
|
|
20
|
+
getTranslation: <T = unknown>(locale: string, routeName: string, key: import('@i18n-micro/types').TranslationKey) => T | null;
|
|
21
|
+
loadPageTranslations(locale: string, routeName: string, translations: Translations): Promise<void>;
|
|
22
|
+
loadTranslations(locale: string, translations: Translations): Promise<void>;
|
|
23
|
+
clearCache(): void;
|
|
24
|
+
};
|
package/fix.ts
ADDED
|
@@ -0,0 +1 @@
|
|
|
1
|
+
import '@types/jest'
|
package/jest.config.cjs
ADDED
package/package.json
ADDED
|
@@ -0,0 +1,35 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "@i18n-micro/core",
|
|
3
|
+
"version": "1.0.27",
|
|
4
|
+
"description": "",
|
|
5
|
+
"repository": "s00d/nuxt-i18n-micro",
|
|
6
|
+
"license": "MIT",
|
|
7
|
+
"type": "module",
|
|
8
|
+
"author": {
|
|
9
|
+
"name": "s00d",
|
|
10
|
+
"email": "Virus191288@gmail.com",
|
|
11
|
+
"url": "https://s00d.github.io/"
|
|
12
|
+
},
|
|
13
|
+
"homepage": "https://github.com/s00d/nuxt-i18n-micro",
|
|
14
|
+
"bugs": {
|
|
15
|
+
"url": "https://github.com/s00d/nuxt-i18n-micro/issues"
|
|
16
|
+
},
|
|
17
|
+
"main": "dist/index.cjs",
|
|
18
|
+
"module": "dist/index.mjs",
|
|
19
|
+
"types": "dist/index.d.ts",
|
|
20
|
+
"publishConfig": {
|
|
21
|
+
"access": "public"
|
|
22
|
+
},
|
|
23
|
+
"keywords": [],
|
|
24
|
+
"dependencies": {
|
|
25
|
+
"@i18n-micro/types": "1.0.15"
|
|
26
|
+
},
|
|
27
|
+
"devDependencies": {
|
|
28
|
+
"vite": "^5.0.0",
|
|
29
|
+
"vite-plugin-dts": "^4.3.0"
|
|
30
|
+
},
|
|
31
|
+
"scripts": {
|
|
32
|
+
"build": "vite build",
|
|
33
|
+
"test": "jest"
|
|
34
|
+
}
|
|
35
|
+
}
|
|
@@ -0,0 +1,41 @@
|
|
|
1
|
+
export class FormatService {
|
|
2
|
+
formatNumber(value: number, locale: string, options?: Intl.NumberFormatOptions): string {
|
|
3
|
+
return new Intl.NumberFormat(locale, options).format(value)
|
|
4
|
+
}
|
|
5
|
+
|
|
6
|
+
formatDate(value: Date | number | string, locale: string, options?: Intl.DateTimeFormatOptions): string {
|
|
7
|
+
const date = new Date(value)
|
|
8
|
+
if (Number.isNaN(date.getTime())) {
|
|
9
|
+
return 'Invalid Date'
|
|
10
|
+
}
|
|
11
|
+
return new Intl.DateTimeFormat(locale, options).format(date)
|
|
12
|
+
}
|
|
13
|
+
|
|
14
|
+
formatRelativeTime(value: Date | number | string, locale: string, options?: Intl.RelativeTimeFormatOptions): string {
|
|
15
|
+
const date = new Date(value)
|
|
16
|
+
if (Number.isNaN(date.getTime())) {
|
|
17
|
+
// Return "0 seconds ago" for invalid dates
|
|
18
|
+
return new Intl.RelativeTimeFormat(locale, options).format(0, 'second')
|
|
19
|
+
}
|
|
20
|
+
const now = new Date()
|
|
21
|
+
const diffInSeconds = Math.floor((now.getTime() - date.getTime()) / 1000)
|
|
22
|
+
|
|
23
|
+
const units: { unit: Intl.RelativeTimeFormatUnit, seconds: number }[] = [
|
|
24
|
+
{ unit: 'year', seconds: 31536000 },
|
|
25
|
+
{ unit: 'month', seconds: 2592000 },
|
|
26
|
+
{ unit: 'day', seconds: 86400 },
|
|
27
|
+
{ unit: 'hour', seconds: 3600 },
|
|
28
|
+
{ unit: 'minute', seconds: 60 },
|
|
29
|
+
{ unit: 'second', seconds: 1 },
|
|
30
|
+
]
|
|
31
|
+
|
|
32
|
+
for (const { unit, seconds } of units) {
|
|
33
|
+
const diff = Math.floor(diffInSeconds / seconds)
|
|
34
|
+
if (diff >= 1) {
|
|
35
|
+
return new Intl.RelativeTimeFormat(locale, options).format(-diff, unit)
|
|
36
|
+
}
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
return new Intl.RelativeTimeFormat(locale, options).format(0, 'second')
|
|
40
|
+
}
|
|
41
|
+
}
|
package/src/helpers.ts
ADDED
|
@@ -0,0 +1,53 @@
|
|
|
1
|
+
import type { Params, Strategies, PluralFunc, Getter, TranslationKey } from '@i18n-micro/types'
|
|
2
|
+
|
|
3
|
+
export function interpolate(template: string, params: Params): string {
|
|
4
|
+
let result = template
|
|
5
|
+
|
|
6
|
+
for (const key in params) {
|
|
7
|
+
result = result.split(`{${key}}`).join(String(params[key]))
|
|
8
|
+
}
|
|
9
|
+
|
|
10
|
+
return result
|
|
11
|
+
}
|
|
12
|
+
|
|
13
|
+
export function withPrefixStrategy(strategy: Strategies) {
|
|
14
|
+
return strategy === 'prefix' || strategy === 'prefix_and_default'
|
|
15
|
+
}
|
|
16
|
+
|
|
17
|
+
export function isNoPrefixStrategy(strategy: Strategies) {
|
|
18
|
+
return strategy === 'no_prefix'
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
export function isPrefixStrategy(strategy: Strategies) {
|
|
22
|
+
return strategy === 'prefix'
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
export function isPrefixExceptDefaultStrategy(strategy: Strategies) {
|
|
26
|
+
return strategy === 'prefix_except_default'
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
export function isPrefixAndDefaultStrategy(strategy: Strategies) {
|
|
30
|
+
return strategy === 'prefix_and_default'
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
/**
|
|
34
|
+
* Default pluralization function
|
|
35
|
+
* Splits translation by '|' and selects form based on count
|
|
36
|
+
* @param key - Translation key
|
|
37
|
+
* @param count - Count for pluralization
|
|
38
|
+
* @param params - Parameters for translation
|
|
39
|
+
* @param _locale - Current locale (unused in default implementation)
|
|
40
|
+
* @param getTranslation - Function to get translation value
|
|
41
|
+
* @returns Selected plural form or null if not found
|
|
42
|
+
*/
|
|
43
|
+
export const defaultPlural: PluralFunc = (key: TranslationKey, count: number, params: Params, _locale: string, getTranslation: Getter) => {
|
|
44
|
+
const translation = getTranslation(key, params)
|
|
45
|
+
if (!translation) {
|
|
46
|
+
return null
|
|
47
|
+
}
|
|
48
|
+
const forms = translation.toString().split('|')
|
|
49
|
+
if (forms.length === 0) return null
|
|
50
|
+
const selectedForm = count < forms.length ? forms[count] : forms[forms.length - 1]
|
|
51
|
+
if (!selectedForm) return null
|
|
52
|
+
return selectedForm.trim().replace('{count}', count.toString())
|
|
53
|
+
}
|
package/src/index.ts
ADDED
|
@@ -0,0 +1,18 @@
|
|
|
1
|
+
import { useTranslationHelper, type TranslationCache } from './translation'
|
|
2
|
+
import { RouteService } from './route-service'
|
|
3
|
+
import { FormatService } from './format-service'
|
|
4
|
+
import { interpolate, withPrefixStrategy, isNoPrefixStrategy, isPrefixStrategy, isPrefixExceptDefaultStrategy, isPrefixAndDefaultStrategy, defaultPlural } from './helpers'
|
|
5
|
+
|
|
6
|
+
export {
|
|
7
|
+
useTranslationHelper,
|
|
8
|
+
interpolate,
|
|
9
|
+
withPrefixStrategy,
|
|
10
|
+
isNoPrefixStrategy,
|
|
11
|
+
isPrefixStrategy,
|
|
12
|
+
isPrefixExceptDefaultStrategy,
|
|
13
|
+
isPrefixAndDefaultStrategy,
|
|
14
|
+
defaultPlural,
|
|
15
|
+
RouteService,
|
|
16
|
+
FormatService,
|
|
17
|
+
type TranslationCache,
|
|
18
|
+
}
|
|
@@ -0,0 +1,473 @@
|
|
|
1
|
+
import type {
|
|
2
|
+
NavigationFailure,
|
|
3
|
+
RouteLocationAsPath, RouteLocationAsPathGeneric,
|
|
4
|
+
RouteLocationAsRelative,
|
|
5
|
+
RouteLocationAsString,
|
|
6
|
+
RouteLocationNamedRaw,
|
|
7
|
+
RouteLocationNormalizedLoaded,
|
|
8
|
+
RouteLocationOptions,
|
|
9
|
+
RouteLocationRaw,
|
|
10
|
+
RouteLocationResolved,
|
|
11
|
+
RouteLocationResolvedGeneric,
|
|
12
|
+
RouteParamsRawGeneric,
|
|
13
|
+
Router,
|
|
14
|
+
} from 'vue-router'
|
|
15
|
+
import type { I18nRouteParams, Locale, ModuleOptionsExtend } from '@i18n-micro/types'
|
|
16
|
+
import { isNoPrefixStrategy, withPrefixStrategy } from './helpers'
|
|
17
|
+
|
|
18
|
+
interface NavigateToInterface {
|
|
19
|
+
replace?: boolean
|
|
20
|
+
redirectCode?: number
|
|
21
|
+
external?: boolean
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
export class RouteService {
|
|
25
|
+
constructor(
|
|
26
|
+
private i18nConfig: ModuleOptionsExtend,
|
|
27
|
+
private router: Router,
|
|
28
|
+
private hashLocaleDefault: string | null | undefined,
|
|
29
|
+
private noPrefixDefault: string | null | undefined,
|
|
30
|
+
private navigateTo: (to: RouteLocationRaw | undefined | null, options?: NavigateToInterface) => Promise<void | NavigationFailure | false> | false | void | RouteLocationRaw,
|
|
31
|
+
private setCookie: (name: string, value: string) => void,
|
|
32
|
+
private cookieLocaleDefault: string | null | undefined = null,
|
|
33
|
+
private cookieLocaleName: string | null | undefined = null,
|
|
34
|
+
) {}
|
|
35
|
+
|
|
36
|
+
/**
|
|
37
|
+
* Extracts locale from URL path by checking the first path segment
|
|
38
|
+
* @param path - URL path (e.g., '/ru/sdfsdf' or '/en/about')
|
|
39
|
+
* @returns Locale code or null if not found
|
|
40
|
+
*/
|
|
41
|
+
private extractLocaleFromPath(path: string): string | null {
|
|
42
|
+
if (!path) {
|
|
43
|
+
return null
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
// Remove query params and hash to ensure clean path comparison
|
|
47
|
+
// This is important when falling back to fullPath which might contain these
|
|
48
|
+
const querySplit = path.split('?')
|
|
49
|
+
const cleanPath = querySplit[0]?.split('#')[0]
|
|
50
|
+
|
|
51
|
+
if (!cleanPath || cleanPath === '/') {
|
|
52
|
+
return null
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
const pathSegments = cleanPath.split('/').filter(Boolean)
|
|
56
|
+
if (pathSegments.length === 0) {
|
|
57
|
+
return null
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
const firstSegment = pathSegments[0]
|
|
61
|
+
if (!firstSegment) {
|
|
62
|
+
return null
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
const availableLocales = this.i18nConfig.locales?.map(l => l.code) || []
|
|
66
|
+
|
|
67
|
+
// Check if the first segment is a valid locale
|
|
68
|
+
if (availableLocales.includes(firstSegment)) {
|
|
69
|
+
return firstSegment
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
return null
|
|
73
|
+
}
|
|
74
|
+
|
|
75
|
+
getCurrentLocale(route?: RouteLocationNormalizedLoaded | RouteLocationResolvedGeneric): string {
|
|
76
|
+
route = route ?? this.router.currentRoute.value
|
|
77
|
+
|
|
78
|
+
// 1. Check hashMode
|
|
79
|
+
if (this.i18nConfig.hashMode && this.hashLocaleDefault) {
|
|
80
|
+
return this.hashLocaleDefault
|
|
81
|
+
}
|
|
82
|
+
|
|
83
|
+
// 2. Check noPrefix strategy
|
|
84
|
+
if (isNoPrefixStrategy(this.i18nConfig.strategy!) && this.noPrefixDefault) {
|
|
85
|
+
return this.noPrefixDefault
|
|
86
|
+
}
|
|
87
|
+
|
|
88
|
+
// 3. Check route.params.locale (for existing routes)
|
|
89
|
+
if (route.params?.locale) {
|
|
90
|
+
return route.params.locale.toString()
|
|
91
|
+
}
|
|
92
|
+
|
|
93
|
+
// 4. Extract locale from URL path (for non-existent routes)
|
|
94
|
+
const path = route.path || route.fullPath || ''
|
|
95
|
+
const localeFromPath = this.extractLocaleFromPath(path)
|
|
96
|
+
if (localeFromPath) {
|
|
97
|
+
return localeFromPath
|
|
98
|
+
}
|
|
99
|
+
|
|
100
|
+
// 5. Check cookie (if provided)
|
|
101
|
+
if (this.cookieLocaleDefault) {
|
|
102
|
+
return this.cookieLocaleDefault
|
|
103
|
+
}
|
|
104
|
+
|
|
105
|
+
// 6. Return defaultLocale as fallback
|
|
106
|
+
return (this.i18nConfig.defaultLocale || 'en').toString()
|
|
107
|
+
}
|
|
108
|
+
|
|
109
|
+
getCurrentName(route: RouteLocationNormalizedLoaded | RouteLocationResolvedGeneric): string | null {
|
|
110
|
+
const currentLocaleCode = this.getCurrentLocale(route)
|
|
111
|
+
const checkLocale = this.i18nConfig.locales?.find(l => l.code === currentLocaleCode)
|
|
112
|
+
return checkLocale?.displayName ?? null
|
|
113
|
+
}
|
|
114
|
+
|
|
115
|
+
getRouteName(route: RouteLocationResolvedGeneric | RouteLocationNamedRaw, locale: string): string {
|
|
116
|
+
const name = (route.name ?? '').toString()
|
|
117
|
+
return name
|
|
118
|
+
.toString()
|
|
119
|
+
.replace('localized-', '')
|
|
120
|
+
.replace(new RegExp(`-${locale}$`), '')
|
|
121
|
+
}
|
|
122
|
+
|
|
123
|
+
getPluginRouteName(route: RouteLocationResolvedGeneric | RouteLocationNamedRaw, locale: string): string {
|
|
124
|
+
if (this.i18nConfig.disablePageLocales) {
|
|
125
|
+
return 'general'
|
|
126
|
+
}
|
|
127
|
+
return this.getRouteName(route, locale)
|
|
128
|
+
}
|
|
129
|
+
|
|
130
|
+
getFullPathWithBaseUrl(currentLocale: Locale, route: RouteLocationRaw): string {
|
|
131
|
+
const resolvedRoute = this.router.resolve(route)
|
|
132
|
+
let fullPath = resolvedRoute.fullPath
|
|
133
|
+
|
|
134
|
+
if (currentLocale?.baseDefault) {
|
|
135
|
+
fullPath = fullPath.replace(new RegExp(`^/${currentLocale!.code}`), '')
|
|
136
|
+
}
|
|
137
|
+
|
|
138
|
+
let baseUrl = currentLocale!.baseUrl
|
|
139
|
+
if (!baseUrl) baseUrl = ''
|
|
140
|
+
if (baseUrl?.endsWith('/')) {
|
|
141
|
+
baseUrl = baseUrl.slice(0, -1)
|
|
142
|
+
}
|
|
143
|
+
|
|
144
|
+
return baseUrl + fullPath
|
|
145
|
+
}
|
|
146
|
+
|
|
147
|
+
switchLocaleRoute(
|
|
148
|
+
fromLocale: string,
|
|
149
|
+
toLocale: string,
|
|
150
|
+
route: RouteLocationResolvedGeneric | RouteLocationNamedRaw,
|
|
151
|
+
i18nRouteParams: I18nRouteParams,
|
|
152
|
+
): RouteLocationRaw {
|
|
153
|
+
const currentLocale = this.i18nConfig.locales?.find(l => l.code === toLocale)
|
|
154
|
+
|
|
155
|
+
const routeName = this.getRouteName(route, fromLocale)
|
|
156
|
+
if (this.router.hasRoute(`localized-${routeName}-${toLocale}`)) {
|
|
157
|
+
// If i18nRouteParams exist for target locale, use them as base, otherwise use route.params
|
|
158
|
+
const baseParams = i18nRouteParams?.[toLocale]
|
|
159
|
+
? { ...i18nRouteParams[toLocale] }
|
|
160
|
+
: { ...route.params ?? {} }
|
|
161
|
+
// Merge remaining route.params that are not in i18nRouteParams
|
|
162
|
+
const newParams = { ...baseParams }
|
|
163
|
+
// Remove locale from params if it exists, we'll add it explicitly
|
|
164
|
+
delete newParams.locale
|
|
165
|
+
if (!isNoPrefixStrategy(this.i18nConfig.strategy!)) newParams.locale = toLocale
|
|
166
|
+
|
|
167
|
+
const newRoute = {
|
|
168
|
+
name: `localized-${routeName}-${toLocale}`,
|
|
169
|
+
params: newParams,
|
|
170
|
+
query: route.query,
|
|
171
|
+
hash: route.hash,
|
|
172
|
+
}
|
|
173
|
+
|
|
174
|
+
if (currentLocale?.baseUrl) {
|
|
175
|
+
return this.getFullPathWithBaseUrl(currentLocale, newRoute)
|
|
176
|
+
}
|
|
177
|
+
|
|
178
|
+
return newRoute
|
|
179
|
+
}
|
|
180
|
+
|
|
181
|
+
let newRouteName = routeName
|
|
182
|
+
// If i18nRouteParams exist for target locale, use them as base, otherwise use route.params
|
|
183
|
+
const baseParams = i18nRouteParams?.[toLocale]
|
|
184
|
+
? { ...i18nRouteParams[toLocale] }
|
|
185
|
+
: { ...route.params ?? {} }
|
|
186
|
+
// Merge remaining route.params that are not in i18nRouteParams
|
|
187
|
+
const newParams = { ...baseParams }
|
|
188
|
+
delete newParams.locale
|
|
189
|
+
|
|
190
|
+
if (!isNoPrefixStrategy(this.i18nConfig.strategy!)) {
|
|
191
|
+
if (routeName === 'custom-fallback-route') {
|
|
192
|
+
newRouteName = routeName
|
|
193
|
+
}
|
|
194
|
+
else {
|
|
195
|
+
newRouteName
|
|
196
|
+
= toLocale !== this.i18nConfig.defaultLocale || withPrefixStrategy(this.i18nConfig.strategy!)
|
|
197
|
+
? `localized-${routeName}`
|
|
198
|
+
: routeName
|
|
199
|
+
}
|
|
200
|
+
|
|
201
|
+
if (!isNoPrefixStrategy(this.i18nConfig.strategy!)) {
|
|
202
|
+
if (toLocale !== this.i18nConfig.defaultLocale || withPrefixStrategy(this.i18nConfig.strategy!)) {
|
|
203
|
+
newParams.locale = toLocale
|
|
204
|
+
}
|
|
205
|
+
}
|
|
206
|
+
}
|
|
207
|
+
|
|
208
|
+
const newRoute = {
|
|
209
|
+
name: newRouteName,
|
|
210
|
+
params: newParams,
|
|
211
|
+
query: route.query,
|
|
212
|
+
hash: route.hash,
|
|
213
|
+
}
|
|
214
|
+
|
|
215
|
+
if (isNoPrefixStrategy(this.i18nConfig.strategy!)) {
|
|
216
|
+
this.i18nConfig.locales?.forEach((locale, _index) => {
|
|
217
|
+
if (newRoute.name.endsWith(`-${locale.code}`)) {
|
|
218
|
+
newRoute.name = newRoute.name.slice(0, -locale.code - 1)
|
|
219
|
+
}
|
|
220
|
+
})
|
|
221
|
+
}
|
|
222
|
+
|
|
223
|
+
if (currentLocale?.baseUrl) {
|
|
224
|
+
return this.getFullPathWithBaseUrl(currentLocale, newRoute)
|
|
225
|
+
}
|
|
226
|
+
|
|
227
|
+
return newRoute
|
|
228
|
+
}
|
|
229
|
+
|
|
230
|
+
private resolveParams(to: RouteLocationAsString | RouteLocationAsRelative | RouteLocationAsPath): RouteParamsRawGeneric {
|
|
231
|
+
const params
|
|
232
|
+
= typeof to === 'object' && 'params' in to && typeof to.params === 'object'
|
|
233
|
+
? { ...to.params }
|
|
234
|
+
: {}
|
|
235
|
+
|
|
236
|
+
if (typeof to === 'string') {
|
|
237
|
+
const resolved = this.router.resolve(to)
|
|
238
|
+
if (resolved && resolved.params) {
|
|
239
|
+
Object.assign(params, resolved.params)
|
|
240
|
+
}
|
|
241
|
+
}
|
|
242
|
+
|
|
243
|
+
return params
|
|
244
|
+
}
|
|
245
|
+
|
|
246
|
+
private handlePrefixStrategy(
|
|
247
|
+
to: RouteLocationResolvedGeneric | RouteLocationAsPathGeneric | RouteLocationNamedRaw | string,
|
|
248
|
+
): RouteLocationResolvedGeneric | RouteLocationAsPathGeneric | RouteLocationNamedRaw | string {
|
|
249
|
+
if (!withPrefixStrategy(this.i18nConfig.strategy!)) {
|
|
250
|
+
return to
|
|
251
|
+
}
|
|
252
|
+
|
|
253
|
+
const defaultLocale = this.i18nConfig.defaultLocale!
|
|
254
|
+
let resolvedTo = to
|
|
255
|
+
|
|
256
|
+
if (typeof to === 'string') {
|
|
257
|
+
resolvedTo = this.router.resolve('/' + defaultLocale + to)
|
|
258
|
+
}
|
|
259
|
+
|
|
260
|
+
const defaultRouteName = this.getRouteName(resolvedTo as RouteLocationResolvedGeneric, defaultLocale)
|
|
261
|
+
const newParams = this.resolveParams(resolvedTo)
|
|
262
|
+
|
|
263
|
+
if (!isNoPrefixStrategy(this.i18nConfig.strategy!)) {
|
|
264
|
+
newParams.locale = defaultLocale
|
|
265
|
+
}
|
|
266
|
+
|
|
267
|
+
if (this.router.hasRoute(`localized-${defaultRouteName}`)) {
|
|
268
|
+
return this.router.resolve({
|
|
269
|
+
name: `localized-${defaultRouteName}`,
|
|
270
|
+
query: (resolvedTo as RouteLocationNormalizedLoaded).query,
|
|
271
|
+
hash: (resolvedTo as RouteLocationNormalizedLoaded).hash,
|
|
272
|
+
params: newParams,
|
|
273
|
+
})
|
|
274
|
+
}
|
|
275
|
+
else if (this.router.hasRoute(`localized-${defaultRouteName}-${defaultLocale}`)) {
|
|
276
|
+
return this.router.resolve({
|
|
277
|
+
name: `localized-${defaultRouteName}-${defaultLocale}`,
|
|
278
|
+
query: (resolvedTo as RouteLocationNormalizedLoaded).query,
|
|
279
|
+
hash: (resolvedTo as RouteLocationNormalizedLoaded).hash,
|
|
280
|
+
params: newParams,
|
|
281
|
+
})
|
|
282
|
+
}
|
|
283
|
+
|
|
284
|
+
return to
|
|
285
|
+
}
|
|
286
|
+
|
|
287
|
+
private createLocalizedRoute(
|
|
288
|
+
to: RouteLocationAsString | RouteLocationAsRelative | RouteLocationAsPath,
|
|
289
|
+
route: RouteLocationNormalizedLoaded,
|
|
290
|
+
locale: string,
|
|
291
|
+
): RouteLocationResolved {
|
|
292
|
+
const selectRoute = this.router.resolve(to)
|
|
293
|
+
const routeName = this.getRouteName(selectRoute, locale)
|
|
294
|
+
.replace(new RegExp(`-${this.i18nConfig.defaultLocale!}$`), '')
|
|
295
|
+
|
|
296
|
+
if (!isNoPrefixStrategy(this.i18nConfig.strategy!)) {
|
|
297
|
+
if (!routeName || routeName === '') {
|
|
298
|
+
const resolved = this.router.resolve(to)
|
|
299
|
+
let url = resolved.path.replace(new RegExp(`^/${locale}/`), '/')
|
|
300
|
+
if (locale !== this.i18nConfig.defaultLocale || withPrefixStrategy(this.i18nConfig.strategy!)) {
|
|
301
|
+
url = '/' + locale + '' + url
|
|
302
|
+
}
|
|
303
|
+
|
|
304
|
+
return this.router.resolve({
|
|
305
|
+
path: url,
|
|
306
|
+
query: selectRoute.query,
|
|
307
|
+
hash: selectRoute.hash,
|
|
308
|
+
})
|
|
309
|
+
}
|
|
310
|
+
}
|
|
311
|
+
|
|
312
|
+
if (this.router.hasRoute(`localized-${routeName}-${locale}`)) {
|
|
313
|
+
const newParams = this.resolveParams(selectRoute)
|
|
314
|
+
if (!isNoPrefixStrategy(this.i18nConfig.strategy!)) newParams.locale = locale
|
|
315
|
+
|
|
316
|
+
return this.router.resolve({
|
|
317
|
+
name: `localized-${routeName}-${locale}`,
|
|
318
|
+
params: newParams,
|
|
319
|
+
query: selectRoute.query,
|
|
320
|
+
hash: selectRoute.hash,
|
|
321
|
+
})
|
|
322
|
+
}
|
|
323
|
+
|
|
324
|
+
const newRouteName
|
|
325
|
+
= locale !== this.i18nConfig.defaultLocale || withPrefixStrategy(this.i18nConfig.strategy!)
|
|
326
|
+
? `localized-${routeName}`
|
|
327
|
+
: routeName
|
|
328
|
+
|
|
329
|
+
if (!this.router.hasRoute(newRouteName)) {
|
|
330
|
+
const newParams = this.resolveParams(to)
|
|
331
|
+
delete newParams.locale
|
|
332
|
+
|
|
333
|
+
if (!this.router.hasRoute(routeName)) {
|
|
334
|
+
return this.router.resolve('/')
|
|
335
|
+
}
|
|
336
|
+
|
|
337
|
+
return this.router.resolve({
|
|
338
|
+
name: routeName,
|
|
339
|
+
params: newParams,
|
|
340
|
+
query: selectRoute.query,
|
|
341
|
+
hash: selectRoute.hash,
|
|
342
|
+
})
|
|
343
|
+
}
|
|
344
|
+
|
|
345
|
+
const newParams = this.resolveParams(to)
|
|
346
|
+
delete newParams.locale
|
|
347
|
+
|
|
348
|
+
if (!isNoPrefixStrategy(this.i18nConfig.strategy!)) {
|
|
349
|
+
if (locale !== this.i18nConfig.defaultLocale || withPrefixStrategy(this.i18nConfig.strategy!)) {
|
|
350
|
+
newParams.locale = locale
|
|
351
|
+
}
|
|
352
|
+
}
|
|
353
|
+
|
|
354
|
+
return this.router.resolve({
|
|
355
|
+
name: newRouteName,
|
|
356
|
+
params: newParams,
|
|
357
|
+
query: selectRoute.query,
|
|
358
|
+
hash: selectRoute.hash,
|
|
359
|
+
})
|
|
360
|
+
}
|
|
361
|
+
|
|
362
|
+
getLocalizedRoute(
|
|
363
|
+
to: RouteLocationResolvedGeneric | RouteLocationAsPathGeneric | RouteLocationNamedRaw | string,
|
|
364
|
+
route: RouteLocationNormalizedLoaded,
|
|
365
|
+
locale?: string,
|
|
366
|
+
): RouteLocationResolved {
|
|
367
|
+
const currentLocale = locale || this.getCurrentLocale(route)
|
|
368
|
+
|
|
369
|
+
// Handle prefix strategy
|
|
370
|
+
const processedTo = this.handlePrefixStrategy(to)
|
|
371
|
+
|
|
372
|
+
// Create localized route
|
|
373
|
+
return this.createLocalizedRoute(processedTo, route, currentLocale)
|
|
374
|
+
}
|
|
375
|
+
|
|
376
|
+
updateCookies(toLocale: string): void {
|
|
377
|
+
if (this.i18nConfig.hashMode) {
|
|
378
|
+
this.setCookie('hash-locale', toLocale)
|
|
379
|
+
// useCookie('hash-locale').value = toLocale
|
|
380
|
+
this.hashLocaleDefault = toLocale
|
|
381
|
+
}
|
|
382
|
+
if (isNoPrefixStrategy(this.i18nConfig.strategy!)) {
|
|
383
|
+
this.setCookie('no-prefix-locale', toLocale)
|
|
384
|
+
// useCookie('no-prefix-locale').value = toLocale
|
|
385
|
+
this.noPrefixDefault = toLocale
|
|
386
|
+
}
|
|
387
|
+
// Update cookie for regular strategy (prefix or prefix_except_default)
|
|
388
|
+
if (!this.i18nConfig.hashMode && !isNoPrefixStrategy(this.i18nConfig.strategy!) && this.cookieLocaleName) {
|
|
389
|
+
this.setCookie(this.cookieLocaleName, toLocale)
|
|
390
|
+
this.cookieLocaleDefault = toLocale
|
|
391
|
+
}
|
|
392
|
+
}
|
|
393
|
+
|
|
394
|
+
getCurrentRoute(): RouteLocationNormalizedLoaded {
|
|
395
|
+
return this.router.currentRoute.value
|
|
396
|
+
}
|
|
397
|
+
|
|
398
|
+
private resolveRouteWithStrategy(
|
|
399
|
+
route: string,
|
|
400
|
+
currentLocale: string,
|
|
401
|
+
fromLocale: string,
|
|
402
|
+
): RouteLocationResolved {
|
|
403
|
+
if (isNoPrefixStrategy(this.i18nConfig.strategy!)) {
|
|
404
|
+
return this.router.resolve(route)
|
|
405
|
+
}
|
|
406
|
+
else if (currentLocale !== this.i18nConfig.defaultLocale || withPrefixStrategy(this.i18nConfig.strategy!)) {
|
|
407
|
+
return this.router.resolve(`/${fromLocale}${route}`)
|
|
408
|
+
}
|
|
409
|
+
else {
|
|
410
|
+
return this.router.resolve(route)
|
|
411
|
+
}
|
|
412
|
+
}
|
|
413
|
+
|
|
414
|
+
switchLocaleLogic(toLocale: string, i18nRouteParams: I18nRouteParams, to?: RouteLocationNamedRaw | RouteLocationResolvedGeneric | string) {
|
|
415
|
+
const fromLocale = this.getCurrentLocale()
|
|
416
|
+
|
|
417
|
+
let current: RouteLocationResolved | RouteLocationNamedRaw
|
|
418
|
+
if (typeof to === 'string') {
|
|
419
|
+
current = this.resolveRouteWithStrategy(to, toLocale, fromLocale)
|
|
420
|
+
}
|
|
421
|
+
else {
|
|
422
|
+
current = to ?? this.getCurrentRoute() as RouteLocationResolved
|
|
423
|
+
}
|
|
424
|
+
|
|
425
|
+
this.updateCookies(toLocale)
|
|
426
|
+
const switchedRoute = this.switchLocaleRoute(fromLocale, toLocale, current, i18nRouteParams)
|
|
427
|
+
|
|
428
|
+
if (typeof switchedRoute === 'string' && switchedRoute.startsWith('http')) {
|
|
429
|
+
return this.navigateTo(switchedRoute, { redirectCode: 200, external: true })
|
|
430
|
+
}
|
|
431
|
+
|
|
432
|
+
if (isNoPrefixStrategy(this.i18nConfig.strategy!)) {
|
|
433
|
+
(switchedRoute as RouteLocationRaw & RouteLocationOptions).force = true
|
|
434
|
+
}
|
|
435
|
+
|
|
436
|
+
return this.router.push(switchedRoute)
|
|
437
|
+
}
|
|
438
|
+
|
|
439
|
+
resolveLocalizedRoute(
|
|
440
|
+
to: RouteLocationNamedRaw | RouteLocationAsPathGeneric | string,
|
|
441
|
+
locale?: string,
|
|
442
|
+
): RouteLocationResolved {
|
|
443
|
+
const currentRoute = this.getCurrentRoute()
|
|
444
|
+
const fromLocale = this.getCurrentLocale()
|
|
445
|
+
const currentLocale = locale ?? fromLocale
|
|
446
|
+
|
|
447
|
+
let current: RouteLocationResolved | RouteLocationNamedRaw | RouteLocationAsPathGeneric
|
|
448
|
+
if (typeof to === 'string') {
|
|
449
|
+
// Try to resolve as route name first (if it doesn't start with / and exists as route name)
|
|
450
|
+
if (!to.startsWith('/')) {
|
|
451
|
+
const routeName = to
|
|
452
|
+
// Check if this is a route name (not a path)
|
|
453
|
+
if (this.router.hasRoute(routeName)) {
|
|
454
|
+
// Resolve by name to get the correct route object
|
|
455
|
+
current = this.router.resolve({ name: routeName })
|
|
456
|
+
}
|
|
457
|
+
else {
|
|
458
|
+
// Treat as path
|
|
459
|
+
to = `/${to}`
|
|
460
|
+
current = this.resolveRouteWithStrategy(to, currentLocale, fromLocale)
|
|
461
|
+
}
|
|
462
|
+
}
|
|
463
|
+
else {
|
|
464
|
+
current = this.resolveRouteWithStrategy(to, currentLocale, fromLocale)
|
|
465
|
+
}
|
|
466
|
+
}
|
|
467
|
+
else {
|
|
468
|
+
current = to
|
|
469
|
+
}
|
|
470
|
+
|
|
471
|
+
return this.getLocalizedRoute(current, currentRoute, currentLocale)
|
|
472
|
+
}
|
|
473
|
+
}
|