astro-intl 2.1.0 → 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/dist/__tests__/config-messages.test.js +39 -0
- package/dist/__tests__/core.test.js +90 -2
- package/dist/__tests__/fallback-routes.test.js +3 -9
- package/dist/__tests__/integration.test.js +14 -3
- package/dist/components/AutoRedirect.astro +31 -0
- package/dist/index.d.ts +2 -1
- package/dist/index.js +14 -1
- package/dist/store.js +27 -6
- package/dist/translations.d.ts +1 -0
- package/dist/translations.js +4 -1
- package/dist/types/index.d.ts +4 -0
- package/package.json +4 -1
|
@@ -158,3 +158,42 @@ describe("defineRequestConfig (next-intl style)", () => {
|
|
|
158
158
|
expect(getTranslations()("greeting")).toBe("From config messages");
|
|
159
159
|
});
|
|
160
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
|
});
|
|
@@ -17,9 +17,7 @@ describe("Fallback Routes (Astro 6.1+)", () => {
|
|
|
17
17
|
expect(getFallbackRoutes()).toEqual(routes);
|
|
18
18
|
});
|
|
19
19
|
it("overwrites previous fallback routes on re-set", () => {
|
|
20
|
-
setFallbackRoutes([
|
|
21
|
-
{ pattern: "/fr/about", pathname: "/fr/about/", locale: "fr" },
|
|
22
|
-
]);
|
|
20
|
+
setFallbackRoutes([{ pattern: "/fr/about", pathname: "/fr/about/", locale: "fr" }]);
|
|
23
21
|
const newRoutes = [
|
|
24
22
|
{ pattern: "/de/contact", pathname: "/de/contact/", locale: "de" },
|
|
25
23
|
];
|
|
@@ -27,16 +25,12 @@ describe("Fallback Routes (Astro 6.1+)", () => {
|
|
|
27
25
|
expect(getFallbackRoutes()).toEqual(newRoutes);
|
|
28
26
|
});
|
|
29
27
|
it("clears fallback routes on reset", () => {
|
|
30
|
-
setFallbackRoutes([
|
|
31
|
-
{ pattern: "/fr/about", pathname: "/fr/about/", locale: "fr" },
|
|
32
|
-
]);
|
|
28
|
+
setFallbackRoutes([{ pattern: "/fr/about", pathname: "/fr/about/", locale: "fr" }]);
|
|
33
29
|
__resetRequestConfig();
|
|
34
30
|
expect(getFallbackRoutes()).toEqual([]);
|
|
35
31
|
});
|
|
36
32
|
it("handles routes without pathname (pattern-only)", () => {
|
|
37
|
-
const routes = [
|
|
38
|
-
{ pattern: "/fr/blog/[...slug]", locale: "fr" },
|
|
39
|
-
];
|
|
33
|
+
const routes = [{ pattern: "/fr/blog/[...slug]", locale: "fr" }];
|
|
40
34
|
setFallbackRoutes(routes);
|
|
41
35
|
expect(getFallbackRoutes()).toEqual(routes);
|
|
42
36
|
expect(getFallbackRoutes()[0].pathname).toBeUndefined();
|
|
@@ -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/index.d.ts
CHANGED
|
@@ -7,6 +7,7 @@ export type AstroIntlOptions = {
|
|
|
7
7
|
defaultLocale?: string;
|
|
8
8
|
locales?: string[];
|
|
9
9
|
messages?: MessagesConfig;
|
|
10
|
+
messagesDir?: string;
|
|
10
11
|
routes?: RoutesMap;
|
|
11
12
|
};
|
|
12
13
|
export default function astroIntl(options?: AstroIntlOptions): AstroIntegration;
|
|
@@ -22,5 +23,5 @@ export declare const __resetRequestConfig: typeof _resetRequestConfig;
|
|
|
22
23
|
export declare const getFallbackRoutes: typeof _getFallbackRoutes;
|
|
23
24
|
export declare const path: typeof _path;
|
|
24
25
|
export declare const switchLocalePath: typeof _switchLocalePath;
|
|
25
|
-
export type { RequestConfig, Primitive, GetRequestConfigFn, MessagesConfig, IntlConfig, RoutesMap, ExtractParams, ParamsForRoute, FallbackRouteInfo, } from "./types/index.js";
|
|
26
|
+
export type { RequestConfig, Primitive, GetRequestConfigFn, MessagesConfig, MessagesDirConfig, IntlConfig, RoutesMap, ExtractParams, ParamsForRoute, FallbackRouteInfo, } from "./types/index.js";
|
|
26
27
|
export type { DotPaths } from "./core.js";
|
package/dist/index.js
CHANGED
|
@@ -1,7 +1,15 @@
|
|
|
1
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
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
|
+
}
|
|
3
11
|
export default function astroIntl(options = {}) {
|
|
4
|
-
const { enabled = true, defaultLocale, locales, messages, routes } = options;
|
|
12
|
+
const { enabled = true, defaultLocale, locales, messages, messagesDir, routes } = options;
|
|
5
13
|
if (defaultLocale || locales || routes) {
|
|
6
14
|
__setIntlConfig({
|
|
7
15
|
...(defaultLocale && { defaultLocale }),
|
|
@@ -12,6 +20,11 @@ export default function astroIntl(options = {}) {
|
|
|
12
20
|
if (messages) {
|
|
13
21
|
__setConfigMessages(messages);
|
|
14
22
|
}
|
|
23
|
+
else if (messagesDir && locales) {
|
|
24
|
+
// Auto-create messages config from directory
|
|
25
|
+
const dirConfig = createMessagesConfigFromDir(messagesDir, locales);
|
|
26
|
+
__setConfigMessages(dirConfig);
|
|
27
|
+
}
|
|
15
28
|
return {
|
|
16
29
|
name: "astro-intl",
|
|
17
30
|
hooks: {
|
package/dist/store.js
CHANGED
|
@@ -184,20 +184,41 @@ export async function runWithLocale(url, fn, getConfig) {
|
|
|
184
184
|
$.fallbackState = state;
|
|
185
185
|
return fn();
|
|
186
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
|
+
}
|
|
187
200
|
// ─── Read current state ─────────────────────────────────────────────
|
|
188
201
|
export function getLocale() {
|
|
189
202
|
const state = getRequestState();
|
|
190
|
-
if (
|
|
191
|
-
|
|
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;
|
|
192
210
|
}
|
|
193
|
-
|
|
211
|
+
throw new Error("[astro-intl] No request config found. Did you call setRequestLocale()?");
|
|
194
212
|
}
|
|
195
213
|
export function getMessages(namespace) {
|
|
196
214
|
const state = getRequestState();
|
|
197
|
-
if (
|
|
198
|
-
|
|
215
|
+
if (state) {
|
|
216
|
+
return namespace ? state.messages[namespace] : state.messages;
|
|
199
217
|
}
|
|
200
|
-
|
|
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()?");
|
|
201
222
|
}
|
|
202
223
|
export function getRequestLocale() {
|
|
203
224
|
return getLocale();
|
package/dist/translations.d.ts
CHANGED
package/dist/translations.js
CHANGED
|
@@ -9,6 +9,9 @@ export function getTranslations(namespace) {
|
|
|
9
9
|
const str = typeof value === "string" ? value : key;
|
|
10
10
|
return interpolateValues(str, values);
|
|
11
11
|
}
|
|
12
|
+
function raw(key) {
|
|
13
|
+
return getNestedValue(messages, key);
|
|
14
|
+
}
|
|
12
15
|
const markup = function (key, options) {
|
|
13
16
|
const isOptionsObject = "tags" in options && typeof options.tags === "object";
|
|
14
17
|
const tags = isOptionsObject
|
|
@@ -28,6 +31,6 @@ export function getTranslations(namespace) {
|
|
|
28
31
|
str = sanitizeHtml(str);
|
|
29
32
|
return str;
|
|
30
33
|
};
|
|
31
|
-
Object.assign(t, { markup });
|
|
34
|
+
Object.assign(t, { markup, raw });
|
|
32
35
|
return t;
|
|
33
36
|
}
|
package/dist/types/index.d.ts
CHANGED
|
@@ -7,6 +7,10 @@ export type GetRequestConfigFn = (locale: string) => Promise<RequestConfig> | Re
|
|
|
7
7
|
export type MessagesConfig = Record<string, Record<string, unknown> | (() => Promise<{
|
|
8
8
|
default: Record<string, unknown>;
|
|
9
9
|
} | Record<string, unknown>>)>;
|
|
10
|
+
export type MessagesDirConfig = {
|
|
11
|
+
/** Directory path containing locale JSON files (e.g., "./src/i18n/messages") */
|
|
12
|
+
dir: string;
|
|
13
|
+
};
|
|
10
14
|
export type RoutesMap = {
|
|
11
15
|
[routeKey: string]: {
|
|
12
16
|
[locale: string]: string;
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "astro-intl",
|
|
3
|
-
"version": "2.
|
|
3
|
+
"version": "2.2.0",
|
|
4
4
|
"type": "module",
|
|
5
5
|
"description": "Sistema de internacionalización simple y type-safe para Astro.",
|
|
6
6
|
"keywords": [
|
|
@@ -55,6 +55,9 @@
|
|
|
55
55
|
"./routing": {
|
|
56
56
|
"types": "./dist/routing.d.ts",
|
|
57
57
|
"import": "./dist/routing.js"
|
|
58
|
+
},
|
|
59
|
+
"./components": {
|
|
60
|
+
"import": "./dist/components/AutoRedirect.astro"
|
|
58
61
|
}
|
|
59
62
|
},
|
|
60
63
|
"peerDependencies": {
|