@techninja/clearstack 0.3.49 → 0.4.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 CHANGED
@@ -36,6 +36,7 @@ A project/task tracker that exercises every pattern in the spec: API-backed enti
36
36
  | [SERVER_AND_DEPS.md](./docs/SERVER_AND_DEPS.md) | Express server, import maps, vendor dependency loading |
37
37
  | [BACKEND_API_SPEC.md](./docs/BACKEND_API_SPEC.md) | REST CRUD, JSON Schema via HEAD, entity management |
38
38
  | [TESTING.md](./docs/TESTING.md) | Testing philosophy, tools, patterns, phase checkpoints |
39
+ | [I18N.md](./docs/I18N.md) | Internationalization — 4-layer cascade, `t()` usage, conventions |
39
40
  | [BUILD_LOG.md](./docs/BUILD_LOG.md) | How this project was built — LLM-human collaboration proof |
40
41
  | [QUICKSTART.md](./docs/QUICKSTART.md) | Scaffolder setup, development workflow, updating, compliance |
41
42
 
package/docs/I18N.md ADDED
@@ -0,0 +1,108 @@
1
+ # Internationalization (i18n)
2
+
3
+ ## Why This Matters
4
+
5
+ Hardcoding UI strings is a foot gun. Retrofitting i18n into an existing app
6
+ requires touching every component — a painful, error-prone refactor. Starting
7
+ with `t('key')` from day one costs almost nothing and keeps the door open while
8
+ also clearly separating UI and exact wording concerns.
9
+
10
+ ## The Pattern
11
+
12
+ A 4-layer message cascade. Last layer wins:
13
+
14
+ ```
15
+ 1. App defaults → DEFAULTS in utils/i18n.js (English, ships with app)
16
+ 2. Locale file → /locales/<lang>.json (e.g. es.json)
17
+ 3. Overrides → /locales/overrides.json (project customizes English)
18
+ 4. Locale overrides → /locales/overrides.<lang>.json (project customizes locale)
19
+ ```
20
+
21
+ This means:
22
+
23
+ - The platform ships sensible English defaults
24
+ - Projects override just what they need in `overrides.json`
25
+ - Adding a new language only requires a `<lang>.json` file
26
+ - No build step, no external dependency
27
+
28
+ ## Usage
29
+
30
+ ```js
31
+ import { t, loadLocale } from '#utils/i18n.js';
32
+
33
+ // Call once at app init (router or top-level component connect)
34
+ await loadLocale(navigator.language);
35
+
36
+ // Simple key
37
+ t('general.loading') // → 'Loading…'
38
+
39
+ // With interpolation
40
+ t('order.thanks', { name: 'James' }) // → 'Thanks, James!'
41
+
42
+ // Falls back to key if not found — never throws
43
+ t('missing.key') // → 'missing.key'
44
+ ```
45
+
46
+ ## Key Naming Convention
47
+
48
+ `domain.action` or `component.concept` — dot-separated, lowercase, no spaces.
49
+
50
+ ```
51
+ general.loading general.error general.back
52
+ nav.home nav.signIn nav.signOut
53
+ cart.add cart.empty cart.checkout
54
+ product.inStock product.outOfStock
55
+ error.notFound error.offline
56
+ ```
57
+
58
+ Avoid generic keys like `button1` or `label`. Keys should be readable as
59
+ documentation.
60
+
61
+ ## App Init
62
+
63
+ Call `loadLocale()` once before rendering. In a Hybrids app, the router's
64
+ `connect` is the right place:
65
+
66
+ ```js
67
+ // router/index.js
68
+ import { loadLocale } from '#utils/i18n.js';
69
+
70
+ await loadLocale(navigator.language);
71
+ ```
72
+
73
+ For SSR/static sites, call it in the page's `connect` handler instead.
74
+
75
+ ## Adding a Language
76
+
77
+ 1. Create `/locales/<lang>.json` with translated keys
78
+ 2. Only include keys that differ from English — missing keys fall back to defaults
79
+ 3. Optionally add `/locales/overrides.<lang>.json` for project-specific overrides
80
+
81
+ ```json
82
+ // locales/es.json
83
+ {
84
+ "general.loading": "Cargando…",
85
+ "general.error": "Algo salió mal.",
86
+ "nav.home": "Inicio"
87
+ }
88
+ ```
89
+
90
+ ## Project Overrides
91
+
92
+ To customize English strings (e.g. brand voice) without touching `i18n.js`:
93
+
94
+ ```json
95
+ // locales/overrides.json
96
+ {
97
+ "general.loading": "Analyzing your DNA…",
98
+ "error.offline": "Check your connection and try again."
99
+ }
100
+ ```
101
+
102
+ ## Rules
103
+
104
+ - **Always use `t()`** for any user-visible string in components
105
+ - **Never hardcode UI strings** in component render functions
106
+ - **Keys fall back to themselves** — `t('missing')` returns `'missing'`, never crashes
107
+ - **`loadLocale()` is safe to call multiple times** — last call wins
108
+ - **Locale files are optional** — English-only apps just use `overrides.json`
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@techninja/clearstack",
3
- "version": "0.3.49",
3
+ "version": "0.4.0",
4
4
  "type": "module",
