canopy-i18n 0.2.0 → 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.
- package/README.md +128 -13
- package/dist/bindLocale.d.ts +1 -1
- package/dist/chainBuilder.d.ts +9 -7
- package/dist/chainBuilder.js +2 -1
- package/dist/message.d.ts +8 -8
- package/dist/message.js +4 -1
- package/dist/types.d.ts +2 -2
- package/package.json +1 -1
package/README.md
CHANGED
|
@@ -7,6 +7,7 @@ A tiny, type-safe i18n library for building localized messages with builder patt
|
|
|
7
7
|
- **Builder pattern**: Chain methods to build multiple messages at once.
|
|
8
8
|
- **String or template functions**: Use plain strings or `(ctx) => string` templates.
|
|
9
9
|
- **Flexible templating**: Templates are plain functions, so you can freely use JavaScript template literals, conditionals, helpers, or any formatting library.
|
|
10
|
+
- **Generic return types**: Return any type (string, React components, etc.) from your messages.
|
|
10
11
|
- **Deep locale application**: Switch locale across entire object/array trees, including nested builders.
|
|
11
12
|
|
|
12
13
|
## Installation
|
|
@@ -78,27 +79,53 @@ const builder = createI18n(['ja', 'en', 'fr'] as const);
|
|
|
78
79
|
### `ChainBuilder`
|
|
79
80
|
A builder class for creating multiple localized messages with method chaining.
|
|
80
81
|
|
|
81
|
-
#### `.add(entries)`
|
|
82
|
-
Adds multiple
|
|
82
|
+
#### `.add<ReturnType = string>(entries)`
|
|
83
|
+
Adds multiple messages at once. By default, returns `string`, but you can specify a custom return type.
|
|
83
84
|
|
|
84
|
-
- **
|
|
85
|
+
- **ReturnType**: (optional) Type parameter for the return value (defaults to `string`)
|
|
86
|
+
- **entries**: `Record<string, Record<Locale, ReturnType>>`
|
|
85
87
|
- Returns: `ChainBuilder` with added messages
|
|
86
88
|
|
|
87
89
|
```ts
|
|
90
|
+
// String messages (default)
|
|
88
91
|
const builder = createI18n(['ja', 'en'] as const)
|
|
89
92
|
.add({
|
|
90
93
|
title: { ja: 'タイトル', en: 'Title' },
|
|
91
94
|
greeting: { ja: 'こんにちは', en: 'Hello' },
|
|
92
95
|
});
|
|
96
|
+
|
|
97
|
+
// Custom return type (e.g., React components)
|
|
98
|
+
const messages = createI18n(['ja', 'en'] as const)
|
|
99
|
+
.add<JSX.Element>({
|
|
100
|
+
badge: {
|
|
101
|
+
ja: <span style={{ color: 'red' }}>新着</span>,
|
|
102
|
+
en: <span style={{ color: 'red' }}>NEW</span>,
|
|
103
|
+
},
|
|
104
|
+
});
|
|
105
|
+
|
|
106
|
+
// Custom return type (objects)
|
|
107
|
+
type MenuItem = {
|
|
108
|
+
label: string;
|
|
109
|
+
url: string;
|
|
110
|
+
};
|
|
111
|
+
|
|
112
|
+
const menu = createI18n(['ja', 'en'] as const)
|
|
113
|
+
.add<MenuItem>({
|
|
114
|
+
home: {
|
|
115
|
+
ja: { label: 'ホーム', url: '/' },
|
|
116
|
+
en: { label: 'Home', url: '/' },
|
|
117
|
+
},
|
|
118
|
+
});
|
|
93
119
|
```
|
|
94
120
|
|
|
95
|
-
#### `.addTemplates<Context>()(entries)`
|
|
96
|
-
Adds multiple template function messages at once with a unified context type.
|
|
121
|
+
#### `.addTemplates<Context, ReturnType = string>()(entries)`
|
|
122
|
+
Adds multiple template function messages at once with a unified context type and custom return type.
|
|
97
123
|
|
|
98
|
-
Note: This uses a curried API for better type inference. Call `addTemplates<Context>()` first, then call the returned function with entries.
|
|
124
|
+
Note: This uses a curried API for better type inference. Call `addTemplates<Context, ReturnType>()` first, then call the returned function with entries.
|
|
99
125
|
|
|
100
126
|
- **Context**: Type parameter for the template function context
|
|
101
|
-
- **
|
|
127
|
+
- **ReturnType**: (optional) Type parameter for the return value (defaults to `string`)
|
|
128
|
+
- **entries**: `Record<string, Record<Locale, (ctx: Context) => ReturnType>>`
|
|
102
129
|
- Returns: `ChainBuilder` with added template messages
|
|
103
130
|
|
|
104
131
|
```ts
|
|
@@ -115,6 +142,8 @@ const builder = createI18n(['ja', 'en'] as const)
|
|
|
115
142
|
});
|
|
116
143
|
```
|
|
117
144
|
|
|
145
|
+
|
|
146
|
+
|
|
118
147
|
#### `.build(locale?)`
|
|
119
148
|
Builds the final messages object.
|
|
120
149
|
|
|
@@ -159,18 +188,18 @@ console.log(messages2.greeting.render()); // "こんにちは"
|
|
|
159
188
|
|
|
160
189
|
**Note**: `clone()` creates a deep copy of all messages, allowing you to branch off from a base builder and add different messages independently.
|
|
161
190
|
|
|
162
|
-
### `I18nMessage<Locales, Context>`
|
|
191
|
+
### `I18nMessage<Locales, Context, ReturnType = string>`
|
|
163
192
|
Represents a single localized message.
|
|
164
193
|
|
|
165
194
|
#### Properties
|
|
166
195
|
- `locales: Locales` — Readonly array of allowed locales
|
|
167
196
|
- `locale: Locale` — Current active locale (getter)
|
|
168
|
-
- `data: Record<Locale, Template<Context>>` — Message data for all locales (getter)
|
|
197
|
+
- `data: Record<Locale, Template<Context, ReturnType>>` — Message data for all locales (getter)
|
|
169
198
|
|
|
170
199
|
#### Methods
|
|
171
200
|
- `setLocale(locale: Locale): this` — Sets the active locale
|
|
172
|
-
- `setData(data: Record<Locale, Template<Context>>): this` — Sets the message data
|
|
173
|
-
- `render(ctx?: Context):
|
|
201
|
+
- `setData(data: Record<Locale, Template<Context, ReturnType>>): this` — Sets the message data
|
|
202
|
+
- `render(ctx?: Context): ReturnType` — Renders the message for the active locale and returns the specified type
|
|
174
203
|
|
|
175
204
|
```ts
|
|
176
205
|
const message = messages.title;
|
|
@@ -206,8 +235,8 @@ console.log(localized.nested.special.msg.render()); // English version
|
|
|
206
235
|
## Types
|
|
207
236
|
|
|
208
237
|
```ts
|
|
209
|
-
export type Template<C
|
|
210
|
-
export type LocalizedMessage<Locales, Context> = I18nMessage<Locales, Context>;
|
|
238
|
+
export type Template<C, R = string> = R | ((ctx: C) => R);
|
|
239
|
+
export type LocalizedMessage<Locales, Context, ReturnType = string> = I18nMessage<Locales, Context, ReturnType>;
|
|
211
240
|
```
|
|
212
241
|
|
|
213
242
|
## Exports
|
|
@@ -221,6 +250,92 @@ export type { Template, LocalizedMessage } from 'canopy-i18n';
|
|
|
221
250
|
|
|
222
251
|
## Usage Patterns
|
|
223
252
|
|
|
253
|
+
### React Components as Messages
|
|
254
|
+
|
|
255
|
+
```ts
|
|
256
|
+
import { createI18n } from 'canopy-i18n';
|
|
257
|
+
|
|
258
|
+
// Static React components
|
|
259
|
+
const messages = createI18n(['ja', 'en'] as const)
|
|
260
|
+
.add<JSX.Element>({
|
|
261
|
+
badge: {
|
|
262
|
+
ja: <span style={{ background: '#4caf50', color: 'white', padding: '4px 8px', borderRadius: '4px' }}>新着</span>,
|
|
263
|
+
en: <span style={{ background: '#4caf50', color: 'white', padding: '4px 8px', borderRadius: '4px' }}>NEW</span>,
|
|
264
|
+
},
|
|
265
|
+
alert: {
|
|
266
|
+
ja: <div style={{ background: '#fff3cd', padding: '12px', borderRadius: '4px' }}>⚠️ これは警告です</div>,
|
|
267
|
+
en: <div style={{ background: '#fff3cd', padding: '12px', borderRadius: '4px' }}>⚠️ This is a warning</div>,
|
|
268
|
+
},
|
|
269
|
+
})
|
|
270
|
+
.build('en');
|
|
271
|
+
|
|
272
|
+
// Render in React
|
|
273
|
+
function MyComponent() {
|
|
274
|
+
return (
|
|
275
|
+
<div>
|
|
276
|
+
{messages.badge.render()}
|
|
277
|
+
{messages.alert.render()}
|
|
278
|
+
</div>
|
|
279
|
+
);
|
|
280
|
+
}
|
|
281
|
+
|
|
282
|
+
// Dynamic React components with context
|
|
283
|
+
type ButtonContext = {
|
|
284
|
+
onClick: () => void;
|
|
285
|
+
text: string;
|
|
286
|
+
};
|
|
287
|
+
|
|
288
|
+
const dynamicMessages = createI18n(['ja', 'en'] as const)
|
|
289
|
+
.addTemplates<ButtonContext, JSX.Element>()({
|
|
290
|
+
button: {
|
|
291
|
+
ja: (ctx) => (
|
|
292
|
+
<button onClick={ctx.onClick} style={{ background: '#2196f3', color: 'white', padding: '8px 16px' }}>
|
|
293
|
+
{ctx.text}
|
|
294
|
+
</button>
|
|
295
|
+
),
|
|
296
|
+
en: (ctx) => (
|
|
297
|
+
<button onClick={ctx.onClick} style={{ background: '#2196f3', color: 'white', padding: '8px 16px' }}>
|
|
298
|
+
{ctx.text}
|
|
299
|
+
</button>
|
|
300
|
+
),
|
|
301
|
+
},
|
|
302
|
+
})
|
|
303
|
+
.build('en');
|
|
304
|
+
|
|
305
|
+
// Use with context
|
|
306
|
+
function AnotherComponent() {
|
|
307
|
+
return <div>{dynamicMessages.button.render({ onClick: () => alert('Clicked!'), text: 'Click me' })}</div>;
|
|
308
|
+
}
|
|
309
|
+
```
|
|
310
|
+
|
|
311
|
+
### Custom Object Types
|
|
312
|
+
|
|
313
|
+
```ts
|
|
314
|
+
type MenuItem = {
|
|
315
|
+
label: string;
|
|
316
|
+
url: string;
|
|
317
|
+
icon: string;
|
|
318
|
+
};
|
|
319
|
+
|
|
320
|
+
const menuMessages = createI18n(['ja', 'en'] as const)
|
|
321
|
+
.add<MenuItem>({
|
|
322
|
+
home: {
|
|
323
|
+
ja: { label: 'ホーム', url: '/', icon: '🏠' },
|
|
324
|
+
en: { label: 'Home', url: '/', icon: '🏠' },
|
|
325
|
+
},
|
|
326
|
+
settings: {
|
|
327
|
+
ja: { label: '設定', url: '/settings', icon: '⚙️' },
|
|
328
|
+
en: { label: 'Settings', url: '/settings', icon: '⚙️' },
|
|
329
|
+
},
|
|
330
|
+
})
|
|
331
|
+
.build('en');
|
|
332
|
+
|
|
333
|
+
const homeMenu = menuMessages.home.render();
|
|
334
|
+
console.log(homeMenu.label); // "Home"
|
|
335
|
+
console.log(homeMenu.url); // "/"
|
|
336
|
+
console.log(homeMenu.icon); // "🏠"
|
|
337
|
+
```
|
|
338
|
+
|
|
224
339
|
### Basic String Messages
|
|
225
340
|
|
|
226
341
|
```ts
|
package/dist/bindLocale.d.ts
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
import { ChainBuilder } from "./chainBuilder";
|
|
2
2
|
import { I18nMessage } from "./message";
|
|
3
3
|
export declare function isChainBuilder(x: unknown): x is ChainBuilder<any, any>;
|
|
4
|
-
type DeepUnwrapChainBuilders<T> = T extends I18nMessage<any, any> ? T : T extends ChainBuilder<any, infer Messages> ? Messages : T extends readonly any[] ? {
|
|
4
|
+
type DeepUnwrapChainBuilders<T> = T extends I18nMessage<any, any, any> ? T : T extends ChainBuilder<any, infer Messages> ? Messages : T extends readonly any[] ? {
|
|
5
5
|
[K in keyof T]: DeepUnwrapChainBuilders<T[K]>;
|
|
6
6
|
} : T extends object ? {
|
|
7
7
|
[K in keyof T]: DeepUnwrapChainBuilders<T[K]>;
|
package/dist/chainBuilder.d.ts
CHANGED
|
@@ -1,23 +1,25 @@
|
|
|
1
|
-
import { I18nMessage
|
|
2
|
-
|
|
1
|
+
import { I18nMessage } from "./message";
|
|
2
|
+
import type { LocalizedMessage } from "./message";
|
|
3
|
+
export declare class ChainBuilder<const Ls extends readonly string[], Messages extends Record<string, I18nMessage<Ls, any, any>> = {}> {
|
|
3
4
|
private readonly locales;
|
|
4
5
|
private messages;
|
|
5
6
|
constructor(locales: Ls, messages?: Messages);
|
|
6
7
|
/**
|
|
7
|
-
*
|
|
8
|
+
* 複数のメッセージを一度に追加
|
|
9
|
+
* 型パラメータRでカスタム型も指定可能(デフォルトはstring)
|
|
8
10
|
*/
|
|
9
|
-
add<Entries extends Record<string, Record<Ls[number], string>>>(entries: {
|
|
11
|
+
add<R = string, Entries extends Record<string, Record<Ls[number], R>> = Record<string, Record<Ls[number], R>>>(entries: {
|
|
10
12
|
[K in keyof Entries]: K extends keyof Messages ? never : Entries[K];
|
|
11
13
|
}): ChainBuilder<Ls, Messages & {
|
|
12
|
-
[K in keyof Entries]: LocalizedMessage<Ls, void>;
|
|
14
|
+
[K in keyof Entries]: LocalizedMessage<Ls, void, R>;
|
|
13
15
|
}>;
|
|
14
16
|
/**
|
|
15
17
|
* 関数指定版: 複数のテンプレート関数を一度に追加(型は統一)
|
|
16
18
|
*/
|
|
17
|
-
addTemplates<C>(): <Entries extends Record<string, Record<Ls[number], (ctx: C) =>
|
|
19
|
+
addTemplates<C, R = string>(): <Entries extends Record<string, Record<Ls[number], (ctx: C) => R>>>(entries: {
|
|
18
20
|
[K in keyof Entries]: K extends keyof Messages ? never : Entries[K];
|
|
19
21
|
}) => ChainBuilder<Ls, Messages & {
|
|
20
|
-
[K in keyof Entries]: LocalizedMessage<Ls, C>;
|
|
22
|
+
[K in keyof Entries]: LocalizedMessage<Ls, C, R>;
|
|
21
23
|
}>;
|
|
22
24
|
private deepCloneWithLocale;
|
|
23
25
|
build(locale?: Ls[number]): Messages;
|
package/dist/chainBuilder.js
CHANGED
package/dist/message.d.ts
CHANGED
|
@@ -1,15 +1,15 @@
|
|
|
1
|
-
import { Template } from "./types";
|
|
2
|
-
export type LocalizedMessage<Ls extends readonly string[], C> = I18nMessage<Ls, C>;
|
|
3
|
-
export declare function isI18nMessage(x: unknown): x is I18nMessage<any, any>;
|
|
4
|
-
export declare class I18nMessage<Ls extends readonly string[], C> {
|
|
1
|
+
import type { Template } from "./types";
|
|
2
|
+
export type LocalizedMessage<Ls extends readonly string[], C, R = string> = I18nMessage<Ls, C, R>;
|
|
3
|
+
export declare function isI18nMessage(x: unknown): x is I18nMessage<any, any, any>;
|
|
4
|
+
export declare class I18nMessage<Ls extends readonly string[], C, R = string> {
|
|
5
5
|
readonly locales: Ls;
|
|
6
6
|
private _locale;
|
|
7
7
|
private _data;
|
|
8
8
|
constructor(locales: Ls, locale: Ls[number]);
|
|
9
9
|
get locale(): Ls[number];
|
|
10
10
|
setLocale(locale: Ls[number]): this;
|
|
11
|
-
get data(): Record<Ls[number], Template<C>>;
|
|
12
|
-
setData(data: Record<Ls[number], Template<C>>): this;
|
|
13
|
-
render(this: I18nMessage<Ls, void>):
|
|
14
|
-
render(ctx: C):
|
|
11
|
+
get data(): Record<Ls[number], Template<C, R>>;
|
|
12
|
+
setData(data: Record<Ls[number], Template<C, R>>): this;
|
|
13
|
+
render(this: I18nMessage<Ls, void, R>): R;
|
|
14
|
+
render(ctx: C): R;
|
|
15
15
|
}
|
package/dist/message.js
CHANGED
|
@@ -30,7 +30,10 @@ class I18nMessage {
|
|
|
30
30
|
}
|
|
31
31
|
render(ctx) {
|
|
32
32
|
const v = this._data[this._locale];
|
|
33
|
-
|
|
33
|
+
if ((0, types_1.isTemplateFunction)(v)) {
|
|
34
|
+
return v(ctx);
|
|
35
|
+
}
|
|
36
|
+
return v;
|
|
34
37
|
}
|
|
35
38
|
}
|
|
36
39
|
exports.I18nMessage = I18nMessage;
|
package/dist/types.d.ts
CHANGED
|
@@ -1,2 +1,2 @@
|
|
|
1
|
-
export type Template<C
|
|
2
|
-
export declare function isTemplateFunction<C>(t: Template<C>): t is (ctx: C) =>
|
|
1
|
+
export type Template<C, R = string> = R | ((ctx: C) => R);
|
|
2
|
+
export declare function isTemplateFunction<C, R = string>(t: Template<C, R>): t is (ctx: C) => R;
|