@zachhandley/ez-i18n 0.3.6 → 0.3.7

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
@@ -83,10 +83,12 @@ await setLocale('es');
83
83
  Core translation functions:
84
84
 
85
85
  ```ts
86
- import { t, locale, setLocale, initLocale } from 'ez-i18n:runtime';
86
+ import { t, tc, locale, setLocale, initLocale } from 'ez-i18n:runtime';
87
87
 
88
88
  t('key'); // Translate a key
89
89
  t('key', { name: 'World' }); // With interpolation
90
+ tc('key'); // Returns ReadableAtom<string> for reactive subscriptions
91
+ tc('key', { name: 'World' }); // With interpolation (reactive)
90
92
  locale; // Reactive store with current locale
91
93
  await setLocale('es'); // Change locale (persists to cookie)
92
94
  initLocale('en', data); // Initialize with translations
@@ -117,6 +119,28 @@ import { loadTranslations, translationLoaders } from 'ez-i18n:translations';
117
119
  const data = await loadTranslations('es');
118
120
  ```
119
121
 
122
+ ## Reactive Translations with tc()
123
+
124
+ The `tc()` function returns a nanostore computed atom (`ReadableAtom<string>`) that automatically updates when the locale or translation data changes. This is useful when:
125
+
126
+ - Translations may load asynchronously after component mount
127
+ - You need fine-grained reactivity for specific translation keys
128
+ - Using with framework bindings (Vue/React) that need reactive subscriptions
129
+
130
+ ```ts
131
+ import { tc } from 'ez-i18n:runtime';
132
+ import { useStore } from '@nanostores/react'; // or @nanostores/vue
133
+
134
+ // Returns a ReadableAtom that updates when locale changes
135
+ const title = tc('welcome.title');
136
+ const greeting = tc('welcome.message', { name: 'Alice' });
137
+
138
+ // In React/Vue, use with framework-specific store hooks
139
+ const titleValue = useStore(title); // Automatically re-renders on locale change
140
+ ```
141
+
142
+ For most use cases, the regular `t()` function is sufficient. Use `tc()` when you need explicit reactive subscriptions in your framework code.
143
+
120
144
  ## Locale Utilities
121
145
 
122
146
  The package includes a comprehensive locale database with 100+ languages:
package/dist/index.js CHANGED
@@ -501,6 +501,7 @@ export const localeDirections = ${JSON.stringify(localeDirections)};
501
501
  }
502
502
  if (id === RESOLVED_PREFIX + VIRTUAL_RUNTIME) {
503
503
  return `
504
+ import { computed } from 'nanostores';
504
505
  import { effectiveLocale, translations, setLocale, initLocale } from '@zachhandley/ez-i18n/runtime';
505
506
 
506
507
  export { setLocale, initLocale };
@@ -530,7 +531,7 @@ function interpolate(str, params) {
530
531
  }
531
532
 
532
533
  /**
533
- * Translate a key to the current locale
534
+ * Translate a key to the current locale (non-reactive)
534
535
  * @param key - Dot-notation key (e.g., 'common.welcome')
535
536
  * @param params - Optional interpolation params
536
537
  */
@@ -547,6 +548,26 @@ export function t(key, params) {
547
548
 
548
549
  return interpolate(value, params);
549
550
  }
551
+
552
+ /**
553
+ * Create a reactive translation computed (nanostore computed atom)
554
+ * @param key - Dot-notation key (e.g., 'common.welcome')
555
+ * @param params - Optional interpolation params
556
+ */
557
+ export function tc(key, params) {
558
+ return computed(translations, (trans) => {
559
+ const value = getNestedValue(trans, key);
560
+
561
+ if (typeof value !== 'string') {
562
+ if (import.meta.env.DEV) {
563
+ console.warn('[ez-i18n] Missing translation:', key);
564
+ }
565
+ return key;
566
+ }
567
+
568
+ return interpolate(value, params);
569
+ });
570
+ }
550
571
  `;
551
572
  }