5
5
  "description": "A no-build web component framework specification — scaffold, validate, and evolve spec-compliant projects",
6
6
  "bin": {
@@ -0,0 +1,108 @@
1
+ # Internationalization (i18n)
2
+
3
+ ## Why This Matters
4
+
5
+ Hardcoding UI strings is a foot gun. Retrofitting i18n into an existing app
6
+ requires touching every component — a painful, error-prone refactor. Starting
7
+ with `t('key')` from day one costs almost nothing and keeps the door open while
8
+ also clearly separating UI and exact wording concerns.
9
+
10
+ ## The Pattern
11
+
12
+ A 4-layer message cascade. Last layer wins:
13
+
14
+ ```
15
+ 1. App defaults → DEFAULTS in utils/i18n.js (English, ships with app)
16
+ 2. Locale file → /locales/<lang>.json (e.g. es.json)
17
+ 3. Overrides → /locales/overrides.json (project customizes English)
18
+ 4. Locale overrides → /locales/overrides.<lang>.json (project customizes locale)
19
+ ```
20
+
21
+ This means:
22
+
23
+ - The platform ships sensible English defaults
24
+ - Projects override just what they need in `overrides.json`
25
+ - Adding a new language only requires a `<lang>.json` file
26
+ - No build step, no external dependency
27
+
28
+ ## Usage
29
+
30
+ ```js
31
+ import { t, loadLocale } from '#utils/i18n.js';
32
+
33
+ // Call once at app init (router or top-level component connect)
34
+ await loadLocale(navigator.language);
35
+
36
+ // Simple key
37
+ t('general.loading') // → 'Loading…'
38
+
39
+ // With interpolation
40
+ t('order.thanks', { name: 'James' }) // → 'Thanks, James!'
41
+
42
+ // Falls back to key if not found — never throws
43
+ t('missing.key') // → 'missing.key'
44
+ ```
45
+
46
+ ## Key Naming Convention
47
+
48
+ `domain.action` or `component.concept` — dot-separated, lowercase, no spaces.
49
+
50
+ ```
51
+ general.loading general.error general.back
52
+ nav.home nav.signIn nav.signOut
53
+ cart.add cart.empty cart.checkout
54
+ product.inStock product.outOfStock
55
+ error.notFound error.offline
56
+ ```
57
+
58
+ Avoid generic keys like `button1` or `label`. Keys should be readable as
59
+ documentation.
60
+
61
+ ## App Init
62
+
63
+ Call `loadLocale()` once before rendering. In a Hybrids app, the router's
64
+ `connect` is the right place:
65
+
66
+ ```js
67
+ // router/index.js
68
+ import { loadLocale } from '#utils/i18n.js';
69
+
70
+ await loadLocale(navigator.language);
71
+ ```
72
+
73
+ For SSR/static sites, call it in the page's `connect` handler instead.
74
+
75
+ ## Adding a Language
76
+
77
+ 1. Create `/locales/<lang>.json` with translated keys
78
+ 2. Only include keys that differ from English — missing keys fall back to defaults
79
+ 3. Optionally add `/locales/overrides.<lang>.json` for project-specific overrides
80
+
81
+ ```json
82
+ // locales/es.json
83
+ {
84
+ "general.loading": "Cargando…",
85
+ "general.error": "Algo salió mal.",
86
+ "nav.home": "Inicio"
87
+ }
88
+ ```
89
+
90
+ ## Project Overrides
91
+
92
+ To customize English strings (e.g. brand voice) without touching `i18n.js`:
93
+
94
+ ```json
95
+ // locales/overrides.json
96
+ {
97
+ "general.loading": "Analyzing your DNA…",
98
+ "error.offline": "Check your connection and try again."
99
+ }
100
+ ```
101
+
102
+ ## Rules
103
+
104
+ - **Always use `t()`** for any user-visible string in components
105
+ - **Never hardcode UI strings** in component render functions
106
+ - **Keys fall back to themselves** — `t('missing')` returns `'missing'`, never crashes
107
+ - **`loadLocale()` is safe to call multiple times** — last call wins
108
+ - **Locale files are optional** — English-only apps just use `overrides.json`
@@ -0,0 +1,3 @@
1
+ {
2
+ "general.loading": "Loading…"
3
+ }
@@ -0,0 +1,87 @@
1
+ /**
2
+ * i18n — 4-layer message cascade.
3
+ *
4
+ * Resolution order (last wins):
5
+ * 1. App defaults (English, defined in this file)
6
+ * 2. Locale file (/locales/<lang>.json)
7
+ * 3. Project overrides (/locales/overrides.json)
8
+ * 4. Project locale overrides (/locales/overrides.<lang>.json)
9
+ *
10
+ * Usage:
11
+ * import { t, loadLocale } from '#utils/i18n.js';
12
+ * await loadLocale(navigator.language); // call once at app init
13
+ * t('general.loading') // → 'Loading…'
14
+ * t('greeting', { name: 'James' }) // → 'Hello, James!'
15
+ *
16
+ * Key convention: component.action or domain.concept
17
+ * Add app-specific defaults below and override per-project in overrides.json.
18
+ * @module utils/i18n
19
+ */
20
+
21
+ /** @type {Record<string, string>} */
22
+ const DEFAULTS = {
23
+ 'general.loading': 'Loading…',
24
+ 'general.error': 'Something went wrong.',
25
+ 'general.retry': 'Try again',
26
+ 'general.back': 'Back',
27
+ 'general.close': 'Close',
28
+ 'general.save': 'Save',
29
+ 'general.cancel': 'Cancel',
30
+ 'general.confirm': 'Confirm',
31
+ 'general.noResults': 'No results found.',
32
+ 'nav.home': 'Home',
33
+ 'error.notFound': 'Page not found.',
34
+ 'error.offline': 'You appear to be offline.',
35
+ };
36
+
37
+ /** @type {Record<string, string>} */
38
+ let active = { ...DEFAULTS };
39
+
40
+ /** @param {string} url @returns {Promise<Record<string, string>>} */
41
+ async function fetchJson(url) {
42
+ try {
43
+ const res = await fetch(url);
44
+ return res.ok ? await res.json() : {};
45
+ } catch {
46
+ return {};
47
+ }
48
+ }
49
+
50
+ /**
51
+ * Load and merge all i18n layers. Call once on app init.
52
+ * @param {string} [locale] - BCP 47 locale string e.g. 'es', 'es-MX'
53
+ */
54
+ export async function loadLocale(locale) {
55
+ const lang = locale?.split('-')[0] || '';
56
+ const isEnglish = !lang || lang === 'en';
57
+
58
+ const [localeStrings, overrides, localeOverrides] = await Promise.all([
59
+ isEnglish ? {} : fetchJson(`/locales/${lang}.json`),
60
+ fetchJson('/locales/overrides.json'),
61
+ isEnglish ? {} : fetchJson(`/locales/overrides.${lang}.json`),
62
+ ]);
63
+
64
+ active = { ...DEFAULTS, ...localeStrings, ...overrides, ...localeOverrides };
65
+ }
66
+
67
+ /**
68
+ * Get a translated string with optional {param} interpolation.
69
+ * Falls back to the key itself if not found.
70
+ * @param {string} key
71
+ * @param {Record<string, string>} [params]
72
+ * @returns {string}
73
+ */
74
+ export function t(key, params) {
75
+ let msg = active[key] ?? key;
76
+ if (params) {
77
+ for (const [k, v] of Object.entries(params)) {
78
+ msg = msg.replace(`{${k}}`, v);
79
+ }
80
+ }
81
+ return msg;
82
+ }
83
+
84
+ /** @returns {string} Active locale language code e.g. 'en', 'es' */
85
+ export function getLocale() {
86
+ return navigator.language?.split('-')[0] || 'en';
87
+ }