@zachhandley/ez-i18n 0.2.2 → 0.3.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 DELETED
@@ -1,371 +0,0 @@
1
- # @zachhandley/ez-i18n
2
-
3
- Cookie-based i18n for Astro + Vue + React. No URL prefixes, reactive language switching.
4
-
5
- ## Installation
6
-
7
- ```bash
8
- pnpm add @zachhandley/ez-i18n nanostores @nanostores/persistent
9
-
10
- # If using Vue:
11
- pnpm add @nanostores/vue
12
-
13
- # If using React:
14
- pnpm add @nanostores/react
15
- ```
16
-
17
- ## Usage
18
-
19
- ### Astro Config
20
-
21
- ```typescript
22
- // astro.config.ts
23
- import { defineConfig } from 'astro/config';
24
- import vue from '@astrojs/vue';
25
- import ezI18n from '@zachhandley/ez-i18n';
26
-
27
- export default defineConfig({
28
- integrations: [
29
- vue(),
30
- ezI18n({
31
- locales: ['en', 'es', 'fr'],
32
- defaultLocale: 'en',
33
- cookieName: 'my-locale', // optional, defaults to 'ez-locale'
34
- translations: {
35
- en: './src/i18n/en.json',
36
- es: './src/i18n/es.json',
37
- fr: './src/i18n/fr.json',
38
- },
39
- }),
40
- ],
41
- });
42
- ```
43
-
44
- ### Translation Files
45
-
46
- ```json
47
- {
48
- "common": {
49
- "welcome": "Welcome",
50
- "save": "Save",
51
- "cancel": "Cancel"
52
- },
53
- "auth": {
54
- "login": "Log in",
55
- "signup": "Sign up"
56
- }
57
- }
58
- ```
59
-
60
- Create similar files for each locale: `src/i18n/en.json`, `src/i18n/es.json`, etc.
61
-
62
- ### Multi-File Translations
63
-
64
- ez-i18n supports flexible translation file organization:
65
-
66
- #### Auto-Discovery (Zero Config)
67
-
68
- Just put your files in `public/i18n/` and ez-i18n will discover them automatically:
69
-
70
- ```
71
- public/i18n/
72
- en/
73
- common.json
74
- auth.json
75
- es/
76
- common.json
77
- auth.json
78
- ```
79
-
80
- ```typescript
81
- // astro.config.ts - locales auto-discovered from folder names!
82
- ezI18n({
83
- defaultLocale: 'en',
84
- // No locales or translations needed - auto-discovered
85
- })
86
- ```
87
-
88
- #### Base Directory
89
-
90
- Point to a folder and locales are discovered from subfolders:
91
-
92
- ```typescript
93
- ezI18n({
94
- defaultLocale: 'en',
95
- translations: './src/i18n/', // Discovers en/, es/, fr/ folders
96
- })
97
- ```
98
-
99
- #### Per-Locale with Multiple Formats
100
-
101
- Mix and match different formats per locale:
102
-
103
- ```typescript
104
- ezI18n({
105
- locales: ['en', 'es', 'fr', 'de'],
106
- defaultLocale: 'en',
107
- translations: {
108
- en: './src/i18n/en.json', // Single file
109
- es: './src/i18n/es/', // Folder (all JSONs merged)
110
- fr: './src/i18n/fr/**/*.json', // Glob pattern
111
- de: ['./src/i18n/de/common.json', // Array of files
112
- './src/i18n/de/auth.json'],
113
- },
114
- })
115
- ```
116
-
117
- #### Merge Order
118
-
119
- When using multiple files per locale, files are merged **alphabetically by filename**. Later files override earlier ones for conflicting keys.
120
-
121
- ```
122
- en/
123
- 01-common.json # Loaded first
124
- 02-features.json # Loaded second, overrides common
125
- 99-overrides.json # Loaded last, highest priority
126
- ```
127
-
128
- #### Path-Based Namespacing
129
-
130
- When using folder-based translation organization, ez-i18n automatically creates namespaces from your file paths. This is **enabled by default** when using folder-based config.
131
-
132
- **Example:**
133
-
134
- ```
135
- public/i18n/
136
- en/
137
- auth/
138
- login.json # { "title": "Sign In", "button": "Log In" }
139
- signup.json # { "title": "Create Account" }
140
- common.json # { "welcome": "Welcome" }
141
- ```
142
-
143
- Access translations using dot notation that mirrors the folder structure:
144
-
145
- ```typescript
146
- $t('auth.login.title') // "Sign In"
147
- $t('auth.login.button') // "Log In"
148
- $t('auth.signup.title') // "Create Account"
149
- $t('common.welcome') // "Welcome"
150
- ```
151
-
152
- **Disable path-based namespacing:**
153
-
154
- If you prefer to manage namespaces manually within your JSON files, you can disable this feature:
155
-
156
- ```typescript
157
- ezI18n({
158
- defaultLocale: 'en',
159
- translations: './src/i18n/',
160
- pathBasedNamespacing: false, // Disable automatic path namespacing
161
- })
162
- ```
163
-
164
- With `pathBasedNamespacing: false`, the file structure is ignored and keys are used directly from each JSON file.
165
-
166
- #### Cache File
167
-
168
- A `.ez-i18n.json` cache file is generated to speed up subsequent builds. Add it to `.gitignore`:
169
-
170
- ```gitignore
171
- .ez-i18n.json
172
- ```
173
-
174
- ### Layout Setup
175
-
176
- Add the `EzI18nHead` component to your layout's head for automatic hydration:
177
-
178
- ```astro
179
- ---
180
- // src/layouts/Layout.astro
181
- import EzI18nHead from '@zachhandley/ez-i18n/astro';
182
- const { locale, translations } = Astro.locals;
183
- ---
184
-
185
- <html lang={locale}>
186
- <head>
187
- <meta charset="utf-8" />
188
- <EzI18nHead locale={locale} translations={translations} />
189
- </head>
190
- <body>
191
- <slot />
192
- </body>
193
- </html>
194
- ```
195
-
196
- ### In Astro Files
197
-
198
- ```astro
199
- ---
200
- import { t, locale } from 'ez-i18n:runtime';
201
- // Or access from locals (auto-loaded by middleware):
202
- const { locale, translations } = Astro.locals;
203
- ---
204
-
205
- <h1>{t('common.welcome')}</h1>
206
- <p>Current locale: {locale}</p>
207
- ```
208
-
209
- ### In Vue Components
210
-
211
- ```vue
212
- <script setup lang="ts">
213
- import { useI18n } from '@zachhandley/ez-i18n/vue';
214
- import { translationLoaders } from 'ez-i18n:translations';
215
-
216
- const { t, locale, setLocale } = useI18n();
217
-
218
- // Change locale with dynamic translation loading
219
- async function switchLocale(newLocale: string) {
220
- await setLocale(newLocale, {
221
- loadTranslations: translationLoaders[newLocale],
222
- });
223
- }
224
- </script>
225
-
226
- <template>
227
- <!-- Global $t is available automatically -->
228
- <h1>{{ $t('common.welcome') }}</h1>
229
-
230
- <!-- Interpolation -->
231
- <p>{{ $t('greeting', { name: 'World' }) }}</p>
232
-
233
- <!-- Change language with dynamic loading -->
234
- <button @click="switchLocale('es')">Español</button>
235
- <button @click="switchLocale('fr')">Français</button>
236
- </template>
237
- ```
238
-
239
- ### Vue Plugin Setup
240
-
241
- Register the Vue plugin in your entrypoint:
242
-
243
- ```typescript
244
- // src/_vueEntrypoint.ts
245
- import type { App } from 'vue';
246
- import { ezI18nVue } from '@zachhandley/ez-i18n/vue';
247
-
248
- export default (app: App) => {
249
- app.use(ezI18nVue);
250
- };
251
- ```
252
-
253
- ### In React Components
254
-
255
- ```tsx
256
- import { useI18n } from '@zachhandley/ez-i18n/react';
257
- import { translationLoaders } from 'ez-i18n:translations';
258
-
259
- function MyComponent() {
260
- const { t, locale, setLocale } = useI18n();
261
-
262
- async function switchLocale(newLocale: string) {
263
- await setLocale(newLocale, {
264
- loadTranslations: translationLoaders[newLocale],
265
- });
266
- }
267
-
268
- return (
269
- <div>
270
- <h1>{t('common.welcome')}</h1>
271
- <p>{t('greeting', { name: 'World' })}</p>
272
- <button onClick={() => switchLocale('es')}>Español</button>
273
- <button onClick={() => switchLocale('fr')}>Français</button>
274
- </div>
275
- );
276
- }
277
- ```
278
-
279
- ## Features
280
-
281
- - **No URL prefixes** - Locale stored in cookie, not URL path
282
- - **Reactive** - Language changes update immediately without page reload
283
- - **SSR compatible** - Proper hydration with server-rendered locale
284
- - **Vue integration** - Global `$t()`, `$locale`, `$setLocale` in templates
285
- - **React integration** - `useI18n()` hook for React components
286
- - **Middleware included** - Auto-detects locale from cookie, query param, or Accept-Language header
287
- - **Multi-file support** - Organize translations in folders, use globs, or arrays
288
- - **Auto-discovery** - Automatic locale detection from folder structure
289
- - **Path-based namespacing** - Automatic namespacing from folder structure (`auth/login.json` becomes `auth.login.*`)
290
- - **HMR in dev** - Hot reload translation changes without restart
291
-
292
- ## Locale Detection Priority
293
-
294
- 1. `?lang=xx` query parameter
295
- 2. Cookie value
296
- 3. Accept-Language header
297
- 4. Default locale
298
-
299
- ## API
300
-
301
- ### `ezI18n(config)`
302
-
303
- Astro integration function.
304
-
305
- | Option | Type | Required | Description |
306
- |--------|------|----------|-------------|
307
- | `locales` | `string[]` | No | Supported locale codes (auto-discovered if not provided) |
308
- | `defaultLocale` | `string` | Yes | Fallback locale |
309
- | `cookieName` | `string` | No | Cookie name (default: `'ez-locale'`) |
310
- | `translations` | `string \| Record<string, TranslationPath>` | No | Base directory or per-locale paths (default: `./public/i18n/`) |
311
- | `pathBasedNamespacing` | `boolean` | No | Auto-namespace translations from folder paths (default: `true` for folder-based config) |
312
-
313
- **TranslationPath** can be:
314
- - Single file: `'./src/i18n/en.json'`
315
- - Folder: `'./src/i18n/en/'`
316
- - Glob: `'./src/i18n/en/**/*.json'`
317
- - Array: `['./common.json', './auth.json']`
318
-
319
- ### `EzI18nHead`
320
-
321
- Astro component for i18n hydration. Place in your layout's `<head>`.
322
-
323
- ```astro
324
- <EzI18nHead locale={Astro.locals.locale} translations={Astro.locals.translations} />
325
- ```
326
-
327
- ### `$t(key, params?)`
328
-
329
- Translate a key with optional interpolation.
330
-
331
- ```typescript
332
- $t('greeting'); // "Hello"
333
- $t('greeting', { name: 'World' }); // "Hello, {name}" -> "Hello, World"
334
- ```
335
-
336
- ### `setLocale(locale, options?)`
337
-
338
- Change the current locale. Updates cookie and triggers reactive update.
339
-
340
- ```typescript
341
- // Simple usage
342
- setLocale('es');
343
-
344
- // With dynamic translation loading
345
- import { translationLoaders } from 'ez-i18n:translations';
346
- setLocale('es', { loadTranslations: translationLoaders['es'] });
347
- ```
348
-
349
- ### `useI18n()`
350
-
351
- Hook for Vue (Composition API) and React.
352
-
353
- ```typescript
354
- // Vue
355
- import { useI18n } from '@zachhandley/ez-i18n/vue';
356
-
357
- // React
358
- import { useI18n } from '@zachhandley/ez-i18n/react';
359
-
360
- const { t, locale, setLocale } = useI18n();
361
- ```
362
-
363
- ### Virtual Modules
364
-
365
- - `ez-i18n:config` - Static config (locales, defaultLocale, cookieName)
366
- - `ez-i18n:runtime` - Runtime functions (t, setLocale, initLocale, locale store)
367
- - `ez-i18n:translations` - Translation loaders (loadTranslations, translationLoaders)
368
-
369
- ## License
370
-
371
- MIT
@@ -1,28 +0,0 @@
1
- import { setLocale } from '@zachhandley/ez-i18n/runtime';
2
- import { T as TranslateFunction } from '../types-Cd9e7Lkc.js';
3
-
4
- /**
5
- * React hook for i18n
6
- *
7
- * @example
8
- * import { useI18n } from '@zachhandley/ez-i18n/react';
9
- *
10
- * function MyComponent() {
11
- * const { t, locale, setLocale } = useI18n();
12
- *
13
- * return (
14
- * <div>
15
- * <h1>{t('common.welcome')}</h1>
16
- * <p>{t('greeting', { name: 'World' })}</p>
17
- * <button onClick={() => setLocale('es')}>Español</button>
18
- * </div>
19
- * );
20
- * }
21
- */
22
- declare function useI18n(): {
23
- t: TranslateFunction;
24
- locale: string;
25
- setLocale: typeof setLocale;
26
- };
27
-
28
- export { useI18n };
@@ -1,42 +0,0 @@
1
- // src/runtime/react-plugin.ts
2
- import { useStore } from "@nanostores/react";
3
- import { effectiveLocale, translations, setLocale } from "@zachhandley/ez-i18n/runtime";
4
- function getNestedValue(obj, path) {
5
- const keys = path.split(".");
6
- let value = obj;
7
- for (const key of keys) {
8
- if (value == null || typeof value !== "object") {
9
- return void 0;
10
- }
11
- value = value[key];
12
- }
13
- return value;
14
- }
15
- function interpolate(str, params) {
16
- if (!params) return str;
17
- return str.replace(/\{(\w+)\}/g, (match, key) => {
18
- return key in params ? String(params[key]) : match;
19
- });
20
- }
21
- function useI18n() {
22
- const locale = useStore(effectiveLocale);
23
- const trans = useStore(translations);
24
- const t = (key, params) => {
25
- const value = getNestedValue(trans, key);
26
- if (typeof value !== "string") {
27
- if (import.meta.env?.DEV) {
28
- console.warn("[ez-i18n] Missing translation:", key);
29
- }
30
- return key;
31
- }
32
- return interpolate(value, params);
33
- };
34
- return {
35
- t,
36
- locale,
37
- setLocale
38
- };
39
- }
40
- export {
41
- useI18n
42
- };
@@ -1,51 +0,0 @@
1
- import * as vue from 'vue';
2
- import { Plugin } from 'vue';
3
- import { setLocale } from '@zachhandley/ez-i18n/runtime';
4
- import { T as TranslateFunction } from '../types-Cd9e7Lkc.js';
5
-
6
- /**
7
- * Vue plugin that provides global $t(), $locale, and $setLocale
8
- *
9
- * @example
10
- * // In _vueEntrypoint.ts or main.ts
11
- * import { ezI18nVue } from '@zachhandley/ez-i18n/vue';
12
- *
13
- * export default (app) => {
14
- * app.use(ezI18nVue);
15
- * };
16
- *
17
- * @example
18
- * // In Vue components
19
- * <template>
20
- * <h1>{{ $t('welcome.title') }}</h1>
21
- * <p>{{ $t('welcome.message', { name: userName }) }}</p>
22
- * <button @click="$setLocale('es')">Español</button>
23
- * </template>
24
- */
25
- declare const ezI18nVue: Plugin;
26
- /**
27
- * Composable for using i18n in Vue components with Composition API
28
- *
29
- * @example
30
- * <script setup>
31
- * import { useI18n } from '@zachhandley/ez-i18n/vue';
32
- *
33
- * const { t, locale, setLocale } = useI18n();
34
- * const greeting = t('welcome.greeting');
35
- * </script>
36
- */
37
- declare function useI18n(): {
38
- t: TranslateFunction;
39
- locale: Readonly<vue.Ref<string, string>>;
40
- setLocale: typeof setLocale;
41
- };
42
- declare module 'vue' {
43
- interface ComponentCustomProperties {
44
- $t: TranslateFunction;
45
- /** Current locale (reactive ref from nanostore) */
46
- $locale: Readonly<vue.Ref<string>>;
47
- $setLocale: typeof setLocale;
48
- }
49
- }
50
-
51
- export { ezI18nVue as default, ezI18nVue, useI18n };
@@ -1,67 +0,0 @@
1
- // src/runtime/vue-plugin.ts
2
- import { computed } from "vue";
3
- import { useStore } from "@nanostores/vue";
4
- import { effectiveLocale, translations, setLocale } from "@zachhandley/ez-i18n/runtime";
5
- function getNestedValue(obj, path) {
6
- const keys = path.split(".");
7
- let value = obj;
8
- for (const key of keys) {
9
- if (value == null || typeof value !== "object") {
10
- return void 0;
11
- }
12
- value = value[key];
13
- }
14
- return value;
15
- }
16
- function interpolate(str, params) {
17
- if (!params) return str;
18
- return str.replace(/\{(\w+)\}/g, (match, key) => {
19
- return key in params ? String(params[key]) : match;
20
- });
21
- }
22
- function createTranslateFunction(translationsRef) {
23
- return (key, params) => {
24
- const trans = translationsRef.value;
25
- const value = getNestedValue(trans, key);
26
- if (typeof value !== "string") {
27
- if (import.meta.env?.DEV) {
28
- console.warn("[ez-i18n] Missing translation:", key);
29
- }
30
- return key;
31
- }
32
- return interpolate(value, params);
33
- };
34
- }
35
- var ezI18nVue = {
36
- install(app) {
37
- const locale = useStore(effectiveLocale);
38
- const trans = useStore(translations);
39
- const transComputed = computed(() => trans.value);
40
- const t = createTranslateFunction(transComputed);
41
- app.config.globalProperties.$t = t;
42
- app.config.globalProperties.$locale = locale;
43
- app.config.globalProperties.$setLocale = setLocale;
44
- app.provide("ez-i18n", {
45
- t,
46
- locale,
47
- setLocale
48
- });
49
- }
50
- };
51
- function useI18n() {
52
- const locale = useStore(effectiveLocale);
53
- const trans = useStore(translations);
54
- const transComputed = computed(() => trans.value);
55
- const t = createTranslateFunction(transComputed);
56
- return {
57
- t,
58
- locale,
59
- setLocale
60
- };
61
- }
62
- var vue_plugin_default = ezI18nVue;
63
- export {
64
- vue_plugin_default as default,
65
- ezI18nVue,
66
- useI18n
67
- };
@@ -1,79 +0,0 @@
1
- import { useStore } from '@nanostores/react';
2
- // Import from package path (not relative) to ensure shared store instance
3
- import { effectiveLocale, translations, setLocale } from '@zachhandley/ez-i18n/runtime';
4
- import type { TranslateFunction } from '../types';
5
-
6
- /**
7
- * Get nested value from object using dot notation
8
- */
9
- function getNestedValue(obj: Record<string, unknown>, path: string): unknown {
10
- const keys = path.split('.');
11
- let value: unknown = obj;
12
-
13
- for (const key of keys) {
14
- if (value == null || typeof value !== 'object') {
15
- return undefined;
16
- }
17
- value = (value as Record<string, unknown>)[key];
18
- }
19
-
20
- return value;
21
- }
22
-
23
- /**
24
- * Interpolate params into string
25
- */
26
- function interpolate(
27
- str: string,
28
- params?: Record<string, string | number>
29
- ): string {
30
- if (!params) return str;
31
- return str.replace(/\{(\w+)\}/g, (match, key) => {
32
- return key in params ? String(params[key]) : match;
33
- });
34
- }
35
-
36
- /**
37
- * React hook for i18n
38
- *
39
- * @example
40
- * import { useI18n } from '@zachhandley/ez-i18n/react';
41
- *
42
- * function MyComponent() {
43
- * const { t, locale, setLocale } = useI18n();
44
- *
45
- * return (
46
- * <div>
47
- * <h1>{t('common.welcome')}</h1>
48
- * <p>{t('greeting', { name: 'World' })}</p>
49
- * <button onClick={() => setLocale('es')}>Español</button>
50
- * </div>
51
- * );
52
- * }
53
- */
54
- export function useI18n() {
55
- const locale = useStore(effectiveLocale);
56
- const trans = useStore(translations);
57
-
58
- const t: TranslateFunction = (
59
- key: string,
60
- params?: Record<string, string | number>
61
- ): string => {
62
- const value = getNestedValue(trans, key);
63
-
64
- if (typeof value !== 'string') {
65
- if (import.meta.env?.DEV) {
66
- console.warn('[ez-i18n] Missing translation:', key);
67
- }
68
- return key;
69
- }
70
-
71
- return interpolate(value, params);
72
- };
73
-
74
- return {
75
- t,
76
- locale,
77
- setLocale,
78
- };
79
- }