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 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 string messages at once.
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
- - **entries**: `Record<string, Record<Locale, string>>`
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
- - **entries**: `Record<string, Record<Locale, (ctx: Context) => string>>`
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): string` — Renders the message for the active locale
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> = string | ((ctx: C) => string);
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
@@ -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]>;
@@ -1,23 +1,25 @@
1
- import { I18nMessage, LocalizedMessage } from "./message";
2
- export declare class ChainBuilder<const Ls extends readonly string[], Messages extends Record<string, I18nMessage<Ls, any>> = {}> {
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) => string>>>(entries: {
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;
@@ -11,7 +11,8 @@ class ChainBuilder {
11
11
  this.messages = (messages ?? {});
12
12
  }
13
13
  /**
14
- * 文字列指定版: 複数のメッセージを一度に追加
14
+ * 複数のメッセージを一度に追加
15
+ * 型パラメータRでカスタム型も指定可能(デフォルトはstring)
15
16
  */
16
17
  add(entries) {
17
18
  const newMessages = { ...this.messages };
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>): string;
14
- render(ctx: C): string;
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
- return (0, types_1.isTemplateFunction)(v) ? v(ctx) : v;
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> = string | ((ctx: C) => string);
2
- export declare function isTemplateFunction<C>(t: Template<C>): t is (ctx: C) => string;
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;
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "canopy-i18n",
3
- "version": "0.2.0",
3
+ "version": "0.3.0",
4
4
  "description": "A tiny, type-safe i18n helper",
5
5
  "type": "commonjs",
6
6
  "main": "dist/index.js",