@mannisto/astro-i18n 1.0.0-alpha.1

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 ADDED
@@ -0,0 +1,21 @@
1
+ MIT License
2
+
3
+ Copyright (c) 2026 Ere Männistö
4
+
5
+ Permission is hereby granted, free of charge, to any person obtaining a copy
6
+ of this software and associated documentation files (the "Software"), to deal
7
+ in the Software without restriction, including without limitation the rights
8
+ to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9
+ copies of the Software, and to permit persons to whom the Software is
10
+ furnished to do so, subject to the following conditions:
11
+
12
+ The above copyright notice and this permission notice shall be included in all
13
+ copies or substantial portions of the Software.
14
+
15
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17
+ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18
+ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19
+ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20
+ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21
+ SOFTWARE.
package/README.md ADDED
@@ -0,0 +1,264 @@
1
+ # Astro Internationalization (i18n)
2
+
3
+ ![npm version](https://img.shields.io/npm/v/@mannisto/astro-i18n)
4
+ ![license](https://img.shields.io/badge/license-MIT-green)
5
+ ![astro peer dependency](https://img.shields.io/npm/dependency-version/@mannisto/astro-i18n/peer/astro)
6
+
7
+ A flexible alternative to Astro's built-in i18n, with locale routing,
8
+ detection, and translations for static and SSR sites.
9
+
10
+ ## Features
11
+
12
+ - Three detection modes: `server`, `client`, and `none`
13
+ - Automatic locale prefixing via middleware
14
+ - Optional static translation files with key validation
15
+ - Works with Astro's static and SSR output modes
16
+
17
+ ## Installation
18
+ ```bash
19
+ pnpm add @mannisto/astro-i18n
20
+ ```
21
+
22
+ ## Setup
23
+
24
+ Add the integration to your `astro.config.ts`:
25
+ ```typescript
26
+ import { defineConfig } from "astro/config"
27
+ import i18n from "@mannisto/astro-i18n"
28
+
29
+ export default defineConfig({
30
+ integrations: [
31
+ i18n({
32
+ locales: [
33
+ { code: "en", name: "English", endonym: "English" },
34
+ { code: "fi", name: "Finnish", endonym: "Suomi", phrase: "Suomeksi" },
35
+ ],
36
+ routing: {
37
+ fallback: "en",
38
+ detection: "server",
39
+ autoPrefix: {
40
+ ignore: ["/keystatic"],
41
+ },
42
+ },
43
+ }),
44
+ ],
45
+ })
46
+ ```
47
+
48
+ ## Translations
49
+
50
+ Translations are optional. To enable them, add a `translations` path to
51
+ your config pointing to a directory with a JSON file for each locale:
52
+ ```
53
+ src/translations/
54
+ en.json
55
+ fi.json
56
+ ```
57
+ ```typescript
58
+ i18n({
59
+ locales: [...],
60
+ translations: "./src/translations",
61
+ })
62
+ ```
63
+
64
+ Each file should contain a flat object of key-value pairs:
65
+ ```json
66
+ {
67
+ "nav.home": "Home",
68
+ "nav.about": "About",
69
+ "footer.copyright": "All rights reserved"
70
+ }
71
+ ```
72
+
73
+ All locales must have the same keys as the fallback locale or the
74
+ integration will throw an error at startup. If translations are not
75
+ configured, `Locale.use()` will log a warning when called.
76
+
77
+ ## Usage
78
+
79
+ Import `Locale` from the runtime subpath in your pages and components:
80
+ ```astro
81
+ ---
82
+ import { Locale } from "@mannisto/astro-i18n/runtime"
83
+
84
+ export const getStaticPaths = () =>
85
+ Locale.supported.map((code) => ({ params: { locale: code } }))
86
+
87
+ const { locale } = Astro.params
88
+ const t = Locale.use(locale)
89
+ ---
90
+ <html lang={locale}>
91
+ <body>
92
+ <h1>{t("nav.home")}</h1>
93
+ </body>
94
+ </html>
95
+ ```
96
+
97
+ ## API
98
+
99
+ ### `Locale.supported`
100
+
101
+ Returns all supported locale codes.
102
+ ```typescript
103
+ Locale.supported // ["en", "fi"]
104
+ ```
105
+
106
+ ### `Locale.fallback`
107
+
108
+ Returns the fallback locale code.
109
+ ```typescript
110
+ Locale.fallback // "en"
111
+ ```
112
+
113
+ ### `Locale.current(locale)`
114
+
115
+ Returns the current locale from `Astro.params`.
116
+ ```typescript
117
+ const locale = Locale.current(Astro.params.locale)
118
+ ```
119
+
120
+ ### `Locale.get(code?)`
121
+
122
+ Returns the config for all locales, or a single locale by code.
123
+ ```typescript
124
+ Locale.get() // all locales
125
+ Locale.get("fi") // { code: "fi", name: "Finnish", endonym: "Suomi", ... }
126
+ ```
127
+
128
+ ### `Locale.use(locale)`
129
+
130
+ Binds a locale and returns a translation function for that locale.
131
+ Call once at the top of your page, then use the returned function
132
+ to look up individual keys.
133
+
134
+ Requires `translations` to be configured. Logs a warning if called
135
+ without translations configured.
136
+ ```typescript
137
+ const t = Locale.use(locale)
138
+ t("nav.home") // "Home"
139
+ t() // { "nav.home": "Home", ... }
140
+ ```
141
+
142
+ ### `Locale.middleware`
143
+
144
+ Middleware that redirects requests without a locale prefix to the correct
145
+ locale based on the user's cookie. Auto-registered when detection is
146
+ `"server"` and `autoPrefix` is enabled.
147
+
148
+ Can also be used manually:
149
+ ```typescript
150
+ import { sequence } from "astro/middleware"
151
+ import { Locale } from "@mannisto/astro-i18n/runtime"
152
+
153
+ export const onRequest = sequence(Locale.middleware, myMiddleware)
154
+ ```
155
+
156
+ ## Detection modes
157
+
158
+ ### `server`
159
+
160
+ Reads the `Accept-Language` header on first visit, sets a cookie, and
161
+ redirects to the appropriate locale URL. Requires an adapter for
162
+ production builds.
163
+ ```typescript
164
+ import { defineConfig } from "astro/config"
165
+ import node from "@astrojs/node"
166
+ import i18n from "@mannisto/astro-i18n"
167
+
168
+ export default defineConfig({
169
+ adapter: node({ mode: "standalone" }),
170
+ integrations: [
171
+ i18n({
172
+ locales: [
173
+ { code: "en", name: "English", endonym: "English" },
174
+ { code: "fi", name: "Finnish", endonym: "Suomi", phrase: "Suomeksi" },
175
+ ],
176
+ routing: {
177
+ fallback: "en",
178
+ detection: "server",
179
+ autoPrefix: {
180
+ ignore: ["/keystatic"],
181
+ },
182
+ },
183
+ }),
184
+ ],
185
+ })
186
+ ```
187
+
188
+ ### `client`
189
+
190
+ Serves a static HTML page at `/` with an inline JS redirect script that
191
+ reads `navigator.language` and stores the result in `localStorage`.
192
+ Works without an adapter.
193
+ ```typescript
194
+ i18n({
195
+ locales: [
196
+ { code: "en", name: "English", endonym: "English" },
197
+ { code: "fi", name: "Finnish", endonym: "Suomi", phrase: "Suomeksi" },
198
+ ],
199
+ routing: {
200
+ fallback: "en",
201
+ detection: "client",
202
+ },
203
+ })
204
+ ```
205
+
206
+ ### `none`
207
+
208
+ No detection. Users must navigate to a locale URL directly, e.g. `/en/`.
209
+ ```typescript
210
+ i18n({
211
+ locales: [
212
+ { code: "en", name: "English", endonym: "English" },
213
+ { code: "fi", name: "Finnish", endonym: "Suomi", phrase: "Suomeksi" },
214
+ ],
215
+ routing: {
216
+ fallback: "en",
217
+ detection: "none",
218
+ },
219
+ })
220
+ ```
221
+
222
+ ## Configuration reference
223
+ ```typescript
224
+ i18n({
225
+ // required — list of supported locales
226
+ locales: [
227
+ {
228
+ code: "en", // locale code used in URLs
229
+ name: "English", // locale name in English
230
+ endonym: "English", // locale name in its own language
231
+ phrase: "In English", // optional, for locale switchers
232
+ },
233
+ ],
234
+
235
+ routing: {
236
+ // locale to use when no match is found
237
+ // defaults to the first locale in the array
238
+ fallback: "en",
239
+
240
+ // how the user's locale is detected on first visit
241
+ // "server" | "client" | "none" — defaults to "client"
242
+ detection: "server",
243
+
244
+ // middleware that prefixes unknown routes with the user's locale
245
+ // only valid when detection is "server"
246
+ // set to false to disable, or pass an object to configure ignore paths
247
+ autoPrefix: {
248
+ ignore: ["/keystatic", "/api"],
249
+ },
250
+ },
251
+
252
+ // optional — path to translation files, relative to the project root
253
+ // if not set, translations are disabled and Locale.use() will warn when called
254
+ translations: "./src/translations",
255
+ })
256
+ ```
257
+
258
+ ## Contributing
259
+
260
+ See [CONTRIBUTING.md](./CONTRIBUTING.md).
261
+
262
+ ## License
263
+
264
+ MIT © [Ere Männistö](https://github.com/eremannisto)
@@ -0,0 +1,123 @@
1
+ // src/lib/locale.ts
2
+ import { defineMiddleware } from "astro/middleware";
3
+ import { config, translations } from "virtual:astro-i18n/config";
4
+ var NAME = "@mannisto/astro-i18n";
5
+ var Locale = {
6
+ // ==========================================================================
7
+ // Locale access
8
+ // ==========================================================================
9
+ /**
10
+ * All supported locale codes.
11
+ * @example ["en", "fi"]
12
+ */
13
+ get supported() {
14
+ return config.locales.map((l) => l.code);
15
+ },
16
+ /**
17
+ * The fallback locale code.
18
+ * @example "en"
19
+ */
20
+ get fallback() {
21
+ return config.routing.fallback;
22
+ },
23
+ /**
24
+ * Returns the current locale from Astro.params.
25
+ * @example
26
+ * const locale = Locale.current(Astro.params.locale)
27
+ */
28
+ current(locale) {
29
+ return locale;
30
+ },
31
+ /**
32
+ * Returns the config for all locales, or a single locale by code.
33
+ * Throws if the requested code is not found.
34
+ *
35
+ * @example
36
+ * Locale.get() // all locales
37
+ * Locale.get("fi") // single locale
38
+ */
39
+ get(code) {
40
+ if (code) {
41
+ const found = config.locales.find((l) => l.code === code);
42
+ if (!found) {
43
+ throw new Error(`${NAME} Locale "${code}" not found.`);
44
+ }
45
+ return found;
46
+ }
47
+ return config.locales;
48
+ },
49
+ // ==========================================================================
50
+ // Translations
51
+ // ==========================================================================
52
+ /**
53
+ * Binds a locale and returns a translation function for that locale.
54
+ *
55
+ * Call once at the top of your page with the current locale, then use
56
+ * the returned function to look up individual keys.
57
+ *
58
+ * Warns if translations are not configured.
59
+ * Throws if the locale or key is not found.
60
+ *
61
+ * @example
62
+ * const t = Locale.use(locale)
63
+ * t("nav.home") // "Home"
64
+ * t() // { "nav.home": "Home", ... }
65
+ */
66
+ use(locale) {
67
+ if (!config.translations) {
68
+ console.warn(
69
+ `${NAME} Locale.use() was called but translations are not configured. Add a translations path to your i18n config to enable translations.`
70
+ );
71
+ return (key) => key ? "" : {};
72
+ }
73
+ const record = translations[locale];
74
+ return (key) => {
75
+ if (key) {
76
+ if (!record) {
77
+ throw new Error(`${NAME} No translations found for locale "${locale}".`);
78
+ }
79
+ if (!(key in record)) {
80
+ throw new Error(`${NAME} Missing translation key "${key}" in ${locale}.json`);
81
+ }
82
+ return record[key];
83
+ }
84
+ return record ?? {};
85
+ };
86
+ },
87
+ // ==========================================================================
88
+ // Middleware
89
+ // ==========================================================================
90
+ /**
91
+ * Middleware that redirects requests without a locale prefix to the
92
+ * correct locale based on the user's cookie.
93
+ *
94
+ * Auto-registered when detection is "server" and autoPrefix is enabled.
95
+ * Can also be used manually via Astro's sequence() helper.
96
+ *
97
+ * @example
98
+ * import { sequence } from "astro/middleware"
99
+ * import { Locale } from "@mannisto/astro-i18n/runtime"
100
+ * export const onRequest = sequence(Locale.middleware, myMiddleware)
101
+ */
102
+ middleware: defineMiddleware(({ url, cookies, redirect }, next) => {
103
+ const pathname = url.pathname;
104
+ const ignoreList = config.routing.autoPrefix !== false ? config.routing.autoPrefix.ignore ?? [] : [];
105
+ if (ignoreList.some((path) => pathname.startsWith(path))) {
106
+ return next();
107
+ }
108
+ if (pathname === "/") {
109
+ return next();
110
+ }
111
+ const firstSegment = pathname.split("/")[1];
112
+ if (config.locales.map((l) => l.code).includes(firstSegment)) {
113
+ return next();
114
+ }
115
+ const stored = cookies.get("locale")?.value;
116
+ const targetLocale = stored && config.locales.map((l) => l.code).includes(stored) ? stored : config.routing.fallback;
117
+ return redirect(`/${targetLocale}${pathname}`, 302);
118
+ })
119
+ };
120
+
121
+ export {
122
+ Locale
123
+ };
@@ -0,0 +1,6 @@
1
+ import { APIRoute } from 'astro';
2
+
3
+ declare const prerender = true;
4
+ declare const GET: APIRoute;
5
+
6
+ export { GET, prerender };
@@ -0,0 +1,30 @@
1
+ // src/detect/client.ts
2
+ import { config } from "virtual:astro-i18n/config";
3
+ var prerender = true;
4
+ var GET = () => {
5
+ const supported = config.locales.map((l) => l.code);
6
+ const fallback = config.routing.fallback;
7
+ const html = `<!DOCTYPE html>
8
+ <html>
9
+ <head>
10
+ <meta charset="UTF-8" />
11
+ <script>
12
+ const supported = ${JSON.stringify(supported)};
13
+ const fallback = "${fallback}";
14
+ const stored = localStorage.getItem("locale");
15
+ const preferred = navigator.language.split("-")[0];
16
+ const locale = stored ?? (supported.includes(preferred) ? preferred : fallback);
17
+ localStorage.setItem("locale", locale);
18
+ window.location.replace("/" + locale + "/");
19
+ </script>
20
+ </head>
21
+ <body></body>
22
+ </html>`;
23
+ return new Response(html, {
24
+ headers: { "content-type": "text/html;charset=utf-8" }
25
+ });
26
+ };
27
+ export {
28
+ GET,
29
+ prerender
30
+ };
@@ -0,0 +1,6 @@
1
+ import { APIRoute } from 'astro';
2
+
3
+ declare const prerender = false;
4
+ declare const GET: APIRoute;
5
+
6
+ export { GET, prerender };
@@ -0,0 +1,21 @@
1
+ import {
2
+ Locale
3
+ } from "../chunk-SMGDNPQN.js";
4
+
5
+ // src/detect/server.ts
6
+ var prerender = false;
7
+ var GET = ({ request, cookies, redirect }) => {
8
+ const stored = cookies.get("locale")?.value;
9
+ if (stored && Locale.supported.includes(stored)) {
10
+ return redirect(`/${stored}/`, 302);
11
+ }
12
+ const header = request.headers.get("accept-language") ?? "";
13
+ const preferred = header.split(",")[0].split(";")[0].split("-")[0].trim().toLowerCase();
14
+ const locale = Locale.supported.includes(preferred) ? preferred : Locale.fallback;
15
+ cookies.set("locale", locale, { path: "/" });
16
+ return redirect(`/${locale}/`, 302);
17
+ };
18
+ export {
19
+ GET,
20
+ prerender
21
+ };
@@ -0,0 +1,32 @@
1
+ import { AstroIntegration } from 'astro';
2
+ import { I as I18nConfig } from './types-DEfk2GVt.js';
3
+
4
+ /**
5
+ * The @mannisto/astro-i18n Astro integration.
6
+ *
7
+ * Adds locale routing, detection, and translations to your Astro project
8
+ * without relying on Astro's built-in i18n system.
9
+ *
10
+ * @example
11
+ * import i18n from "@mannisto/astro-i18n"
12
+ *
13
+ * export default defineConfig({
14
+ * integrations: [
15
+ * i18n({
16
+ * locales: [
17
+ * { code: "en", name: "English", endonym: "English" },
18
+ * { code: "fi", name: "Finnish", endonym: "Suomi", phrase: "Suomeksi" },
19
+ * ],
20
+ * routing: {
21
+ * fallback: "en",
22
+ * detection: "server",
23
+ * autoPrefix: { ignore: ["/keystatic"] },
24
+ * },
25
+ * translations: "./src/translations",
26
+ * }),
27
+ * ],
28
+ * })
29
+ */
30
+ declare function i18n(config: I18nConfig): AstroIntegration;
31
+
32
+ export { i18n as default };
package/dist/index.js ADDED
@@ -0,0 +1,185 @@
1
+ // src/lib/validate.ts
2
+ import fs from "fs";
3
+ var NAME = "@mannisto/astro-i18n";
4
+ var Validate = {
5
+ /**
6
+ * Validates the user-supplied config object.
7
+ * Throws a descriptive error if anything is invalid.
8
+ */
9
+ config(config) {
10
+ if (!config.locales || config.locales.length === 0) {
11
+ throw new Error(`${NAME} No locales defined.`);
12
+ }
13
+ for (const locale of config.locales) {
14
+ if (!locale.code) {
15
+ throw new Error(`${NAME} A locale is missing a code.`);
16
+ }
17
+ if (!locale.name) {
18
+ throw new Error(`${NAME} Locale "${locale.code}" is missing a name.`);
19
+ }
20
+ if (!locale.endonym) {
21
+ throw new Error(`${NAME} Locale "${locale.code}" is missing an endonym.`);
22
+ }
23
+ }
24
+ if (config.routing?.fallback) {
25
+ const codes = config.locales.map((l) => l.code);
26
+ if (!codes.includes(config.routing.fallback)) {
27
+ throw new Error(
28
+ `${NAME} Fallback locale "${config.routing.fallback}" not found in locales.`
29
+ );
30
+ }
31
+ }
32
+ if (config.routing?.autoPrefix && config.routing?.detection !== "server") {
33
+ throw new Error(`${NAME} autoPrefix is only valid when detection is "server".`);
34
+ }
35
+ },
36
+ /**
37
+ * Validates that translation files exist for all locales and that all
38
+ * locales share the same keys as the fallback locale.
39
+ *
40
+ * Returns the loaded translation data for use in the virtual module.
41
+ */
42
+ translations(config) {
43
+ const data = {};
44
+ for (const locale of config.locales) {
45
+ const filePath = `${config.translations}/${locale.code}.json`;
46
+ if (!fs.existsSync(filePath)) {
47
+ throw new Error(`${NAME} Missing translation file: ${filePath}`);
48
+ }
49
+ data[locale.code] = JSON.parse(fs.readFileSync(filePath, "utf-8"));
50
+ }
51
+ const fallbackKeys = Object.keys(data[config.routing.fallback]);
52
+ for (const locale of config.locales) {
53
+ if (locale.code === config.routing.fallback) continue;
54
+ const localeKeys = Object.keys(data[locale.code]);
55
+ for (const key of fallbackKeys) {
56
+ if (!localeKeys.includes(key)) {
57
+ throw new Error(`${NAME} Missing translation key "${key}" in ${locale.code}.json`);
58
+ }
59
+ }
60
+ }
61
+ return data;
62
+ },
63
+ /**
64
+ * Validates that there is no conflicting src/pages/index.astro when
65
+ * detection is not "none". If one exists it would intercept the injected
66
+ * detection route at /.
67
+ */
68
+ index(root, detection) {
69
+ if (detection === "none") return;
70
+ const indexPath = new URL("./src/pages/index.astro", root);
71
+ if (fs.existsSync(indexPath)) {
72
+ throw new Error(
73
+ `${NAME} Found conflicting src/pages/index.astro \u2014 remove it or set routing.detection to "none".`
74
+ );
75
+ }
76
+ }
77
+ };
78
+
79
+ // src/index.ts
80
+ var NAME2 = "@mannisto/astro-i18n";
81
+ var DEFAULT_IGNORE = ["/_astro"];
82
+ function resolveConfig(config) {
83
+ const fallback = config.routing?.fallback ?? config.locales[0].code;
84
+ const detection = config.routing?.detection ?? "client";
85
+ const autoPrefix = config.routing?.autoPrefix === false ? false : {
86
+ ignore: [
87
+ ...DEFAULT_IGNORE,
88
+ ...typeof config.routing?.autoPrefix === "object" ? config.routing.autoPrefix.ignore ?? [] : []
89
+ ]
90
+ };
91
+ return {
92
+ locales: config.locales,
93
+ routing: { fallback, detection, autoPrefix },
94
+ // translations are optional — undefined means disabled
95
+ translations: config.translations
96
+ };
97
+ }
98
+ function i18n(config) {
99
+ let resolved;
100
+ let translationData = {};
101
+ return {
102
+ name: NAME2,
103
+ hooks: {
104
+ // ======================================================================
105
+ // astro:config:setup
106
+ //
107
+ // Validates config, registers the Vite virtual module plugin, injects
108
+ // detection routes, and registers the autoPrefix middleware.
109
+ // ======================================================================
110
+ "astro:config:setup": ({
111
+ config: astroConfig,
112
+ updateConfig,
113
+ injectRoute,
114
+ addMiddleware,
115
+ logger
116
+ }) => {
117
+ if (astroConfig.i18n) {
118
+ logger.warn(
119
+ "Astro's built-in i18n is configured. Remove the i18n key from astro.config to avoid conflicts."
120
+ );
121
+ }
122
+ Validate.config(config);
123
+ Validate.index(astroConfig.root, config.routing?.detection ?? "client");
124
+ resolved = resolveConfig(config);
125
+ updateConfig({
126
+ vite: {
127
+ plugins: [
128
+ {
129
+ name: "astro-i18n-virtual",
130
+ resolveId(id) {
131
+ if (id === "virtual:astro-i18n/config") {
132
+ return "\0virtual:astro-i18n/config";
133
+ }
134
+ },
135
+ load(id) {
136
+ if (id === "\0virtual:astro-i18n/config") {
137
+ return `
138
+ export const config = ${JSON.stringify(resolved)}
139
+ export const translations = ${JSON.stringify(translationData)}
140
+ `;
141
+ }
142
+ }
143
+ }
144
+ ]
145
+ }
146
+ });
147
+ if (resolved.routing.detection === "server") {
148
+ injectRoute({
149
+ pattern: "/",
150
+ entrypoint: "@mannisto/astro-i18n/detect/server",
151
+ prerender: false
152
+ });
153
+ }
154
+ if (resolved.routing.detection === "client") {
155
+ injectRoute({
156
+ pattern: "/",
157
+ entrypoint: "@mannisto/astro-i18n/detect/client",
158
+ prerender: true
159
+ });
160
+ }
161
+ if (resolved.routing.detection === "server" && resolved.routing.autoPrefix !== false) {
162
+ addMiddleware({
163
+ entrypoint: "@mannisto/astro-i18n/middleware",
164
+ order: "pre"
165
+ });
166
+ }
167
+ logger.info("i18n configured.");
168
+ },
169
+ // ======================================================================
170
+ // astro:config:done
171
+ //
172
+ // Validates and loads translation files only if translations are
173
+ // configured. Runs after all integrations have finished setup.
174
+ // ======================================================================
175
+ "astro:config:done": () => {
176
+ if (resolved.translations) {
177
+ translationData = Validate.translations(resolved);
178
+ }
179
+ }
180
+ }
181
+ };
182
+ }
183
+ export {
184
+ i18n as default
185
+ };
@@ -0,0 +1,5 @@
1
+ import * as astro from 'astro';
2
+
3
+ declare const onRequest: astro.MiddlewareHandler;
4
+
5
+ export { onRequest };
@@ -0,0 +1,9 @@
1
+ import {
2
+ Locale
3
+ } from "./chunk-SMGDNPQN.js";
4
+
5
+ // src/middleware.ts
6
+ var onRequest = Locale.middleware;
7
+ export {
8
+ onRequest
9
+ };
@@ -0,0 +1,67 @@
1
+ import * as astro from 'astro';
2
+ import { L as LocaleCode, a as LocaleConfig } from './types-DEfk2GVt.js';
3
+
4
+ /**
5
+ * Primary public API for locale access, translations, and middleware.
6
+ *
7
+ * Always import from the runtime subpath in pages and components:
8
+ * @example
9
+ * import { Locale } from "@mannisto/astro-i18n/runtime"
10
+ */
11
+ declare const Locale: {
12
+ /**
13
+ * All supported locale codes.
14
+ * @example ["en", "fi"]
15
+ */
16
+ readonly supported: LocaleCode[];
17
+ /**
18
+ * The fallback locale code.
19
+ * @example "en"
20
+ */
21
+ readonly fallback: LocaleCode;
22
+ /**
23
+ * Returns the current locale from Astro.params.
24
+ * @example
25
+ * const locale = Locale.current(Astro.params.locale)
26
+ */
27
+ current(locale: string): LocaleCode;
28
+ /**
29
+ * Returns the config for all locales, or a single locale by code.
30
+ * Throws if the requested code is not found.
31
+ *
32
+ * @example
33
+ * Locale.get() // all locales
34
+ * Locale.get("fi") // single locale
35
+ */
36
+ get(code?: LocaleCode): LocaleConfig | LocaleConfig[];
37
+ /**
38
+ * Binds a locale and returns a translation function for that locale.
39
+ *
40
+ * Call once at the top of your page with the current locale, then use
41
+ * the returned function to look up individual keys.
42
+ *
43
+ * Warns if translations are not configured.
44
+ * Throws if the locale or key is not found.
45
+ *
46
+ * @example
47
+ * const t = Locale.use(locale)
48
+ * t("nav.home") // "Home"
49
+ * t() // { "nav.home": "Home", ... }
50
+ */
51
+ use(locale: LocaleCode): (key?: string) => string | Record<string, string>;
52
+ /**
53
+ * Middleware that redirects requests without a locale prefix to the
54
+ * correct locale based on the user's cookie.
55
+ *
56
+ * Auto-registered when detection is "server" and autoPrefix is enabled.
57
+ * Can also be used manually via Astro's sequence() helper.
58
+ *
59
+ * @example
60
+ * import { sequence } from "astro/middleware"
61
+ * import { Locale } from "@mannisto/astro-i18n/runtime"
62
+ * export const onRequest = sequence(Locale.middleware, myMiddleware)
63
+ */
64
+ middleware: astro.MiddlewareHandler;
65
+ };
66
+
67
+ export { Locale };
@@ -0,0 +1,6 @@
1
+ import {
2
+ Locale
3
+ } from "./chunk-SMGDNPQN.js";
4
+ export {
5
+ Locale
6
+ };
@@ -0,0 +1,74 @@
1
+ /** A locale code, e.g. "en", "fi", "fr" */
2
+ type LocaleCode = string;
3
+ /** Configuration for a single locale */
4
+ type LocaleConfig = {
5
+ /** The locale code, e.g. "en" */
6
+ code: LocaleCode;
7
+ /** The locale name in English, e.g. "English" */
8
+ name: string;
9
+ /** The locale name in its own language, e.g. "Suomi" */
10
+ endonym: string;
11
+ /** Optional short phrase for locale switchers, e.g. "Suomeksi" */
12
+ phrase?: string;
13
+ };
14
+ /**
15
+ * How the user's preferred locale is detected on their first visit.
16
+ *
17
+ * - "server" — reads the Accept-Language header via a server-side API route
18
+ * - "client" — reads navigator.language via a static JS redirect page
19
+ * - "none" — no detection, user must navigate to a locale URL directly
20
+ */
21
+ type DetectionMode = "server" | "client" | "none";
22
+ /**
23
+ * Configuration for the autoPrefix middleware.
24
+ * Only valid when detection is "server".
25
+ */
26
+ type AutoPrefixConfig = {
27
+ /**
28
+ * URL path prefixes that should bypass the autoPrefix middleware.
29
+ * Useful for CMS admin routes, API routes, etc.
30
+ *
31
+ * @example ["/keystatic", "/api"]
32
+ */
33
+ ignore?: string[];
34
+ };
35
+ /** User-supplied routing configuration */
36
+ type RoutingConfig = {
37
+ /**
38
+ * The locale code to fall back to when no match is found.
39
+ * Defaults to the first locale in the locales array.
40
+ */
41
+ fallback?: LocaleCode;
42
+ /**
43
+ * How the user's preferred locale is detected on first visit.
44
+ * @default "client"
45
+ */
46
+ detection?: DetectionMode;
47
+ /**
48
+ * When enabled, the middleware automatically prefixes unknown routes
49
+ * with the user's preferred locale. Only valid when detection is "server".
50
+ *
51
+ * Set to false to disable entirely, or pass an object to configure
52
+ * which paths should be ignored.
53
+ *
54
+ * @default { ignore: ["/_astro"] }
55
+ */
56
+ autoPrefix?: boolean | AutoPrefixConfig;
57
+ };
58
+ /** The configuration object passed to the i18n() integration */
59
+ type I18nConfig = {
60
+ /** The list of supported locales, in order of preference */
61
+ locales: LocaleConfig[];
62
+ /** Routing and detection options */
63
+ routing?: RoutingConfig;
64
+ /**
65
+ * Path to the translations directory, relative to the project root.
66
+ * Each locale should have a corresponding JSON file, e.g. en.json, fi.json.
67
+ * If not set, translations are disabled and Locale.t() will warn when called.
68
+ *
69
+ * @example "./src/translations"
70
+ */
71
+ translations?: string;
72
+ };
73
+
74
+ export type { I18nConfig as I, LocaleCode as L, LocaleConfig as a };
package/package.json ADDED
@@ -0,0 +1,87 @@
1
+ {
2
+ "name": "@mannisto/astro-i18n",
3
+ "version": "1.0.0-alpha.1",
4
+ "description": "A flexible alternative to Astro's built-in i18n, with locale routing, detection, and translations for static and SSR sites.",
5
+ "license": "MIT",
6
+ "type": "module",
7
+ "homepage": "https://github.com/eremannisto/astro-i18n#readme",
8
+ "repository": {
9
+ "type": "git",
10
+ "url": "https://github.com/eremannisto/astro-i18n"
11
+ },
12
+ "bugs": {
13
+ "url": "https://github.com/eremannisto/astro-i18n/issues"
14
+ },
15
+ "keywords": [
16
+ "astro",
17
+ "astro-integration",
18
+ "i18n",
19
+ "internationalisation",
20
+ "routing",
21
+ "translations"
22
+ ],
23
+ "exports": {
24
+ ".": {
25
+ "types": "./dist/index.d.ts",
26
+ "default": "./dist/index.js"
27
+ },
28
+ "./runtime": {
29
+ "types": "./dist/runtime.d.ts",
30
+ "default": "./dist/runtime.js"
31
+ },
32
+ "./middleware": {
33
+ "types": "./dist/middleware.d.ts",
34
+ "default": "./dist/middleware.js"
35
+ },
36
+ "./detect/server": {
37
+ "types": "./dist/detect/server.d.ts",
38
+ "default": "./dist/detect/server.js"
39
+ },
40
+ "./detect/client": {
41
+ "types": "./dist/detect/client.d.ts",
42
+ "default": "./dist/detect/client.js"
43
+ }
44
+ },
45
+ "typesVersions": {
46
+ "*": {
47
+ "runtime": [
48
+ "./dist/runtime.d.ts"
49
+ ],
50
+ "middleware": [
51
+ "./dist/middleware.d.ts"
52
+ ],
53
+ "detect/server": [
54
+ "./dist/detect/server.d.ts"
55
+ ],
56
+ "detect/client": [
57
+ "./dist/detect/client.d.ts"
58
+ ]
59
+ }
60
+ },
61
+ "files": [
62
+ "dist"
63
+ ],
64
+ "peerDependencies": {
65
+ "astro": "^5.0.0"
66
+ },
67
+ "devDependencies": {
68
+ "@biomejs/biome": "^2.0.0",
69
+ "@playwright/test": "^1.50.0",
70
+ "@types/node": "^22.0.0",
71
+ "astro": "^5.17.3",
72
+ "prettier": "^3.8.1",
73
+ "prettier-plugin-astro": "^0.14.1",
74
+ "tsup": "^8.5.1",
75
+ "typescript": "^5.7.0",
76
+ "vitest": "^3.0.0"
77
+ },
78
+ "scripts": {
79
+ "build": "tsup",
80
+ "format": "prettier --write .",
81
+ "lint": "biome lint",
82
+ "check": "biome check",
83
+ "test:unit": "vitest run",
84
+ "test:e2e": "pnpm build > /dev/null 2>&1 && playwright test",
85
+ "test": "pnpm run test:unit && pnpm run test:e2e"
86
+ }
87
+ }