@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 +21 -0
- package/README.md +68 -0
- package/dist/bridge/astro-bridge.d.ts +13 -0
- package/dist/components/index.d.ts +0 -0
- package/dist/composer.d.ts +44 -0
- package/dist/env.d.ts +15 -0
- package/dist/index-C-UMdqSG.cjs +1 -0
- package/dist/index-CVhedN6W.js +146 -0
- package/dist/index.cjs +1 -0
- package/dist/index.d.ts +10 -0
- package/dist/index.mjs +414 -0
- package/dist/integration.d.ts +24 -0
- package/dist/middleware.d.ts +23 -0
- package/dist/routing.d.ts +23 -0
- package/dist/toolbar-app.d.ts +2 -0
- package/dist/utils.d.ts +71 -0
- package/package.json +53 -0
- package/src/components/i18n-group.astro +15 -0
- package/src/components/i18n-link.astro +40 -0
- package/src/components/i18n-switcher.astro +52 -0
- package/src/components/i18n-t.astro +71 -0
- package/src/components/index.ts +5 -0
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;
|
package/dist/index.d.ts
ADDED
|
@@ -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;
|
package/dist/utils.d.ts
ADDED
|
@@ -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
|
+
|