@zachhandley/ez-i18n 0.1.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +211 -0
- package/dist/index.d.ts +29 -0
- package/dist/index.js +212 -0
- package/dist/middleware.js +37 -0
- package/dist/runtime/index.d.ts +51 -0
- package/dist/runtime/index.js +68 -0
- package/dist/runtime/vue-plugin.d.ts +52 -0
- package/dist/runtime/vue-plugin.js +110 -0
- package/dist/types-DwCG8sp8.d.ts +48 -0
- package/package.json +89 -0
- package/src/components/EzI18nHead.astro +62 -0
- package/src/index.ts +122 -0
- package/src/middleware.ts +61 -0
- package/src/runtime/index.ts +19 -0
- package/src/runtime/store.ts +122 -0
- package/src/runtime/vue-plugin.ts +137 -0
- package/src/types.ts +61 -0
- package/src/virtual.d.ts +64 -0
- package/src/vite-plugin.ts +146 -0
package/README.md
ADDED
|
@@ -0,0 +1,211 @@
|
|
|
1
|
+
# ez-i18n
|
|
2
|
+
|
|
3
|
+
Cookie-based i18n for Astro + Vue. No URL prefixes, reactive language switching.
|
|
4
|
+
|
|
5
|
+
## Installation
|
|
6
|
+
|
|
7
|
+
```bash
|
|
8
|
+
pnpm add ez-i18n nanostores @nanostores/persistent
|
|
9
|
+
# If using Vue:
|
|
10
|
+
pnpm add @nanostores/vue
|
|
11
|
+
```
|
|
12
|
+
|
|
13
|
+
## Usage
|
|
14
|
+
|
|
15
|
+
### Astro Config
|
|
16
|
+
|
|
17
|
+
```typescript
|
|
18
|
+
// astro.config.ts
|
|
19
|
+
import { defineConfig } from 'astro/config';
|
|
20
|
+
import vue from '@astrojs/vue';
|
|
21
|
+
import ezI18n from 'ez-i18n';
|
|
22
|
+
|
|
23
|
+
export default defineConfig({
|
|
24
|
+
integrations: [
|
|
25
|
+
vue(),
|
|
26
|
+
ezI18n({
|
|
27
|
+
locales: ['en', 'es', 'fr'],
|
|
28
|
+
defaultLocale: 'en',
|
|
29
|
+
cookieName: 'my-locale', // optional, defaults to 'ez-locale'
|
|
30
|
+
translations: {
|
|
31
|
+
en: './src/i18n/en.ts',
|
|
32
|
+
es: './src/i18n/es.ts',
|
|
33
|
+
fr: './src/i18n/fr.ts',
|
|
34
|
+
},
|
|
35
|
+
}),
|
|
36
|
+
],
|
|
37
|
+
});
|
|
38
|
+
```
|
|
39
|
+
|
|
40
|
+
### Translation Files
|
|
41
|
+
|
|
42
|
+
```typescript
|
|
43
|
+
// src/i18n/en.ts
|
|
44
|
+
export default {
|
|
45
|
+
common: {
|
|
46
|
+
welcome: 'Welcome',
|
|
47
|
+
save: 'Save',
|
|
48
|
+
cancel: 'Cancel',
|
|
49
|
+
},
|
|
50
|
+
auth: {
|
|
51
|
+
login: 'Log in',
|
|
52
|
+
signup: 'Sign up',
|
|
53
|
+
},
|
|
54
|
+
};
|
|
55
|
+
```
|
|
56
|
+
|
|
57
|
+
### Layout Setup
|
|
58
|
+
|
|
59
|
+
Add the `EzI18nHead` component to your layout's head for automatic hydration:
|
|
60
|
+
|
|
61
|
+
```astro
|
|
62
|
+
---
|
|
63
|
+
// src/layouts/Layout.astro
|
|
64
|
+
import { EzI18nHead } from 'ez-i18n/astro';
|
|
65
|
+
const { locale, translations } = Astro.locals;
|
|
66
|
+
---
|
|
67
|
+
|
|
68
|
+
<html lang={locale}>
|
|
69
|
+
<head>
|
|
70
|
+
<meta charset="utf-8" />
|
|
71
|
+
<EzI18nHead locale={locale} translations={translations} />
|
|
72
|
+
</head>
|
|
73
|
+
<body>
|
|
74
|
+
<slot />
|
|
75
|
+
</body>
|
|
76
|
+
</html>
|
|
77
|
+
```
|
|
78
|
+
|
|
79
|
+
### In Astro Files
|
|
80
|
+
|
|
81
|
+
```astro
|
|
82
|
+
---
|
|
83
|
+
import { t, locale } from 'ez-i18n:runtime';
|
|
84
|
+
// Or access from locals (auto-loaded by middleware):
|
|
85
|
+
const { locale, translations } = Astro.locals;
|
|
86
|
+
---
|
|
87
|
+
|
|
88
|
+
<h1>{t('common.welcome')}</h1>
|
|
89
|
+
<p>Current locale: {locale}</p>
|
|
90
|
+
```
|
|
91
|
+
|
|
92
|
+
### In Vue Components
|
|
93
|
+
|
|
94
|
+
```vue
|
|
95
|
+
<script setup lang="ts">
|
|
96
|
+
import { useI18n } from 'ez-i18n/vue';
|
|
97
|
+
import { translationLoaders } from 'ez-i18n:translations';
|
|
98
|
+
|
|
99
|
+
const { t, locale, setLocale } = useI18n();
|
|
100
|
+
|
|
101
|
+
// Change locale with dynamic translation loading
|
|
102
|
+
async function switchLocale(newLocale: string) {
|
|
103
|
+
await setLocale(newLocale, {
|
|
104
|
+
loadTranslations: translationLoaders[newLocale],
|
|
105
|
+
});
|
|
106
|
+
}
|
|
107
|
+
</script>
|
|
108
|
+
|
|
109
|
+
<template>
|
|
110
|
+
<!-- Global $t is available automatically -->
|
|
111
|
+
<h1>{{ $t('common.welcome') }}</h1>
|
|
112
|
+
|
|
113
|
+
<!-- Interpolation -->
|
|
114
|
+
<p>{{ $t('greeting', { name: 'World' }) }}</p>
|
|
115
|
+
|
|
116
|
+
<!-- Change language with dynamic loading -->
|
|
117
|
+
<button @click="switchLocale('es')">Español</button>
|
|
118
|
+
<button @click="switchLocale('fr')">Français</button>
|
|
119
|
+
</template>
|
|
120
|
+
```
|
|
121
|
+
|
|
122
|
+
### Vue Plugin Setup
|
|
123
|
+
|
|
124
|
+
Register the Vue plugin in your entrypoint:
|
|
125
|
+
|
|
126
|
+
```typescript
|
|
127
|
+
// src/_vueEntrypoint.ts
|
|
128
|
+
import type { App } from 'vue';
|
|
129
|
+
import { ezI18nVue } from 'ez-i18n/vue';
|
|
130
|
+
|
|
131
|
+
export default (app: App) => {
|
|
132
|
+
app.use(ezI18nVue);
|
|
133
|
+
};
|
|
134
|
+
```
|
|
135
|
+
|
|
136
|
+
## Features
|
|
137
|
+
|
|
138
|
+
- **No URL prefixes** - Locale stored in cookie, not URL path
|
|
139
|
+
- **Reactive** - Language changes update immediately without page reload
|
|
140
|
+
- **SSR compatible** - Proper hydration with server-rendered locale
|
|
141
|
+
- **Vue integration** - Global `$t()`, `$locale`, `$setLocale` in templates
|
|
142
|
+
- **Composable API** - `useI18n()` for Composition API usage
|
|
143
|
+
- **Middleware included** - Auto-detects locale from cookie, query param, or Accept-Language header
|
|
144
|
+
|
|
145
|
+
## Locale Detection Priority
|
|
146
|
+
|
|
147
|
+
1. `?lang=xx` query parameter
|
|
148
|
+
2. Cookie value
|
|
149
|
+
3. Accept-Language header
|
|
150
|
+
4. Default locale
|
|
151
|
+
|
|
152
|
+
## API
|
|
153
|
+
|
|
154
|
+
### `ezI18n(config)`
|
|
155
|
+
|
|
156
|
+
Astro integration function.
|
|
157
|
+
|
|
158
|
+
| Option | Type | Required | Description |
|
|
159
|
+
|--------|------|----------|-------------|
|
|
160
|
+
| `locales` | `string[]` | Yes | Supported locale codes |
|
|
161
|
+
| `defaultLocale` | `string` | Yes | Fallback locale |
|
|
162
|
+
| `cookieName` | `string` | No | Cookie name (default: `'ez-locale'`) |
|
|
163
|
+
| `translations` | `Record<string, string>` | No | Paths to translation files (auto-loaded) |
|
|
164
|
+
|
|
165
|
+
### `EzI18nHead`
|
|
166
|
+
|
|
167
|
+
Astro component for i18n hydration. Place in your layout's `<head>`.
|
|
168
|
+
|
|
169
|
+
```astro
|
|
170
|
+
<EzI18nHead locale={Astro.locals.locale} translations={Astro.locals.translations} />
|
|
171
|
+
```
|
|
172
|
+
|
|
173
|
+
### `$t(key, params?)`
|
|
174
|
+
|
|
175
|
+
Translate a key with optional interpolation.
|
|
176
|
+
|
|
177
|
+
```typescript
|
|
178
|
+
$t('greeting'); // "Hello"
|
|
179
|
+
$t('greeting', { name: 'World' }); // "Hello, {name}" -> "Hello, World"
|
|
180
|
+
```
|
|
181
|
+
|
|
182
|
+
### `setLocale(locale, options?)`
|
|
183
|
+
|
|
184
|
+
Change the current locale. Updates cookie and triggers reactive update.
|
|
185
|
+
|
|
186
|
+
```typescript
|
|
187
|
+
// Simple usage
|
|
188
|
+
setLocale('es');
|
|
189
|
+
|
|
190
|
+
// With dynamic translation loading
|
|
191
|
+
import { translationLoaders } from 'ez-i18n:translations';
|
|
192
|
+
setLocale('es', { loadTranslations: translationLoaders['es'] });
|
|
193
|
+
```
|
|
194
|
+
|
|
195
|
+
### `useI18n()`
|
|
196
|
+
|
|
197
|
+
Vue composable for Composition API usage.
|
|
198
|
+
|
|
199
|
+
```typescript
|
|
200
|
+
const { t, locale, setLocale } = useI18n();
|
|
201
|
+
```
|
|
202
|
+
|
|
203
|
+
### Virtual Modules
|
|
204
|
+
|
|
205
|
+
- `ez-i18n:config` - Static config (locales, defaultLocale, cookieName)
|
|
206
|
+
- `ez-i18n:runtime` - Runtime functions (t, setLocale, initLocale, locale store)
|
|
207
|
+
- `ez-i18n:translations` - Translation loaders (loadTranslations, translationLoaders)
|
|
208
|
+
|
|
209
|
+
## License
|
|
210
|
+
|
|
211
|
+
MIT
|
package/dist/index.d.ts
ADDED
|
@@ -0,0 +1,29 @@
|
|
|
1
|
+
import { AstroIntegration } from 'astro';
|
|
2
|
+
import { E as EzI18nConfig } from './types-DwCG8sp8.js';
|
|
3
|
+
export { T as TranslateFunction } from './types-DwCG8sp8.js';
|
|
4
|
+
|
|
5
|
+
/**
|
|
6
|
+
* ez-i18n Astro integration
|
|
7
|
+
*
|
|
8
|
+
* Provides cookie-based i18n without URL prefixes.
|
|
9
|
+
*
|
|
10
|
+
* @example
|
|
11
|
+
* // astro.config.ts
|
|
12
|
+
* import ezI18n from '@zachhandley/ez-i18n';
|
|
13
|
+
*
|
|
14
|
+
* export default defineConfig({
|
|
15
|
+
* integrations: [
|
|
16
|
+
* ezI18n({
|
|
17
|
+
* locales: ['en', 'es', 'fr'],
|
|
18
|
+
* defaultLocale: 'en',
|
|
19
|
+
* translations: {
|
|
20
|
+
* en: './src/i18n/en.json',
|
|
21
|
+
* es: './src/i18n/es.json',
|
|
22
|
+
* },
|
|
23
|
+
* }),
|
|
24
|
+
* ],
|
|
25
|
+
* });
|
|
26
|
+
*/
|
|
27
|
+
declare function ezI18n(config: EzI18nConfig): AstroIntegration;
|
|
28
|
+
|
|
29
|
+
export { EzI18nConfig, ezI18n as default };
|
package/dist/index.js
ADDED
|
@@ -0,0 +1,212 @@
|
|
|
1
|
+
// src/vite-plugin.ts
|
|
2
|
+
var VIRTUAL_CONFIG = "ez-i18n:config";
|
|
3
|
+
var VIRTUAL_RUNTIME = "ez-i18n:runtime";
|
|
4
|
+
var VIRTUAL_TRANSLATIONS = "ez-i18n:translations";
|
|
5
|
+
var RESOLVED_PREFIX = "\0";
|
|
6
|
+
function resolveConfig(config) {
|
|
7
|
+
return {
|
|
8
|
+
locales: config.locales,
|
|
9
|
+
defaultLocale: config.defaultLocale,
|
|
10
|
+
cookieName: config.cookieName ?? "ez-locale",
|
|
11
|
+
translations: config.translations ?? {}
|
|
12
|
+
};
|
|
13
|
+
}
|
|
14
|
+
function vitePlugin(config) {
|
|
15
|
+
const resolved = resolveConfig(config);
|
|
16
|
+
return {
|
|
17
|
+
name: "ez-i18n-vite",
|
|
18
|
+
enforce: "pre",
|
|
19
|
+
resolveId(id) {
|
|
20
|
+
if (id === VIRTUAL_CONFIG || id === VIRTUAL_RUNTIME || id === VIRTUAL_TRANSLATIONS) {
|
|
21
|
+
return RESOLVED_PREFIX + id;
|
|
22
|
+
}
|
|
23
|
+
return null;
|
|
24
|
+
},
|
|
25
|
+
load(id) {
|
|
26
|
+
if (id === RESOLVED_PREFIX + VIRTUAL_CONFIG) {
|
|
27
|
+
return `
|
|
28
|
+
export const locales = ${JSON.stringify(resolved.locales)};
|
|
29
|
+
export const defaultLocale = ${JSON.stringify(resolved.defaultLocale)};
|
|
30
|
+
export const cookieName = ${JSON.stringify(resolved.cookieName)};
|
|
31
|
+
`;
|
|
32
|
+
}
|
|
33
|
+
if (id === RESOLVED_PREFIX + VIRTUAL_RUNTIME) {
|
|
34
|
+
return `
|
|
35
|
+
import { effectiveLocale, translations, setLocale, initLocale } from '@zachhandley/ez-i18n/runtime';
|
|
36
|
+
|
|
37
|
+
export { setLocale, initLocale };
|
|
38
|
+
export { effectiveLocale as locale };
|
|
39
|
+
|
|
40
|
+
/**
|
|
41
|
+
* Get nested value from object using dot notation
|
|
42
|
+
*/
|
|
43
|
+
function getNestedValue(obj, path) {
|
|
44
|
+
const keys = path.split('.');
|
|
45
|
+
let value = obj;
|
|
46
|
+
for (const key of keys) {
|
|
47
|
+
if (value == null || typeof value !== 'object') return undefined;
|
|
48
|
+
value = value[key];
|
|
49
|
+
}
|
|
50
|
+
return value;
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
/**
|
|
54
|
+
* Interpolate params into string
|
|
55
|
+
*/
|
|
56
|
+
function interpolate(str, params) {
|
|
57
|
+
if (!params) return str;
|
|
58
|
+
return str.replace(/\\{(\\w+)\\}/g, (match, key) => {
|
|
59
|
+
return key in params ? String(params[key]) : match;
|
|
60
|
+
});
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
/**
|
|
64
|
+
* Translate a key to the current locale
|
|
65
|
+
* @param key - Dot-notation key (e.g., 'common.welcome')
|
|
66
|
+
* @param params - Optional interpolation params
|
|
67
|
+
*/
|
|
68
|
+
export function t(key, params) {
|
|
69
|
+
const trans = translations.get();
|
|
70
|
+
const value = getNestedValue(trans, key);
|
|
71
|
+
|
|
72
|
+
if (typeof value !== 'string') {
|
|
73
|
+
if (import.meta.env.DEV) {
|
|
74
|
+
console.warn('[ez-i18n] Missing translation:', key);
|
|
75
|
+
}
|
|
76
|
+
return key;
|
|
77
|
+
}
|
|
78
|
+
|
|
79
|
+
return interpolate(value, params);
|
|
80
|
+
}
|
|
81
|
+
`;
|
|
82
|
+
}
|
|
83
|
+
if (id === RESOLVED_PREFIX + VIRTUAL_TRANSLATIONS) {
|
|
84
|
+
const loaderEntries = Object.entries(resolved.translations).map(([locale, path]) => ` ${JSON.stringify(locale)}: () => import(${JSON.stringify(path)})`).join(",\n");
|
|
85
|
+
return `
|
|
86
|
+
/**
|
|
87
|
+
* Translation loaders for ez-i18n
|
|
88
|
+
* Auto-generated from config
|
|
89
|
+
*/
|
|
90
|
+
export const translationLoaders = {
|
|
91
|
+
${loaderEntries}
|
|
92
|
+
};
|
|
93
|
+
|
|
94
|
+
/**
|
|
95
|
+
* Load translations for a specific locale
|
|
96
|
+
* @param locale - Locale code to load translations for
|
|
97
|
+
* @returns Translations object or empty object if not found
|
|
98
|
+
*/
|
|
99
|
+
export async function loadTranslations(locale) {
|
|
100
|
+
const loader = translationLoaders[locale];
|
|
101
|
+
if (!loader) {
|
|
102
|
+
if (import.meta.env.DEV) {
|
|
103
|
+
console.warn('[ez-i18n] No translations configured for locale:', locale);
|
|
104
|
+
}
|
|
105
|
+
return {};
|
|
106
|
+
}
|
|
107
|
+
|
|
108
|
+
try {
|
|
109
|
+
const mod = await loader();
|
|
110
|
+
return mod.default ?? mod;
|
|
111
|
+
} catch (error) {
|
|
112
|
+
if (import.meta.env.DEV) {
|
|
113
|
+
console.error('[ez-i18n] Failed to load translations for locale:', locale, error);
|
|
114
|
+
}
|
|
115
|
+
return {};
|
|
116
|
+
}
|
|
117
|
+
}
|
|
118
|
+
`;
|
|
119
|
+
}
|
|
120
|
+
return null;
|
|
121
|
+
}
|
|
122
|
+
};
|
|
123
|
+
}
|
|
124
|
+
|
|
125
|
+
// src/index.ts
|
|
126
|
+
function ezI18n(config) {
|
|
127
|
+
const resolved = resolveConfig(config);
|
|
128
|
+
return {
|
|
129
|
+
name: "ez-i18n",
|
|
130
|
+
hooks: {
|
|
131
|
+
"astro:config:setup": ({
|
|
132
|
+
updateConfig,
|
|
133
|
+
addMiddleware,
|
|
134
|
+
injectScript
|
|
135
|
+
}) => {
|
|
136
|
+
updateConfig({
|
|
137
|
+
vite: {
|
|
138
|
+
plugins: [vitePlugin(config)]
|
|
139
|
+
}
|
|
140
|
+
});
|
|
141
|
+
addMiddleware({
|
|
142
|
+
entrypoint: "@zachhandley/ez-i18n/middleware",
|
|
143
|
+
order: "pre"
|
|
144
|
+
});
|
|
145
|
+
const hydrationScript = `
|
|
146
|
+
(function() {
|
|
147
|
+
try {
|
|
148
|
+
var cookieName = ${JSON.stringify(resolved.cookieName)};
|
|
149
|
+
var stored = localStorage.getItem(cookieName);
|
|
150
|
+
var cookieMatch = document.cookie.match(new RegExp(cookieName + '=([^;]+)'));
|
|
151
|
+
var cookie = cookieMatch ? cookieMatch[1] : null;
|
|
152
|
+
if (cookie && stored !== cookie) {
|
|
153
|
+
localStorage.setItem(cookieName, cookie);
|
|
154
|
+
}
|
|
155
|
+
} catch (e) {}
|
|
156
|
+
})();
|
|
157
|
+
`;
|
|
158
|
+
injectScript("head-inline", hydrationScript);
|
|
159
|
+
},
|
|
160
|
+
"astro:config:done": ({
|
|
161
|
+
injectTypes
|
|
162
|
+
}) => {
|
|
163
|
+
injectTypes({
|
|
164
|
+
filename: "virtual.d.ts",
|
|
165
|
+
content: `declare module 'ez-i18n:config' {
|
|
166
|
+
/** List of all supported locale codes */
|
|
167
|
+
export const locales: readonly string[];
|
|
168
|
+
/** Default locale when no preference is detected */
|
|
169
|
+
export const defaultLocale: string;
|
|
170
|
+
/** Cookie name used to store locale preference */
|
|
171
|
+
export const cookieName: string;
|
|
172
|
+
}
|
|
173
|
+
|
|
174
|
+
declare module 'ez-i18n:runtime' {
|
|
175
|
+
import type { ReadableAtom } from 'nanostores';
|
|
176
|
+
/** Reactive store containing the current locale */
|
|
177
|
+
export const locale: ReadableAtom<string>;
|
|
178
|
+
/**
|
|
179
|
+
* Translate a key to the current locale
|
|
180
|
+
* @param key - Dot-notation key (e.g., 'common.welcome')
|
|
181
|
+
* @param params - Optional interpolation params for {placeholder} syntax
|
|
182
|
+
*/
|
|
183
|
+
export function t(key: string, params?: Record<string, string | number>): string;
|
|
184
|
+
/**
|
|
185
|
+
* Set the current locale and persist to cookie/localStorage
|
|
186
|
+
* @param locale - Locale code to switch to
|
|
187
|
+
* @param cookieName - Optional custom cookie name
|
|
188
|
+
*/
|
|
189
|
+
export function setLocale(locale: string, cookieName?: string): Promise<void>;
|
|
190
|
+
/**
|
|
191
|
+
* Initialize the locale store with translations
|
|
192
|
+
* @param locale - Initial locale code
|
|
193
|
+
* @param translations - Optional initial translations object
|
|
194
|
+
*/
|
|
195
|
+
export function initLocale(locale: string, translations?: Record<string, unknown>): void;
|
|
196
|
+
}
|
|
197
|
+
|
|
198
|
+
declare module 'ez-i18n:translations' {
|
|
199
|
+
/** Load translations for a specific locale */
|
|
200
|
+
export function loadTranslations(locale: string): Promise<Record<string, unknown>>;
|
|
201
|
+
/** Get the translation loader map from config */
|
|
202
|
+
export const translationLoaders: Record<string, () => Promise<{ default: Record<string, unknown> }>>;
|
|
203
|
+
}
|
|
204
|
+
`
|
|
205
|
+
});
|
|
206
|
+
}
|
|
207
|
+
}
|
|
208
|
+
};
|
|
209
|
+
}
|
|
210
|
+
export {
|
|
211
|
+
ezI18n as default
|
|
212
|
+
};
|
|
@@ -0,0 +1,37 @@
|
|
|
1
|
+
// src/middleware.ts
|
|
2
|
+
import { defineMiddleware } from "astro:middleware";
|
|
3
|
+
var onRequest = defineMiddleware(async ({ cookies, request, locals }, next) => {
|
|
4
|
+
const { locales, defaultLocale, cookieName } = await import("ez-i18n:config");
|
|
5
|
+
const url = new URL(request.url);
|
|
6
|
+
const langParam = url.searchParams.get("lang");
|
|
7
|
+
const cookieValue = cookies.get(cookieName)?.value;
|
|
8
|
+
const acceptLang = request.headers.get("accept-language");
|
|
9
|
+
const browserLang = acceptLang?.split(",")[0]?.split("-")[0];
|
|
10
|
+
let locale = defaultLocale;
|
|
11
|
+
if (langParam && locales.includes(langParam)) {
|
|
12
|
+
locale = langParam;
|
|
13
|
+
} else if (cookieValue && locales.includes(cookieValue)) {
|
|
14
|
+
locale = cookieValue;
|
|
15
|
+
} else if (browserLang && locales.includes(browserLang)) {
|
|
16
|
+
locale = browserLang;
|
|
17
|
+
}
|
|
18
|
+
locals.locale = locale;
|
|
19
|
+
try {
|
|
20
|
+
const { loadTranslations } = await import("ez-i18n:translations");
|
|
21
|
+
locals.translations = await loadTranslations(locale);
|
|
22
|
+
} catch {
|
|
23
|
+
locals.translations = {};
|
|
24
|
+
}
|
|
25
|
+
if (langParam && langParam !== cookieValue && locales.includes(langParam)) {
|
|
26
|
+
cookies.set(cookieName, locale, {
|
|
27
|
+
path: "/",
|
|
28
|
+
maxAge: 60 * 60 * 24 * 365,
|
|
29
|
+
// 1 year
|
|
30
|
+
sameSite: "lax"
|
|
31
|
+
});
|
|
32
|
+
}
|
|
33
|
+
return next();
|
|
34
|
+
});
|
|
35
|
+
export {
|
|
36
|
+
onRequest
|
|
37
|
+
};
|
|
@@ -0,0 +1,51 @@
|
|
|
1
|
+
import * as nanostores from 'nanostores';
|
|
2
|
+
|
|
3
|
+
/**
|
|
4
|
+
* Client-side locale preference (persisted to localStorage)
|
|
5
|
+
*/
|
|
6
|
+
declare const localePreference: nanostores.WritableAtom<string>;
|
|
7
|
+
/**
|
|
8
|
+
* Effective locale - uses server locale if set, otherwise client preference
|
|
9
|
+
*/
|
|
10
|
+
declare const effectiveLocale: nanostores.ReadableAtom<string>;
|
|
11
|
+
/**
|
|
12
|
+
* Current translations object (reactive)
|
|
13
|
+
*/
|
|
14
|
+
declare const translations: nanostores.PreinitializedWritableAtom<Record<string, unknown>> & object;
|
|
15
|
+
/**
|
|
16
|
+
* Whether locale is currently being changed
|
|
17
|
+
*/
|
|
18
|
+
declare const localeLoading: nanostores.PreinitializedWritableAtom<boolean> & object;
|
|
19
|
+
/**
|
|
20
|
+
* Initialize locale from server-provided value
|
|
21
|
+
* Called during hydration to sync server and client state
|
|
22
|
+
*/
|
|
23
|
+
declare function initLocale(locale: string, trans?: Record<string, unknown>): void;
|
|
24
|
+
/**
|
|
25
|
+
* Set the translations object
|
|
26
|
+
*/
|
|
27
|
+
declare function setTranslations(trans: Record<string, unknown>): void;
|
|
28
|
+
/** Type for translation loader function */
|
|
29
|
+
type TranslationLoader = () => Promise<{
|
|
30
|
+
default?: Record<string, unknown>;
|
|
31
|
+
} | Record<string, unknown>>;
|
|
32
|
+
/**
|
|
33
|
+
* Change locale and update cookie
|
|
34
|
+
* Optionally loads new translations dynamically
|
|
35
|
+
* @param locale - New locale code
|
|
36
|
+
* @param options - Options object or cookie name for backwards compatibility
|
|
37
|
+
*/
|
|
38
|
+
declare function setLocale(locale: string, options?: string | {
|
|
39
|
+
cookieName?: string;
|
|
40
|
+
loadTranslations?: TranslationLoader;
|
|
41
|
+
}): Promise<void>;
|
|
42
|
+
/**
|
|
43
|
+
* Get current locale value (non-reactive)
|
|
44
|
+
*/
|
|
45
|
+
declare function getLocale(): string;
|
|
46
|
+
/**
|
|
47
|
+
* Get current translations (non-reactive)
|
|
48
|
+
*/
|
|
49
|
+
declare function getTranslations(): Record<string, unknown>;
|
|
50
|
+
|
|
51
|
+
export { type TranslationLoader, effectiveLocale, getLocale, getTranslations, initLocale, localeLoading, localePreference, setLocale, setTranslations, translations };
|
|
@@ -0,0 +1,68 @@
|
|
|
1
|
+
// src/runtime/store.ts
|
|
2
|
+
import { atom, computed } from "nanostores";
|
|
3
|
+
import { persistentAtom } from "@nanostores/persistent";
|
|
4
|
+
var serverLocale = atom(null);
|
|
5
|
+
var localePreference = persistentAtom("ez-locale", "en", {
|
|
6
|
+
encode: (value) => value,
|
|
7
|
+
decode: (value) => value
|
|
8
|
+
});
|
|
9
|
+
var effectiveLocale = computed(
|
|
10
|
+
[serverLocale, localePreference],
|
|
11
|
+
(server, client) => server ?? client
|
|
12
|
+
);
|
|
13
|
+
var translations = atom({});
|
|
14
|
+
var localeLoading = atom(false);
|
|
15
|
+
function initLocale(locale, trans) {
|
|
16
|
+
serverLocale.set(locale);
|
|
17
|
+
localePreference.set(locale);
|
|
18
|
+
if (trans) {
|
|
19
|
+
translations.set(trans);
|
|
20
|
+
}
|
|
21
|
+
}
|
|
22
|
+
function setTranslations(trans) {
|
|
23
|
+
translations.set(trans);
|
|
24
|
+
}
|
|
25
|
+
async function setLocale(locale, options = {}) {
|
|
26
|
+
const opts = typeof options === "string" ? { cookieName: options } : options;
|
|
27
|
+
const { cookieName = "ez-locale", loadTranslations } = opts;
|
|
28
|
+
localeLoading.set(true);
|
|
29
|
+
try {
|
|
30
|
+
if (loadTranslations) {
|
|
31
|
+
const mod = await loadTranslations();
|
|
32
|
+
const trans = "default" in mod ? mod.default : mod;
|
|
33
|
+
translations.set(trans);
|
|
34
|
+
}
|
|
35
|
+
localePreference.set(locale);
|
|
36
|
+
serverLocale.set(locale);
|
|
37
|
+
if (typeof document !== "undefined") {
|
|
38
|
+
document.cookie = `${cookieName}=${locale}; path=/; max-age=31536000; samesite=lax`;
|
|
39
|
+
}
|
|
40
|
+
if (typeof document !== "undefined") {
|
|
41
|
+
document.dispatchEvent(
|
|
42
|
+
new CustomEvent("ez-i18n:locale-changed", {
|
|
43
|
+
detail: { locale },
|
|
44
|
+
bubbles: true
|
|
45
|
+
})
|
|
46
|
+
);
|
|
47
|
+
}
|
|
48
|
+
} finally {
|
|
49
|
+
localeLoading.set(false);
|
|
50
|
+
}
|
|
51
|
+
}
|
|
52
|
+
function getLocale() {
|
|
53
|
+
return effectiveLocale.get();
|
|
54
|
+
}
|
|
55
|
+
function getTranslations() {
|
|
56
|
+
return translations.get();
|
|
57
|
+
}
|
|
58
|
+
export {
|
|
59
|
+
effectiveLocale,
|
|
60
|
+
getLocale,
|
|
61
|
+
getTranslations,
|
|
62
|
+
initLocale,
|
|
63
|
+
localeLoading,
|
|
64
|
+
localePreference,
|
|
65
|
+
setLocale,
|
|
66
|
+
setTranslations,
|
|
67
|
+
translations
|
|
68
|
+
};
|
|
@@ -0,0 +1,52 @@
|
|
|
1
|
+
import * as vue from 'vue';
|
|
2
|
+
import { Plugin } from 'vue';
|
|
3
|
+
import { setLocale } from './index.js';
|
|
4
|
+
import { T as TranslateFunction } from '../types-DwCG8sp8.js';
|
|
5
|
+
import 'nanostores';
|
|
6
|
+
|
|
7
|
+
/**
|
|
8
|
+
* Vue plugin that provides global $t(), $locale, and $setLocale
|
|
9
|
+
*
|
|
10
|
+
* @example
|
|
11
|
+
* // In _vueEntrypoint.ts or main.ts
|
|
12
|
+
* import { ezI18nVue } from 'ez-i18n/vue';
|
|
13
|
+
*
|
|
14
|
+
* export default (app) => {
|
|
15
|
+
* app.use(ezI18nVue);
|
|
16
|
+
* };
|
|
17
|
+
*
|
|
18
|
+
* @example
|
|
19
|
+
* // In Vue components
|
|
20
|
+
* <template>
|
|
21
|
+
* <h1>{{ $t('welcome.title') }}</h1>
|
|
22
|
+
* <p>{{ $t('welcome.message', { name: userName }) }}</p>
|
|
23
|
+
* <button @click="$setLocale('es')">Español</button>
|
|
24
|
+
* </template>
|
|
25
|
+
*/
|
|
26
|
+
declare const ezI18nVue: Plugin;
|
|
27
|
+
/**
|
|
28
|
+
* Composable for using i18n in Vue components with Composition API
|
|
29
|
+
*
|
|
30
|
+
* @example
|
|
31
|
+
* <script setup>
|
|
32
|
+
* import { useI18n } from 'ez-i18n/vue';
|
|
33
|
+
*
|
|
34
|
+
* const { t, locale, setLocale } = useI18n();
|
|
35
|
+
* const greeting = t('welcome.greeting');
|
|
36
|
+
* </script>
|
|
37
|
+
*/
|
|
38
|
+
declare function useI18n(): {
|
|
39
|
+
t: TranslateFunction;
|
|
40
|
+
locale: Readonly<vue.Ref<string, string>>;
|
|
41
|
+
setLocale: typeof setLocale;
|
|
42
|
+
};
|
|
43
|
+
declare module 'vue' {
|
|
44
|
+
interface ComponentCustomProperties {
|
|
45
|
+
$t: TranslateFunction;
|
|
46
|
+
/** Current locale (reactive ref from nanostore) */
|
|
47
|
+
$locale: Readonly<vue.Ref<string>>;
|
|
48
|
+
$setLocale: typeof setLocale;
|
|
49
|
+
}
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
export { ezI18nVue as default, ezI18nVue, useI18n };
|