@zachhandley/ez-i18n 0.3.4 → 0.3.5

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 CHANGED
@@ -76,6 +76,75 @@ t('common.countdown', { seconds: 5 }); // "Ready in 5 seconds"
76
76
  await setLocale('es');
77
77
  ```
78
78
 
79
+ ## Virtual Modules
80
+
81
+ ### `ez-i18n:runtime`
82
+
83
+ Core translation functions:
84
+
85
+ ```ts
86
+ import { t, locale, setLocale, initLocale } from 'ez-i18n:runtime';
87
+
88
+ t('key'); // Translate a key
89
+ t('key', { name: 'World' }); // With interpolation
90
+ locale; // Reactive store with current locale
91
+ await setLocale('es'); // Change locale (persists to cookie)
92
+ initLocale('en', data); // Initialize with translations
93
+ ```
94
+
95
+ ### `ez-i18n:config`
96
+
97
+ Access your i18n configuration:
98
+
99
+ ```ts
100
+ import {
101
+ locales, // ['en', 'es', 'fr']
102
+ defaultLocale, // 'en'
103
+ cookieName, // 'ez-locale'
104
+ localeNames, // { en: 'English', es: 'Español', fr: 'Français' }
105
+ localeToBCP47, // { en: 'en-US', es: 'es-ES', fr: 'fr-FR' }
106
+ localeDirections, // { en: 'ltr', es: 'ltr', ar: 'rtl' }
107
+ } from 'ez-i18n:config';
108
+ ```
109
+
110
+ ### `ez-i18n:translations`
111
+
112
+ Dynamic translation loading:
113
+
114
+ ```ts
115
+ import { loadTranslations, translationLoaders } from 'ez-i18n:translations';
116
+
117
+ const data = await loadTranslations('es');
118
+ ```
119
+
120
+ ## Locale Utilities
121
+
122
+ The package includes a comprehensive locale database with 100+ languages:
123
+
124
+ ```ts
125
+ import {
126
+ LOCALE_DATABASE,
127
+ getLocaleInfo,
128
+ buildLocaleNames,
129
+ buildLocaleToBCP47,
130
+ buildLocaleDirections,
131
+ } from '@zachhandley/ez-i18n';
132
+ import type { LocaleInfo } from '@zachhandley/ez-i18n';
133
+
134
+ // Get info for any locale
135
+ const info = getLocaleInfo('es');
136
+ // { name: 'Español', englishName: 'Spanish', bcp47: 'es-ES', dir: 'ltr' }
137
+
138
+ // Build mappings for your supported locales
139
+ const names = buildLocaleNames(['en', 'es', 'ar']);
140
+ // { en: 'English', es: 'Español', ar: 'العربية' }
141
+
142
+ const directions = buildLocaleDirections(['en', 'es', 'ar']);
143
+ // { en: 'ltr', es: 'ltr', ar: 'rtl' }
144
+ ```
145
+
146
+ Includes native display names, English names, BCP47 codes, and text direction (LTR/RTL) for all major languages and regional variants.
147
+
79
148
  ## Framework Bindings
80
149
 
81
150
  - React: `@zachhandley/ez-i18n-react`
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@zachhandley/ez-i18n",
3
- "version": "0.3.4",
3
+ "version": "0.3.5",
4
4
  "publishConfig": {
5
5
  "access": "public"
6
6
  },
@@ -30,7 +30,7 @@
30
30
  },
31
31
  "files": [
32
32
  "dist",
33
- "src",
33
+ "src/components",
34
34
  "README.md"
35
35
  ],
36
36
  "keywords": [
package/src/index.ts DELETED
@@ -1,130 +0,0 @@
1
- import type { AstroIntegration, HookParameters } from 'astro';
2
- import type { EzI18nConfig } from './types';
3
- import { vitePlugin, resolveConfig } from './vite-plugin';
4
-
5
- export type { EzI18nConfig, TranslateFunction } from './types';
6
- export { LOCALE_DATABASE, getLocaleInfo, buildLocaleNames, buildLocaleToBCP47, buildLocaleDirections } from './utils/locales';
7
- export type { LocaleInfo } from './utils/locales';
8
-
9
- /**
10
- * ez-i18n Astro integration
11
- *
12
- * Provides cookie-based i18n without URL prefixes.
13
- *
14
- * @example
15
- * // astro.config.ts
16
- * import ezI18n from '@zachhandley/ez-i18n';
17
- *
18
- * export default defineConfig({
19
- * integrations: [
20
- * ezI18n({
21
- * locales: ['en', 'es', 'fr'],
22
- * defaultLocale: 'en',
23
- * translations: {
24
- * en: './src/i18n/en.json',
25
- * es: './src/i18n/es.json',
26
- * },
27
- * }),
28
- * ],
29
- * });
30
- */
31
- export default function ezI18n(config: EzI18nConfig): AstroIntegration {
32
- const resolved = resolveConfig(config);
33
-
34
- return {
35
- name: 'ez-i18n',
36
- hooks: {
37
- 'astro:config:setup': ({
38
- updateConfig,
39
- addMiddleware,
40
- injectScript,
41
- }: HookParameters<'astro:config:setup'>) => {
42
- // Add Vite plugin for virtual modules
43
- updateConfig({
44
- vite: {
45
- plugins: [vitePlugin(config)],
46
- },
47
- });
48
-
49
- // Add locale detection middleware
50
- addMiddleware({
51
- entrypoint: '@zachhandley/ez-i18n/middleware',
52
- order: 'pre',
53
- });
54
-
55
- // Inject hydration script to sync localStorage with cookie
56
- // This prevents hydration mismatch when server and client disagree
57
- const hydrationScript = `
58
- (function() {
59
- try {
60
- var cookieName = ${JSON.stringify(resolved.cookieName)};
61
- var stored = localStorage.getItem(cookieName);
62
- var cookieMatch = document.cookie.match(new RegExp(cookieName + '=([^;]+)'));
63
- var cookie = cookieMatch ? cookieMatch[1] : null;
64
- if (cookie && stored !== cookie) {
65
- localStorage.setItem(cookieName, cookie);
66
- }
67
- } catch (e) {}
68
- })();
69
- `;
70
- injectScript('head-inline', hydrationScript);
71
- },
72
-
73
- 'astro:config:done': ({
74
- injectTypes,
75
- }: HookParameters<'astro:config:done'>) => {
76
- // Inject type declarations for virtual modules
77
- injectTypes({
78
- filename: 'virtual.d.ts',
79
- content: `\
80
- declare module 'ez-i18n:config' {
81
- /** List of all supported locale codes */
82
- export const locales: readonly string[];
83
- /** Default locale when no preference is detected */
84
- export const defaultLocale: string;
85
- /** Cookie name used to store locale preference */
86
- export const cookieName: string;
87
- /** Display names for each locale (in native language) */
88
- export const localeNames: Record<string, string>;
89
- /** BCP47 language tags for each locale */
90
- export const localeToBCP47: Record<string, string>;
91
- /** Text direction for each locale ('ltr' or 'rtl') */
92
- export const localeDirections: Record<string, 'ltr' | 'rtl'>;
93
- }
94
-
95
- declare module 'ez-i18n:runtime' {
96
- import type { ReadableAtom } from 'nanostores';
97
- /** Reactive store containing the current locale */
98
- export const locale: ReadableAtom<string>;
99
- /**
100
- * Translate a key to the current locale
101
- * @param key - Dot-notation key (e.g., 'common.welcome')
102
- * @param params - Optional interpolation params for {placeholder} syntax
103
- */
104
- export function t(key: string, params?: Record<string, string | number>): string;
105
- /**
106
- * Set the current locale and persist to cookie/localStorage
107
- * @param locale - Locale code to switch to
108
- * @param cookieName - Optional custom cookie name
109
- */
110
- export function setLocale(locale: string, cookieName?: string): Promise<void>;
111
- /**
112
- * Initialize the locale store with translations
113
- * @param locale - Initial locale code
114
- * @param translations - Optional initial translations object
115
- */
116
- export function initLocale(locale: string, translations?: Record<string, unknown>): void;
117
- }
118
-
119
- declare module 'ez-i18n:translations' {
120
- /** Load translations for a specific locale */
121
- export function loadTranslations(locale: string): Promise<Record<string, unknown>>;
122
- /** Get the translation loader map from config */
123
- export const translationLoaders: Record<string, () => Promise<{ default: Record<string, unknown> }>>;
124
- }
125
- `,
126
- });
127
- },
128
- },
129
- };
130
- }
package/src/middleware.ts DELETED
@@ -1,82 +0,0 @@
1
- import { defineMiddleware } from 'astro:middleware';
2
- import type { TranslateFunction } from './types';
3
-
4
- /**
5
- * Create a server-side translation function for the given translations object
6
- */
7
- function createT(translations: Record<string, unknown>): TranslateFunction {
8
- return (key: string, params?: Record<string, string | number>): string => {
9
- const keys = key.split('.');
10
- let value: unknown = translations;
11
- for (const k of keys) {
12
- if (value == null || typeof value !== 'object') return key;
13
- value = (value as Record<string, unknown>)[k];
14
- }
15
- if (typeof value !== 'string') return key;
16
- if (!params) return value;
17
- return value.replace(/\{(\w+)\}/g, (_, p) => String(params[p] ?? `{${p}}`));
18
- };
19
- }
20
-
21
- /**
22
- * Locale detection middleware for ez-i18n
23
- *
24
- * Detection priority:
25
- * 1. ?lang query parameter (allows explicit switching)
26
- * 2. Cookie value
27
- * 3. Accept-Language header
28
- * 4. Default locale
29
- */
30
- export const onRequest = defineMiddleware(async ({ cookies, request, locals }, next) => {
31
- // Import config from virtual module (provided by vite-plugin)
32
- const { locales, defaultLocale, cookieName } = await import('ez-i18n:config');
33
-
34
- const url = new URL(request.url);
35
-
36
- // Priority 1: Query parameter
37
- const langParam = url.searchParams.get('lang');
38
-
39
- // Priority 2: Cookie
40
- const cookieValue = cookies.get(cookieName)?.value;
41
-
42
- // Priority 3: Accept-Language header
43
- const acceptLang = request.headers.get('accept-language');
44
- const browserLang = acceptLang?.split(',')[0]?.split('-')[0];
45
-
46
- // Determine locale with priority
47
- let locale = defaultLocale;
48
-
49
- if (langParam && locales.includes(langParam)) {
50
- locale = langParam;
51
- } else if (cookieValue && locales.includes(cookieValue)) {
52
- locale = cookieValue;
53
- } else if (browserLang && locales.includes(browserLang)) {
54
- locale = browserLang;
55
- }
56
-
57
- // Set locale on locals for use in pages
58
- locals.locale = locale;
59
-
60
- // Load translations for the current locale
61
- try {
62
- const { loadTranslations } = await import('ez-i18n:translations');
63
- locals.translations = await loadTranslations(locale);
64
- } catch {
65
- // Fallback to empty translations if loader not configured
66
- locals.translations = {};
67
- }
68
-
69
- // Create server-side translation function
70
- locals.t = createT(locals.translations);
71
-
72
- // Update cookie if changed via query param
73
- if (langParam && langParam !== cookieValue && locales.includes(langParam)) {
74
- cookies.set(cookieName, locale, {
75
- path: '/',
76
- maxAge: 60 * 60 * 24 * 365, // 1 year
77
- sameSite: 'lax',
78
- });
79
- }
80
-
81
- return next();
82
- });
@@ -1,19 +0,0 @@
1
- /**
2
- * Runtime exports for ez-i18n
3
- *
4
- * This module is imported by the ez-i18n:runtime virtual module
5
- * and can also be used directly in Vue components
6
- */
7
- export {
8
- effectiveLocale,
9
- translations,
10
- localePreference,
11
- localeLoading,
12
- initLocale,
13
- setLocale,
14
- setTranslations,
15
- getLocale,
16
- getTranslations,
17
- } from './store';
18
-
19
- export type { TranslationLoader } from './store';
@@ -1,122 +0,0 @@
1
- import { atom, computed } from 'nanostores';
2
- import { persistentAtom } from '@nanostores/persistent';
3
-
4
- /**
5
- * Server-provided locale (set during SSR/hydration)
6
- * Takes precedence over client preference to prevent hydration mismatch
7
- */
8
- const serverLocale = atom<string | null>(null);
9
-
10
- /**
11
- * Client-side locale preference (persisted to localStorage)
12
- */
13
- export const localePreference = persistentAtom<string>('ez-locale', 'en', {
14
- encode: (value) => value,
15
- decode: (value) => value,
16
- });
17
-
18
- /**
19
- * Effective locale - uses server locale if set, otherwise client preference
20
- */
21
- export const effectiveLocale = computed(
22
- [serverLocale, localePreference],
23
- (server, client) => server ?? client
24
- );
25
-
26
- /**
27
- * Current translations object (reactive)
28
- */
29
- export const translations = atom<Record<string, unknown>>({});
30
-
31
- /**
32
- * Whether locale is currently being changed
33
- */
34
- export const localeLoading = atom<boolean>(false);
35
-
36
- /**
37
- * Initialize locale from server-provided value
38
- * Called during hydration to sync server and client state
39
- */
40
- export function initLocale(locale: string, trans?: Record<string, unknown>): void {
41
- serverLocale.set(locale);
42
- localePreference.set(locale);
43
- if (trans) {
44
- translations.set(trans);
45
- }
46
- }
47
-
48
- /**
49
- * Set the translations object
50
- */
51
- export function setTranslations(trans: Record<string, unknown>): void {
52
- translations.set(trans);
53
- }
54
-
55
- /** Type for translation loader function */
56
- export type TranslationLoader = () => Promise<{ default?: Record<string, unknown> } | Record<string, unknown>>;
57
-
58
- /**
59
- * Change locale and update cookie
60
- * Optionally loads new translations dynamically
61
- * @param locale - New locale code
62
- * @param options - Options object or cookie name for backwards compatibility
63
- */
64
- export async function setLocale(
65
- locale: string,
66
- options: string | {
67
- cookieName?: string;
68
- loadTranslations?: TranslationLoader;
69
- } = {}
70
- ): Promise<void> {
71
- // Handle backwards compatibility with string cookieName
72
- const opts = typeof options === 'string'
73
- ? { cookieName: options }
74
- : options;
75
- const { cookieName = 'ez-locale', loadTranslations } = opts;
76
-
77
- localeLoading.set(true);
78
-
79
- try {
80
- // Load new translations if loader provided
81
- if (loadTranslations) {
82
- const mod = await loadTranslations();
83
- const trans = 'default' in mod ? mod.default : mod;
84
- translations.set(trans as Record<string, unknown>);
85
- }
86
-
87
- // Update stores
88
- localePreference.set(locale);
89
- serverLocale.set(locale);
90
-
91
- // Update cookie
92
- if (typeof document !== 'undefined') {
93
- document.cookie = `${cookieName}=${locale}; path=/; max-age=31536000; samesite=lax`;
94
- }
95
-
96
- // Dispatch event for components that need to react
97
- if (typeof document !== 'undefined') {
98
- document.dispatchEvent(
99
- new CustomEvent('ez-i18n:locale-changed', {
100
- detail: { locale },
101
- bubbles: true,
102
- })
103
- );
104
- }
105
- } finally {
106
- localeLoading.set(false);
107
- }
108
- }
109
-
110
- /**
111
- * Get current locale value (non-reactive)
112
- */
113
- export function getLocale(): string {
114
- return effectiveLocale.get();
115
- }
116
-
117
- /**
118
- * Get current translations (non-reactive)
119
- */
120
- export function getTranslations(): Record<string, unknown> {
121
- return translations.get();
122
- }
package/src/types.ts DELETED
@@ -1,125 +0,0 @@
1
- /**
2
- * Translation path for a single locale:
3
- * - Single file: `./src/i18n/en.json`
4
- * - Folder: `./src/i18n/en/` (auto-discover all JSONs inside)
5
- * - Glob: `./src/i18n/en/**.json` (recursive)
6
- * - Array: `['./common.json', './auth.json']`
7
- */
8
- export type LocaleTranslationPath = string | string[];
9
-
10
- /**
11
- * Translation config can be:
12
- * - A base directory string (auto-discovers locale folders): './public/i18n/'
13
- * - Per-locale mapping: { en: './src/i18n/en/', es: './src/i18n/es.json' }
14
- */
15
- export type TranslationsConfig = string | Record<string, LocaleTranslationPath>;
16
-
17
- /**
18
- * Configuration for ez-i18n Astro integration
19
- */
20
- export interface EzI18nConfig {
21
- /**
22
- * List of supported locale codes (e.g., ['en', 'es', 'fr'])
23
- * Optional if using directory-based auto-discovery - locales will be
24
- * detected from folder names in the translations directory.
25
- */
26
- locales?: string[];
27
-
28
- /**
29
- * Default locale to use when no preference is detected.
30
- * Required - this tells us what to fall back to.
31
- */
32
- defaultLocale: string;
33
-
34
- /**
35
- * Cookie name for storing locale preference
36
- * @default 'ez-locale'
37
- */
38
- cookieName?: string;
39
-
40
- /**
41
- * Translation file paths configuration.
42
- * Paths are relative to your project root.
43
- *
44
- * Can be:
45
- * - A base directory (auto-discovers locale folders):
46
- * translations: './public/i18n/'
47
- * → Scans for en/, es/, fr/ folders and their JSON files
48
- * → Auto-populates `locales` from discovered folders
49
- *
50
- * - Per-locale mapping with flexible path types:
51
- * translations: {
52
- * en: './src/i18n/en.json', // single file
53
- * es: './src/i18n/es/', // folder (all JSONs)
54
- * fr: './src/i18n/fr/**.json', // glob pattern
55
- * de: ['./common.json', './auth.json'] // array of files
56
- * }
57
- *
58
- * If not specified, auto-discovers from ./public/i18n/
59
- */
60
- translations?: TranslationsConfig;
61
-
62
- /**
63
- * Derive namespace from file path relative to locale folder.
64
- *
65
- * When enabled:
66
- * - `en/auth/login.json` with `{ "title": "..." }` → `$t('auth.login.title')`
67
- * - `en/common.json` with `{ "actions": {...} }` → `$t('common.actions.save')`
68
- *
69
- * The file path (minus locale folder and .json extension) becomes the key prefix.
70
- *
71
- * @default true when using folder-based translations config
72
- */
73
- pathBasedNamespacing?: boolean;
74
- }
75
-
76
- /**
77
- * Resolved config with defaults applied.
78
- * After resolution:
79
- * - locales is always populated (from config or auto-discovered)
80
- * - translations is normalized to arrays of absolute file paths
81
- */
82
- export interface ResolvedEzI18nConfig {
83
- locales: string[];
84
- defaultLocale: string;
85
- cookieName: string;
86
- /** Normalized: locale → array of resolved absolute file paths */
87
- translations: Record<string, string[]>;
88
- /** Whether to derive namespace from file path */
89
- pathBasedNamespacing: boolean;
90
- /** Base directory for each locale (used for namespace calculation) */
91
- localeBaseDirs: Record<string, string>;
92
- }
93
-
94
- /**
95
- * Cache file structure (.ez-i18n.json)
96
- * Used to speed up subsequent builds by caching discovered translations
97
- */
98
- export interface TranslationCache {
99
- version: number;
100
- /** Discovered locale → file paths mapping */
101
- discovered: Record<string, string[]>;
102
- /** ISO timestamp of last scan */
103
- lastScan: string;
104
- }
105
-
106
- /**
107
- * Translation function type
108
- */
109
- export type TranslateFunction = (key: string, params?: Record<string, string | number>) => string;
110
-
111
- /**
112
- * Augment Astro's locals type
113
- */
114
- declare global {
115
- namespace App {
116
- interface Locals {
117
- /** Current locale code */
118
- locale: string;
119
- /** Loaded translations for the current locale */
120
- translations: Record<string, unknown>;
121
- /** Server-side translation function */
122
- t: TranslateFunction;
123
- }
124
- }
125
- }
@@ -1,16 +0,0 @@
1
- export {
2
- detectPathType,
3
- resolveTranslationPaths,
4
- autoDiscoverTranslations,
5
- resolveTranslationsConfig,
6
- deepMerge,
7
- loadCache,
8
- saveCache,
9
- isCacheValid,
10
- toRelativeImport,
11
- toGlobPattern,
12
- getNamespaceFromPath,
13
- wrapWithNamespace,
14
- generateNamespaceWrapperCode,
15
- type PathType,
16
- } from './translations';