@zachhandley/ez-i18n 0.1.0 → 0.1.2
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 +61 -24
- package/dist/runtime/react-plugin.d.ts +29 -0
- package/dist/runtime/react-plugin.js +85 -0
- package/dist/runtime/vue-plugin.d.ts +2 -2
- package/package.json +105 -89
- package/src/components/EzI18nHead.astro +1 -1
- package/src/runtime/react-plugin.ts +78 -0
- package/src/runtime/vue-plugin.ts +2 -2
package/README.md
CHANGED
|
@@ -1,13 +1,17 @@
|
|
|
1
|
-
# ez-i18n
|
|
1
|
+
# @zachhandley/ez-i18n
|
|
2
2
|
|
|
3
|
-
Cookie-based i18n for Astro + Vue. No URL prefixes, reactive language switching.
|
|
3
|
+
Cookie-based i18n for Astro + Vue + React. No URL prefixes, reactive language switching.
|
|
4
4
|
|
|
5
5
|
## Installation
|
|
6
6
|
|
|
7
7
|
```bash
|
|
8
|
-
pnpm add ez-i18n nanostores @nanostores/persistent
|
|
8
|
+
pnpm add @zachhandley/ez-i18n nanostores @nanostores/persistent
|
|
9
|
+
|
|
9
10
|
# If using Vue:
|
|
10
11
|
pnpm add @nanostores/vue
|
|
12
|
+
|
|
13
|
+
# If using React:
|
|
14
|
+
pnpm add @nanostores/react
|
|
11
15
|
```
|
|
12
16
|
|
|
13
17
|
## Usage
|
|
@@ -18,7 +22,7 @@ pnpm add @nanostores/vue
|
|
|
18
22
|
// astro.config.ts
|
|
19
23
|
import { defineConfig } from 'astro/config';
|
|
20
24
|
import vue from '@astrojs/vue';
|
|
21
|
-
import ezI18n from 'ez-i18n';
|
|
25
|
+
import ezI18n from '@zachhandley/ez-i18n';
|
|
22
26
|
|
|
23
27
|
export default defineConfig({
|
|
24
28
|
integrations: [
|
|
@@ -28,9 +32,9 @@ export default defineConfig({
|
|
|
28
32
|
defaultLocale: 'en',
|
|
29
33
|
cookieName: 'my-locale', // optional, defaults to 'ez-locale'
|
|
30
34
|
translations: {
|
|
31
|
-
en: './src/i18n/en.
|
|
32
|
-
es: './src/i18n/es.
|
|
33
|
-
fr: './src/i18n/fr.
|
|
35
|
+
en: './src/i18n/en.json',
|
|
36
|
+
es: './src/i18n/es.json',
|
|
37
|
+
fr: './src/i18n/fr.json',
|
|
34
38
|
},
|
|
35
39
|
}),
|
|
36
40
|
],
|
|
@@ -39,21 +43,22 @@ export default defineConfig({
|
|
|
39
43
|
|
|
40
44
|
### Translation Files
|
|
41
45
|
|
|
42
|
-
```
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
cancel: 'Cancel',
|
|
49
|
-
},
|
|
50
|
-
auth: {
|
|
51
|
-
login: 'Log in',
|
|
52
|
-
signup: 'Sign up',
|
|
46
|
+
```json
|
|
47
|
+
{
|
|
48
|
+
"common": {
|
|
49
|
+
"welcome": "Welcome",
|
|
50
|
+
"save": "Save",
|
|
51
|
+
"cancel": "Cancel"
|
|
53
52
|
},
|
|
54
|
-
|
|
53
|
+
"auth": {
|
|
54
|
+
"login": "Log in",
|
|
55
|
+
"signup": "Sign up"
|
|
56
|
+
}
|
|
57
|
+
}
|
|
55
58
|
```
|
|
56
59
|
|
|
60
|
+
Create similar files for each locale: `src/i18n/en.json`, `src/i18n/es.json`, etc.
|
|
61
|
+
|
|
57
62
|
### Layout Setup
|
|
58
63
|
|
|
59
64
|
Add the `EzI18nHead` component to your layout's head for automatic hydration:
|
|
@@ -61,7 +66,7 @@ Add the `EzI18nHead` component to your layout's head for automatic hydration:
|
|
|
61
66
|
```astro
|
|
62
67
|
---
|
|
63
68
|
// src/layouts/Layout.astro
|
|
64
|
-
import
|
|
69
|
+
import EzI18nHead from '@zachhandley/ez-i18n/astro';
|
|
65
70
|
const { locale, translations } = Astro.locals;
|
|
66
71
|
---
|
|
67
72
|
|
|
@@ -93,7 +98,7 @@ const { locale, translations } = Astro.locals;
|
|
|
93
98
|
|
|
94
99
|
```vue
|
|
95
100
|
<script setup lang="ts">
|
|
96
|
-
import { useI18n } from 'ez-i18n/vue';
|
|
101
|
+
import { useI18n } from '@zachhandley/ez-i18n/vue';
|
|
97
102
|
import { translationLoaders } from 'ez-i18n:translations';
|
|
98
103
|
|
|
99
104
|
const { t, locale, setLocale } = useI18n();
|
|
@@ -126,20 +131,46 @@ Register the Vue plugin in your entrypoint:
|
|
|
126
131
|
```typescript
|
|
127
132
|
// src/_vueEntrypoint.ts
|
|
128
133
|
import type { App } from 'vue';
|
|
129
|
-
import { ezI18nVue } from 'ez-i18n/vue';
|
|
134
|
+
import { ezI18nVue } from '@zachhandley/ez-i18n/vue';
|
|
130
135
|
|
|
131
136
|
export default (app: App) => {
|
|
132
137
|
app.use(ezI18nVue);
|
|
133
138
|
};
|
|
134
139
|
```
|
|
135
140
|
|
|
141
|
+
### In React Components
|
|
142
|
+
|
|
143
|
+
```tsx
|
|
144
|
+
import { useI18n } from '@zachhandley/ez-i18n/react';
|
|
145
|
+
import { translationLoaders } from 'ez-i18n:translations';
|
|
146
|
+
|
|
147
|
+
function MyComponent() {
|
|
148
|
+
const { t, locale, setLocale } = useI18n();
|
|
149
|
+
|
|
150
|
+
async function switchLocale(newLocale: string) {
|
|
151
|
+
await setLocale(newLocale, {
|
|
152
|
+
loadTranslations: translationLoaders[newLocale],
|
|
153
|
+
});
|
|
154
|
+
}
|
|
155
|
+
|
|
156
|
+
return (
|
|
157
|
+
<div>
|
|
158
|
+
<h1>{t('common.welcome')}</h1>
|
|
159
|
+
<p>{t('greeting', { name: 'World' })}</p>
|
|
160
|
+
<button onClick={() => switchLocale('es')}>Español</button>
|
|
161
|
+
<button onClick={() => switchLocale('fr')}>Français</button>
|
|
162
|
+
</div>
|
|
163
|
+
);
|
|
164
|
+
}
|
|
165
|
+
```
|
|
166
|
+
|
|
136
167
|
## Features
|
|
137
168
|
|
|
138
169
|
- **No URL prefixes** - Locale stored in cookie, not URL path
|
|
139
170
|
- **Reactive** - Language changes update immediately without page reload
|
|
140
171
|
- **SSR compatible** - Proper hydration with server-rendered locale
|
|
141
172
|
- **Vue integration** - Global `$t()`, `$locale`, `$setLocale` in templates
|
|
142
|
-
- **
|
|
173
|
+
- **React integration** - `useI18n()` hook for React components
|
|
143
174
|
- **Middleware included** - Auto-detects locale from cookie, query param, or Accept-Language header
|
|
144
175
|
|
|
145
176
|
## Locale Detection Priority
|
|
@@ -194,9 +225,15 @@ setLocale('es', { loadTranslations: translationLoaders['es'] });
|
|
|
194
225
|
|
|
195
226
|
### `useI18n()`
|
|
196
227
|
|
|
197
|
-
|
|
228
|
+
Hook for Vue (Composition API) and React.
|
|
198
229
|
|
|
199
230
|
```typescript
|
|
231
|
+
// Vue
|
|
232
|
+
import { useI18n } from '@zachhandley/ez-i18n/vue';
|
|
233
|
+
|
|
234
|
+
// React
|
|
235
|
+
import { useI18n } from '@zachhandley/ez-i18n/react';
|
|
236
|
+
|
|
200
237
|
const { t, locale, setLocale } = useI18n();
|
|
201
238
|
```
|
|
202
239
|
|
|
@@ -0,0 +1,29 @@
|
|
|
1
|
+
import { setLocale } from './index.js';
|
|
2
|
+
import { T as TranslateFunction } from '../types-DwCG8sp8.js';
|
|
3
|
+
import 'nanostores';
|
|
4
|
+
|
|
5
|
+
/**
|
|
6
|
+
* React hook for i18n
|
|
7
|
+
*
|
|
8
|
+
* @example
|
|
9
|
+
* import { useI18n } from '@zachhandley/ez-i18n/react';
|
|
10
|
+
*
|
|
11
|
+
* function MyComponent() {
|
|
12
|
+
* const { t, locale, setLocale } = useI18n();
|
|
13
|
+
*
|
|
14
|
+
* return (
|
|
15
|
+
* <div>
|
|
16
|
+
* <h1>{t('common.welcome')}</h1>
|
|
17
|
+
* <p>{t('greeting', { name: 'World' })}</p>
|
|
18
|
+
* <button onClick={() => setLocale('es')}>Español</button>
|
|
19
|
+
* </div>
|
|
20
|
+
* );
|
|
21
|
+
* }
|
|
22
|
+
*/
|
|
23
|
+
declare function useI18n(): {
|
|
24
|
+
t: TranslateFunction;
|
|
25
|
+
locale: string;
|
|
26
|
+
setLocale: typeof setLocale;
|
|
27
|
+
};
|
|
28
|
+
|
|
29
|
+
export { useI18n };
|
|
@@ -0,0 +1,85 @@
|
|
|
1
|
+
// src/runtime/react-plugin.ts
|
|
2
|
+
import { useStore } from "@nanostores/react";
|
|
3
|
+
|
|
4
|
+
// src/runtime/store.ts
|
|
5
|
+
import { atom, computed } from "nanostores";
|
|
6
|
+
import { persistentAtom } from "@nanostores/persistent";
|
|
7
|
+
var serverLocale = atom(null);
|
|
8
|
+
var localePreference = persistentAtom("ez-locale", "en", {
|
|
9
|
+
encode: (value) => value,
|
|
10
|
+
decode: (value) => value
|
|
11
|
+
});
|
|
12
|
+
var effectiveLocale = computed(
|
|
13
|
+
[serverLocale, localePreference],
|
|
14
|
+
(server, client) => server ?? client
|
|
15
|
+
);
|
|
16
|
+
var translations = atom({});
|
|
17
|
+
var localeLoading = atom(false);
|
|
18
|
+
async function setLocale(locale, options = {}) {
|
|
19
|
+
const opts = typeof options === "string" ? { cookieName: options } : options;
|
|
20
|
+
const { cookieName = "ez-locale", loadTranslations } = opts;
|
|
21
|
+
localeLoading.set(true);
|
|
22
|
+
try {
|
|
23
|
+
if (loadTranslations) {
|
|
24
|
+
const mod = await loadTranslations();
|
|
25
|
+
const trans = "default" in mod ? mod.default : mod;
|
|
26
|
+
translations.set(trans);
|
|
27
|
+
}
|
|
28
|
+
localePreference.set(locale);
|
|
29
|
+
serverLocale.set(locale);
|
|
30
|
+
if (typeof document !== "undefined") {
|
|
31
|
+
document.cookie = `${cookieName}=${locale}; path=/; max-age=31536000; samesite=lax`;
|
|
32
|
+
}
|
|
33
|
+
if (typeof document !== "undefined") {
|
|
34
|
+
document.dispatchEvent(
|
|
35
|
+
new CustomEvent("ez-i18n:locale-changed", {
|
|
36
|
+
detail: { locale },
|
|
37
|
+
bubbles: true
|
|
38
|
+
})
|
|
39
|
+
);
|
|
40
|
+
}
|
|
41
|
+
} finally {
|
|
42
|
+
localeLoading.set(false);
|
|
43
|
+
}
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
// src/runtime/react-plugin.ts
|
|
47
|
+
function getNestedValue(obj, path) {
|
|
48
|
+
const keys = path.split(".");
|
|
49
|
+
let value = obj;
|
|
50
|
+
for (const key of keys) {
|
|
51
|
+
if (value == null || typeof value !== "object") {
|
|
52
|
+
return void 0;
|
|
53
|
+
}
|
|
54
|
+
value = value[key];
|
|
55
|
+
}
|
|
56
|
+
return value;
|
|
57
|
+
}
|
|
58
|
+
function interpolate(str, params) {
|
|
59
|
+
if (!params) return str;
|
|
60
|
+
return str.replace(/\{(\w+)\}/g, (match, key) => {
|
|
61
|
+
return key in params ? String(params[key]) : match;
|
|
62
|
+
});
|
|
63
|
+
}
|
|
64
|
+
function useI18n() {
|
|
65
|
+
const locale = useStore(effectiveLocale);
|
|
66
|
+
const trans = useStore(translations);
|
|
67
|
+
const t = (key, params) => {
|
|
68
|
+
const value = getNestedValue(trans, key);
|
|
69
|
+
if (typeof value !== "string") {
|
|
70
|
+
if (import.meta.env?.DEV) {
|
|
71
|
+
console.warn("[ez-i18n] Missing translation:", key);
|
|
72
|
+
}
|
|
73
|
+
return key;
|
|
74
|
+
}
|
|
75
|
+
return interpolate(value, params);
|
|
76
|
+
};
|
|
77
|
+
return {
|
|
78
|
+
t,
|
|
79
|
+
locale,
|
|
80
|
+
setLocale
|
|
81
|
+
};
|
|
82
|
+
}
|
|
83
|
+
export {
|
|
84
|
+
useI18n
|
|
85
|
+
};
|
|
@@ -9,7 +9,7 @@ import 'nanostores';
|
|
|
9
9
|
*
|
|
10
10
|
* @example
|
|
11
11
|
* // In _vueEntrypoint.ts or main.ts
|
|
12
|
-
* import { ezI18nVue } from 'ez-i18n/vue';
|
|
12
|
+
* import { ezI18nVue } from '@zachhandley/ez-i18n/vue';
|
|
13
13
|
*
|
|
14
14
|
* export default (app) => {
|
|
15
15
|
* app.use(ezI18nVue);
|
|
@@ -29,7 +29,7 @@ declare const ezI18nVue: Plugin;
|
|
|
29
29
|
*
|
|
30
30
|
* @example
|
|
31
31
|
* <script setup>
|
|
32
|
-
* import { useI18n } from 'ez-i18n/vue';
|
|
32
|
+
* import { useI18n } from '@zachhandley/ez-i18n/vue';
|
|
33
33
|
*
|
|
34
34
|
* const { t, locale, setLocale } = useI18n();
|
|
35
35
|
* const greeting = t('welcome.greeting');
|
package/package.json
CHANGED
|
@@ -1,89 +1,105 @@
|
|
|
1
|
-
{
|
|
2
|
-
"name": "@zachhandley/ez-i18n",
|
|
3
|
-
"version": "0.1.
|
|
4
|
-
"publishConfig": {
|
|
5
|
-
"access": "public"
|
|
6
|
-
},
|
|
7
|
-
"description": "Cookie-based i18n for Astro + Vue. No URL prefixes, reactive language switching.",
|
|
8
|
-
"type": "module",
|
|
9
|
-
"main": "./dist/index.js",
|
|
10
|
-
"types": "./dist/index.d.ts",
|
|
11
|
-
"exports": {
|
|
12
|
-
".": {
|
|
13
|
-
"types": "./dist/index.d.ts",
|
|
14
|
-
"import": "./dist/index.js"
|
|
15
|
-
},
|
|
16
|
-
"./vue": {
|
|
17
|
-
"types": "./dist/runtime/vue-plugin.d.ts",
|
|
18
|
-
"import": "./dist/runtime/vue-plugin.js"
|
|
19
|
-
},
|
|
20
|
-
"./middleware": {
|
|
21
|
-
"import": "./dist/middleware.js"
|
|
22
|
-
},
|
|
23
|
-
"./runtime": {
|
|
24
|
-
"types": "./dist/runtime/index.d.ts",
|
|
25
|
-
"import": "./dist/runtime/index.js"
|
|
26
|
-
},
|
|
27
|
-
"./astro": {
|
|
28
|
-
"import": "./src/components/EzI18nHead.astro"
|
|
29
|
-
}
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
"
|
|
36
|
-
"
|
|
37
|
-
"
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
"
|
|
41
|
-
"
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
"
|
|
45
|
-
"
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
"
|
|
49
|
-
"
|
|
50
|
-
"
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
"
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
"
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
"vue":
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
"
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
"
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
"
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
"
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
}
|
|
1
|
+
{
|
|
2
|
+
"name": "@zachhandley/ez-i18n",
|
|
3
|
+
"version": "0.1.2",
|
|
4
|
+
"publishConfig": {
|
|
5
|
+
"access": "public"
|
|
6
|
+
},
|
|
7
|
+
"description": "Cookie-based i18n for Astro + Vue + React. No URL prefixes, reactive language switching.",
|
|
8
|
+
"type": "module",
|
|
9
|
+
"main": "./dist/index.js",
|
|
10
|
+
"types": "./dist/index.d.ts",
|
|
11
|
+
"exports": {
|
|
12
|
+
".": {
|
|
13
|
+
"types": "./dist/index.d.ts",
|
|
14
|
+
"import": "./dist/index.js"
|
|
15
|
+
},
|
|
16
|
+
"./vue": {
|
|
17
|
+
"types": "./dist/runtime/vue-plugin.d.ts",
|
|
18
|
+
"import": "./dist/runtime/vue-plugin.js"
|
|
19
|
+
},
|
|
20
|
+
"./middleware": {
|
|
21
|
+
"import": "./dist/middleware.js"
|
|
22
|
+
},
|
|
23
|
+
"./runtime": {
|
|
24
|
+
"types": "./dist/runtime/index.d.ts",
|
|
25
|
+
"import": "./dist/runtime/index.js"
|
|
26
|
+
},
|
|
27
|
+
"./astro": {
|
|
28
|
+
"import": "./src/components/EzI18nHead.astro"
|
|
29
|
+
},
|
|
30
|
+
"./react": {
|
|
31
|
+
"types": "./dist/runtime/react-plugin.d.ts",
|
|
32
|
+
"import": "./dist/runtime/react-plugin.js"
|
|
33
|
+
}
|
|
34
|
+
},
|
|
35
|
+
"files": [
|
|
36
|
+
"dist",
|
|
37
|
+
"src"
|
|
38
|
+
],
|
|
39
|
+
"scripts": {
|
|
40
|
+
"build": "tsup",
|
|
41
|
+
"dev": "tsup --watch",
|
|
42
|
+
"typecheck": "tsc --noEmit",
|
|
43
|
+
"prepublishOnly": "pnpm build",
|
|
44
|
+
"release": "pnpm build && npm publish",
|
|
45
|
+
"release:dry": "pnpm build && npm publish --dry-run"
|
|
46
|
+
},
|
|
47
|
+
"keywords": [
|
|
48
|
+
"astro",
|
|
49
|
+
"astro-integration",
|
|
50
|
+
"i18n",
|
|
51
|
+
"internationalization",
|
|
52
|
+
"vue",
|
|
53
|
+
"react",
|
|
54
|
+
"cookie-based",
|
|
55
|
+
"no-url-prefix"
|
|
56
|
+
],
|
|
57
|
+
"author": "Zach Handley <zachhandley@gmail.com>",
|
|
58
|
+
"license": "MIT",
|
|
59
|
+
"homepage": "https://github.com/zachhandley/ez-i18n#readme",
|
|
60
|
+
"repository": {
|
|
61
|
+
"type": "git",
|
|
62
|
+
"url": "git+https://github.com/zachhandley/ez-i18n.git"
|
|
63
|
+
},
|
|
64
|
+
"bugs": {
|
|
65
|
+
"url": "https://github.com/zachhandley/ez-i18n/issues"
|
|
66
|
+
},
|
|
67
|
+
"peerDependencies": {
|
|
68
|
+
"@nanostores/persistent": "^0.10.0",
|
|
69
|
+
"@nanostores/react": "^0.7.0 || ^0.8.0 || ^1.0.0",
|
|
70
|
+
"@nanostores/vue": "^0.10.0",
|
|
71
|
+
"astro": "^4.0.0 || ^5.0.0",
|
|
72
|
+
"nanostores": "^0.9.0 || ^0.10.0 || ^0.11.0",
|
|
73
|
+
"react": "^18.0.0 || ^19.0.0",
|
|
74
|
+
"vue": "^3.4.0"
|
|
75
|
+
},
|
|
76
|
+
"peerDependenciesMeta": {
|
|
77
|
+
"vue": {
|
|
78
|
+
"optional": true
|
|
79
|
+
},
|
|
80
|
+
"react": {
|
|
81
|
+
"optional": true
|
|
82
|
+
},
|
|
83
|
+
"@nanostores/vue": {
|
|
84
|
+
"optional": true
|
|
85
|
+
},
|
|
86
|
+
"@nanostores/react": {
|
|
87
|
+
"optional": true
|
|
88
|
+
}
|
|
89
|
+
},
|
|
90
|
+
"devDependencies": {
|
|
91
|
+
"@nanostores/persistent": "^0.10.2",
|
|
92
|
+
"@nanostores/react": "^1.0.0",
|
|
93
|
+
"@nanostores/vue": "^0.10.0",
|
|
94
|
+
"@types/node": "^22.0.0",
|
|
95
|
+
"@types/react": "^19.2.7",
|
|
96
|
+
"astro": "^5.1.1",
|
|
97
|
+
"nanostores": "^0.11.3",
|
|
98
|
+
"react": "^19.2.1",
|
|
99
|
+
"tsup": "^8.3.5",
|
|
100
|
+
"typescript": "^5.7.2",
|
|
101
|
+
"vite": "^6.0.3",
|
|
102
|
+
"vue": "^3.5.13"
|
|
103
|
+
},
|
|
104
|
+
"packageManager": "pnpm@10.17.1+sha512.17c560fca4867ae9473a3899ad84a88334914f379be46d455cbf92e5cf4b39d34985d452d2583baf19967fa76cb5c17bc9e245529d0b98745721aa7200ecaf7a"
|
|
105
|
+
}
|
|
@@ -0,0 +1,78 @@
|
|
|
1
|
+
import { useStore } from '@nanostores/react';
|
|
2
|
+
import { effectiveLocale, translations, setLocale } from './store';
|
|
3
|
+
import type { TranslateFunction } from '../types';
|
|
4
|
+
|
|
5
|
+
/**
|
|
6
|
+
* Get nested value from object using dot notation
|
|
7
|
+
*/
|
|
8
|
+
function getNestedValue(obj: Record<string, unknown>, path: string): unknown {
|
|
9
|
+
const keys = path.split('.');
|
|
10
|
+
let value: unknown = obj;
|
|
11
|
+
|
|
12
|
+
for (const key of keys) {
|
|
13
|
+
if (value == null || typeof value !== 'object') {
|
|
14
|
+
return undefined;
|
|
15
|
+
}
|
|
16
|
+
value = (value as Record<string, unknown>)[key];
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
return value;
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
/**
|
|
23
|
+
* Interpolate params into string
|
|
24
|
+
*/
|
|
25
|
+
function interpolate(
|
|
26
|
+
str: string,
|
|
27
|
+
params?: Record<string, string | number>
|
|
28
|
+
): string {
|
|
29
|
+
if (!params) return str;
|
|
30
|
+
return str.replace(/\{(\w+)\}/g, (match, key) => {
|
|
31
|
+
return key in params ? String(params[key]) : match;
|
|
32
|
+
});
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
/**
|
|
36
|
+
* React hook for i18n
|
|
37
|
+
*
|
|
38
|
+
* @example
|
|
39
|
+
* import { useI18n } from '@zachhandley/ez-i18n/react';
|
|
40
|
+
*
|
|
41
|
+
* function MyComponent() {
|
|
42
|
+
* const { t, locale, setLocale } = useI18n();
|
|
43
|
+
*
|
|
44
|
+
* return (
|
|
45
|
+
* <div>
|
|
46
|
+
* <h1>{t('common.welcome')}</h1>
|
|
47
|
+
* <p>{t('greeting', { name: 'World' })}</p>
|
|
48
|
+
* <button onClick={() => setLocale('es')}>Español</button>
|
|
49
|
+
* </div>
|
|
50
|
+
* );
|
|
51
|
+
* }
|
|
52
|
+
*/
|
|
53
|
+
export function useI18n() {
|
|
54
|
+
const locale = useStore(effectiveLocale);
|
|
55
|
+
const trans = useStore(translations);
|
|
56
|
+
|
|
57
|
+
const t: TranslateFunction = (
|
|
58
|
+
key: string,
|
|
59
|
+
params?: Record<string, string | number>
|
|
60
|
+
): string => {
|
|
61
|
+
const value = getNestedValue(trans, key);
|
|
62
|
+
|
|
63
|
+
if (typeof value !== 'string') {
|
|
64
|
+
if (import.meta.env?.DEV) {
|
|
65
|
+
console.warn('[ez-i18n] Missing translation:', key);
|
|
66
|
+
}
|
|
67
|
+
return key;
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
return interpolate(value, params);
|
|
71
|
+
};
|
|
72
|
+
|
|
73
|
+
return {
|
|
74
|
+
t,
|
|
75
|
+
locale,
|
|
76
|
+
setLocale,
|
|
77
|
+
};
|
|
78
|
+
}
|
|
@@ -60,7 +60,7 @@ function createTranslateFunction(
|
|
|
60
60
|
*
|
|
61
61
|
* @example
|
|
62
62
|
* // In _vueEntrypoint.ts or main.ts
|
|
63
|
-
* import { ezI18nVue } from 'ez-i18n/vue';
|
|
63
|
+
* import { ezI18nVue } from '@zachhandley/ez-i18n/vue';
|
|
64
64
|
*
|
|
65
65
|
* export default (app) => {
|
|
66
66
|
* app.use(ezI18nVue);
|
|
@@ -105,7 +105,7 @@ export const ezI18nVue: Plugin = {
|
|
|
105
105
|
*
|
|
106
106
|
* @example
|
|
107
107
|
* <script setup>
|
|
108
|
-
* import { useI18n } from 'ez-i18n/vue';
|
|
108
|
+
* import { useI18n } from '@zachhandley/ez-i18n/vue';
|
|
109
109
|
*
|
|
110
110
|
* const { t, locale, setLocale } = useI18n();
|
|
111
111
|
* const greeting = t('welcome.greeting');
|