@flightdev/i18n 0.1.5
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 +644 -0
- package/dist/adapters/formatjs.d.ts +61 -0
- package/dist/adapters/formatjs.js +81 -0
- package/dist/adapters/i18next.d.ts +15 -0
- package/dist/adapters/i18next.js +51 -0
- package/dist/adapters/lingui.d.ts +82 -0
- package/dist/adapters/lingui.js +73 -0
- package/dist/adapters/paraglide.d.ts +65 -0
- package/dist/adapters/paraglide.js +43 -0
- package/dist/chunk-F5CV7JNA.js +76 -0
- package/dist/chunk-O3FQ7FPU.js +150 -0
- package/dist/index.d.ts +165 -0
- package/dist/index.js +18 -0
- package/dist/middleware.d.ts +107 -0
- package/dist/middleware.js +127 -0
- package/dist/routing.d.ts +179 -0
- package/dist/routing.js +22 -0
- package/dist/typegen.d.ts +127 -0
- package/dist/typegen.js +214 -0
- package/package.json +72 -0
package/dist/index.d.ts
ADDED
|
@@ -0,0 +1,165 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* @flightdev/i18n - Agnostic Internationalization
|
|
3
|
+
*
|
|
4
|
+
* @example
|
|
5
|
+
* ```typescript
|
|
6
|
+
* import { createI18n } from '@flightdev/i18n';
|
|
7
|
+
* import { i18next } from '@flightdev/i18n/i18next';
|
|
8
|
+
*
|
|
9
|
+
* const i18n = createI18n(i18next({
|
|
10
|
+
* locales: ['en', 'es', 'fr'],
|
|
11
|
+
* defaultLocale: 'en',
|
|
12
|
+
* }));
|
|
13
|
+
*
|
|
14
|
+
* const t = i18n.t;
|
|
15
|
+
* t('hello'); // "Hello"
|
|
16
|
+
* ```
|
|
17
|
+
*/
|
|
18
|
+
type Locale = string;
|
|
19
|
+
interface TranslationOptions {
|
|
20
|
+
count?: number;
|
|
21
|
+
context?: string;
|
|
22
|
+
defaultValue?: string;
|
|
23
|
+
[key: string]: unknown;
|
|
24
|
+
}
|
|
25
|
+
interface I18nAdapter {
|
|
26
|
+
readonly name: string;
|
|
27
|
+
readonly locale: Locale;
|
|
28
|
+
readonly locales: Locale[];
|
|
29
|
+
init(): Promise<void>;
|
|
30
|
+
setLocale(locale: Locale): Promise<void>;
|
|
31
|
+
t(key: string, options?: TranslationOptions): string;
|
|
32
|
+
exists(key: string): boolean;
|
|
33
|
+
loadNamespace(namespace: string): Promise<void>;
|
|
34
|
+
}
|
|
35
|
+
type I18nAdapterFactory<TConfig = unknown> = (config: TConfig) => I18nAdapter;
|
|
36
|
+
interface I18nServiceOptions {
|
|
37
|
+
defaultLocale?: Locale;
|
|
38
|
+
fallbackLocale?: Locale;
|
|
39
|
+
}
|
|
40
|
+
interface I18nService {
|
|
41
|
+
readonly adapter: I18nAdapter;
|
|
42
|
+
readonly locale: Locale;
|
|
43
|
+
readonly locales: Locale[];
|
|
44
|
+
init(): Promise<void>;
|
|
45
|
+
setLocale(locale: Locale): Promise<void>;
|
|
46
|
+
t(key: string, options?: TranslationOptions): string;
|
|
47
|
+
exists(key: string): boolean;
|
|
48
|
+
}
|
|
49
|
+
declare function createI18n(adapter: I18nAdapter, _options?: I18nServiceOptions): I18nService;
|
|
50
|
+
declare function getLocaleFromRequest(request: Request, supportedLocales: Locale[], defaultLocale: Locale): Locale;
|
|
51
|
+
declare function formatNumber(value: number, locale: Locale, options?: Intl.NumberFormatOptions): string;
|
|
52
|
+
declare function formatDate(date: Date, locale: Locale, options?: Intl.DateTimeFormatOptions): string;
|
|
53
|
+
declare function formatRelativeTime(date: Date, locale: Locale): string;
|
|
54
|
+
/**
|
|
55
|
+
* Type-safe i18n service with autocompletion for translation keys.
|
|
56
|
+
*
|
|
57
|
+
* Use with generated types from `@flightdev/i18n/typegen` for
|
|
58
|
+
* full TypeScript-level validation of translation keys.
|
|
59
|
+
*
|
|
60
|
+
* @typeParam TKey - Union type of valid translation keys
|
|
61
|
+
*
|
|
62
|
+
* @example
|
|
63
|
+
* ```typescript
|
|
64
|
+
* import type { TranslationKey } from './i18n.d.ts';
|
|
65
|
+
* import { createTypedI18n } from '@flightdev/i18n';
|
|
66
|
+
*
|
|
67
|
+
* const i18n = createTypedI18n<TranslationKey>(adapter);
|
|
68
|
+
*
|
|
69
|
+
* i18n.t('nav.home'); // Valid - autocomplete suggests keys
|
|
70
|
+
* i18n.t('nonexistent'); // TypeScript error
|
|
71
|
+
* ```
|
|
72
|
+
*/
|
|
73
|
+
interface TypedI18nService<TKey extends string> {
|
|
74
|
+
/** The underlying adapter instance */
|
|
75
|
+
readonly adapter: I18nAdapter;
|
|
76
|
+
/** Current active locale */
|
|
77
|
+
readonly locale: Locale;
|
|
78
|
+
/** All supported locales */
|
|
79
|
+
readonly locales: Locale[];
|
|
80
|
+
/** Initialize the i18n service */
|
|
81
|
+
init(): Promise<void>;
|
|
82
|
+
/** Change the current locale */
|
|
83
|
+
setLocale(locale: Locale): Promise<void>;
|
|
84
|
+
/**
|
|
85
|
+
* Translate a key to the current locale.
|
|
86
|
+
* TypeScript will validate that key is a valid TranslationKey.
|
|
87
|
+
*
|
|
88
|
+
* @param key - Translation key (type-checked)
|
|
89
|
+
* @param options - Interpolation values and options
|
|
90
|
+
* @returns Translated string
|
|
91
|
+
*/
|
|
92
|
+
t(key: TKey, options?: TranslationOptions): string;
|
|
93
|
+
/**
|
|
94
|
+
* Check if a translation key exists.
|
|
95
|
+
*
|
|
96
|
+
* @param key - Translation key to check
|
|
97
|
+
* @returns true if key exists in current locale
|
|
98
|
+
*/
|
|
99
|
+
exists(key: TKey): boolean;
|
|
100
|
+
/**
|
|
101
|
+
* Load a namespace dynamically (if supported by adapter).
|
|
102
|
+
*
|
|
103
|
+
* @param namespace - Namespace to load
|
|
104
|
+
*/
|
|
105
|
+
loadNamespace(namespace: string): Promise<void>;
|
|
106
|
+
}
|
|
107
|
+
/**
|
|
108
|
+
* Creates a type-safe i18n service with autocompletion for translation keys.
|
|
109
|
+
*
|
|
110
|
+
* This is the recommended way to use i18n in TypeScript projects.
|
|
111
|
+
* Generate types using `@flightdev/i18n/typegen` and pass
|
|
112
|
+
* the `TranslationKey` type to get full IDE support.
|
|
113
|
+
*
|
|
114
|
+
* @typeParam TKey - Union type of valid translation keys
|
|
115
|
+
* @param adapter - i18n adapter instance
|
|
116
|
+
* @param options - Service options
|
|
117
|
+
* @returns Type-safe i18n service
|
|
118
|
+
*
|
|
119
|
+
* @example
|
|
120
|
+
* ```typescript
|
|
121
|
+
* // 1. Generate types (one-time or in build)
|
|
122
|
+
* // npx flight i18n:typegen --input ./locales/en --output ./src/i18n.d.ts
|
|
123
|
+
*
|
|
124
|
+
* // 2. Import generated types
|
|
125
|
+
* import type { TranslationKey } from './i18n.d.ts';
|
|
126
|
+
* import { createTypedI18n } from '@flightdev/i18n';
|
|
127
|
+
* import { formatjs } from '@flightdev/i18n/formatjs';
|
|
128
|
+
*
|
|
129
|
+
* // 3. Create type-safe service
|
|
130
|
+
* const i18n = createTypedI18n<TranslationKey>(formatjs({ ... }));
|
|
131
|
+
*
|
|
132
|
+
* // 4. Enjoy type safety!
|
|
133
|
+
* i18n.t('common.buttons.submit'); // Autocomplete works
|
|
134
|
+
* i18n.t('typo.here'); // TypeScript error
|
|
135
|
+
* ```
|
|
136
|
+
*/
|
|
137
|
+
declare function createTypedI18n<TKey extends string>(adapter: I18nAdapter, _options?: I18nServiceOptions): TypedI18nService<TKey>;
|
|
138
|
+
/**
|
|
139
|
+
* Helper type to extract parameters from a translation string.
|
|
140
|
+
* Matches {{param}} and {param} patterns.
|
|
141
|
+
*
|
|
142
|
+
* @example
|
|
143
|
+
* ```typescript
|
|
144
|
+
* type Params = ExtractParams<'Hello, {{name}}! You have {{count}} messages.'>;
|
|
145
|
+
* // { name: unknown; count: unknown }
|
|
146
|
+
* ```
|
|
147
|
+
*/
|
|
148
|
+
type ExtractParams<T extends string> = T extends `${string}{{${infer Param}}}${infer Rest}` ? {
|
|
149
|
+
[K in Param]: unknown;
|
|
150
|
+
} & ExtractParams<Rest> : T extends `${string}{${infer Param}}${infer Rest}` ? {
|
|
151
|
+
[K in Param]: unknown;
|
|
152
|
+
} & ExtractParams<Rest> : Record<string, never>;
|
|
153
|
+
/**
|
|
154
|
+
* Namespace separator for translation keys (e.g., 'common:button.submit')
|
|
155
|
+
*/
|
|
156
|
+
type NamespacedKey<NS extends string, Key extends string> = `${NS}:${Key}`;
|
|
157
|
+
/**
|
|
158
|
+
* Split a namespaced key into namespace and key parts
|
|
159
|
+
*/
|
|
160
|
+
declare function parseNamespacedKey(key: string): {
|
|
161
|
+
namespace: string | null;
|
|
162
|
+
key: string;
|
|
163
|
+
};
|
|
164
|
+
|
|
165
|
+
export { type ExtractParams, type I18nAdapter, type I18nAdapterFactory, type I18nService, type I18nServiceOptions, type Locale, type NamespacedKey, type TranslationOptions, type TypedI18nService, createI18n, createTypedI18n, formatDate, formatNumber, formatRelativeTime, getLocaleFromRequest, parseNamespacedKey };
|
package/dist/index.js
ADDED
|
@@ -0,0 +1,18 @@
|
|
|
1
|
+
import {
|
|
2
|
+
createI18n,
|
|
3
|
+
createTypedI18n,
|
|
4
|
+
formatDate,
|
|
5
|
+
formatNumber,
|
|
6
|
+
formatRelativeTime,
|
|
7
|
+
getLocaleFromRequest,
|
|
8
|
+
parseNamespacedKey
|
|
9
|
+
} from "./chunk-F5CV7JNA.js";
|
|
10
|
+
export {
|
|
11
|
+
createI18n,
|
|
12
|
+
createTypedI18n,
|
|
13
|
+
formatDate,
|
|
14
|
+
formatNumber,
|
|
15
|
+
formatRelativeTime,
|
|
16
|
+
getLocaleFromRequest,
|
|
17
|
+
parseNamespacedKey
|
|
18
|
+
};
|
|
@@ -0,0 +1,107 @@
|
|
|
1
|
+
import { I18nRoutingConfig } from './routing.js';
|
|
2
|
+
export { I18nRoutingStrategy } from './routing.js';
|
|
3
|
+
import { Locale } from './index.js';
|
|
4
|
+
|
|
5
|
+
/**
|
|
6
|
+
* i18n Middleware Factory for Flight Framework
|
|
7
|
+
*
|
|
8
|
+
* Creates file-based middleware for automatic locale detection and routing.
|
|
9
|
+
* Designed to integrate seamlessly with Flight's `_middleware.ts` convention.
|
|
10
|
+
*
|
|
11
|
+
* @example
|
|
12
|
+
* ```typescript
|
|
13
|
+
* // src/routes/_middleware.ts
|
|
14
|
+
* import { createI18nMiddleware } from '@flightdev/i18n/middleware';
|
|
15
|
+
*
|
|
16
|
+
* export const middleware = createI18nMiddleware({
|
|
17
|
+
* locales: ['en', 'es', 'fr'],
|
|
18
|
+
* defaultLocale: 'en',
|
|
19
|
+
* routing: 'prefix',
|
|
20
|
+
* prefixDefault: false, // /about for 'en', /es/about for 'es'
|
|
21
|
+
* });
|
|
22
|
+
* ```
|
|
23
|
+
*
|
|
24
|
+
* @module @flightdev/i18n/middleware
|
|
25
|
+
*/
|
|
26
|
+
|
|
27
|
+
/**
|
|
28
|
+
* Options for creating i18n middleware
|
|
29
|
+
*/
|
|
30
|
+
interface I18nMiddlewareOptions extends I18nRoutingConfig {
|
|
31
|
+
/**
|
|
32
|
+
* Whether to redirect to localized URL if no locale is detected
|
|
33
|
+
* Only applies to 'prefix' strategy
|
|
34
|
+
* @default true
|
|
35
|
+
*/
|
|
36
|
+
redirectOnMissingLocale?: boolean;
|
|
37
|
+
/**
|
|
38
|
+
* Custom locale detection function
|
|
39
|
+
* If provided, this runs first before other detection strategies
|
|
40
|
+
* Return null to fall back to standard detection
|
|
41
|
+
*/
|
|
42
|
+
detectLocale?: (request: Request) => Locale | null | Promise<Locale | null>;
|
|
43
|
+
/**
|
|
44
|
+
* Callback when locale is resolved
|
|
45
|
+
* Useful for analytics or custom logging
|
|
46
|
+
*/
|
|
47
|
+
onLocaleResolved?: (locale: Locale, request: Request) => void | Promise<void>;
|
|
48
|
+
}
|
|
49
|
+
/**
|
|
50
|
+
* Middleware function signature for Flight
|
|
51
|
+
*/
|
|
52
|
+
type I18nMiddleware = (request: Request) => Promise<Response | null>;
|
|
53
|
+
/**
|
|
54
|
+
* Creates an i18n middleware for Flight's file-based routing.
|
|
55
|
+
*
|
|
56
|
+
* The middleware handles:
|
|
57
|
+
* 1. Locale detection from URL, domain, cookie, or Accept-Language header
|
|
58
|
+
* 2. Automatic redirects to localized URLs (prefix strategy)
|
|
59
|
+
* 3. Setting locale cookies for persistence
|
|
60
|
+
*
|
|
61
|
+
* @param options - Middleware configuration
|
|
62
|
+
* @returns Middleware function for use in `_middleware.ts`
|
|
63
|
+
*
|
|
64
|
+
* @example
|
|
65
|
+
* ```typescript
|
|
66
|
+
* // Simple prefix strategy
|
|
67
|
+
* export const middleware = createI18nMiddleware({
|
|
68
|
+
* locales: ['en', 'es', 'fr'],
|
|
69
|
+
* defaultLocale: 'en',
|
|
70
|
+
* routing: 'prefix',
|
|
71
|
+
* });
|
|
72
|
+
*
|
|
73
|
+
* // Domain-based routing
|
|
74
|
+
* export const middleware = createI18nMiddleware({
|
|
75
|
+
* locales: ['en', 'es'],
|
|
76
|
+
* defaultLocale: 'en',
|
|
77
|
+
* routing: 'domain',
|
|
78
|
+
* domains: {
|
|
79
|
+
* en: 'example.com',
|
|
80
|
+
* es: 'es.example.com',
|
|
81
|
+
* },
|
|
82
|
+
* });
|
|
83
|
+
*
|
|
84
|
+
* // Custom locale detection
|
|
85
|
+
* export const middleware = createI18nMiddleware({
|
|
86
|
+
* locales: ['en', 'es'],
|
|
87
|
+
* defaultLocale: 'en',
|
|
88
|
+
* routing: 'cookie',
|
|
89
|
+
* detectLocale: (request) => {
|
|
90
|
+
* // Check user preferences from session
|
|
91
|
+
* return getUserPreferredLocale(request);
|
|
92
|
+
* },
|
|
93
|
+
* });
|
|
94
|
+
* ```
|
|
95
|
+
*/
|
|
96
|
+
declare function createI18nMiddleware(options: I18nMiddlewareOptions): I18nMiddleware;
|
|
97
|
+
/**
|
|
98
|
+
* Create a redirect response to a localized URL
|
|
99
|
+
*
|
|
100
|
+
* @param request - Original request
|
|
101
|
+
* @param locale - Target locale
|
|
102
|
+
* @param config - Routing configuration
|
|
103
|
+
* @returns Redirect response
|
|
104
|
+
*/
|
|
105
|
+
declare function createLocaleRedirect(request: Request, locale: Locale, config: I18nRoutingConfig): Response;
|
|
106
|
+
|
|
107
|
+
export { type I18nMiddleware, type I18nMiddlewareOptions, I18nRoutingConfig, Locale, createI18nMiddleware, createLocaleRedirect };
|
|
@@ -0,0 +1,127 @@
|
|
|
1
|
+
import {
|
|
2
|
+
getLocaleFromRequest
|
|
3
|
+
} from "./chunk-F5CV7JNA.js";
|
|
4
|
+
import {
|
|
5
|
+
buildLocalizedUrl,
|
|
6
|
+
generateLocaleCookie,
|
|
7
|
+
matchLocaleFromCookie,
|
|
8
|
+
matchLocaleFromDomain,
|
|
9
|
+
matchLocaleFromPath,
|
|
10
|
+
shouldIgnorePath
|
|
11
|
+
} from "./chunk-O3FQ7FPU.js";
|
|
12
|
+
|
|
13
|
+
// src/middleware.ts
|
|
14
|
+
function createI18nMiddleware(options) {
|
|
15
|
+
const {
|
|
16
|
+
locales,
|
|
17
|
+
defaultLocale,
|
|
18
|
+
routing = "none",
|
|
19
|
+
prefixDefault = false,
|
|
20
|
+
domains,
|
|
21
|
+
cookieName = "FLIGHT_LOCALE",
|
|
22
|
+
cookieMaxAge = 365 * 24 * 60 * 60,
|
|
23
|
+
ignorePaths,
|
|
24
|
+
redirectOnMissingLocale = true,
|
|
25
|
+
detectLocale,
|
|
26
|
+
onLocaleResolved
|
|
27
|
+
} = options;
|
|
28
|
+
const config = {
|
|
29
|
+
locales,
|
|
30
|
+
defaultLocale,
|
|
31
|
+
routing,
|
|
32
|
+
prefixDefault,
|
|
33
|
+
domains,
|
|
34
|
+
cookieName,
|
|
35
|
+
cookieMaxAge,
|
|
36
|
+
ignorePaths
|
|
37
|
+
};
|
|
38
|
+
return async function i18nMiddleware(request) {
|
|
39
|
+
const url = new URL(request.url);
|
|
40
|
+
const { pathname, hostname } = url;
|
|
41
|
+
if (shouldIgnorePath(pathname, config)) {
|
|
42
|
+
return null;
|
|
43
|
+
}
|
|
44
|
+
let detectedLocale = null;
|
|
45
|
+
let shouldSetCookie = false;
|
|
46
|
+
if (detectLocale) {
|
|
47
|
+
detectedLocale = await detectLocale(request);
|
|
48
|
+
}
|
|
49
|
+
if (!detectedLocale) {
|
|
50
|
+
switch (routing) {
|
|
51
|
+
case "prefix": {
|
|
52
|
+
const match = matchLocaleFromPath(pathname, config);
|
|
53
|
+
if (match.hasLocalePrefix) {
|
|
54
|
+
detectedLocale = match.locale;
|
|
55
|
+
}
|
|
56
|
+
break;
|
|
57
|
+
}
|
|
58
|
+
case "domain": {
|
|
59
|
+
detectedLocale = matchLocaleFromDomain(hostname, config);
|
|
60
|
+
break;
|
|
61
|
+
}
|
|
62
|
+
case "cookie": {
|
|
63
|
+
const cookieHeader = request.headers.get("Cookie");
|
|
64
|
+
detectedLocale = matchLocaleFromCookie(cookieHeader, config);
|
|
65
|
+
break;
|
|
66
|
+
}
|
|
67
|
+
case "none":
|
|
68
|
+
default:
|
|
69
|
+
break;
|
|
70
|
+
}
|
|
71
|
+
}
|
|
72
|
+
if (!detectedLocale && routing !== "cookie") {
|
|
73
|
+
const cookieHeader = request.headers.get("Cookie");
|
|
74
|
+
detectedLocale = matchLocaleFromCookie(cookieHeader, config);
|
|
75
|
+
}
|
|
76
|
+
if (!detectedLocale) {
|
|
77
|
+
detectedLocale = getLocaleFromRequest(request, locales, defaultLocale);
|
|
78
|
+
if (routing === "prefix" || routing === "cookie") {
|
|
79
|
+
shouldSetCookie = true;
|
|
80
|
+
}
|
|
81
|
+
}
|
|
82
|
+
if (routing === "prefix" && redirectOnMissingLocale) {
|
|
83
|
+
const match = matchLocaleFromPath(pathname, config);
|
|
84
|
+
const needsRedirect = (
|
|
85
|
+
// No locale prefix, but we detect a non-default locale
|
|
86
|
+
!match.hasLocalePrefix && detectedLocale !== defaultLocale || // No locale prefix, default locale, but prefixDefault is true
|
|
87
|
+
!match.hasLocalePrefix && detectedLocale === defaultLocale && prefixDefault
|
|
88
|
+
);
|
|
89
|
+
if (needsRedirect) {
|
|
90
|
+
const newPathname = buildLocalizedUrl(pathname, detectedLocale, config);
|
|
91
|
+
const newUrl = new URL(newPathname, request.url);
|
|
92
|
+
newUrl.search = url.search;
|
|
93
|
+
const response = new Response(null, {
|
|
94
|
+
status: 307,
|
|
95
|
+
// Temporary redirect (preserves method)
|
|
96
|
+
headers: { Location: newUrl.toString() }
|
|
97
|
+
});
|
|
98
|
+
response.headers.set("Set-Cookie", generateLocaleCookie(detectedLocale, config));
|
|
99
|
+
return response;
|
|
100
|
+
}
|
|
101
|
+
}
|
|
102
|
+
if (onLocaleResolved) {
|
|
103
|
+
await onLocaleResolved(detectedLocale, request);
|
|
104
|
+
}
|
|
105
|
+
if (routing === "cookie" && shouldSetCookie) {
|
|
106
|
+
}
|
|
107
|
+
return null;
|
|
108
|
+
};
|
|
109
|
+
}
|
|
110
|
+
function createLocaleRedirect(request, locale, config) {
|
|
111
|
+
const url = new URL(request.url);
|
|
112
|
+
const match = matchLocaleFromPath(url.pathname, config);
|
|
113
|
+
const cleanPathname = match.pathname;
|
|
114
|
+
const newPathname = buildLocalizedUrl(cleanPathname, locale, config);
|
|
115
|
+
const newUrl = new URL(newPathname, request.url);
|
|
116
|
+
newUrl.search = url.search;
|
|
117
|
+
const response = new Response(null, {
|
|
118
|
+
status: 307,
|
|
119
|
+
headers: { Location: newUrl.toString() }
|
|
120
|
+
});
|
|
121
|
+
response.headers.set("Set-Cookie", generateLocaleCookie(locale, config));
|
|
122
|
+
return response;
|
|
123
|
+
}
|
|
124
|
+
export {
|
|
125
|
+
createI18nMiddleware,
|
|
126
|
+
createLocaleRedirect
|
|
127
|
+
};
|
|
@@ -0,0 +1,179 @@
|
|
|
1
|
+
import { Locale } from './index.js';
|
|
2
|
+
|
|
3
|
+
/**
|
|
4
|
+
* First-Class i18n Routing for Flight Framework
|
|
5
|
+
*
|
|
6
|
+
* Provides configurable routing strategies for internationalized applications:
|
|
7
|
+
* - 'prefix': URL path prefix (/en/about, /es/about)
|
|
8
|
+
* - 'domain': Subdomain-based (en.example.com, es.example.com)
|
|
9
|
+
* - 'cookie': Cookie-based locale persistence (no URL change)
|
|
10
|
+
* - 'none': No automatic routing (manual control)
|
|
11
|
+
*
|
|
12
|
+
* @example
|
|
13
|
+
* ```typescript
|
|
14
|
+
* // flight.config.ts
|
|
15
|
+
* export default defineConfig({
|
|
16
|
+
* i18n: {
|
|
17
|
+
* locales: ['en', 'es', 'fr'],
|
|
18
|
+
* defaultLocale: 'en',
|
|
19
|
+
* routing: 'prefix', // 'prefix' | 'domain' | 'cookie' | 'none'
|
|
20
|
+
* prefixDefault: false, // /about for 'en', /es/about for 'es'
|
|
21
|
+
* },
|
|
22
|
+
* });
|
|
23
|
+
* ```
|
|
24
|
+
*
|
|
25
|
+
* @module @flightdev/i18n/routing
|
|
26
|
+
*/
|
|
27
|
+
|
|
28
|
+
/**
|
|
29
|
+
* Available routing strategies for i18n
|
|
30
|
+
*
|
|
31
|
+
* - `prefix`: Locale is part of URL path (e.g., /en/about, /es/about)
|
|
32
|
+
* - `domain`: Locale is determined by domain/subdomain (e.g., en.example.com)
|
|
33
|
+
* - `cookie`: Locale is stored in a cookie, URL remains unchanged
|
|
34
|
+
* - `none`: No automatic routing, full manual control (default)
|
|
35
|
+
*/
|
|
36
|
+
type I18nRoutingStrategy = 'prefix' | 'domain' | 'cookie' | 'none';
|
|
37
|
+
/**
|
|
38
|
+
* Configuration for i18n routing
|
|
39
|
+
*/
|
|
40
|
+
interface I18nRoutingConfig {
|
|
41
|
+
/** Supported locales (e.g., ['en', 'es', 'fr']) */
|
|
42
|
+
locales: Locale[];
|
|
43
|
+
/** Default locale when none is detected */
|
|
44
|
+
defaultLocale: Locale;
|
|
45
|
+
/** Routing strategy (default: 'none') */
|
|
46
|
+
routing?: I18nRoutingStrategy;
|
|
47
|
+
/** Whether to include prefix for default locale in 'prefix' strategy */
|
|
48
|
+
prefixDefault?: boolean;
|
|
49
|
+
/**
|
|
50
|
+
* Domain mapping for 'domain' strategy
|
|
51
|
+
* @example { en: 'en.example.com', es: 'es.example.com' }
|
|
52
|
+
*/
|
|
53
|
+
domains?: Record<Locale, string>;
|
|
54
|
+
/** Cookie name for 'cookie' strategy (default: 'FLIGHT_LOCALE') */
|
|
55
|
+
cookieName?: string;
|
|
56
|
+
/** Cookie max age in seconds (default: 31536000 = 1 year) */
|
|
57
|
+
cookieMaxAge?: number;
|
|
58
|
+
/**
|
|
59
|
+
* Paths to ignore (static assets, API routes, etc.)
|
|
60
|
+
* Supports strings (prefix match) and RegExp
|
|
61
|
+
*/
|
|
62
|
+
ignorePaths?: (string | RegExp)[];
|
|
63
|
+
}
|
|
64
|
+
/**
|
|
65
|
+
* Result of matching a locale from a URL path
|
|
66
|
+
*/
|
|
67
|
+
interface I18nRouteMatch {
|
|
68
|
+
/** Detected locale */
|
|
69
|
+
locale: Locale;
|
|
70
|
+
/** Pathname without locale prefix */
|
|
71
|
+
pathname: string;
|
|
72
|
+
/** Whether this is the default locale */
|
|
73
|
+
isDefault: boolean;
|
|
74
|
+
/** Whether the URL had an explicit locale prefix */
|
|
75
|
+
hasLocalePrefix: boolean;
|
|
76
|
+
}
|
|
77
|
+
declare const DEFAULT_CONFIG: Required<Omit<I18nRoutingConfig, 'locales' | 'defaultLocale' | 'domains'>>;
|
|
78
|
+
/**
|
|
79
|
+
* Match locale from URL pathname (prefix strategy)
|
|
80
|
+
*
|
|
81
|
+
* @param pathname - URL pathname (e.g., '/es/about')
|
|
82
|
+
* @param config - Routing configuration
|
|
83
|
+
* @returns Route match result
|
|
84
|
+
*
|
|
85
|
+
* @example
|
|
86
|
+
* ```typescript
|
|
87
|
+
* const match = matchLocaleFromPath('/es/about', config);
|
|
88
|
+
* // { locale: 'es', pathname: '/about', isDefault: false, hasLocalePrefix: true }
|
|
89
|
+
* ```
|
|
90
|
+
*/
|
|
91
|
+
declare function matchLocaleFromPath(pathname: string, config: I18nRoutingConfig): I18nRouteMatch;
|
|
92
|
+
/**
|
|
93
|
+
* Match locale from hostname (domain strategy)
|
|
94
|
+
*
|
|
95
|
+
* @param hostname - Request hostname (e.g., 'es.example.com')
|
|
96
|
+
* @param config - Routing configuration
|
|
97
|
+
* @returns Matched locale
|
|
98
|
+
*
|
|
99
|
+
* @example
|
|
100
|
+
* ```typescript
|
|
101
|
+
* const locale = matchLocaleFromDomain('es.example.com', config);
|
|
102
|
+
* // 'es'
|
|
103
|
+
* ```
|
|
104
|
+
*/
|
|
105
|
+
declare function matchLocaleFromDomain(hostname: string, config: I18nRoutingConfig): Locale;
|
|
106
|
+
/**
|
|
107
|
+
* Match locale from cookie header
|
|
108
|
+
*
|
|
109
|
+
* @param cookieHeader - Cookie header value
|
|
110
|
+
* @param config - Routing configuration
|
|
111
|
+
* @returns Matched locale or null if not found
|
|
112
|
+
*
|
|
113
|
+
* @example
|
|
114
|
+
* ```typescript
|
|
115
|
+
* const locale = matchLocaleFromCookie('FLIGHT_LOCALE=es; other=value', config);
|
|
116
|
+
* // 'es'
|
|
117
|
+
* ```
|
|
118
|
+
*/
|
|
119
|
+
declare function matchLocaleFromCookie(cookieHeader: string | null, config: I18nRoutingConfig): Locale | null;
|
|
120
|
+
/**
|
|
121
|
+
* Build a localized URL based on routing strategy
|
|
122
|
+
*
|
|
123
|
+
* @param pathname - Original pathname
|
|
124
|
+
* @param locale - Target locale
|
|
125
|
+
* @param config - Routing configuration
|
|
126
|
+
* @returns Localized URL
|
|
127
|
+
*
|
|
128
|
+
* @example
|
|
129
|
+
* ```typescript
|
|
130
|
+
* // Prefix strategy
|
|
131
|
+
* buildLocalizedUrl('/about', 'es', config);
|
|
132
|
+
* // '/es/about'
|
|
133
|
+
*
|
|
134
|
+
* // Domain strategy
|
|
135
|
+
* buildLocalizedUrl('/about', 'es', configWithDomains);
|
|
136
|
+
* // 'https://es.example.com/about'
|
|
137
|
+
* ```
|
|
138
|
+
*/
|
|
139
|
+
declare function buildLocalizedUrl(pathname: string, locale: Locale, config: I18nRoutingConfig): string;
|
|
140
|
+
/**
|
|
141
|
+
* Remove locale prefix from pathname
|
|
142
|
+
*
|
|
143
|
+
* @param pathname - Pathname possibly containing locale prefix
|
|
144
|
+
* @param config - Routing configuration
|
|
145
|
+
* @returns Pathname without locale prefix
|
|
146
|
+
*/
|
|
147
|
+
declare function removeLocalePrefix(pathname: string, config: I18nRoutingConfig): string;
|
|
148
|
+
/**
|
|
149
|
+
* Get all alternate URLs for a given page (for SEO hreflang)
|
|
150
|
+
*
|
|
151
|
+
* @param pathname - Original pathname (without locale prefix)
|
|
152
|
+
* @param config - Routing configuration
|
|
153
|
+
* @returns Map of locale to URL
|
|
154
|
+
*
|
|
155
|
+
* @example
|
|
156
|
+
* ```typescript
|
|
157
|
+
* const alternates = getAlternateUrls('/about', config);
|
|
158
|
+
* // { en: '/about', es: '/es/about', fr: '/fr/about' }
|
|
159
|
+
* ```
|
|
160
|
+
*/
|
|
161
|
+
declare function getAlternateUrls(pathname: string, config: I18nRoutingConfig): Record<Locale, string>;
|
|
162
|
+
/**
|
|
163
|
+
* Check if a path should be ignored by i18n routing
|
|
164
|
+
*
|
|
165
|
+
* @param pathname - URL pathname
|
|
166
|
+
* @param config - Routing configuration
|
|
167
|
+
* @returns Whether the path should be ignored
|
|
168
|
+
*/
|
|
169
|
+
declare function shouldIgnorePath(pathname: string, config: I18nRoutingConfig): boolean;
|
|
170
|
+
/**
|
|
171
|
+
* Generate Set-Cookie header value for locale
|
|
172
|
+
*
|
|
173
|
+
* @param locale - Locale to set
|
|
174
|
+
* @param config - Routing configuration
|
|
175
|
+
* @returns Cookie header value
|
|
176
|
+
*/
|
|
177
|
+
declare function generateLocaleCookie(locale: Locale, config: I18nRoutingConfig): string;
|
|
178
|
+
|
|
179
|
+
export { type I18nRouteMatch, type I18nRoutingConfig, type I18nRoutingStrategy, Locale, buildLocalizedUrl, DEFAULT_CONFIG as defaultRoutingConfig, generateLocaleCookie, getAlternateUrls, matchLocaleFromCookie, matchLocaleFromDomain, matchLocaleFromPath, removeLocalePrefix, shouldIgnorePath };
|
package/dist/routing.js
ADDED
|
@@ -0,0 +1,22 @@
|
|
|
1
|
+
import {
|
|
2
|
+
DEFAULT_CONFIG,
|
|
3
|
+
buildLocalizedUrl,
|
|
4
|
+
generateLocaleCookie,
|
|
5
|
+
getAlternateUrls,
|
|
6
|
+
matchLocaleFromCookie,
|
|
7
|
+
matchLocaleFromDomain,
|
|
8
|
+
matchLocaleFromPath,
|
|
9
|
+
removeLocalePrefix,
|
|
10
|
+
shouldIgnorePath
|
|
11
|
+
} from "./chunk-O3FQ7FPU.js";
|
|
12
|
+
export {
|
|
13
|
+
buildLocalizedUrl,
|
|
14
|
+
DEFAULT_CONFIG as defaultRoutingConfig,
|
|
15
|
+
generateLocaleCookie,
|
|
16
|
+
getAlternateUrls,
|
|
17
|
+
matchLocaleFromCookie,
|
|
18
|
+
matchLocaleFromDomain,
|
|
19
|
+
matchLocaleFromPath,
|
|
20
|
+
removeLocalePrefix,
|
|
21
|
+
shouldIgnorePath
|
|
22
|
+
};
|