@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
|
@@ -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,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
|
+
}
|