552
573
  if (id === RESOLVED_PREFIX + VIRTUAL_TRANSLATIONS) {
@@ -1,5 +1,16 @@
1
1
  import * as nanostores from 'nanostores';
2
+ import { ReadableAtom } from 'nanostores';
2
3
 
4
+ /**
5
+ * Get nested value from object using dot notation
6
+ * @example getNestedValue({ a: { b: 'hello' } }, 'a.b') // 'hello'
7
+ */
8
+ declare function getNestedValue(obj: Record<string, unknown>, path: string): unknown;
9
+ /**
10
+ * Interpolate params into string using {placeholder} syntax
11
+ * @example interpolate('Hello {name}!', { name: 'World' }) // 'Hello World!'
12
+ */
13
+ declare function interpolate(str: string, params?: Record<string, string | number>): string;
3
14
  /**
4
15
  * Client-side locale preference (persisted to localStorage)
5
16
  */
@@ -7,7 +18,7 @@ declare const localePreference: nanostores.WritableAtom<string>;
7
18
  /**
8
19
  * Effective locale - uses server locale if set, otherwise client preference
9
20
  */
10
- declare const effectiveLocale: nanostores.ReadableAtom<string>;
21
+ declare const effectiveLocale: ReadableAtom<string>;
11
22
  /**
12
23
  * Current translations object (reactive)
13
24
  */
@@ -48,5 +59,34 @@ declare function getLocale(): string;
48
59
  * Get current translations (non-reactive)
49
60
  */
50
61
  declare function getTranslations(): Record<string, unknown>;
62
+ /**
63
+ * Translate a key to its value (non-reactive, imperative)
64
+ * Use this in event handlers, callbacks, or non-reactive contexts.
65
+ *
66
+ * @example
67
+ * const message = t('welcome.title');
68
+ * const greeting = t('welcome.hello', { name: 'World' });
69
+ */
70
+ declare function t(key: string, params?: Record<string, string | number>): string;
71
+ /**
72
+ * Create a reactive translation computed (nanostore computed atom)
73
+ * Returns a ReadableAtom<string> that updates when translations change.
74
+ *
75
+ * Use this when you need a reactive translation that updates automatically:
76
+ * - In Vue/React components where translations may load after initial render
77
+ * - When locale changes should trigger re-renders
78
+ * - To avoid hydration mismatches in SSR
79
+ *
80
+ * @example
81
+ * // Static key
82
+ * const $title = tc('welcome.title');
83
+ *
84
+ * // In Vue (with @nanostores/vue)
85
+ * const title = useStore(tc('welcome.title'));
86
+ *
87
+ * // In React (with @nanostores/react)
88
+ * const title = useStore(tc('welcome.title'));
89
+ */
90
+ declare function tc(key: string, params?: Record<string, string | number>): ReadableAtom<string>;
51
91
 
52
- export { type TranslationLoader, effectiveLocale, getLocale, getTranslations, initLocale, localeLoading, localePreference, setLocale, setTranslations, translations };
92
+ export { type TranslationLoader, effectiveLocale, getLocale, getNestedValue, getTranslations, initLocale, interpolate, localeLoading, localePreference, setLocale, setTranslations, t, tc, translations };
@@ -1,6 +1,23 @@
1
1
  // src/runtime/store.ts
2
2
  import { atom, computed } from "nanostores";
3
3
  import { persistentAtom } from "@nanostores/persistent";
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
+ }
4
21
  var serverLocale = atom(null);
5
22
  var localePreference = persistentAtom("ez-locale", "en", {
6
23
  encode: (value) => value,
@@ -61,14 +78,41 @@ function getLocale() {
61
78
  function getTranslations() {
62
79
  return translations.get();
63
80
  }
81
+ function t(key, params) {
82
+ const trans = translations.get();
83
+ const value = getNestedValue(trans, key);
84
+ if (typeof value !== "string") {
85
+ if (typeof import.meta !== "undefined" && import.meta.env?.DEV) {
86
+ console.warn("[ez-i18n] Missing translation:", key);
87
+ }
88
+ return key;
89
+ }
90
+ return interpolate(value, params);
91
+ }
92
+ function tc(key, params) {
93
+ return computed(translations, (trans) => {
94
+ const value = getNestedValue(trans, key);
95
+ if (typeof value !== "string") {
96
+ if (typeof import.meta !== "undefined" && import.meta.env?.DEV) {
97
+ console.warn("[ez-i18n] Missing translation:", key);
98
+ }
99
+ return key;
100
+ }
101
+ return interpolate(value, params);
102
+ });
103
+ }
64
104
  export {
65
105
  effectiveLocale,
66
106
  getLocale,
107
+ getNestedValue,
67
108
  getTranslations,
68
109
  initLocale,
110
+ interpolate,
69
111
  localeLoading,
70
112
  localePreference,
71
113
  setLocale,
72
114
  setTranslations,
115
+ t,
116
+ tc,
73
117
  translations
74
118
  };
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@zachhandley/ez-i18n",
3
- "version": "0.3.6",
3
+ "version": "0.3.7",
4
4
  "publishConfig": {
5
5
  "access": "public"
6
6
  },