@i18n-micro/astro 1.0.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/LICENSE ADDED
@@ -0,0 +1,21 @@
1
+ The MIT License (MIT)
2
+
3
+ Copyright (c) 2016
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,68 @@
1
+ # @i18n-micro/astro
2
+
3
+ Astro integration for internationalization using the same core logic as Nuxt I18n Micro. Provides translations, route-specific support, and full TypeScript support for Astro projects.
4
+
5
+ ## Installation
6
+
7
+ ```bash
8
+ pnpm add @i18n-micro/astro
9
+ # or
10
+ npm install @i18n-micro/astro
11
+ # or
12
+ yarn add @i18n-micro/astro
13
+ ```
14
+
15
+ ## Quick Start
16
+
17
+ ### 1. Add Integration to `astro.config.mjs`
18
+
19
+ ```javascript
20
+ import { defineConfig } from 'astro/config'
21
+ import { i18nIntegration } from '@i18n-micro/astro'
22
+
23
+ export default defineConfig({
24
+ integrations: [
25
+ i18nIntegration({
26
+ locale: 'en',
27
+ fallbackLocale: 'en',
28
+ locales: [
29
+ { code: 'en', iso: 'en-US', displayName: 'English' },
30
+ { code: 'fr', iso: 'fr-FR', displayName: 'Français' },
31
+ ],
32
+ messages: {
33
+ en: { greeting: 'Hello, {name}!' },
34
+ fr: { greeting: 'Bonjour, {name}!' },
35
+ },
36
+ }),
37
+ ],
38
+ })
39
+ ```
40
+
41
+ ### 2. Use in Pages
42
+
43
+ ```astro
44
+ ---
45
+ // src/pages/index.astro
46
+ import { useI18n } from '@i18n-micro/astro'
47
+
48
+ const { t, locale } = useI18n(Astro)
49
+ ---
50
+
51
+ <html>
52
+ <head>
53
+ <title>{t('greeting', { name: 'World' })}</title>
54
+ </head>
55
+ <body>
56
+ <h1>{t('greeting', { name: 'World' })}</h1>
57
+ </body>
58
+ </html>
59
+ ```
60
+
61
+ ## Resources
62
+
63
+ - **Repository**: [https://github.com/s00d/nuxt-i18n-micro](https://github.com/s00d/nuxt-i18n-micro)
64
+ - **Documentation**: [https://s00d.github.io/nuxt-i18n-micro/](https://s00d.github.io/nuxt-i18n-micro/)
65
+
66
+ ## License
67
+
68
+ MIT
@@ -0,0 +1,13 @@
1
+ import { I18nDevToolsBridge } from '@i18n-micro/devtools-ui';
2
+ /**
3
+ * Astro Toolbar Server interface (simplified)
4
+ */
5
+ export interface AstroToolbarServer {
6
+ send(event: string, data?: unknown): void;
7
+ on(event: string, callback: (data: unknown) => void): void;
8
+ off(event: string, callback: (data: unknown) => void): void;
9
+ }
10
+ /**
11
+ * Create an Astro Toolbar bridge that adapts Astro Toolbar Server API to I18nDevToolsBridge interface
12
+ */
13
+ export declare function createAstroBridge(server: AstroToolbarServer): I18nDevToolsBridge;
File without changes
@@ -0,0 +1,44 @@
1
+ import { TranslationCache } from '@i18n-micro/core';
2
+ import { Translations, Params, PluralFunc, CleanTranslation, TranslationKey } from '@i18n-micro/types';
3
+ export interface AstroI18nOptions {
4
+ locale: string;
5
+ fallbackLocale?: string;
6
+ messages?: Record<string, Translations>;
7
+ plural?: PluralFunc;
8
+ missingWarn?: boolean;
9
+ missingHandler?: (locale: string, key: string, routeName: string) => void;
10
+ _cache?: TranslationCache;
11
+ }
12
+ export declare class AstroI18n {
13
+ private _locale;
14
+ private _fallbackLocale;
15
+ private _currentRoute;
16
+ private helper;
17
+ private formatter;
18
+ private pluralFunc;
19
+ private missingWarn;
20
+ private missingHandler?;
21
+ readonly cache: TranslationCache;
22
+ private initialMessages;
23
+ constructor(options: AstroI18nOptions);
24
+ clone(newLocale?: string): AstroI18n;
25
+ get locale(): string;
26
+ set locale(val: string);
27
+ get fallbackLocale(): string;
28
+ set fallbackLocale(val: string);
29
+ get currentRoute(): string;
30
+ setRoute(routeName: string): void;
31
+ getRoute(): string;
32
+ t(key: TranslationKey, params?: Params, defaultValue?: string | null, routeName?: string): CleanTranslation;
33
+ ts(key: TranslationKey, params?: Params, defaultValue?: string, routeName?: string): string;
34
+ tc(key: TranslationKey, count: number | Params, defaultValue?: string): string;
35
+ tn(value: number, options?: Intl.NumberFormatOptions): string;
36
+ td(value: Date | number | string, options?: Intl.DateTimeFormatOptions): string;
37
+ tdr(value: Date | number | string, options?: Intl.RelativeTimeFormatOptions): string;
38
+ has(key: TranslationKey, _routeName?: string): boolean;
39
+ addTranslations(locale: string, translations: Translations, merge?: boolean): void;
40
+ addRouteTranslations(locale: string, routeName: string, translations: Translations, merge?: boolean): void;
41
+ mergeTranslations(locale: string, routeName: string, translations: Translations): void;
42
+ mergeGlobalTranslations(locale: string, translations: Translations): void;
43
+ clearCache(): void;
44
+ }
package/dist/env.d.ts ADDED
@@ -0,0 +1,15 @@
1
+ import { AstroI18n } from './composer';
2
+ import { Locale } from '@i18n-micro/types';
3
+ declare global {
4
+ namespace App {
5
+ interface Locals {
6
+ i18n: AstroI18n
7
+ locale: string
8
+ defaultLocale: string
9
+ locales: Locale[]
10
+ currentUrl: URL
11
+ }
12
+ }
13
+ }
14
+
15
+ export {}
@@ -0,0 +1 @@
1
+ "use strict";Object.defineProperty(exports,Symbol.toStringTag,{value:"Module"});const p=require("node:fs/promises"),u=require("node:path"),l=require("@i18n-micro/core");async function g(c,e=!1){const t={global:{},routes:{}};try{const a=await p.readdir(c,{recursive:!0,withFileTypes:!0});for(const o of a){if(!o.isFile()||!o.name.endsWith(".json"))continue;const r=u.join(o.path,o.name),s=u.relative(c,r).split(u.sep),i=u.basename(o.name,".json");try{const n=await p.readFile(r,"utf-8"),h=JSON.parse(n);if(!e&&s[0]==="pages"&&s.length>=2){const f=s.slice(1,-1);if(f.length>0){const d=f.join("-");t.routes[d]||(t.routes[d]={}),t.routes[d][i]=h}}else t.global[i]=h}catch(n){console.error(`Failed to load translation file ${r}:`,n)}}}catch(a){a&&typeof a=="object"&&"code"in a&&a.code!=="ENOENT"&&console.error(`Failed to read directory ${c}:`,a)}return t}class m{constructor(e){this.currentRoute="general",this.cache={generalLocaleCache:{},routeLocaleCache:{},dynamicTranslationsCaches:[],serverTranslationCache:{}},this.helper=l.useTranslationHelper(this.cache),this.formatter=new l.FormatService,this.locale=e.locale,this.fallbackLocale=e.fallbackLocale||e.locale,this.translationDir=e.translationDir,this.disablePageLocales=e.disablePageLocales??!1,this.pluralFunc=e.plural||l.defaultPlural,this.missingWarn=e.missingWarn??!1,this.missingHandler=e.missingHandler}setRoute(e){this.currentRoute=e}getRoute(){return this.currentRoute}t(e,t,a,o){if(!e)return"";const r=o||this.currentRoute;let s=this.helper.getTranslation(this.locale,r,e);return!s&&this.locale!==this.fallbackLocale&&(s=this.helper.getTranslation(this.fallbackLocale,r,e)),s||(this.missingHandler?this.missingHandler(this.locale,e,r):this.missingWarn&&console.warn(`[i18n] Translation key '${e}' not found for locale '${this.locale}' (route: '${r}').`),s=a===void 0?e:a||e),typeof s=="string"&&t?l.interpolate(s,t):s||e}ts(e,t,a,o){var r;return((r=this.t(e,t,a,o))==null?void 0:r.toString())??a??e}tc(e,t,a){const{count:o,...r}=typeof t=="number"?{count:t}:t;if(o===void 0)return a??e;const s=(i,n,h)=>this.t(i,n,h);return this.pluralFunc(e,Number.parseInt(o.toString()),r,this.locale,s)??a??e}tn(e,t){return this.formatter.formatNumber(e,this.locale,t)}td(e,t){return this.formatter.formatDate(e,this.locale,t)}tdr(e,t){return this.formatter.formatRelativeTime(e,this.locale,t)}async loadTranslations(e){const t=e||this.translationDir;if(!t){console.warn("[i18n-node] No translation directory specified");return}const{global:a,routes:o}=await g(t,this.disablePageLocales);for(const[r,s]of Object.entries(a))this.helper.mergeGlobalTranslation(r,s,!0);for(const[r,s]of Object.entries(o))for(const[i,n]of Object.entries(s))await this.helper.loadPageTranslations(i,r,n)}async reload(){this.helper.clearCache(),await this.loadTranslations(),console.log("[i18n-node] Cache cleared and translations reloaded.")}addTranslations(e,t,a=!0){a?this.helper.mergeGlobalTranslation(e,t,!0):this.helper.loadTranslations(e,t)}addRouteTranslations(e,t,a,o=!0){o?this.helper.mergeTranslation(e,t,a,!0):this.helper.loadPageTranslations(e,t,a)}hasTranslation(e){return this.helper.hasTranslation(this.locale,e)}clear(){this.helper.clearCache()}}Object.defineProperty(exports,"FormatService",{enumerable:!0,get:()=>l.FormatService});Object.defineProperty(exports,"interpolate",{enumerable:!0,get:()=>l.interpolate});exports.I18n=m;exports.loadTranslations=g;
@@ -0,0 +1,146 @@
1
+ import { readdir as d, readFile as f } from "node:fs/promises";
2
+ import { join as m, relative as p, sep as g, basename as T } from "node:path";
3
+ import { useTranslationHelper as b, FormatService as y, defaultPlural as j, interpolate as w } from "@i18n-micro/core";
4
+ import { FormatService as k, interpolate as H } from "@i18n-micro/core";
5
+ async function F(i, t = !1) {
6
+ const e = {
7
+ global: {},
8
+ routes: {}
9
+ };
10
+ try {
11
+ const a = await d(i, { recursive: !0, withFileTypes: !0 });
12
+ for (const o of a) {
13
+ if (!o.isFile() || !o.name.endsWith(".json"))
14
+ continue;
15
+ const r = m(o.path, o.name), s = p(i, r).split(g), n = T(o.name, ".json");
16
+ try {
17
+ const l = await f(r, "utf-8"), c = JSON.parse(l);
18
+ if (!t && s[0] === "pages" && s.length >= 2) {
19
+ const u = s.slice(1, -1);
20
+ if (u.length > 0) {
21
+ const h = u.join("-");
22
+ e.routes[h] || (e.routes[h] = {}), e.routes[h][n] = c;
23
+ }
24
+ } else
25
+ e.global[n] = c;
26
+ } catch (l) {
27
+ console.error(`Failed to load translation file ${r}:`, l);
28
+ }
29
+ }
30
+ } catch (a) {
31
+ a && typeof a == "object" && "code" in a && a.code !== "ENOENT" && console.error(`Failed to read directory ${i}:`, a);
32
+ }
33
+ return e;
34
+ }
35
+ class C {
36
+ constructor(t) {
37
+ this.currentRoute = "general", this.cache = {
38
+ generalLocaleCache: {},
39
+ routeLocaleCache: {},
40
+ dynamicTranslationsCaches: [],
41
+ serverTranslationCache: {}
42
+ }, this.helper = b(this.cache), this.formatter = new y(), this.locale = t.locale, this.fallbackLocale = t.fallbackLocale || t.locale, this.translationDir = t.translationDir, this.disablePageLocales = t.disablePageLocales ?? !1, this.pluralFunc = t.plural || j, this.missingWarn = t.missingWarn ?? !1, this.missingHandler = t.missingHandler;
43
+ }
44
+ /**
45
+ * Set the current route name context.
46
+ * Useful when processing a specific page request in Node.
47
+ */
48
+ setRoute(t) {
49
+ this.currentRoute = t;
50
+ }
51
+ getRoute() {
52
+ return this.currentRoute;
53
+ }
54
+ /**
55
+ * Get translation for a key
56
+ *
57
+ * Search order:
58
+ * 1. Current Locale + Current Route (Specific)
59
+ * 2. Current Locale + General (Global)
60
+ * 3. Fallback Locale + Current Route
61
+ * 4. Fallback Locale + General
62
+ */
63
+ t(t, e, a, o) {
64
+ if (!t) return "";
65
+ const r = o || this.currentRoute;
66
+ let s = this.helper.getTranslation(this.locale, r, t);
67
+ return !s && this.locale !== this.fallbackLocale && (s = this.helper.getTranslation(this.fallbackLocale, r, t)), s || (this.missingHandler ? this.missingHandler(this.locale, t, r) : this.missingWarn && console.warn(
68
+ `[i18n] Translation key '${t}' not found for locale '${this.locale}' (route: '${r}').`
69
+ ), s = a === void 0 ? t : a || t), typeof s == "string" && e ? w(s, e) : s || t;
70
+ }
71
+ /**
72
+ * Get translation as string
73
+ */
74
+ ts(t, e, a, o) {
75
+ var r;
76
+ return ((r = this.t(t, e, a, o)) == null ? void 0 : r.toString()) ?? a ?? t;
77
+ }
78
+ /**
79
+ * Plural translation
80
+ */
81
+ tc(t, e, a) {
82
+ const { count: o, ...r } = typeof e == "number" ? { count: e } : e;
83
+ if (o === void 0)
84
+ return a ?? t;
85
+ const s = (n, l, c) => this.t(n, l, c);
86
+ return this.pluralFunc(
87
+ t,
88
+ Number.parseInt(o.toString()),
89
+ r,
90
+ this.locale,
91
+ s
92
+ ) ?? a ?? t;
93
+ }
94
+ // --- Formatters ---
95
+ tn(t, e) {
96
+ return this.formatter.formatNumber(t, this.locale, e);
97
+ }
98
+ td(t, e) {
99
+ return this.formatter.formatDate(t, this.locale, e);
100
+ }
101
+ tdr(t, e) {
102
+ return this.formatter.formatRelativeTime(t, this.locale, e);
103
+ }
104
+ // --- Loader & Cache Management ---
105
+ /**
106
+ * Load translations from directory.
107
+ */
108
+ async loadTranslations(t) {
109
+ const e = t || this.translationDir;
110
+ if (!e) {
111
+ console.warn("[i18n-node] No translation directory specified");
112
+ return;
113
+ }
114
+ const { global: a, routes: o } = await F(e, this.disablePageLocales);
115
+ for (const [r, s] of Object.entries(a))
116
+ this.helper.mergeGlobalTranslation(r, s, !0);
117
+ for (const [r, s] of Object.entries(o))
118
+ for (const [n, l] of Object.entries(s))
119
+ await this.helper.loadPageTranslations(n, r, l);
120
+ }
121
+ /**
122
+ * Clear cache and reload translations from disk
123
+ */
124
+ async reload() {
125
+ this.helper.clearCache(), await this.loadTranslations(), console.log("[i18n-node] Cache cleared and translations reloaded.");
126
+ }
127
+ // --- Manual Manipulation ---
128
+ addTranslations(t, e, a = !0) {
129
+ a ? this.helper.mergeGlobalTranslation(t, e, !0) : this.helper.loadTranslations(t, e);
130
+ }
131
+ addRouteTranslations(t, e, a, o = !0) {
132
+ o ? this.helper.mergeTranslation(t, e, a, !0) : this.helper.loadPageTranslations(t, e, a);
133
+ }
134
+ hasTranslation(t) {
135
+ return this.helper.hasTranslation(this.locale, t);
136
+ }
137
+ clear() {
138
+ this.helper.clearCache();
139
+ }
140
+ }
141
+ export {
142
+ k as FormatService,
143
+ C as I18n,
144
+ H as interpolate,
145
+ F as loadTranslations
146
+ };
package/dist/index.cjs ADDED
@@ -0,0 +1 @@
1
+ "use strict";Object.defineProperty(exports,Symbol.toStringTag,{value:"Module"});const L=require("@i18n-micro/core"),D=require("node:fs"),R=require("node:path");function $(s){const e=Object.create(null,{[Symbol.toStringTag]:{value:"Module"}});if(s){for(const t in s)if(t!=="default"){const r=Object.getOwnPropertyDescriptor(s,t);Object.defineProperty(e,t,r.get?r:{enumerable:!0,get:()=>s[t]})}}return e.default=s,Object.freeze(e)}const b=$(D),y=$(R);class S{constructor(e){if(this.formatter=new L.FormatService,this.initialMessages={},this._locale=e.locale,this._fallbackLocale=e.fallbackLocale||e.locale,this._currentRoute="general",this.pluralFunc=e.plural||L.defaultPlural,this.missingWarn=e.missingWarn??!1,this.missingHandler=e.missingHandler,this.cache=e._cache||{generalLocaleCache:{},routeLocaleCache:{},dynamicTranslationsCaches:[],serverTranslationCache:{}},this.helper=L.useTranslationHelper(this.cache),e.messages){this.initialMessages={...e.messages};for(const[t,r]of Object.entries(e.messages))this.helper.loadTranslations(t,r)}}clone(e){return new S({locale:e||this._locale,fallbackLocale:this._fallbackLocale,plural:this.pluralFunc,missingWarn:this.missingWarn,missingHandler:this.missingHandler,_cache:this.cache})}get locale(){return this._locale}set locale(e){this._locale=e}get fallbackLocale(){return this._fallbackLocale}set fallbackLocale(e){this._fallbackLocale=e}get currentRoute(){return this._currentRoute}setRoute(e){this._currentRoute=e}getRoute(){return this._currentRoute}t(e,t,r,o){if(!e)return"";const l=o||this._currentRoute,a=this._locale;let n=this.helper.getTranslation(a,l,e);return!n&&a!==this._fallbackLocale&&(n=this.helper.getTranslation(this._fallbackLocale,l,e)),n||(this.missingHandler?this.missingHandler(a,e,l):this.missingWarn&&console.warn(`[i18n] Translation key '${e}' not found for locale '${a}' (route: '${l}').`),n=r===void 0?e:r||e),typeof n=="string"&&t?L.interpolate(n,t):n||e}ts(e,t,r,o){const l=this.t(e,t,r,o);return(l==null?void 0:l.toString())??r??e}tc(e,t,r){const{count:o,...l}=typeof t=="number"?{count:t}:t;if(o===void 0)return r??e;const a=(i,c,u)=>{const h=this.t(i,c,u);return typeof h=="string"?h:""};return this.pluralFunc(e,Number.parseInt(o.toString()),l,this._locale,a)??r??e}tn(e,t){return this.formatter.formatNumber(e,this._locale,t)}td(e,t){return this.formatter.formatDate(e,this._locale,t)}tdr(e,t){return this.formatter.formatRelativeTime(e,this._locale,t)}has(e,t){return this.helper.hasTranslation(this._locale,e)}addTranslations(e,t,r=!0){r?this.helper.mergeGlobalTranslation(e,t,!0):this.helper.loadTranslations(e,t)}addRouteTranslations(e,t,r,o=!0){o?this.helper.mergeTranslation(e,t,r,!0):this.helper.loadPageTranslations(e,t,r)}mergeTranslations(e,t,r){this.helper.mergeTranslation(e,t,r,!0)}mergeGlobalTranslations(e,t){this.helper.mergeGlobalTranslation(e,t,!0)}clearCache(){const e={...this.initialMessages};if(this.helper.clearCache(),Object.keys(e).length>0)for(const[t,r]of Object.entries(e))this.helper.loadTranslations(t,r)}}const F='<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><circle cx="12" cy="12" r="10"/><line x1="2" y1="12" x2="22" y2="12"/><path d="M12 2a15.3 15.3 0 0 1 4 10 15.3 15.3 0 0 1-4 10 15.3 15.3 0 0 1-4-10 15.3 15.3 0 0 1 4-10z"/></svg>';function C(s){const{locale:e,fallbackLocale:t,translationDir:r}=s;return{name:"@i18n-micro/astro",hooks:{"astro:config:setup":async({addDevToolbarApp:o,command:l,injectTypes:a})=>{if(a&&a({filename:"i18n-micro-env.d.ts",content:'/// <reference types="@i18n-micro/astro/env" />'}),r)try{const{I18n:n}=await Promise.resolve().then(()=>require("./index-C-UMdqSG.cjs"));await new n({locale:e,fallbackLocale:t||e,translationDir:r,disablePageLocales:s.disablePageLocales??!1}).loadTranslations()}catch(n){console.warn("[i18n] Could not load translations from directory:",n)}l==="dev"&&o({id:"i18n-micro",name:"i18n Micro",icon:F,entrypoint:"@i18n-micro/astro/toolbar-app"})},"astro:server:setup":async({toolbar:o,logger:l})=>{if(!o)return;const a=process.cwd(),n=y.join(a,r||"src/locales"),i=()=>{const c={};if(!b.existsSync(n))return l.warn(`[i18n] Locales directory does not exist: ${n}`),c;const u=h=>{b.readdirSync(h).forEach(d=>{const f=y.join(h,d);if(d.startsWith("."))return;if(b.lstatSync(f).isDirectory())u(f);else if(d.endsWith(".json"))try{const g=y.relative(n,f),m=JSON.parse(b.readFileSync(f,"utf-8"));m&&typeof m=="object"&&!Array.isArray(m)?c[g]=m:l.warn(`[i18n] Invalid content format in ${f}, expected object`)}catch(g){l.warn(`Error parsing locale file ${f}: ${g instanceof Error?g.message:String(g)}`)}})};return u(n),c};o.on("i18n:get-locales",async()=>{try{const c=i(),u=Object.keys(c);l.info(`[i18n] Sending locales data: ${u.length} files`),l.info(`[i18n] Locales keys: ${u.join(", ")}`),u.length>0&&u[0]&&l.info(`[i18n] Sample file content keys: ${Object.keys(c[u[0]]).slice(0,5).join(", ")}`),o.send("i18n:locales-data",c)}catch(c){l.error(`Error in i18n:get-locales handler: ${c}`),o.send("i18n:locales-data",{})}}),o.on("i18n:get-configs",()=>{o.send("i18n:configs-data",{defaultLocale:e,fallbackLocale:t||e,locales:s.locales||[],translationDir:r||"src/locales",...s})}),o.on("i18n:save",async c=>{try{const u=y.join(n,c.file);if(!u.startsWith(n))throw new Error("Access denied: Cannot write outside locales directory");b.existsSync(u)?(b.writeFileSync(u,JSON.stringify(c.content,null,2),"utf-8"),o.send("i18n:save-result",{success:!0}),o.send("i18n:updated",i())):o.send("i18n:save-result",{success:!1,error:`File not found: ${c.file}`})}catch(u){l.error(`Error saving translation: ${u instanceof Error?u.message:String(u)}`),o.send("i18n:save-result",{success:!1,error:String(u)})}})}}}}function M(s){return new S(s)}function P(s,e=[]){const t=s.replace(/^\//,"").replace(/\/$/,"");if(!t)return"index";const r=t.split("/").filter(Boolean),o=r[0];return o&&e.includes(o)&&r.shift(),r.length===0?"index":r.join("-")}function v(s,e="en",t=[]){const o=s.split("/").filter(Boolean)[0];return o&&t.includes(o)?o:e}function w(s,e,t=[],r){const o=s.split("/").filter(Boolean),l=o[0];return l&&t.includes(l)&&o.shift(),(e!==r||r===void 0)&&o.unshift(e),`/${o.join("/")}`}function k(s,e,t=[],r){const l=(s.replace(/^\//,"").replace(/\/$/,"")||"").split("/").filter(Boolean),a=l[0];return a&&t.includes(a)&&l.shift(),(e!==r||r===void 0)&&l.unshift(e),`/${l.join("/")}`}function N(s,e=[]){const t=s.split("/").filter(Boolean),r=t[0];return r&&e.includes(r)&&t.shift(),`/${t.join("/")}`}function B(s){const{i18n:e,defaultLocale:t,locales:r,localeObjects:o,autoDetect:l=!0,redirectToDefault:a=!1}=s;return async(n,i)=>{if(n.locals.locale&&n.locals.i18n)return i();const c=n.url,u=c.pathname,d=u.split("/").filter(Boolean)[0],f=d!==void 0&&r.includes(d);let p;f&&d?p=d:p=t;const g=e.clone(p),m=P(u,r);return g.setRoute(m),n.locals.i18n=g,n.locals.locale=p,n.locals.defaultLocale=t,n.locals.locales=o||r.map(O=>({code:O})),n.locals.currentUrl=c,i()}}function q(s){var r;const e=[],t=s.split(",");for(const o of t){const[l,a="1.0"]=o.trim().split(";q=");if(Number.parseFloat(a)>0&&l){const i=(r=l.split("-")[0])==null?void 0:r.toLowerCase();i&&(e.push(i),l!==i&&e.push(l.toLowerCase()))}}return e}function A(s,e,t,r,o,l="i18n-locale"){var n;let a=v(s,r,o);if(a===r&&e.get(l)){const i=(n=e.get(l))==null?void 0:n.value;i&&o.includes(i)&&(a=i)}if(a===r)try{const i=t.get("accept-language");if(i){const c=q(i);for(const u of c)if(o.includes(u)){a=u;break}}}catch{}return a}function I(s){const e=s.locals.i18n;if(!e)throw new Error("i18n instance not found. Make sure i18n middleware is configured.");return e}function T(s){return s.locals.locale||"en"}function _(s){return s.locals.defaultLocale||"en"}function j(s){return s.locals.locales||[]}function E(s){const e=I(s),t=T(s),r=_(s),o=j(s),l=o.map(a=>a.code);return{locale:t,defaultLocale:r,locales:o,t:(a,n,i,c)=>e.t(a,n,i,c),ts:(a,n,i,c)=>e.ts(a,n,i,c),tc:(a,n,i)=>e.tc(a,n,i),tn:(a,n)=>e.tn(a,n),td:(a,n)=>e.td(a,n),tdr:(a,n)=>e.tdr(a,n),has:(a,n)=>e.has(a,n),getRoute:()=>e.getRoute(),getRouteName:a=>P(a||s.url.pathname,l),getLocaleFromPath:a=>v(a||s.url.pathname,r,l),switchLocalePath:a=>w(s.url.pathname,a,l,r),localizePath:(a,n)=>k(a,n||t,l,r),getI18n:()=>e,getBasePath:a=>{const c=(a||s.url).pathname.split("/").filter(Boolean),u=c[0];return u&&l.includes(u)&&c.shift(),c.length>0?`/${c.join("/")}`:"/"},addTranslations:(a,n,i=!0)=>{e.addTranslations(a,n,i)},addRouteTranslations:(a,n,i,c=!0)=>{e.addRouteTranslations(a,n,i,c)},mergeTranslations:(a,n,i)=>{e.mergeTranslations(a,n,i)},mergeGlobalTranslations:(a,n)=>{e.mergeGlobalTranslations(a,n)},clearCache:()=>{e.clearCache()}}}function H(s,e={}){const{baseUrl:t="/",addDirAttribute:r=!0,addSeoAttributes:o=!0}=e,l=T(s),a=_(s),n=j(s),i=n.find(f=>f.code===l);if(!i)return{htmlAttrs:{},link:[],meta:[]};const c=i.iso||l,u=i.dir||"auto",h={htmlAttrs:{lang:c,...r?{dir:u}:{}},link:[],meta:[]};if(!o)return h;const d=`${t}${s.url.pathname}`;h.link.push({rel:"canonical",href:d});for(const f of n){if(f.code===l)continue;const p=w(s.url.pathname,f.code,n.map(m=>m.code),a),g=`${t}${p}`;h.link.push({rel:"alternate",href:g,hreflang:f.code}),f.iso&&f.iso!==f.code&&h.link.push({rel:"alternate",href:g,hreflang:f.iso})}h.meta.push({property:"og:locale",content:c}),h.meta.push({property:"og:url",content:d});for(const f of n)f.code!==l&&h.meta.push({property:"og:locale:alternate",content:f.iso||f.code});return h}Object.defineProperty(exports,"FormatService",{enumerable:!0,get:()=>L.FormatService});Object.defineProperty(exports,"defaultPlural",{enumerable:!0,get:()=>L.defaultPlural});Object.defineProperty(exports,"interpolate",{enumerable:!0,get:()=>L.interpolate});exports.AstroI18n=S;exports.createI18n=M;exports.createI18nMiddleware=B;exports.detectLocale=A;exports.getDefaultLocale=_;exports.getI18n=I;exports.getLocale=T;exports.getLocaleFromPath=v;exports.getLocales=j;exports.getRouteName=P;exports.i18nIntegration=C;exports.localizePath=k;exports.removeLocaleFromPath=N;exports.switchLocalePath=w;exports.useI18n=E;exports.useLocaleHead=H;
@@ -0,0 +1,10 @@
1
+ export { i18nIntegration, createI18n } from './integration';
2
+ export { AstroI18n, type AstroI18nOptions } from './composer';
3
+ export { createI18nMiddleware, detectLocale } from './middleware';
4
+ export type { I18nMiddlewareOptions } from './middleware';
5
+ export { useI18n, getI18n, getLocale, getDefaultLocale, getLocales, useLocaleHead, } from './utils';
6
+ export type { LocaleHeadOptions, LocaleHeadResult } from './utils';
7
+ export { getRouteName, getLocaleFromPath, switchLocalePath, localizePath, removeLocaleFromPath, } from './routing';
8
+ export type { Translations, Params, PluralFunc, Getter, Locale, LocaleCode, CleanTranslation, } from '@i18n-micro/types';
9
+ export { interpolate, FormatService, defaultPlural } from '@i18n-micro/core';
10
+ export type { I18nIntegrationOptions } from './integration';
package/dist/index.mjs ADDED
@@ -0,0 +1,414 @@
1
+ import { FormatService as k, defaultPlural as j, useTranslationHelper as R, interpolate as D } from "@i18n-micro/core";
2
+ import { FormatService as q, defaultPlural as z, interpolate as J } from "@i18n-micro/core";
3
+ import * as L from "node:fs";
4
+ import * as y from "node:path";
5
+ class w {
6
+ constructor(e) {
7
+ if (this.formatter = new k(), this.initialMessages = {}, this._locale = e.locale, this._fallbackLocale = e.fallbackLocale || e.locale, this._currentRoute = "general", this.pluralFunc = e.plural || j, this.missingWarn = e.missingWarn ?? !1, this.missingHandler = e.missingHandler, this.cache = e._cache || {
8
+ generalLocaleCache: {},
9
+ routeLocaleCache: {},
10
+ dynamicTranslationsCaches: [],
11
+ serverTranslationCache: {}
12
+ }, this.helper = R(this.cache), e.messages) {
13
+ this.initialMessages = { ...e.messages };
14
+ for (const [r, s] of Object.entries(e.messages))
15
+ this.helper.loadTranslations(r, s);
16
+ }
17
+ }
18
+ // Создать легкую копию для нового запроса с тем же кэшем
19
+ clone(e) {
20
+ return new w({
21
+ locale: e || this._locale,
22
+ fallbackLocale: this._fallbackLocale,
23
+ plural: this.pluralFunc,
24
+ missingWarn: this.missingWarn,
25
+ missingHandler: this.missingHandler,
26
+ _cache: this.cache
27
+ // ШЕРИМ КЭШ
28
+ });
29
+ }
30
+ // Геттер/Сеттер для локали
31
+ get locale() {
32
+ return this._locale;
33
+ }
34
+ set locale(e) {
35
+ this._locale = e;
36
+ }
37
+ // Геттер/Сеттер для fallback локали
38
+ get fallbackLocale() {
39
+ return this._fallbackLocale;
40
+ }
41
+ set fallbackLocale(e) {
42
+ this._fallbackLocale = e;
43
+ }
44
+ // Геттер/Сеттер для текущего роута
45
+ get currentRoute() {
46
+ return this._currentRoute;
47
+ }
48
+ setRoute(e) {
49
+ this._currentRoute = e;
50
+ }
51
+ getRoute() {
52
+ return this._currentRoute;
53
+ }
54
+ // Методы перевода
55
+ t(e, r, s, l) {
56
+ if (!e) return "";
57
+ const a = l || this._currentRoute, n = this._locale;
58
+ let t = this.helper.getTranslation(n, a, e);
59
+ return !t && n !== this._fallbackLocale && (t = this.helper.getTranslation(this._fallbackLocale, a, e)), t || (this.missingHandler ? this.missingHandler(n, e, a) : this.missingWarn && console.warn(
60
+ `[i18n] Translation key '${e}' not found for locale '${n}' (route: '${a}').`
61
+ ), t = s === void 0 ? e : s || e), typeof t == "string" && r ? D(t, r) : t || e;
62
+ }
63
+ ts(e, r, s, l) {
64
+ const a = this.t(e, r, s, l);
65
+ return (a == null ? void 0 : a.toString()) ?? s ?? e;
66
+ }
67
+ tc(e, r, s) {
68
+ const { count: l, ...a } = typeof r == "number" ? { count: r } : r;
69
+ if (l === void 0)
70
+ return s ?? e;
71
+ const n = (o, c, u) => {
72
+ const h = this.t(o, c, u);
73
+ return typeof h == "string" ? h : "";
74
+ };
75
+ return this.pluralFunc(
76
+ e,
77
+ Number.parseInt(l.toString()),
78
+ a,
79
+ this._locale,
80
+ n
81
+ ) ?? s ?? e;
82
+ }
83
+ tn(e, r) {
84
+ return this.formatter.formatNumber(e, this._locale, r);
85
+ }
86
+ td(e, r) {
87
+ return this.formatter.formatDate(e, this._locale, r);
88
+ }
89
+ tdr(e, r) {
90
+ return this.formatter.formatRelativeTime(e, this._locale, r);
91
+ }
92
+ has(e, r) {
93
+ return this.helper.hasTranslation(this._locale, e);
94
+ }
95
+ // Методы для добавления переводов
96
+ addTranslations(e, r, s = !0) {
97
+ s ? this.helper.mergeGlobalTranslation(e, r, !0) : this.helper.loadTranslations(e, r);
98
+ }
99
+ addRouteTranslations(e, r, s, l = !0) {
100
+ l ? this.helper.mergeTranslation(e, r, s, !0) : this.helper.loadPageTranslations(e, r, s);
101
+ }
102
+ mergeTranslations(e, r, s) {
103
+ this.helper.mergeTranslation(e, r, s, !0);
104
+ }
105
+ mergeGlobalTranslations(e, r) {
106
+ this.helper.mergeGlobalTranslation(e, r, !0);
107
+ }
108
+ clearCache() {
109
+ const e = { ...this.initialMessages };
110
+ if (this.helper.clearCache(), Object.keys(e).length > 0)
111
+ for (const [r, s] of Object.entries(e))
112
+ this.helper.loadTranslations(r, s);
113
+ }
114
+ }
115
+ const I = '<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><circle cx="12" cy="12" r="10"/><line x1="2" y1="12" x2="22" y2="12"/><path d="M12 2a15.3 15.3 0 0 1 4 10 15.3 15.3 0 0 1-4 10 15.3 15.3 0 0 1-4-10 15.3 15.3 0 0 1 4-10z"/></svg>';
116
+ function E(i) {
117
+ const {
118
+ locale: e,
119
+ fallbackLocale: r,
120
+ translationDir: s
121
+ } = i;
122
+ return {
123
+ name: "@i18n-micro/astro",
124
+ hooks: {
125
+ "astro:config:setup": async ({ addDevToolbarApp: l, command: a, injectTypes: n }) => {
126
+ if (n && n({
127
+ filename: "i18n-micro-env.d.ts",
128
+ content: '/// <reference types="@i18n-micro/astro/env" />'
129
+ }), s)
130
+ try {
131
+ const { I18n: t } = await import("./index-CVhedN6W.js");
132
+ await new t({
133
+ locale: e,
134
+ fallbackLocale: r || e,
135
+ translationDir: s,
136
+ disablePageLocales: i.disablePageLocales ?? !1
137
+ }).loadTranslations();
138
+ } catch (t) {
139
+ console.warn("[i18n] Could not load translations from directory:", t);
140
+ }
141
+ a === "dev" && l({
142
+ id: "i18n-micro",
143
+ name: "i18n Micro",
144
+ icon: I,
145
+ // Используем package export путь, который определен в package.json
146
+ // Это работает как в монорепозитории, так и после установки пакета
147
+ entrypoint: "@i18n-micro/astro/toolbar-app"
148
+ });
149
+ },
150
+ "astro:server:setup": async ({ toolbar: l, logger: a }) => {
151
+ if (!l) return;
152
+ const n = process.cwd(), t = y.join(n, s || "src/locales"), o = () => {
153
+ const c = {};
154
+ if (!L.existsSync(t))
155
+ return a.warn(`[i18n] Locales directory does not exist: ${t}`), c;
156
+ const u = (h) => {
157
+ L.readdirSync(h).forEach((d) => {
158
+ const f = y.join(h, d);
159
+ if (d.startsWith(".")) return;
160
+ if (L.lstatSync(f).isDirectory())
161
+ u(f);
162
+ else if (d.endsWith(".json"))
163
+ try {
164
+ const g = y.relative(t, f), m = JSON.parse(L.readFileSync(f, "utf-8"));
165
+ m && typeof m == "object" && !Array.isArray(m) ? c[g] = m : a.warn(`[i18n] Invalid content format in ${f}, expected object`);
166
+ } catch (g) {
167
+ a.warn(`Error parsing locale file ${f}: ${g instanceof Error ? g.message : String(g)}`);
168
+ }
169
+ });
170
+ };
171
+ return u(t), c;
172
+ };
173
+ l.on("i18n:get-locales", async () => {
174
+ try {
175
+ const c = o(), u = Object.keys(c);
176
+ a.info(`[i18n] Sending locales data: ${u.length} files`), a.info(`[i18n] Locales keys: ${u.join(", ")}`), u.length > 0 && u[0] && a.info(`[i18n] Sample file content keys: ${Object.keys(c[u[0]]).slice(0, 5).join(", ")}`), l.send("i18n:locales-data", c);
177
+ } catch (c) {
178
+ a.error(`Error in i18n:get-locales handler: ${c}`), l.send("i18n:locales-data", {});
179
+ }
180
+ }), l.on("i18n:get-configs", () => {
181
+ l.send("i18n:configs-data", {
182
+ defaultLocale: e,
183
+ fallbackLocale: r || e,
184
+ locales: i.locales || [],
185
+ translationDir: s || "src/locales",
186
+ ...i
187
+ });
188
+ }), l.on("i18n:save", async (c) => {
189
+ try {
190
+ const u = y.join(t, c.file);
191
+ if (!u.startsWith(t))
192
+ throw new Error("Access denied: Cannot write outside locales directory");
193
+ L.existsSync(u) ? (L.writeFileSync(u, JSON.stringify(c.content, null, 2), "utf-8"), l.send("i18n:save-result", { success: !0 }), l.send("i18n:updated", o())) : l.send("i18n:save-result", { success: !1, error: `File not found: ${c.file}` });
194
+ } catch (u) {
195
+ a.error(`Error saving translation: ${u instanceof Error ? u.message : String(u)}`), l.send("i18n:save-result", { success: !1, error: String(u) });
196
+ }
197
+ });
198
+ }
199
+ }
200
+ };
201
+ }
202
+ function N(i) {
203
+ return new w(i);
204
+ }
205
+ function b(i, e = []) {
206
+ const r = i.replace(/^\//, "").replace(/\/$/, "");
207
+ if (!r)
208
+ return "index";
209
+ const s = r.split("/").filter(Boolean), l = s[0];
210
+ return l && e.includes(l) && s.shift(), s.length === 0 ? "index" : s.join("-");
211
+ }
212
+ function T(i, e = "en", r = []) {
213
+ const l = i.split("/").filter(Boolean)[0];
214
+ return l && r.includes(l) ? l : e;
215
+ }
216
+ function v(i, e, r = [], s) {
217
+ const l = i.split("/").filter(Boolean), a = l[0];
218
+ return a && r.includes(a) && l.shift(), (e !== s || s === void 0) && l.unshift(e), `/${l.join("/")}`;
219
+ }
220
+ function C(i, e, r = [], s) {
221
+ const a = (i.replace(/^\//, "").replace(/\/$/, "") || "").split("/").filter(Boolean), n = a[0];
222
+ return n && r.includes(n) && a.shift(), (e !== s || s === void 0) && a.unshift(e), `/${a.join("/")}`;
223
+ }
224
+ function A(i, e = []) {
225
+ const r = i.split("/").filter(Boolean), s = r[0];
226
+ return s && e.includes(s) && r.shift(), `/${r.join("/")}`;
227
+ }
228
+ function H(i) {
229
+ const {
230
+ i18n: e,
231
+ // Это глобальный синглтон с кэшем
232
+ defaultLocale: r,
233
+ locales: s,
234
+ localeObjects: l,
235
+ autoDetect: a = !0,
236
+ redirectToDefault: n = !1
237
+ } = i;
238
+ return async (t, o) => {
239
+ if (t.locals.locale && t.locals.i18n)
240
+ return o();
241
+ const c = t.url, u = c.pathname, d = u.split("/").filter(Boolean)[0], f = d !== void 0 && s.includes(d);
242
+ let p;
243
+ f && d ? p = d : p = r;
244
+ const g = e.clone(p), m = b(u, s);
245
+ return g.setRoute(m), t.locals.i18n = g, t.locals.locale = p, t.locals.defaultLocale = r, t.locals.locales = l || s.map(($) => ({ code: $ })), t.locals.currentUrl = c, o();
246
+ };
247
+ }
248
+ function F(i) {
249
+ var s;
250
+ const e = [], r = i.split(",");
251
+ for (const l of r) {
252
+ const [a, n = "1.0"] = l.trim().split(";q=");
253
+ if (Number.parseFloat(n) > 0 && a) {
254
+ const o = (s = a.split("-")[0]) == null ? void 0 : s.toLowerCase();
255
+ o && (e.push(o), a !== o && e.push(a.toLowerCase()));
256
+ }
257
+ }
258
+ return e;
259
+ }
260
+ function M(i, e, r, s, l, a = "i18n-locale") {
261
+ var t;
262
+ let n = T(i, s, l);
263
+ if (n === s && e.get(a)) {
264
+ const o = (t = e.get(a)) == null ? void 0 : t.value;
265
+ o && l.includes(o) && (n = o);
266
+ }
267
+ if (n === s)
268
+ try {
269
+ const o = r.get("accept-language");
270
+ if (o) {
271
+ const c = F(o);
272
+ for (const u of c)
273
+ if (l.includes(u)) {
274
+ n = u;
275
+ break;
276
+ }
277
+ }
278
+ } catch {
279
+ }
280
+ return n;
281
+ }
282
+ function O(i) {
283
+ const e = i.locals.i18n;
284
+ if (!e)
285
+ throw new Error("i18n instance not found. Make sure i18n middleware is configured.");
286
+ return e;
287
+ }
288
+ function S(i) {
289
+ return i.locals.locale || "en";
290
+ }
291
+ function _(i) {
292
+ return i.locals.defaultLocale || "en";
293
+ }
294
+ function P(i) {
295
+ return i.locals.locales || [];
296
+ }
297
+ function W(i) {
298
+ const e = O(i), r = S(i), s = _(i), l = P(i), a = l.map((n) => n.code);
299
+ return {
300
+ // Current locale
301
+ locale: r,
302
+ defaultLocale: s,
303
+ locales: l,
304
+ // Translation methods
305
+ t: (n, t, o, c) => e.t(n, t, o, c),
306
+ ts: (n, t, o, c) => e.ts(n, t, o, c),
307
+ tc: (n, t, o) => e.tc(n, t, o),
308
+ tn: (n, t) => e.tn(n, t),
309
+ td: (n, t) => e.td(n, t),
310
+ tdr: (n, t) => e.tdr(n, t),
311
+ has: (n, t) => e.has(n, t),
312
+ // Route management
313
+ getRoute: () => e.getRoute(),
314
+ getRouteName: (n) => b(n || i.url.pathname, a),
315
+ getLocaleFromPath: (n) => T(n || i.url.pathname, s, a),
316
+ // Path utilities
317
+ switchLocalePath: (n) => v(i.url.pathname, n, a, s),
318
+ localizePath: (n, t) => C(n, t || r, a, s),
319
+ // Get i18n instance
320
+ getI18n: () => e,
321
+ // Get base path without locale (for rewrite)
322
+ getBasePath: (n) => {
323
+ const c = (n || i.url).pathname.split("/").filter(Boolean), u = c[0];
324
+ return u && a.includes(u) && c.shift(), c.length > 0 ? `/${c.join("/")}` : "/";
325
+ },
326
+ // Translation management
327
+ addTranslations: (n, t, o = !0) => {
328
+ e.addTranslations(n, t, o);
329
+ },
330
+ addRouteTranslations: (n, t, o, c = !0) => {
331
+ e.addRouteTranslations(n, t, o, c);
332
+ },
333
+ mergeTranslations: (n, t, o) => {
334
+ e.mergeTranslations(n, t, o);
335
+ },
336
+ mergeGlobalTranslations: (n, t) => {
337
+ e.mergeGlobalTranslations(n, t);
338
+ },
339
+ clearCache: () => {
340
+ e.clearCache();
341
+ }
342
+ };
343
+ }
344
+ function G(i, e = {}) {
345
+ const {
346
+ baseUrl: r = "/",
347
+ addDirAttribute: s = !0,
348
+ addSeoAttributes: l = !0
349
+ } = e, a = S(i), n = _(i), t = P(i), o = t.find((f) => f.code === a);
350
+ if (!o)
351
+ return { htmlAttrs: {}, link: [], meta: [] };
352
+ const c = o.iso || a, u = o.dir || "auto", h = {
353
+ htmlAttrs: {
354
+ lang: c,
355
+ ...s ? { dir: u } : {}
356
+ },
357
+ link: [],
358
+ meta: []
359
+ };
360
+ if (!l)
361
+ return h;
362
+ const d = `${r}${i.url.pathname}`;
363
+ h.link.push({
364
+ rel: "canonical",
365
+ href: d
366
+ });
367
+ for (const f of t) {
368
+ if (f.code === a) continue;
369
+ const p = v(i.url.pathname, f.code, t.map((m) => m.code), n), g = `${r}${p}`;
370
+ h.link.push({
371
+ rel: "alternate",
372
+ href: g,
373
+ hreflang: f.code
374
+ }), f.iso && f.iso !== f.code && h.link.push({
375
+ rel: "alternate",
376
+ href: g,
377
+ hreflang: f.iso
378
+ });
379
+ }
380
+ h.meta.push({
381
+ property: "og:locale",
382
+ content: c
383
+ }), h.meta.push({
384
+ property: "og:url",
385
+ content: d
386
+ });
387
+ for (const f of t)
388
+ f.code !== a && h.meta.push({
389
+ property: "og:locale:alternate",
390
+ content: f.iso || f.code
391
+ });
392
+ return h;
393
+ }
394
+ export {
395
+ w as AstroI18n,
396
+ q as FormatService,
397
+ N as createI18n,
398
+ H as createI18nMiddleware,
399
+ z as defaultPlural,
400
+ M as detectLocale,
401
+ _ as getDefaultLocale,
402
+ O as getI18n,
403
+ S as getLocale,
404
+ T as getLocaleFromPath,
405
+ P as getLocales,
406
+ b as getRouteName,
407
+ E as i18nIntegration,
408
+ J as interpolate,
409
+ C as localizePath,
410
+ A as removeLocaleFromPath,
411
+ v as switchLocalePath,
412
+ W as useI18n,
413
+ G as useLocaleHead
414
+ };
@@ -0,0 +1,24 @@
1
+ import { AstroIntegration } from 'astro';
2
+ import { AstroI18n, AstroI18nOptions } from './composer';
3
+ import { Locale, ModuleOptions, PluralFunc } from '@i18n-micro/types';
4
+ export interface I18nIntegrationOptions extends Omit<ModuleOptions, 'plural'> {
5
+ locale: string;
6
+ fallbackLocale?: string;
7
+ locales?: Locale[];
8
+ messages?: Record<string, Record<string, unknown>>;
9
+ plural?: PluralFunc;
10
+ missingWarn?: boolean;
11
+ missingHandler?: (locale: string, key: string, routeName: string) => void;
12
+ localeCookie?: string;
13
+ autoDetect?: boolean;
14
+ redirectToDefault?: boolean;
15
+ translationDir?: string;
16
+ }
17
+ /**
18
+ * Astro Integration for i18n-micro
19
+ */
20
+ export declare function i18nIntegration(options: I18nIntegrationOptions): AstroIntegration;
21
+ /**
22
+ * Create i18n instance (for manual setup)
23
+ */
24
+ export declare function createI18n(options: AstroI18nOptions): AstroI18n;
@@ -0,0 +1,23 @@
1
+ import { AstroI18n } from './composer';
2
+ import { MiddlewareHandler } from 'astro';
3
+ import { Locale } from '@i18n-micro/types';
4
+ export interface I18nMiddlewareOptions {
5
+ i18n: AstroI18n;
6
+ defaultLocale: string;
7
+ locales: string[];
8
+ localeObjects?: Locale[];
9
+ autoDetect?: boolean;
10
+ redirectToDefault?: boolean;
11
+ }
12
+ /**
13
+ * Create Astro middleware for i18n locale detection
14
+ */
15
+ export declare function createI18nMiddleware(options: I18nMiddlewareOptions): MiddlewareHandler;
16
+ /**
17
+ * Detect locale from request
18
+ */
19
+ export declare function detectLocale(pathname: string, cookies: {
20
+ get: (name: string) => {
21
+ value: string;
22
+ } | undefined;
23
+ }, headers: Headers, defaultLocale: string, locales: string[], localeCookie?: string): string;
@@ -0,0 +1,23 @@
1
+ /**
2
+ * Get route name from Astro path
3
+ * Extracts route name from path (e.g., /en/about -> about)
4
+ */
5
+ export declare function getRouteName(path: string, locales?: string[]): string;
6
+ /**
7
+ * Get locale from path
8
+ * Checks if first segment is a locale code
9
+ */
10
+ export declare function getLocaleFromPath(path: string, defaultLocale?: string, locales?: string[]): string;
11
+ /**
12
+ * Switch locale in path
13
+ * Replaces or adds locale prefix to path
14
+ */
15
+ export declare function switchLocalePath(path: string, newLocale: string, locales?: string[], defaultLocale?: string): string;
16
+ /**
17
+ * Localize path with locale prefix
18
+ */
19
+ export declare function localizePath(path: string, locale: string, locales?: string[], defaultLocale?: string): string;
20
+ /**
21
+ * Remove locale from path
22
+ */
23
+ export declare function removeLocaleFromPath(path: string, locales?: string[]): string;
@@ -0,0 +1,2 @@
1
+ declare const _default: import('astro').DevToolbarApp;
2
+ export default _default;
@@ -0,0 +1,71 @@
1
+ import { AstroI18n } from './composer';
2
+ import { Params, Locale, CleanTranslation, TranslationKey } from '@i18n-micro/types';
3
+ import { AstroGlobal } from 'astro';
4
+ /**
5
+ * Get i18n instance from Astro context
6
+ */
7
+ export declare function getI18n(astro: AstroGlobal): AstroI18n;
8
+ /**
9
+ * Get current locale from Astro context
10
+ */
11
+ export declare function getLocale(astro: AstroGlobal): string;
12
+ /**
13
+ * Get default locale from Astro context
14
+ */
15
+ export declare function getDefaultLocale(astro: AstroGlobal): string;
16
+ /**
17
+ * Get all locales from Astro context
18
+ */
19
+ export declare function getLocales(astro: AstroGlobal): Locale[];
20
+ /**
21
+ * Use i18n in Astro pages/components
22
+ * Returns helper functions for translations
23
+ */
24
+ export declare function useI18n(astro: AstroGlobal): {
25
+ locale: string;
26
+ defaultLocale: string;
27
+ locales: Locale[];
28
+ t: (key: TranslationKey, params?: Params, defaultValue?: string | null, routeName?: string) => CleanTranslation;
29
+ ts: (key: TranslationKey, params?: Params, defaultValue?: string, routeName?: string) => string;
30
+ tc: (key: TranslationKey, count: number | Params, defaultValue?: string) => string;
31
+ tn: (value: number, options?: Intl.NumberFormatOptions) => string;
32
+ td: (value: Date | number | string, options?: Intl.DateTimeFormatOptions) => string;
33
+ tdr: (value: Date | number | string, options?: Intl.RelativeTimeFormatOptions) => string;
34
+ has: (key: TranslationKey, routeName?: string) => boolean;
35
+ getRoute: () => string;
36
+ getRouteName: (path?: string) => string;
37
+ getLocaleFromPath: (path?: string) => string;
38
+ switchLocalePath: (newLocale: string) => string;
39
+ localizePath: (path: string, targetLocale?: string) => string;
40
+ getI18n: () => AstroI18n;
41
+ getBasePath: (url?: URL) => string;
42
+ addTranslations: (locale: string, translations: Record<string, unknown>, merge?: boolean) => void;
43
+ addRouteTranslations: (locale: string, routeName: string, translations: Record<string, unknown>, merge?: boolean) => void;
44
+ mergeTranslations: (locale: string, routeName: string, translations: Record<string, unknown>) => void;
45
+ mergeGlobalTranslations: (locale: string, translations: Record<string, unknown>) => void;
46
+ clearCache: () => void;
47
+ };
48
+ /**
49
+ * Generate locale head meta tags for SEO
50
+ */
51
+ export interface LocaleHeadOptions {
52
+ baseUrl?: string;
53
+ addDirAttribute?: boolean;
54
+ addSeoAttributes?: boolean;
55
+ }
56
+ export interface LocaleHeadResult {
57
+ htmlAttrs: {
58
+ lang?: string;
59
+ dir?: 'ltr' | 'rtl' | 'auto';
60
+ };
61
+ link: Array<{
62
+ rel: string;
63
+ href: string;
64
+ hreflang?: string;
65
+ }>;
66
+ meta: Array<{
67
+ property: string;
68
+ content: string;
69
+ }>;
70
+ }
71
+ export declare function useLocaleHead(astro: AstroGlobal, options?: LocaleHeadOptions): LocaleHeadResult;
package/package.json ADDED
@@ -0,0 +1,53 @@
1
+ {
2
+ "name": "@i18n-micro/astro",
3
+ "version": "1.0.0",
4
+ "type": "module",
5
+ "main": "./dist/index.cjs",
6
+ "module": "./dist/index.mjs",
7
+ "types": "./dist/index.d.ts",
8
+ "exports": {
9
+ ".": {
10
+ "types": "./dist/index.d.ts",
11
+ "import": "./dist/index.mjs",
12
+ "require": "./dist/index.cjs"
13
+ },
14
+ "./env": {
15
+ "types": "./dist/env.d.ts"
16
+ },
17
+ "./components/i18n-t.astro": "./src/components/i18n-t.astro",
18
+ "./components/i18n-link.astro": "./src/components/i18n-link.astro",
19
+ "./components/i18n-switcher.astro": "./src/components/i18n-switcher.astro",
20
+ "./components/i18n-group.astro": "./src/components/i18n-group.astro",
21
+ "./toolbar-app": "./src/toolbar-app.ts"
22
+ },
23
+ "files": [
24
+ "dist",
25
+ "src/components"
26
+ ],
27
+ "publishConfig": {
28
+ "access": "public"
29
+ },
30
+ "dependencies": {
31
+ "@i18n-micro/devtools-ui": "1.0.0",
32
+ "@i18n-micro/core": "1.0.27",
33
+ "@i18n-micro/types": "1.0.15",
34
+ "@i18n-micro/node": "1.0.0"
35
+ },
36
+ "peerDependencies": {
37
+ "astro": "^5.0.0"
38
+ },
39
+ "devDependencies": {
40
+ "astro": "^5.0.0",
41
+ "vite": "^5.0.0",
42
+ "vite-plugin-dts": "^4.3.0",
43
+ "jest": "^29.7.0",
44
+ "ts-jest": "^29.1.2",
45
+ "@types/jest": "^29.5.0"
46
+ },
47
+ "scripts": {
48
+ "build": "vite build",
49
+ "dev": "cd playground && astro dev",
50
+ "test": "jest",
51
+ "typecheck": "tsc --noEmit"
52
+ }
53
+ }
@@ -0,0 +1,15 @@
1
+ ---
2
+ interface Props {
3
+ route?: string
4
+ [key: string]: unknown
5
+ }
6
+
7
+ const { route: _route, ...attrs } = Astro.props
8
+ // Note: Route setting would need to be handled at page level
9
+ // This component is mainly for grouping translations
10
+ ---
11
+
12
+ <div {...attrs}>
13
+ <slot />
14
+ </div>
15
+
@@ -0,0 +1,40 @@
1
+ ---
2
+ import { useI18n } from '../utils'
3
+ import { localizePath, removeLocaleFromPath } from '../routing'
4
+
5
+ interface Props {
6
+ href: string
7
+ locale?: string
8
+ class?: string
9
+ [key: string]: unknown
10
+ }
11
+
12
+ const { href, locale, class: className, ...attrs } = Astro.props
13
+ const { locale: currentLocale, defaultLocale, locales } = useI18n(Astro)
14
+ const localeCodes = locales.map(l => l.code)
15
+ const currentPath = Astro.url.pathname
16
+
17
+ // Get current locale from path or use detected locale
18
+ const targetLocale = locale || currentLocale
19
+
20
+ // Remove locale from current path to get base path
21
+ const basePath = removeLocaleFromPath(currentPath, localeCodes)
22
+
23
+ // If href is relative, resolve it relative to current base path
24
+ let resolvedHref = href
25
+ if (href.startsWith('/')) {
26
+ // Absolute path - remove locale if present and use as base
27
+ resolvedHref = removeLocaleFromPath(href, localeCodes)
28
+ } else {
29
+ // Relative path - combine with current base path
30
+ resolvedHref = basePath === '/' ? `/${href}` : `${basePath}/${href}`
31
+ }
32
+
33
+ // Localize the href
34
+ const localizedHref = localizePath(resolvedHref, targetLocale, localeCodes, defaultLocale)
35
+ ---
36
+
37
+ <a href={localizedHref} class={className} {...attrs}>
38
+ <slot />
39
+ </a>
40
+
@@ -0,0 +1,52 @@
1
+ ---
2
+ import { useI18n } from '../utils'
3
+ import { switchLocalePath } from '../routing'
4
+ import type { Locale } from '@i18n-micro/types'
5
+
6
+ interface Props {
7
+ class?: string
8
+ activeClass?: string
9
+ [key: string]: unknown
10
+ }
11
+
12
+ const { class: className, activeClass = 'active', ...attrs } = Astro.props
13
+
14
+ const { locale: currentLocale, locales, defaultLocale, getBasePath } = useI18n(Astro)
15
+ const localeCodes = locales.map((l: Locale) => l.code)
16
+ const currentPath = Astro.url.pathname
17
+
18
+ // Filter out disabled locales
19
+ const availableLocales = locales.filter((loc: Locale) => !loc.disabled)
20
+
21
+ // Generate locale links
22
+ const localeLinks = availableLocales.map((loc: Locale) => {
23
+ const isActive = loc.code === currentLocale
24
+ // Get base path without locale
25
+ const basePath = getBasePath()
26
+ // Generate path with locale
27
+ const switchPath = switchLocalePath(basePath, loc.code, localeCodes, defaultLocale)
28
+
29
+ return {
30
+ code: loc.code,
31
+ displayName: loc.displayName || loc.code,
32
+ path: switchPath,
33
+ isActive,
34
+ }
35
+ })
36
+ ---
37
+
38
+ {localeLinks.length > 0 && (
39
+ <Fragment>
40
+ {localeLinks.map((link) => (
41
+ <a
42
+ href={link.path}
43
+ class={link.isActive ? activeClass : ''}
44
+ aria-current={link.isActive ? 'page' : undefined}
45
+ data-locale={link.code}
46
+ >
47
+ {link.displayName}
48
+ </a>
49
+ ))}
50
+ </Fragment>
51
+ )}
52
+
@@ -0,0 +1,71 @@
1
+ ---
2
+ import { useI18n } from '../utils'
3
+ import type { Params } from '@i18n-micro/types'
4
+
5
+ interface Props {
6
+ keypath: string
7
+ plural?: number | string
8
+ params?: Params
9
+ defaultValue?: string
10
+ tag?: string
11
+ html?: boolean
12
+ number?: number | string
13
+ date?: Date | string | number
14
+ relativeDate?: Date | string | number
15
+ }
16
+
17
+ const {
18
+ keypath,
19
+ plural,
20
+ params = {},
21
+ defaultValue = '',
22
+ tag = 'span',
23
+ html: renderHtml = false,
24
+ number,
25
+ date,
26
+ relativeDate,
27
+ } = Astro.props
28
+
29
+ const { t, tc, tn, td, tdr } = useI18n(Astro)
30
+
31
+ let translation: string = ''
32
+
33
+ // Handle number formatting
34
+ if (number !== undefined) {
35
+ const numberValue = Number(number)
36
+ const formattedNumber = tn(numberValue)
37
+ translation = t(keypath, { number: formattedNumber, ...params }, defaultValue)?.toString() || defaultValue || keypath
38
+ }
39
+ // Handle date formatting
40
+ else if (date !== undefined) {
41
+ const formattedDate = td(date)
42
+ translation = t(keypath, { date: formattedDate, ...params }, defaultValue)?.toString() || defaultValue || keypath
43
+ }
44
+ // Handle relative date formatting
45
+ else if (relativeDate !== undefined) {
46
+ const formattedRelativeDate = tdr(relativeDate)
47
+ translation = t(keypath, { relativeDate: formattedRelativeDate, ...params }, defaultValue)?.toString() || defaultValue || keypath
48
+ }
49
+ // Handle pluralization
50
+ else if (plural !== undefined) {
51
+ const count = Number.parseInt(plural.toString())
52
+ translation = tc(keypath, { count, ...params }, defaultValue)
53
+ }
54
+ // Regular translation
55
+ else {
56
+ translation = t(keypath, params, defaultValue)?.toString() || defaultValue || keypath
57
+ }
58
+ ---
59
+
60
+ {renderHtml ? (
61
+ <Fragment set:html={`<${tag}>${translation}</${tag}>`} />
62
+ ) : tag === 'span' ? (
63
+ <span>{translation}</span>
64
+ ) : tag === 'div' ? (
65
+ <div>{translation}</div>
66
+ ) : tag === 'p' ? (
67
+ <p>{translation}</p>
68
+ ) : (
69
+ <span>{translation}</span>
70
+ )}
71
+
@@ -0,0 +1,5 @@
1
+ // Astro components are imported directly from their file paths
2
+ // This file is for documentation purposes only
3
+
4
+ // Astro components are imported directly from their file paths
5
+ // Example: import I18nT from '@i18n-micro/astro/components/i18n-t.astro'