canopy-i18n 0.8.0 → 0.8.2
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 +116 -112
- package/package.json +7 -6
package/README.md
CHANGED
|
@@ -7,6 +7,9 @@ A tiny, type-safe i18n library for building localized messages with builder patt
|
|
|
7
7
|
- **Type-safe**: Compile-time safety for locale keys with full TypeScript IntelliSense support.
|
|
8
8
|
- **Flexible templating**: Plain functions support any JavaScript logic, template literals, or formatting library.
|
|
9
9
|
- **Zero dependencies**: Lightweight with native TypeScript syntax, no custom {{placeholder}} format.
|
|
10
|
+
- **React-ready**: Provider, hooks, and built-in source wrappers for URL hash / search param / pathname / localStorage / Cookie. See [React Integration](#react-integration).
|
|
11
|
+
|
|
12
|
+
> **Using React?** Jump straight to [React Integration](#react-integration) — the Provider, hooks, and ready-made source wrappers cover most app setups out of the box.
|
|
10
13
|
## Why Canopy i18n?
|
|
11
14
|
|
|
12
15
|
Traditional i18n libraries require separate JSON files and string-based key lookups:
|
|
@@ -110,6 +113,119 @@ console.log(jaMessages.greeting()); // "こんにちは"
|
|
|
110
113
|
console.log(jaMessages.welcome({ name: 'Tanaka', age: 20 })); // "こんにちは、Tanakaさん。あなたは20歳です。"
|
|
111
114
|
```
|
|
112
115
|
|
|
116
|
+
## React Integration
|
|
117
|
+
|
|
118
|
+
`canopy-i18n/react` provides a tiny factory that returns a Provider, hooks, and a pre-bound `i18n` shorthand — all sharing the same `Locale` type.
|
|
119
|
+
|
|
120
|
+
```tsx
|
|
121
|
+
// i18n.ts
|
|
122
|
+
import { createI18nReact } from 'canopy-i18n/react';
|
|
123
|
+
|
|
124
|
+
export const LOCALES = ['en', 'ja'] as const;
|
|
125
|
+
|
|
126
|
+
export const { i18n, LocaleProvider, useLocale, useBindLocale } =
|
|
127
|
+
createI18nReact(LOCALES);
|
|
128
|
+
|
|
129
|
+
export const appI18n = i18n({
|
|
130
|
+
title: { en: 'My App', ja: 'マイアプリ' },
|
|
131
|
+
greeting: (ctx: { name: string }) => ({
|
|
132
|
+
en: `Hello, ${ctx.name}!`,
|
|
133
|
+
ja: `こんにちは、${ctx.name}さん!`,
|
|
134
|
+
}),
|
|
135
|
+
});
|
|
136
|
+
```
|
|
137
|
+
|
|
138
|
+
```tsx
|
|
139
|
+
// main.tsx
|
|
140
|
+
import { LocaleProvider } from './i18n';
|
|
141
|
+
|
|
142
|
+
<LocaleProvider defaultLocale="en">
|
|
143
|
+
<App />
|
|
144
|
+
</LocaleProvider>
|
|
145
|
+
```
|
|
146
|
+
|
|
147
|
+
`LocaleProvider` also supports a controlled mode for integrating with URL routing, cookies, or external state:
|
|
148
|
+
|
|
149
|
+
```tsx
|
|
150
|
+
// Controlled mode: locale is owned by the parent
|
|
151
|
+
<LocaleProvider locale={currentLocale} onLocaleChange={setCurrentLocale}>
|
|
152
|
+
<App />
|
|
153
|
+
</LocaleProvider>
|
|
154
|
+
```
|
|
155
|
+
|
|
156
|
+
In controlled mode, `setLocale` from `useLocale()` calls your `onLocaleChange` handler instead of mutating internal state.
|
|
157
|
+
|
|
158
|
+
`createI18nReact` also accepts a factory-level `useLocaleSource` for source-driven locale (e.g. external store, URL hook, cookie). When set, the Provider reads the locale from this hook and `setLocale` calls `onLocaleChange` instead of mutating internal state:
|
|
159
|
+
|
|
160
|
+
```tsx
|
|
161
|
+
export const { LocaleProvider, useLocale } = createI18nReact(LOCALES, {
|
|
162
|
+
useLocaleSource: () => useMyStore((s) => s.locale),
|
|
163
|
+
onLocaleChange: (l) => useMyStore.getState().setLocale(l),
|
|
164
|
+
});
|
|
165
|
+
|
|
166
|
+
<LocaleProvider>
|
|
167
|
+
<App />
|
|
168
|
+
</LocaleProvider>
|
|
169
|
+
```
|
|
170
|
+
|
|
171
|
+
### Built-in source wrappers
|
|
172
|
+
|
|
173
|
+
For common sources, `canopy-i18n/react` ships ready-made factories. Each returns the same shape as `createI18nReact` and operates in source-driven mode:
|
|
174
|
+
|
|
175
|
+
```tsx
|
|
176
|
+
import {
|
|
177
|
+
createHashI18nReact, // URL hash (#ja)
|
|
178
|
+
createSearchI18nReact, // URL search param (?lang=ja, configurable via { param })
|
|
179
|
+
createPathnameI18nReact, // URL pathname prefix (/ja/..., configurable via { basePath })
|
|
180
|
+
createStorageI18nReact, // localStorage (configurable via { key })
|
|
181
|
+
createCookieI18nReact, // Cookie (configurable via { key, maxAge, path, sameSite })
|
|
182
|
+
} from 'canopy-i18n/react';
|
|
183
|
+
|
|
184
|
+
export const { LocaleProvider, useLocale, useBindLocale } =
|
|
185
|
+
createHashI18nReact(LOCALES);
|
|
186
|
+
|
|
187
|
+
// Render <LocaleProvider> with no props.
|
|
188
|
+
```
|
|
189
|
+
|
|
190
|
+
```tsx
|
|
191
|
+
// App.tsx
|
|
192
|
+
import { appI18n, useBindLocale, useLocale } from './i18n';
|
|
193
|
+
|
|
194
|
+
function App() {
|
|
195
|
+
const m = useBindLocale({ appI18n });
|
|
196
|
+
const { locale, setLocale } = useLocale();
|
|
197
|
+
|
|
198
|
+
return (
|
|
199
|
+
<div>
|
|
200
|
+
<h1>{m.appI18n.title()}</h1>
|
|
201
|
+
<p>{m.appI18n.greeting({ name: 'Taro' })}</p>
|
|
202
|
+
<button onClick={() => setLocale(locale === 'en' ? 'ja' : 'en')}>
|
|
203
|
+
{locale}
|
|
204
|
+
</button>
|
|
205
|
+
</div>
|
|
206
|
+
);
|
|
207
|
+
}
|
|
208
|
+
```
|
|
209
|
+
|
|
210
|
+
### Factory return value
|
|
211
|
+
|
|
212
|
+
```ts
|
|
213
|
+
const {
|
|
214
|
+
locales, // the LOCALES tuple you passed in
|
|
215
|
+
i18n, // function: i18n(entries) → ChainBuilder bound to LOCALES
|
|
216
|
+
LocaleProvider, // uncontrolled or controlled (see above)
|
|
217
|
+
useLocale, // () => { locale, setLocale }
|
|
218
|
+
useBindLocale, // memoized bindLocale, locale type-checked
|
|
219
|
+
} = createI18nReact(['en', 'ja'] as const);
|
|
220
|
+
```
|
|
221
|
+
|
|
222
|
+
`i18n` is a shorthand for `ChainBuilder.add` bound to a base builder. Each `i18n({...})` call returns an independent `ChainBuilder`, so you can keep chaining `.add({...}).add({...})` for additional entries.
|
|
223
|
+
|
|
224
|
+
- `useBindLocale(msgsDef)` is memoized per `(msgsDef, locale)` pair.
|
|
225
|
+
- The `Locale` type is derived from the `LOCALES` tuple. Passing a `ChainBuilder` whose locales differ from the Provider's locales is rejected at compile time.
|
|
226
|
+
- `createI18nReact` itself has no built-in persistence. Use a built-in wrapper (`createHash/Search/Pathname/Storage/CookieI18nReact`) for common sources, or pass your own `useLocaleSource` / `onLocaleChange` for anything else.
|
|
227
|
+
- React is a `peerDependency` (`>=18`). Non-React users can ignore the `/react` subpath entirely.
|
|
228
|
+
|
|
113
229
|
## API
|
|
114
230
|
|
|
115
231
|
### `createI18n(locales)`
|
|
@@ -344,118 +460,6 @@ console.log(localized.content.main.body()); // "Body"
|
|
|
344
460
|
console.log(localized.content.sidebar.widget()); // "Widget"
|
|
345
461
|
```
|
|
346
462
|
|
|
347
|
-
|
|
348
|
-
## React Integration
|
|
349
|
-
|
|
350
|
-
`canopy-i18n/react` provides a tiny factory that returns a Provider, hooks, and a pre-bound `defineMessage` — all sharing the same `Locale` type.
|
|
351
|
-
|
|
352
|
-
```tsx
|
|
353
|
-
// i18n.ts
|
|
354
|
-
import { createI18nReact } from 'canopy-i18n/react';
|
|
355
|
-
|
|
356
|
-
export const LOCALES = ['en', 'ja'] as const;
|
|
357
|
-
|
|
358
|
-
export const { i18n, LocaleProvider, useLocale, useBindLocale } =
|
|
359
|
-
createI18nReact(LOCALES);
|
|
360
|
-
|
|
361
|
-
export const appI18n = i18n({
|
|
362
|
-
title: { en: 'My App', ja: 'マイアプリ' },
|
|
363
|
-
greeting: (ctx: { name: string }) => ({
|
|
364
|
-
en: `Hello, ${ctx.name}!`,
|
|
365
|
-
ja: `こんにちは、${ctx.name}さん!`,
|
|
366
|
-
}),
|
|
367
|
-
});
|
|
368
|
-
```
|
|
369
|
-
|
|
370
|
-
```tsx
|
|
371
|
-
// main.tsx
|
|
372
|
-
import { LocaleProvider } from './i18n';
|
|
373
|
-
|
|
374
|
-
<LocaleProvider defaultLocale="en">
|
|
375
|
-
<App />
|
|
376
|
-
</LocaleProvider>
|
|
377
|
-
```
|
|
378
|
-
|
|
379
|
-
`LocaleProvider` also supports a controlled mode for integrating with URL routing, cookies, or external state:
|
|
380
|
-
|
|
381
|
-
```tsx
|
|
382
|
-
// Controlled mode: locale is owned by the parent
|
|
383
|
-
<LocaleProvider locale={currentLocale} onLocaleChange={setCurrentLocale}>
|
|
384
|
-
<App />
|
|
385
|
-
</LocaleProvider>
|
|
386
|
-
```
|
|
387
|
-
|
|
388
|
-
In controlled mode, `setLocale` from `useLocale()` calls your `onLocaleChange` handler instead of mutating internal state.
|
|
389
|
-
|
|
390
|
-
`createI18nReact` also accepts a factory-level `useLocaleSource` for source-driven locale (e.g. external store, URL hook, cookie). When set, the Provider reads the locale from this hook and `setLocale` calls `onLocaleChange` instead of mutating internal state:
|
|
391
|
-
|
|
392
|
-
```tsx
|
|
393
|
-
export const { LocaleProvider, useLocale } = createI18nReact(LOCALES, {
|
|
394
|
-
useLocaleSource: () => useMyStore((s) => s.locale),
|
|
395
|
-
onLocaleChange: (l) => useMyStore.getState().setLocale(l),
|
|
396
|
-
});
|
|
397
|
-
|
|
398
|
-
<LocaleProvider>
|
|
399
|
-
<App />
|
|
400
|
-
</LocaleProvider>
|
|
401
|
-
```
|
|
402
|
-
|
|
403
|
-
For common sources, `canopy-i18n/react` ships ready-made factories. Each returns the same shape as `createI18nReact` and operates in source-driven mode:
|
|
404
|
-
|
|
405
|
-
```tsx
|
|
406
|
-
import {
|
|
407
|
-
createHashI18nReact, // URL hash (#ja)
|
|
408
|
-
createSearchI18nReact, // URL search param (?lang=ja, configurable via { param })
|
|
409
|
-
createPathnameI18nReact, // URL pathname prefix (/ja/..., configurable via { basePath })
|
|
410
|
-
createStorageI18nReact, // localStorage (configurable via { key })
|
|
411
|
-
createCookieI18nReact, // Cookie (configurable via { key, maxAge, path, sameSite })
|
|
412
|
-
} from 'canopy-i18n/react';
|
|
413
|
-
|
|
414
|
-
export const { LocaleProvider, useLocale, useBindLocale } =
|
|
415
|
-
createHashI18nReact(LOCALES);
|
|
416
|
-
|
|
417
|
-
// Render <LocaleProvider> with no props.
|
|
418
|
-
```
|
|
419
|
-
|
|
420
|
-
```tsx
|
|
421
|
-
// App.tsx
|
|
422
|
-
import { appI18n, useBindLocale, useLocale } from './i18n';
|
|
423
|
-
|
|
424
|
-
function App() {
|
|
425
|
-
const m = useBindLocale({ appI18n });
|
|
426
|
-
const { locale, setLocale } = useLocale();
|
|
427
|
-
|
|
428
|
-
return (
|
|
429
|
-
<div>
|
|
430
|
-
<h1>{m.appI18n.title()}</h1>
|
|
431
|
-
<p>{m.appI18n.greeting({ name: 'Taro' })}</p>
|
|
432
|
-
<button onClick={() => setLocale(locale === 'en' ? 'ja' : 'en')}>
|
|
433
|
-
{locale}
|
|
434
|
-
</button>
|
|
435
|
-
</div>
|
|
436
|
-
);
|
|
437
|
-
}
|
|
438
|
-
```
|
|
439
|
-
|
|
440
|
-
### Factory return value
|
|
441
|
-
|
|
442
|
-
```ts
|
|
443
|
-
const {
|
|
444
|
-
locales, // the LOCALES tuple you passed in
|
|
445
|
-
i18n, // function: i18n(entries) → ChainBuilder bound to LOCALES
|
|
446
|
-
LocaleProvider, // uncontrolled or controlled (see above)
|
|
447
|
-
useLocale, // () => { locale, setLocale }
|
|
448
|
-
useBindLocale, // memoized bindLocale, locale type-checked
|
|
449
|
-
} = createI18nReact(['en', 'ja'] as const);
|
|
450
|
-
```
|
|
451
|
-
|
|
452
|
-
`i18n` is a shorthand for `ChainBuilder.add` bound to a base builder. Each `i18n({...})` call returns an independent `ChainBuilder`, so you can keep chaining `.add({...}).add({...})` for additional entries.
|
|
453
|
-
|
|
454
|
-
- `useBindLocale(msgsDef)` is memoized per `(msgsDef, locale)` pair.
|
|
455
|
-
- The `Locale` type is derived from the `LOCALES` tuple. Passing a `ChainBuilder` whose locales differ from the Provider's locales is rejected at compile time.
|
|
456
|
-
- `createI18nReact` itself has no built-in persistence. Use a built-in wrapper (`createHash/Search/Pathname/Storage/CookieI18nReact`) for common sources, or pass your own `useLocaleSource` / `onLocaleChange` for anything else.
|
|
457
|
-
- React is a `peerDependency` (`>=18`). Non-React users can ignore the `/react` subpath entirely.
|
|
458
|
-
|
|
459
463
|
## Repository
|
|
460
464
|
|
|
461
465
|
https://github.com/MOhhh-ok/canopy-i18n
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "canopy-i18n",
|
|
3
|
-
"version": "0.8.
|
|
3
|
+
"version": "0.8.2",
|
|
4
4
|
"description": "The Type-Safe i18n library that your IDE will Love",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"main": "dist/index.js",
|
|
@@ -49,16 +49,17 @@
|
|
|
49
49
|
"type-check": "tsc -p . --noEmit",
|
|
50
50
|
"test": "vitest run",
|
|
51
51
|
"test:watch": "vitest",
|
|
52
|
-
"
|
|
52
|
+
"release": "GITHUB_TOKEN=\"$(gh auth token)\" node ./node_modules/release-it/bin/release-it.js"
|
|
53
53
|
},
|
|
54
54
|
"devDependencies": {
|
|
55
55
|
"@tsconfig/node20": "^20.1.9",
|
|
56
|
-
"@types/node": "^25.
|
|
57
|
-
"@types/react": "^19.2.
|
|
56
|
+
"@types/node": "^25.9.1",
|
|
57
|
+
"@types/react": "^19.2.15",
|
|
58
58
|
"next": "16",
|
|
59
59
|
"react": "^19.2.6",
|
|
60
|
-
"
|
|
61
|
-
"
|
|
60
|
+
"release-it": "^19.2.4",
|
|
61
|
+
"typescript": "^6.0.3",
|
|
62
|
+
"vitest": "^4.1.7"
|
|
62
63
|
},
|
|
63
64
|
"keywords": [
|
|
64
65
|
"i18n",
|