@zachhandley/ez-i18n 0.2.1 → 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.
@@ -1,122 +1,122 @@
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
- }
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 CHANGED
@@ -1,123 +1,123 @@
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
- }
122
- }
123
- }
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
+ }
122
+ }
123
+ }
@@ -1,16 +1,16 @@
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';
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';