astro-intl 2.0.3 → 2.2.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/README.md +701 -701
- package/dist/__tests__/config-messages.test.js +42 -2
- package/dist/__tests__/core.test.js +90 -2
- package/dist/__tests__/fallback-routes.test.d.ts +1 -0
- package/dist/__tests__/fallback-routes.test.js +42 -0
- package/dist/__tests__/integration.test.js +14 -3
- package/dist/components/AutoRedirect.astro +31 -0
- package/dist/core.d.ts +1 -1
- package/dist/core.js +1 -1
- package/dist/index.d.ts +5 -2
- package/dist/index.js +43 -2
- package/dist/middleware.js +3 -1
- package/dist/store.d.ts +3 -1
- package/dist/store.js +91 -50
- package/dist/translations.d.ts +1 -0
- package/dist/translations.js +4 -1
- package/dist/types/index.d.ts +10 -0
- package/package.json +102 -99
|
@@ -75,9 +75,10 @@ describe("Config-based messages (simple approach)", () => {
|
|
|
75
75
|
await setRequestLocale(new URL("https://example.com/fr/page"));
|
|
76
76
|
expect(getTranslations()("greeting")).toBe("Bonjour");
|
|
77
77
|
});
|
|
78
|
-
it("should
|
|
78
|
+
it("should return false when no config or messages are provided", async () => {
|
|
79
79
|
const url = new URL("https://example.com/en/page");
|
|
80
|
-
await
|
|
80
|
+
const result = await setRequestLocale(url);
|
|
81
|
+
expect(result).toBe(false);
|
|
81
82
|
});
|
|
82
83
|
});
|
|
83
84
|
describe("defineRequestConfig (next-intl style)", () => {
|
|
@@ -157,3 +158,42 @@ describe("defineRequestConfig (next-intl style)", () => {
|
|
|
157
158
|
expect(getTranslations()("greeting")).toBe("From config messages");
|
|
158
159
|
});
|
|
159
160
|
});
|
|
161
|
+
describe("createMessagesConfigFromDir helper", () => {
|
|
162
|
+
beforeEach(() => {
|
|
163
|
+
__resetRequestConfig();
|
|
164
|
+
});
|
|
165
|
+
it("should create a MessagesConfig from directory path and locales", () => {
|
|
166
|
+
// Import the helper function (we'll need to export it for testing)
|
|
167
|
+
// For now, we test the integration behavior via __setConfigMessages
|
|
168
|
+
// Simulate what createMessagesConfigFromDir does
|
|
169
|
+
const dir = "./src/i18n/messages";
|
|
170
|
+
const locales = ["en", "es", "fr"];
|
|
171
|
+
const config = {};
|
|
172
|
+
for (const locale of locales) {
|
|
173
|
+
config[locale] = () => import(/* @vite-ignore */ `${dir}/${locale}.json`, { with: { type: "json" } });
|
|
174
|
+
}
|
|
175
|
+
// Verify the config structure
|
|
176
|
+
expect(Object.keys(config)).toEqual(["en", "es", "fr"]);
|
|
177
|
+
expect(typeof config.en).toBe("function");
|
|
178
|
+
expect(typeof config.es).toBe("function");
|
|
179
|
+
expect(typeof config.fr).toBe("function");
|
|
180
|
+
});
|
|
181
|
+
it("should work with messagesDir integration option", async () => {
|
|
182
|
+
// This test verifies that when messagesDir is provided along with locales,
|
|
183
|
+
// the integration creates the proper config internally
|
|
184
|
+
// Since we can't easily test the integration hooks directly in unit tests,
|
|
185
|
+
// we verify that the internal logic works by simulating what the integration does
|
|
186
|
+
const _dir = "./src/i18n/messages";
|
|
187
|
+
const locales = ["en", "es"];
|
|
188
|
+
// Simulate the integration's createMessagesConfigFromDir behavior
|
|
189
|
+
const dirConfig = {};
|
|
190
|
+
for (const locale of locales) {
|
|
191
|
+
dirConfig[locale] = () => Promise.resolve({ greeting: `Hello in ${locale}` });
|
|
192
|
+
}
|
|
193
|
+
__setConfigMessages(dirConfig);
|
|
194
|
+
const url = new URL("https://example.com/es/page");
|
|
195
|
+
await setRequestLocale(url);
|
|
196
|
+
expect(getLocale()).toBe("es");
|
|
197
|
+
expect(getTranslations()("greeting")).toBe("Hello in es");
|
|
198
|
+
});
|
|
199
|
+
});
|
|
@@ -80,8 +80,12 @@ describe("core.ts", () => {
|
|
|
80
80
|
});
|
|
81
81
|
});
|
|
82
82
|
describe("getLocale", () => {
|
|
83
|
-
it("should
|
|
84
|
-
|
|
83
|
+
it("should return default locale via auto-detection when called before setRequestLocale", () => {
|
|
84
|
+
// With auto-detection, getLocale now tries to detect from window.location
|
|
85
|
+
// In test environment (no window), it should fall back to the default locale
|
|
86
|
+
const locale = getLocale();
|
|
87
|
+
expect(typeof locale).toBe("string");
|
|
88
|
+
expect(locale.length).toBeGreaterThan(0);
|
|
85
89
|
});
|
|
86
90
|
it("should return current locale after setRequestLocale", async () => {
|
|
87
91
|
const url = new URL("https://example.com/de/page");
|
|
@@ -272,5 +276,89 @@ describe("core.ts", () => {
|
|
|
272
276
|
expect(result).toBe('Click <a href="/go">here</a>');
|
|
273
277
|
});
|
|
274
278
|
});
|
|
279
|
+
describe("t.raw()", () => {
|
|
280
|
+
it("should return array values without coercion", async () => {
|
|
281
|
+
const url = new URL("https://example.com/en/home");
|
|
282
|
+
await setRequestLocale(url, () => ({
|
|
283
|
+
locale: "en",
|
|
284
|
+
messages: {
|
|
285
|
+
about: {
|
|
286
|
+
intro: ["Line 1", "Line 2", "Line 3"],
|
|
287
|
+
pillars: ["Pillar A", "Pillar B"],
|
|
288
|
+
},
|
|
289
|
+
},
|
|
290
|
+
}));
|
|
291
|
+
const t = getTranslations("about");
|
|
292
|
+
const intro = t.raw("intro");
|
|
293
|
+
const pillars = t.raw("pillars");
|
|
294
|
+
expect(Array.isArray(intro)).toBe(true);
|
|
295
|
+
expect(intro).toEqual(["Line 1", "Line 2", "Line 3"]);
|
|
296
|
+
expect(Array.isArray(pillars)).toBe(true);
|
|
297
|
+
expect(pillars).toEqual(["Pillar A", "Pillar B"]);
|
|
298
|
+
});
|
|
299
|
+
it("should return object values without coercion", async () => {
|
|
300
|
+
const url = new URL("https://example.com/en/home");
|
|
301
|
+
await setRequestLocale(url, () => ({
|
|
302
|
+
locale: "en",
|
|
303
|
+
messages: {
|
|
304
|
+
metadata: {
|
|
305
|
+
social: {
|
|
306
|
+
twitter: { handle: "@example", url: "https://twitter.com/example" },
|
|
307
|
+
github: { handle: "example", url: "https://github.com/example" },
|
|
308
|
+
},
|
|
309
|
+
},
|
|
310
|
+
},
|
|
311
|
+
}));
|
|
312
|
+
const t = getTranslations("metadata");
|
|
313
|
+
const social = t.raw("social");
|
|
314
|
+
expect(typeof social).toBe("object");
|
|
315
|
+
expect(social.twitter.handle).toBe("@example");
|
|
316
|
+
expect(social.github.url).toBe("https://github.com/example");
|
|
317
|
+
});
|
|
318
|
+
it("should return number values without coercion", async () => {
|
|
319
|
+
const url = new URL("https://example.com/en/home");
|
|
320
|
+
await setRequestLocale(url, () => ({
|
|
321
|
+
locale: "en",
|
|
322
|
+
messages: {
|
|
323
|
+
stats: {
|
|
324
|
+
count: 42,
|
|
325
|
+
percentage: 99.9,
|
|
326
|
+
},
|
|
327
|
+
},
|
|
328
|
+
}));
|
|
329
|
+
const t = getTranslations("stats");
|
|
330
|
+
const count = t.raw("count");
|
|
331
|
+
const percentage = t.raw("percentage");
|
|
332
|
+
expect(typeof count).toBe("number");
|
|
333
|
+
expect(count).toBe(42);
|
|
334
|
+
expect(typeof percentage).toBe("number");
|
|
335
|
+
expect(percentage).toBe(99.9);
|
|
336
|
+
});
|
|
337
|
+
it("should return string values as-is", async () => {
|
|
338
|
+
const url = new URL("https://example.com/en/home");
|
|
339
|
+
await setRequestLocale(url, () => ({
|
|
340
|
+
locale: "en",
|
|
341
|
+
messages: {
|
|
342
|
+
greeting: "Hello World",
|
|
343
|
+
},
|
|
344
|
+
}));
|
|
345
|
+
const t = getTranslations();
|
|
346
|
+
const greeting = t.raw("greeting");
|
|
347
|
+
expect(typeof greeting).toBe("string");
|
|
348
|
+
expect(greeting).toBe("Hello World");
|
|
349
|
+
});
|
|
350
|
+
it("should return undefined for non-existent keys", async () => {
|
|
351
|
+
const url = new URL("https://example.com/en/home");
|
|
352
|
+
await setRequestLocale(url, () => ({
|
|
353
|
+
locale: "en",
|
|
354
|
+
messages: {
|
|
355
|
+
greeting: "Hello",
|
|
356
|
+
},
|
|
357
|
+
}));
|
|
358
|
+
const t = getTranslations();
|
|
359
|
+
const result = t.raw("nonexistent");
|
|
360
|
+
expect(result).toBeUndefined();
|
|
361
|
+
});
|
|
362
|
+
});
|
|
275
363
|
});
|
|
276
364
|
});
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export {};
|
|
@@ -0,0 +1,42 @@
|
|
|
1
|
+
import { describe, it, expect, beforeEach } from "vitest";
|
|
2
|
+
import { __resetRequestConfig } from "../core.js";
|
|
3
|
+
import { setFallbackRoutes, getFallbackRoutes } from "../store.js";
|
|
4
|
+
describe("Fallback Routes (Astro 6.1+)", () => {
|
|
5
|
+
beforeEach(() => {
|
|
6
|
+
__resetRequestConfig();
|
|
7
|
+
});
|
|
8
|
+
it("returns empty array by default", () => {
|
|
9
|
+
expect(getFallbackRoutes()).toEqual([]);
|
|
10
|
+
});
|
|
11
|
+
it("stores and retrieves fallback routes", () => {
|
|
12
|
+
const routes = [
|
|
13
|
+
{ pattern: "/fr/about", pathname: "/fr/about/", locale: "fr" },
|
|
14
|
+
{ pattern: "/de/about", pathname: "/de/about/", locale: "de" },
|
|
15
|
+
];
|
|
16
|
+
setFallbackRoutes(routes);
|
|
17
|
+
expect(getFallbackRoutes()).toEqual(routes);
|
|
18
|
+
});
|
|
19
|
+
it("overwrites previous fallback routes on re-set", () => {
|
|
20
|
+
setFallbackRoutes([{ pattern: "/fr/about", pathname: "/fr/about/", locale: "fr" }]);
|
|
21
|
+
const newRoutes = [
|
|
22
|
+
{ pattern: "/de/contact", pathname: "/de/contact/", locale: "de" },
|
|
23
|
+
];
|
|
24
|
+
setFallbackRoutes(newRoutes);
|
|
25
|
+
expect(getFallbackRoutes()).toEqual(newRoutes);
|
|
26
|
+
});
|
|
27
|
+
it("clears fallback routes on reset", () => {
|
|
28
|
+
setFallbackRoutes([{ pattern: "/fr/about", pathname: "/fr/about/", locale: "fr" }]);
|
|
29
|
+
__resetRequestConfig();
|
|
30
|
+
expect(getFallbackRoutes()).toEqual([]);
|
|
31
|
+
});
|
|
32
|
+
it("handles routes without pathname (pattern-only)", () => {
|
|
33
|
+
const routes = [{ pattern: "/fr/blog/[...slug]", locale: "fr" }];
|
|
34
|
+
setFallbackRoutes(routes);
|
|
35
|
+
expect(getFallbackRoutes()).toEqual(routes);
|
|
36
|
+
expect(getFallbackRoutes()[0].pathname).toBeUndefined();
|
|
37
|
+
});
|
|
38
|
+
it("handles empty array without errors", () => {
|
|
39
|
+
setFallbackRoutes([]);
|
|
40
|
+
expect(getFallbackRoutes()).toEqual([]);
|
|
41
|
+
});
|
|
42
|
+
});
|
|
@@ -125,9 +125,20 @@ describe("Integration Tests", () => {
|
|
|
125
125
|
});
|
|
126
126
|
});
|
|
127
127
|
describe("Error handling", () => {
|
|
128
|
-
it("should throw descriptive error when accessing translations before setup", () => {
|
|
129
|
-
|
|
130
|
-
|
|
128
|
+
it("should auto-detect locale or throw descriptive error when accessing translations before setup", () => {
|
|
129
|
+
// With auto-detection, getLocale now tries to detect from window.location
|
|
130
|
+
// In test environment (no window), it may return default or throw
|
|
131
|
+
try {
|
|
132
|
+
const locale = getLocale();
|
|
133
|
+
// If auto-detection works, we should get a valid locale string
|
|
134
|
+
expect(typeof locale).toBe("string");
|
|
135
|
+
}
|
|
136
|
+
catch (error) {
|
|
137
|
+
// If it throws, it should have the expected error message
|
|
138
|
+
expect(error.message).toContain("No request config found");
|
|
139
|
+
}
|
|
140
|
+
// getTranslations should still throw since it needs messages
|
|
141
|
+
expect(() => getTranslations()).toThrow(/No request config found/);
|
|
131
142
|
});
|
|
132
143
|
it("should handle missing translations gracefully", async () => {
|
|
133
144
|
const url = new URL("https://example.com/en/page");
|
|
@@ -0,0 +1,31 @@
|
|
|
1
|
+
---
|
|
2
|
+
/**
|
|
3
|
+
* AutoRedirect Component
|
|
4
|
+
*
|
|
5
|
+
* Detects the user's browser language and redirects to the appropriate
|
|
6
|
+
* localized route. Use this in your root page (e.g., src/pages/index.astro)
|
|
7
|
+
* to avoid the blank page issue with Astro.redirect().
|
|
8
|
+
*
|
|
9
|
+
* @example
|
|
10
|
+
* ---
|
|
11
|
+
* import { AutoRedirect } from 'astro-intl/components';
|
|
12
|
+
* ---
|
|
13
|
+
* <AutoRedirect locales={['en', 'es']} defaultLocale="en" />
|
|
14
|
+
*/
|
|
15
|
+
export interface Props {
|
|
16
|
+
/** Array of supported locale codes */
|
|
17
|
+
locales: string[];
|
|
18
|
+
/** Default locale to fallback to if browser language is not supported */
|
|
19
|
+
defaultLocale: string;
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
const { locales, defaultLocale } = Astro.props;
|
|
23
|
+
---
|
|
24
|
+
|
|
25
|
+
<script define:vars={{ locales, defaultLocale }}>
|
|
26
|
+
(function() {
|
|
27
|
+
const lang = (navigator.language || navigator.languages?.[0] || "").slice(0, 2).toLowerCase();
|
|
28
|
+
const locale = locales.includes(lang) ? lang : defaultLocale;
|
|
29
|
+
window.location.replace("/" + locale + "/");
|
|
30
|
+
})();
|
|
31
|
+
</script>
|
package/dist/core.d.ts
CHANGED
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
export { setRequestLocale, runWithLocale, getLocale, getLocales, isValidLocale, getMessages, getDefaultLocale, defineRequestConfig, __setConfigMessages, __resetRequestConfig, __setIntlConfig, } from "./store.js";
|
|
1
|
+
export { setRequestLocale, runWithLocale, getLocale, getLocales, isValidLocale, getMessages, getDefaultLocale, defineRequestConfig, getFallbackRoutes, setFallbackRoutes, __setConfigMessages, __resetRequestConfig, __setIntlConfig, } from "./store.js";
|
|
2
2
|
export { getTranslations } from "./translations.js";
|
|
3
3
|
export { getNestedValue, type DotPaths } from "./interpolation.js";
|
|
4
4
|
export { sanitizeLocale, sanitizeHtml, escapeRegExp } from "./sanitize.js";
|
package/dist/core.js
CHANGED
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
// ─── Store (request lifecycle) ──────────────────────────────────────
|
|
2
|
-
export { setRequestLocale, runWithLocale, getLocale, getLocales, isValidLocale, getMessages, getDefaultLocale, defineRequestConfig, __setConfigMessages, __resetRequestConfig, __setIntlConfig, } from "./store.js";
|
|
2
|
+
export { setRequestLocale, runWithLocale, getLocale, getLocales, isValidLocale, getMessages, getDefaultLocale, defineRequestConfig, getFallbackRoutes, setFallbackRoutes, __setConfigMessages, __resetRequestConfig, __setIntlConfig, } from "./store.js";
|
|
3
3
|
// ─── Translations ───────────────────────────────────────────────────
|
|
4
4
|
export { getTranslations } from "./translations.js";
|
|
5
5
|
// ─── Interpolation utilities ────────────────────────────────────────
|
package/dist/index.d.ts
CHANGED
|
@@ -1,11 +1,13 @@
|
|
|
1
1
|
import type { AstroIntegration } from "astro";
|
|
2
2
|
import type { MessagesConfig, RoutesMap } from "./types/index.js";
|
|
3
|
-
import { setRequestLocale as _setRequestLocale, runWithLocale as _runWithLocale, getLocale as _getLocale, getLocales as _getLocales, isValidLocale as _isValidLocale, getMessages as _getMessages, getTranslations as _getTranslations, defineRequestConfig as _defineRequestConfig,
|
|
3
|
+
import { setRequestLocale as _setRequestLocale, runWithLocale as _runWithLocale, getLocale as _getLocale, getLocales as _getLocales, isValidLocale as _isValidLocale, getMessages as _getMessages, getTranslations as _getTranslations, defineRequestConfig as _defineRequestConfig, getFallbackRoutes as _getFallbackRoutes, __resetRequestConfig as _resetRequestConfig } from "./core.js";
|
|
4
|
+
import { path as _path, switchLocalePath as _switchLocalePath } from "./core.js";
|
|
4
5
|
export type AstroIntlOptions = {
|
|
5
6
|
enabled?: boolean;
|
|
6
7
|
defaultLocale?: string;
|
|
7
8
|
locales?: string[];
|
|
8
9
|
messages?: MessagesConfig;
|
|
10
|
+
messagesDir?: string;
|
|
9
11
|
routes?: RoutesMap;
|
|
10
12
|
};
|
|
11
13
|
export default function astroIntl(options?: AstroIntlOptions): AstroIntegration;
|
|
@@ -18,7 +20,8 @@ export declare const getMessages: typeof _getMessages;
|
|
|
18
20
|
export declare const getTranslations: typeof _getTranslations;
|
|
19
21
|
export declare const defineRequestConfig: typeof _defineRequestConfig;
|
|
20
22
|
export declare const __resetRequestConfig: typeof _resetRequestConfig;
|
|
23
|
+
export declare const getFallbackRoutes: typeof _getFallbackRoutes;
|
|
21
24
|
export declare const path: typeof _path;
|
|
22
25
|
export declare const switchLocalePath: typeof _switchLocalePath;
|
|
23
|
-
export type { RequestConfig, Primitive, GetRequestConfigFn, MessagesConfig, IntlConfig, RoutesMap, ExtractParams, ParamsForRoute, } from "./types/index.js";
|
|
26
|
+
export type { RequestConfig, Primitive, GetRequestConfigFn, MessagesConfig, MessagesDirConfig, IntlConfig, RoutesMap, ExtractParams, ParamsForRoute, FallbackRouteInfo, } from "./types/index.js";
|
|
24
27
|
export type { DotPaths } from "./core.js";
|
package/dist/index.js
CHANGED
|
@@ -1,6 +1,15 @@
|
|
|
1
|
-
import { setRequestLocale as _setRequestLocale, runWithLocale as _runWithLocale, getLocale as _getLocale, getLocales as _getLocales, isValidLocale as _isValidLocale, getMessages as _getMessages, getTranslations as _getTranslations, defineRequestConfig as _defineRequestConfig,
|
|
1
|
+
import { setRequestLocale as _setRequestLocale, runWithLocale as _runWithLocale, getLocale as _getLocale, getLocales as _getLocales, isValidLocale as _isValidLocale, getMessages as _getMessages, getTranslations as _getTranslations, defineRequestConfig as _defineRequestConfig, getFallbackRoutes as _getFallbackRoutes, setFallbackRoutes as _setFallbackRoutes, __resetRequestConfig as _resetRequestConfig, __setConfigMessages, __setIntlConfig, } from "./core.js";
|
|
2
|
+
import { path as _path, switchLocalePath as _switchLocalePath } from "./core.js";
|
|
3
|
+
// Helper to create MessagesConfig from a directory path
|
|
4
|
+
function createMessagesConfigFromDir(dir, locales) {
|
|
5
|
+
const config = {};
|
|
6
|
+
for (const locale of locales) {
|
|
7
|
+
config[locale] = () => import(/* @vite-ignore */ `${dir}/${locale}.json`, { with: { type: "json" } });
|
|
8
|
+
}
|
|
9
|
+
return config;
|
|
10
|
+
}
|
|
2
11
|
export default function astroIntl(options = {}) {
|
|
3
|
-
const { enabled = true, defaultLocale, locales, messages, routes } = options;
|
|
12
|
+
const { enabled = true, defaultLocale, locales, messages, messagesDir, routes } = options;
|
|
4
13
|
if (defaultLocale || locales || routes) {
|
|
5
14
|
__setIntlConfig({
|
|
6
15
|
...(defaultLocale && { defaultLocale }),
|
|
@@ -11,6 +20,11 @@ export default function astroIntl(options = {}) {
|
|
|
11
20
|
if (messages) {
|
|
12
21
|
__setConfigMessages(messages);
|
|
13
22
|
}
|
|
23
|
+
else if (messagesDir && locales) {
|
|
24
|
+
// Auto-create messages config from directory
|
|
25
|
+
const dirConfig = createMessagesConfigFromDir(messagesDir, locales);
|
|
26
|
+
__setConfigMessages(dirConfig);
|
|
27
|
+
}
|
|
14
28
|
return {
|
|
15
29
|
name: "astro-intl",
|
|
16
30
|
hooks: {
|
|
@@ -27,6 +41,32 @@ export default function astroIntl(options = {}) {
|
|
|
27
41
|
},
|
|
28
42
|
});
|
|
29
43
|
},
|
|
44
|
+
"astro:routes:resolved": ({ routes }) => {
|
|
45
|
+
if (!enabled)
|
|
46
|
+
return;
|
|
47
|
+
const collected = [];
|
|
48
|
+
for (const route of routes) {
|
|
49
|
+
// fallbackRoutes is available in Astro 6.1+
|
|
50
|
+
const fallbacks = route.fallbackRoutes;
|
|
51
|
+
if (!fallbacks)
|
|
52
|
+
continue;
|
|
53
|
+
for (const fb of fallbacks) {
|
|
54
|
+
// Extract locale from the pattern (first segment after /)
|
|
55
|
+
const segments = (fb.pathname ?? fb.pattern).split("/");
|
|
56
|
+
const locale = segments[1] || "";
|
|
57
|
+
if (locale) {
|
|
58
|
+
collected.push({
|
|
59
|
+
pattern: fb.pattern,
|
|
60
|
+
pathname: fb.pathname,
|
|
61
|
+
locale,
|
|
62
|
+
});
|
|
63
|
+
}
|
|
64
|
+
}
|
|
65
|
+
}
|
|
66
|
+
if (collected.length > 0) {
|
|
67
|
+
_setFallbackRoutes(collected);
|
|
68
|
+
}
|
|
69
|
+
},
|
|
30
70
|
},
|
|
31
71
|
};
|
|
32
72
|
}
|
|
@@ -40,5 +80,6 @@ export const getMessages = _getMessages;
|
|
|
40
80
|
export const getTranslations = _getTranslations;
|
|
41
81
|
export const defineRequestConfig = _defineRequestConfig;
|
|
42
82
|
export const __resetRequestConfig = _resetRequestConfig;
|
|
83
|
+
export const getFallbackRoutes = _getFallbackRoutes;
|
|
43
84
|
export const path = _path;
|
|
44
85
|
export const switchLocalePath = _switchLocalePath;
|
package/dist/middleware.js
CHANGED
|
@@ -44,7 +44,9 @@ export function createIntlMiddleware(options) {
|
|
|
44
44
|
if (!lang || !locales.includes(lang)) {
|
|
45
45
|
return next();
|
|
46
46
|
}
|
|
47
|
-
await setRequestLocale(context.url);
|
|
47
|
+
const ok = await setRequestLocale(context.url);
|
|
48
|
+
if (!ok)
|
|
49
|
+
return next();
|
|
48
50
|
// Rewrite translated routes to their canonical filesystem paths
|
|
49
51
|
if (routes) {
|
|
50
52
|
const rewrittenPath = resolveTranslatedRoute(context.url.pathname, lang, routes, resolvedDefaultLocale);
|
package/dist/store.d.ts
CHANGED
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import type { GetRequestConfigFn, MessagesConfig, IntlConfig, RequestConfig, RoutesMap } from "./types/index.js";
|
|
1
|
+
import type { GetRequestConfigFn, MessagesConfig, IntlConfig, RequestConfig, RoutesMap, FallbackRouteInfo } from "./types/index.js";
|
|
2
2
|
export declare function __setIntlConfig(config: Partial<IntlConfig>): void;
|
|
3
3
|
export declare function getDefaultLocale(): string;
|
|
4
4
|
export declare function getRoutes(): RoutesMap | undefined;
|
|
@@ -7,6 +7,8 @@ export declare function isValidLocale(locale: string): boolean;
|
|
|
7
7
|
export declare function defineRequestConfig(fn: (locale: string) => Promise<RequestConfig> | RequestConfig): GetRequestConfigFn;
|
|
8
8
|
export declare function __setConfigMessages(messages: MessagesConfig): void;
|
|
9
9
|
export declare function __resetRequestConfig(): void;
|
|
10
|
+
export declare function setFallbackRoutes(routes: FallbackRouteInfo[]): void;
|
|
11
|
+
export declare function getFallbackRoutes(): FallbackRouteInfo[];
|
|
10
12
|
export declare function setRequestLocale(url: URL, getConfig?: GetRequestConfigFn): Promise<boolean>;
|
|
11
13
|
export declare function runWithLocale<R>(url: URL, fn: () => R | Promise<R>, getConfig?: GetRequestConfigFn): Promise<R>;
|
|
12
14
|
export declare function getLocale(): string;
|
package/dist/store.js
CHANGED
|
@@ -1,73 +1,91 @@
|
|
|
1
1
|
import { sanitizeLocale } from "./sanitize.js";
|
|
2
|
-
|
|
3
|
-
|
|
4
|
-
|
|
5
|
-
|
|
6
|
-
|
|
7
|
-
|
|
2
|
+
const GLOBAL_KEY = Symbol.for("__astro_intl_store__");
|
|
3
|
+
const g = globalThis;
|
|
4
|
+
function getGlobalState() {
|
|
5
|
+
const existing = g[GLOBAL_KEY];
|
|
6
|
+
if (existing)
|
|
7
|
+
return existing;
|
|
8
|
+
const fresh = {
|
|
9
|
+
registeredGetRequestConfig: null,
|
|
10
|
+
configMessages: null,
|
|
11
|
+
intlConfig: { defaultLocale: "en", locales: [] },
|
|
12
|
+
als: null,
|
|
13
|
+
alsInitialized: false,
|
|
14
|
+
fallbackState: null,
|
|
15
|
+
};
|
|
16
|
+
g[GLOBAL_KEY] = fresh;
|
|
17
|
+
return fresh;
|
|
18
|
+
}
|
|
19
|
+
const $ = getGlobalState();
|
|
20
|
+
// ─── AsyncLocalStorage detection ────────────────────────────────────
|
|
8
21
|
function ensureAls() {
|
|
9
|
-
if (alsInitialized)
|
|
22
|
+
if ($.alsInitialized)
|
|
10
23
|
return;
|
|
11
|
-
alsInitialized = true;
|
|
24
|
+
$.alsInitialized = true;
|
|
12
25
|
try {
|
|
13
26
|
const g = globalThis;
|
|
14
27
|
if (typeof g.AsyncLocalStorage === "function") {
|
|
15
|
-
als = new g.AsyncLocalStorage();
|
|
28
|
+
$.als = new g.AsyncLocalStorage();
|
|
16
29
|
}
|
|
17
30
|
}
|
|
18
31
|
catch {
|
|
19
32
|
// Not available — fallback mode
|
|
20
33
|
}
|
|
21
34
|
}
|
|
22
|
-
// ─── Fallback global variable (for runtimes without ALS) ────────────
|
|
23
|
-
let fallbackState = null;
|
|
24
35
|
// ─── Internal getters/setters ───────────────────────────────────────
|
|
25
36
|
function getRequestState() {
|
|
26
37
|
ensureAls();
|
|
27
|
-
if (als) {
|
|
28
|
-
return als.getStore() ?? null;
|
|
38
|
+
if ($.als) {
|
|
39
|
+
return $.als.getStore() ?? null;
|
|
29
40
|
}
|
|
30
|
-
return fallbackState;
|
|
41
|
+
return $.fallbackState;
|
|
31
42
|
}
|
|
32
43
|
// ─── Public API ─────────────────────────────────────────────────────
|
|
33
44
|
export function __setIntlConfig(config) {
|
|
34
45
|
if (config.defaultLocale) {
|
|
35
|
-
intlConfig = {
|
|
46
|
+
$.intlConfig = { ...$.intlConfig, defaultLocale: config.defaultLocale };
|
|
36
47
|
}
|
|
37
48
|
if (config.locales) {
|
|
38
|
-
intlConfig = {
|
|
49
|
+
$.intlConfig = { ...$.intlConfig, locales: config.locales };
|
|
39
50
|
}
|
|
40
51
|
if (config.routes) {
|
|
41
|
-
intlConfig = {
|
|
52
|
+
$.intlConfig = { ...$.intlConfig, routes: config.routes };
|
|
42
53
|
detectRouteConflicts(config.routes);
|
|
43
54
|
}
|
|
44
55
|
}
|
|
45
56
|
export function getDefaultLocale() {
|
|
46
|
-
return intlConfig.defaultLocale;
|
|
57
|
+
return $.intlConfig.defaultLocale;
|
|
47
58
|
}
|
|
48
59
|
export function getRoutes() {
|
|
49
|
-
return intlConfig.routes;
|
|
60
|
+
return $.intlConfig.routes;
|
|
50
61
|
}
|
|
51
62
|
export function getLocales() {
|
|
52
|
-
return intlConfig.locales;
|
|
63
|
+
return $.intlConfig.locales;
|
|
53
64
|
}
|
|
54
65
|
export function isValidLocale(locale) {
|
|
55
|
-
if (intlConfig.locales.length === 0)
|
|
66
|
+
if ($.intlConfig.locales.length === 0)
|
|
56
67
|
return true;
|
|
57
|
-
return intlConfig.locales.includes(locale);
|
|
68
|
+
return $.intlConfig.locales.includes(locale);
|
|
58
69
|
}
|
|
59
70
|
export function defineRequestConfig(fn) {
|
|
60
|
-
registeredGetRequestConfig = fn;
|
|
71
|
+
$.registeredGetRequestConfig = fn;
|
|
61
72
|
return fn;
|
|
62
73
|
}
|
|
63
74
|
export function __setConfigMessages(messages) {
|
|
64
|
-
configMessages = messages;
|
|
75
|
+
$.configMessages = messages;
|
|
65
76
|
}
|
|
66
77
|
export function __resetRequestConfig() {
|
|
67
|
-
registeredGetRequestConfig = null;
|
|
68
|
-
configMessages = null;
|
|
69
|
-
fallbackState = null;
|
|
70
|
-
intlConfig = { defaultLocale: "en", locales: [], routes: undefined };
|
|
78
|
+
$.registeredGetRequestConfig = null;
|
|
79
|
+
$.configMessages = null;
|
|
80
|
+
$.fallbackState = null;
|
|
81
|
+
$.intlConfig = { defaultLocale: "en", locales: [], routes: undefined, fallbackRoutes: [] };
|
|
82
|
+
}
|
|
83
|
+
// ─── Fallback routes (Astro 6.1+ astro:routes:resolved) ─────────────
|
|
84
|
+
export function setFallbackRoutes(routes) {
|
|
85
|
+
$.intlConfig = { ...$.intlConfig, fallbackRoutes: routes };
|
|
86
|
+
}
|
|
87
|
+
export function getFallbackRoutes() {
|
|
88
|
+
return $.intlConfig.fallbackRoutes ?? [];
|
|
71
89
|
}
|
|
72
90
|
// ─── Route conflict detection ────────────────────────────────────────
|
|
73
91
|
function normalizeTemplate(template) {
|
|
@@ -113,11 +131,11 @@ async function resolveMessages(locale, source) {
|
|
|
113
131
|
// ─── setRequestLocale ───────────────────────────────────────────────
|
|
114
132
|
export async function setRequestLocale(url, getConfig) {
|
|
115
133
|
const [, lang] = url.pathname.split("/");
|
|
116
|
-
if (lang && intlConfig.locales.length > 0 &&
|
|
134
|
+
if (lang && $.intlConfig.locales.length > 0 && !$.intlConfig.locales.includes(lang)) {
|
|
117
135
|
return false;
|
|
118
136
|
}
|
|
119
|
-
const locale = sanitizeLocale(lang || intlConfig.defaultLocale);
|
|
120
|
-
const resolvedGetConfig = getConfig ?? registeredGetRequestConfig;
|
|
137
|
+
const locale = sanitizeLocale(lang || $.intlConfig.defaultLocale);
|
|
138
|
+
const resolvedGetConfig = getConfig ?? $.registeredGetRequestConfig;
|
|
121
139
|
let state;
|
|
122
140
|
if (resolvedGetConfig) {
|
|
123
141
|
const config = await resolvedGetConfig(locale);
|
|
@@ -126,58 +144,81 @@ export async function setRequestLocale(url, getConfig) {
|
|
|
126
144
|
messages: config.messages,
|
|
127
145
|
};
|
|
128
146
|
}
|
|
129
|
-
else if (configMessages) {
|
|
130
|
-
const messages = await resolveMessages(locale, configMessages);
|
|
147
|
+
else if ($.configMessages) {
|
|
148
|
+
const messages = await resolveMessages(locale, $.configMessages);
|
|
131
149
|
state = { locale, messages };
|
|
132
150
|
}
|
|
133
151
|
else {
|
|
134
|
-
|
|
135
|
-
|
|
152
|
+
// No config available — this can happen when Astro 6's built-in i18n
|
|
153
|
+
// router triggers internal reroutes before the user middleware runs.
|
|
154
|
+
// Return false so the caller can decide what to do.
|
|
155
|
+
return false;
|
|
136
156
|
}
|
|
137
|
-
fallbackState = state;
|
|
157
|
+
$.fallbackState = state;
|
|
138
158
|
return true;
|
|
139
159
|
}
|
|
140
160
|
// ─── runWithLocale (concurrency-safe via AsyncLocalStorage) ─────────
|
|
141
161
|
export async function runWithLocale(url, fn, getConfig) {
|
|
142
162
|
const [, lang] = url.pathname.split("/");
|
|
143
|
-
const locale = sanitizeLocale(lang || intlConfig.defaultLocale);
|
|
144
|
-
const resolvedGetConfig = getConfig ?? registeredGetRequestConfig;
|
|
163
|
+
const locale = sanitizeLocale(lang || $.intlConfig.defaultLocale);
|
|
164
|
+
const resolvedGetConfig = getConfig ?? $.registeredGetRequestConfig;
|
|
145
165
|
let state;
|
|
146
166
|
if (resolvedGetConfig) {
|
|
147
167
|
const config = await resolvedGetConfig(locale);
|
|
148
168
|
state = { locale: config.locale, messages: config.messages };
|
|
149
169
|
}
|
|
150
|
-
else if (configMessages) {
|
|
151
|
-
const messages = await resolveMessages(locale, configMessages);
|
|
170
|
+
else if ($.configMessages) {
|
|
171
|
+
const messages = await resolveMessages(locale, $.configMessages);
|
|
152
172
|
state = { locale, messages };
|
|
153
173
|
}
|
|
154
174
|
else {
|
|
155
175
|
throw new Error("[astro-intl] No getRequestConfig or messages provided. " +
|
|
156
176
|
"Either pass getConfig to setRequestLocale(), use defineRequestConfig(), or add messages to the integration options.");
|
|
157
177
|
}
|
|
158
|
-
if (als) {
|
|
159
|
-
return als.run(state, () => {
|
|
160
|
-
fallbackState = state;
|
|
178
|
+
if ($.als) {
|
|
179
|
+
return $.als.run(state, () => {
|
|
180
|
+
$.fallbackState = state;
|
|
161
181
|
return fn();
|
|
162
182
|
});
|
|
163
183
|
}
|
|
164
|
-
fallbackState = state;
|
|
184
|
+
$.fallbackState = state;
|
|
165
185
|
return fn();
|
|
166
186
|
}
|
|
187
|
+
// ─── Auto-detect locale from URL (for static mode without explicit setRequestLocale) ────────────────────────────────────────────
|
|
188
|
+
function autoDetectLocaleFromUrl() {
|
|
189
|
+
// Try to detect from browser URL (client-side)
|
|
190
|
+
if (typeof window !== "undefined" && window.location) {
|
|
191
|
+
const pathname = window.location.pathname;
|
|
192
|
+
const [, lang] = pathname.split("/");
|
|
193
|
+
if (lang && ($.intlConfig.locales.length === 0 || $.intlConfig.locales.includes(lang))) {
|
|
194
|
+
return sanitizeLocale(lang);
|
|
195
|
+
}
|
|
196
|
+
return $.intlConfig.defaultLocale;
|
|
197
|
+
}
|
|
198
|
+
return null;
|
|
199
|
+
}
|
|
167
200
|
// ─── Read current state ─────────────────────────────────────────────
|
|
168
201
|
export function getLocale() {
|
|
169
202
|
const state = getRequestState();
|
|
170
|
-
if (
|
|
171
|
-
|
|
203
|
+
if (state) {
|
|
204
|
+
return state.locale;
|
|
205
|
+
}
|
|
206
|
+
// Try auto-detection for static mode
|
|
207
|
+
const detectedLocale = autoDetectLocaleFromUrl();
|
|
208
|
+
if (detectedLocale) {
|
|
209
|
+
return detectedLocale;
|
|
172
210
|
}
|
|
173
|
-
|
|
211
|
+
throw new Error("[astro-intl] No request config found. Did you call setRequestLocale()?");
|
|
174
212
|
}
|
|
175
213
|
export function getMessages(namespace) {
|
|
176
214
|
const state = getRequestState();
|
|
177
|
-
if (
|
|
178
|
-
|
|
215
|
+
if (state) {
|
|
216
|
+
return namespace ? state.messages[namespace] : state.messages;
|
|
179
217
|
}
|
|
180
|
-
|
|
218
|
+
// For async initialization case, throw with helpful message
|
|
219
|
+
// The user should either call setRequestLocale or we need to be in a context
|
|
220
|
+
// where auto-initialization has already happened
|
|
221
|
+
throw new Error("[astro-intl] No request config found. Did you call setRequestLocale()?");
|
|
181
222
|
}
|
|
182
223
|
export function getRequestLocale() {
|
|
183
224
|
return getLocale();
|
package/dist/translations.d.ts
CHANGED