canopy-i18n 0.1.1 → 0.2.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
@@ -1,13 +1,13 @@
1
- ## canopy-i18n
1
+ # canopy-i18n
2
2
 
3
- A tiny, type-safe i18n helper for building localized messages and applying a locale across nested data structures.
3
+ A tiny, type-safe i18n library for building localized messages with builder pattern and applying locales across nested data structures.
4
4
 
5
- ### Features
5
+ ## Features
6
6
  - **Type-safe locales**: Compile-time safety for allowed locale keys.
7
- - **Per-message fallback**: Each message knows its default and fallback locale.
8
- - **Template or string**: Use plain strings or `(ctx) => string` templates.
9
- - **Flexible templating**: Since templates are plain functions, you can freely use JavaScript template literals, conditionals, helpers, or any formatting library. This library does not provide a tagged template literal API.
10
- - **Deep locale application**: Switch locale across entire object/array trees.
7
+ - **Builder pattern**: Chain methods to build multiple messages at once.
8
+ - **String or template functions**: Use plain strings or `(ctx) => string` templates.
9
+ - **Flexible templating**: Templates are plain functions, so you can freely use JavaScript template literals, conditionals, helpers, or any formatting library.
10
+ - **Deep locale application**: Switch locale across entire object/array trees, including nested builders.
11
11
 
12
12
  ## Installation
13
13
 
@@ -17,179 +17,371 @@ npm install canopy-i18n
17
17
  pnpm add canopy-i18n
18
18
  # or
19
19
  yarn add canopy-i18n
20
+ # or
21
+ bun add canopy-i18n
20
22
  ```
21
23
 
22
- ## Quick start
24
+ ## Quick Start
23
25
 
24
26
  ```ts
25
- import { createI18n, applyLocale } from 'canopy-i18n';
26
-
27
- // 1) Declare allowed locales and fallback
28
- const defineMessage = createI18n(['ja', 'en'] as const, 'ja');
29
-
30
- // 2) Define messages
31
- const title = defineMessage({
32
- ja: 'タイトルテスト',
33
- en: 'Title Test',
34
- });
35
-
36
- const msg = defineMessage<{ name: string; age: number }>({
37
- ja: c => `こんにちは、${c.name}さん。あなたは${c.age}歳です。`,
38
- en: c => `Hello, ${c.name}. You are ${c.age} years old.`,
39
- });
40
-
41
- // 3) Compose nested data structures
42
- const data = {
43
- title,
44
- nested: {
45
- hello: defineMessage({ ja: 'こんにちは', en: 'Hello' }),
46
- },
47
- };
48
-
49
- // 4) Apply locale across the tree
50
- const localized = applyLocale(data, 'en');
51
-
52
- console.log(localized.title.render()); // "Title Test"
53
- console.log(localized.nested.hello.render()); // "Hello"
54
- console.log(msg.setLocale('en').render({ name: 'Tanaka', age: 20 }));
27
+ import { createI18n, bindLocale } from 'canopy-i18n';
28
+
29
+ // 1) Create a builder with allowed locales
30
+ const baseBuilder = createI18n(['ja', 'en'] as const);
31
+
32
+ // 2) Define messages using method chaining (store in a variable)
33
+ const builder = baseBuilder
34
+ .add({
35
+ title: {
36
+ ja: 'タイトルテスト',
37
+ en: 'Title Test',
38
+ },
39
+ greeting: {
40
+ ja: 'こんにちは',
41
+ en: 'Hello',
42
+ },
43
+ })
44
+ .addTemplates<{ name: string; age: number }>()({
45
+ welcome: {
46
+ ja: (ctx) => `こんにちは、${ctx.name}さん。あなたは${ctx.age}歳です。`,
47
+ en: (ctx) => `Hello, ${ctx.name}. You are ${ctx.age} years old.`,
48
+ },
49
+ });
50
+
51
+ // 3) Reuse the builder to create messages for different locales
52
+ const enMessages = builder.build('en');
53
+ const jaMessages = builder.build('ja');
54
+
55
+ // 4) Render messages (English)
56
+ console.log(enMessages.title.render()); // "Title Test"
57
+ console.log(enMessages.greeting.render()); // "Hello"
58
+ console.log(enMessages.welcome.render({ name: 'Tanaka', age: 20 })); // "Hello, Tanaka. You are 20 years old."
59
+
60
+ // 5) Render messages (Japanese)
61
+ console.log(jaMessages.title.render()); // "タイトルテスト"
62
+ console.log(jaMessages.greeting.render()); // "こんにちは"
63
+ console.log(jaMessages.welcome.render({ name: 'Tanaka', age: 20 })); // "こんにちは、Tanakaさん。あなたは20歳です。"
55
64
  ```
56
65
 
57
66
  ## API
58
67
 
59
- ### createI18n(locales, fallbackLocale)
60
- Returns a `defineMessage` function to create localized messages.
68
+ ### `createI18n(locales)`
69
+ Creates a `ChainBuilder` instance to build localized messages.
61
70
 
62
71
  - **locales**: `readonly string[]` — Allowed locale keys (e.g. `['ja', 'en'] as const`).
63
- - **fallbackLocale**: fallback locale when the active locale value is missing. New messages start with this locale active.
72
+ - Returns: `ChainBuilder<Locales, {}>` A builder instance to chain message definitions.
73
+
74
+ ```ts
75
+ const builder = createI18n(['ja', 'en', 'fr'] as const);
76
+ ```
64
77
 
65
- Overloads:
66
- - `defineMessage<Record<L[number], string>>() -> I18nMessage<L, void>`
67
- - `defineMessage<Record<L[number], Template<C>>>() -> I18nMessage<L, C>`
78
+ ### `ChainBuilder`
79
+ A builder class for creating multiple localized messages with method chaining.
68
80
 
69
- ### I18nMessage<L, C>
70
- Represents a single localized message.
81
+ #### `.add(entries)`
82
+ Adds multiple string messages at once.
71
83
 
72
- - **properties**
73
- - `locales: L`
74
- - `locale: L[number]` (getter)
75
- - `fallbackLocale: L[number]` (getter)
76
- - `data: Record<L[number], Template<C>>`
77
- - **methods**
78
- - `setLocale(locale: L[number]): this`
79
- - `setFallbackLocale(locale: L[number]): this`
80
- - `render(ctx?: C): string` — If the value for the active locale is a function, it’s invoked with `ctx`; otherwise the string is returned. Falls back to `fallbackLocale` if needed.
84
+ - **entries**: `Record<string, Record<Locale, string>>`
85
+ - Returns: `ChainBuilder` with added messages
81
86
 
82
- ### applyLocale(obj, locale)
83
- Recursively traverses arrays/objects and sets the given `locale` on all `I18nMessage` instances encountered.
87
+ ```ts
88
+ const builder = createI18n(['ja', 'en'] as const)
89
+ .add({
90
+ title: { ja: 'タイトル', en: 'Title' },
91
+ greeting: { ja: 'こんにちは', en: 'Hello' },
92
+ });
93
+ ```
84
94
 
85
- - Returns a new container (arrays/objects are cloned), but reuses the same message instances after updating their locale.
95
+ #### `.addTemplates<Context>()(entries)`
96
+ Adds multiple template function messages at once with a unified context type.
86
97
 
87
- ## Types
98
+ Note: This uses a curried API for better type inference. Call `addTemplates<Context>()` first, then call the returned function with entries.
99
+
100
+ - **Context**: Type parameter for the template function context
101
+ - **entries**: `Record<string, Record<Locale, (ctx: Context) => string>>`
102
+ - Returns: `ChainBuilder` with added template messages
88
103
 
89
104
  ```ts
90
- export type Template<C> = string | ((ctx: C) => string);
105
+ const builder = createI18n(['ja', 'en'] as const)
106
+ .addTemplates<{ name: string; age: number }>()({
107
+ greet: {
108
+ ja: (ctx) => `こんにちは、${ctx.name}さん。${ctx.age}歳ですね。`,
109
+ en: (ctx) => `Hello, ${ctx.name}. You are ${ctx.age}.`,
110
+ },
111
+ farewell: {
112
+ ja: (ctx) => `さようなら、${ctx.name}さん。`,
113
+ en: (ctx) => `Goodbye, ${ctx.name}.`,
114
+ },
115
+ });
91
116
  ```
92
117
 
93
- ## Exports
118
+ #### `.build(locale?)`
119
+ Builds the final messages object.
120
+
121
+ - **locale**: (optional) `Locale` — If provided, sets this locale on all messages before returning. If omitted, uses the first locale in the locales array as default.
122
+ - Returns: `Messages` — An object containing all defined messages
94
123
 
95
124
  ```ts
96
- export { I18nMessage, isI18nMessage } from 'canopy-i18n';
97
- export { createI18n } from 'canopy-i18n';
98
- export { applyLocale } from 'canopy-i18n';
99
- export type { Template } from 'canopy-i18n';
100
- export type { LocalizedMessage } from 'canopy-i18n';
125
+ // Build with default locale (first in array)
126
+ const defaultMessages = builder.build();
127
+
128
+ // Build with specific locale
129
+ const englishMessages = builder.build('en');
130
+ const japaneseMessages = builder.build('ja');
101
131
  ```
102
132
 
103
- ## Notes
104
- - CommonJS build (`main: dist/index.js`) with TypeScript type declarations (`types: dist/index.d.ts`).
105
- - Works in Node or bundlers; recommended usage is TypeScript/ESM import via your build tool.
106
- - License: MIT.
107
- - Not a tagged template library: you write plain functions (examples use JS template literals inside those functions).
133
+ **Note**: `build(locale)` creates a deep clone and does not mutate the builder instance, allowing you to build multiple locale versions from the same builder.
108
134
 
109
- ### Split files example (namespace import)
135
+ #### `.clone()`
136
+ Creates an independent copy of the current builder with all its messages.
110
137
 
111
- Import all message exports as a namespace and set the locale across the whole tree.
138
+ - Returns: `ChainBuilder<Locales, Messages>` A new builder instance with cloned messages
112
139
 
113
140
  ```ts
114
- // messages.ts
115
- import { createI18n } from 'canopy-i18n';
116
- const defineMessage = createI18n(['ja', 'en'] as const, 'ja');
141
+ const builder1 = createI18n(['ja', 'en'] as const)
142
+ .add({
143
+ title: { ja: 'タイトル', en: 'Title' },
144
+ });
117
145
 
118
- export const title = defineMessage({
119
- ja: 'タイトルテスト',
120
- en: 'Title Test',
146
+ const builder2 = builder1.clone().add({
147
+ greeting: { ja: 'こんにちは', en: 'Hello' },
121
148
  });
122
149
 
123
- export const msg = defineMessage<{ name: string; age: number }>({
124
- ja: c => `こんにちは、${c.name}さん。あなたは${c.age}歳です。`,
125
- en: c => `Hello, ${c.name}. You are ${c.age} years old.`,
126
- });
150
+ const messages1 = builder1.build('ja');
151
+ const messages2 = builder2.build('ja');
152
+
153
+ console.log(messages1.title.render()); // "タイトル"
154
+ console.log(messages1.greeting); // undefined
155
+
156
+ console.log(messages2.title.render()); // "タイトル"
157
+ console.log(messages2.greeting.render()); // "こんにちは"
127
158
  ```
128
159
 
160
+ **Note**: `clone()` creates a deep copy of all messages, allowing you to branch off from a base builder and add different messages independently.
161
+
162
+ ### `I18nMessage<Locales, Context>`
163
+ Represents a single localized message.
164
+
165
+ #### Properties
166
+ - `locales: Locales` — Readonly array of allowed locales
167
+ - `locale: Locale` — Current active locale (getter)
168
+ - `data: Record<Locale, Template<Context>>` — Message data for all locales (getter)
169
+
170
+ #### Methods
171
+ - `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
174
+
129
175
  ```ts
130
- // usage.ts
131
- import * as messages from './messages';
132
- import { applyLocale } from 'canopy-i18n';
176
+ const message = messages.title;
177
+ console.log(message.locale); // Current locale
178
+ console.log(message.render()); // Rendered string
133
179
 
134
- const m = applyLocale(messages, 'en');
180
+ message.setLocale('ja');
181
+ console.log(message.render()); // Japanese version
182
+ ```
183
+
184
+ ### `bindLocale(obj, locale)`
185
+ Recursively traverses objects/arrays and sets the given locale on all `I18nMessage` instances and builds all `ChainBuilder` instances encountered.
186
+
187
+ - **obj**: Any object/array structure containing messages or builders
188
+ - **locale**: The locale to apply
189
+ - Returns: A new structure with locale applied (containers are cloned, message instances are updated in place)
190
+
191
+ ```ts
192
+ const data = {
193
+ common: builder1,
194
+ nested: {
195
+ special: builder2,
196
+ },
197
+ };
135
198
 
136
- console.log(m.title.render()); // "Title Test"
137
- console.log(m.msg.render({ name: 'Tanaka', age: 20 }));
199
+ const localized = bindLocale(data, 'en');
200
+ console.log(localized.common.title.render()); // English version
201
+ console.log(localized.nested.special.msg.render()); // English version
138
202
  ```
139
203
 
140
- #### Multi-file structure
204
+ **Note**: `bindLocale` works with both `ChainBuilder` instances (automatically building them with the specified locale) and already-built message objects (updating their locale).
205
+
206
+ ## Types
141
207
 
142
208
  ```ts
143
- // i18n/defineMessage.ts
144
- import { createI18n } from 'canopy-i18n';
145
- export const defineMessage = createI18n(['ja', 'en'] as const, 'ja');
209
+ export type Template<C> = string | ((ctx: C) => string);
210
+ export type LocalizedMessage<Locales, Context> = I18nMessage<Locales, Context>;
146
211
  ```
147
212
 
213
+ ## Exports
214
+
148
215
  ```ts
149
- // i18n/messages/common.ts
150
- import { defineMessage } from '../defineMessage';
151
- export const hello = defineMessage({ ja: 'こんにちは', en: 'Hello' });
216
+ export { createI18n, ChainBuilder } from 'canopy-i18n';
217
+ export { I18nMessage, isI18nMessage } from 'canopy-i18n';
218
+ export { bindLocale, isChainBuilder } from 'canopy-i18n';
219
+ export type { Template, LocalizedMessage } from 'canopy-i18n';
152
220
  ```
153
221
 
222
+ ## Usage Patterns
223
+
224
+ ### Basic String Messages
225
+
154
226
  ```ts
155
- // i18n/messages/home.ts
156
- import { defineMessage } from '../defineMessage';
157
- export const title = defineMessage({ ja: 'タイトル', en: 'Title' });
227
+ const messages = createI18n(['ja', 'en'] as const)
228
+ .add({
229
+ title: { ja: 'タイトル', en: 'Title' },
230
+ greeting: { ja: 'こんにちは', en: 'Hello' },
231
+ farewell: { ja: 'さようなら', en: 'Goodbye' },
232
+ })
233
+ .build('en');
234
+
235
+ console.log(messages.title.render()); // "Title"
236
+ console.log(messages.greeting.render()); // "Hello"
158
237
  ```
159
238
 
239
+ ### Template Functions with Context
240
+
160
241
  ```ts
161
- // i18n/messages/index.ts
162
- export * as common from './common';
163
- export * as home from './home';
242
+ const messages = createI18n(['ja', 'en'] as const)
243
+ .addTemplates<{ name: string; age: number }>()({
244
+ profile: {
245
+ ja: (ctx) => `名前: ${ctx.name}、年齢: ${ctx.age}歳`,
246
+ en: (ctx) => `Name: ${ctx.name}, Age: ${ctx.age}`,
247
+ },
248
+ })
249
+ .build('en');
250
+
251
+ console.log(messages.profile.render({ name: 'Taro', age: 25 }));
252
+ // "Name: Taro, Age: 25"
164
253
  ```
165
254
 
255
+ ### Mixing String and Template Messages
256
+
166
257
  ```ts
167
- // usage.ts
168
- import * as msgs from './i18n/messages';
169
- import { applyLocale } from 'canopy-i18n';
258
+ const messages = createI18n(['ja', 'en'] as const)
259
+ .add({
260
+ title: { ja: 'タイトル', en: 'Title' },
261
+ })
262
+ .addTemplates<{ count: number }>()({
263
+ items: {
264
+ ja: (ctx) => `${ctx.count}個のアイテム`,
265
+ en: (ctx) => `${ctx.count} items`,
266
+ },
267
+ })
268
+ .build('ja');
269
+
270
+ console.log(messages.title.render()); // "タイトル"
271
+ console.log(messages.items.render({ count: 5 })); // "5個のアイテム"
272
+ ```
170
273
 
171
- const m = applyLocale(msgs, 'en');
274
+ ### Using Clone for Shared Base Messages
172
275
 
173
- console.log(m.common.hello.render()); // "Hello"
174
- console.log(m.home.title.render()); // "Title"
276
+ ```ts
277
+ // Create a base builder with common messages
278
+ const baseBuilder = createI18n(['ja', 'en'] as const)
279
+ .add({
280
+ common: { ja: '共通', en: 'Common' },
281
+ error: { ja: 'エラー', en: 'Error' },
282
+ });
283
+
284
+ // Clone and extend for admin pages
285
+ const adminMessages = baseBuilder.clone()
286
+ .add({
287
+ adminTitle: { ja: '管理画面', en: 'Admin Panel' },
288
+ })
289
+ .build('en');
290
+
291
+ // Clone and extend for user pages
292
+ const userMessages = baseBuilder.clone()
293
+ .add({
294
+ userTitle: { ja: 'ユーザー画面', en: 'User Panel' },
295
+ })
296
+ .build('en');
297
+
298
+ console.log(adminMessages.common.render()); // "Common"
299
+ console.log(adminMessages.adminTitle.render()); // "Admin Panel"
300
+ console.log(userMessages.common.render()); // "Common"
301
+ console.log(userMessages.userTitle.render()); // "User Panel"
175
302
  ```
176
303
 
177
- Note: Module namespace objects are read-only; `applyLocale` returns a cloned plain object while updating each `I18nMessage` instance's locale in place.
304
+ ### Namespace Pattern (Split Files)
305
+
306
+ ```ts
307
+ // i18n/locales.ts
308
+ export const LOCALES = ['ja', 'en'] as const;
309
+
310
+ // i18n/common.ts
311
+ import { createI18n } from 'canopy-i18n';
312
+ import { LOCALES } from './locales';
313
+
314
+ export const common = createI18n(LOCALES).add({
315
+ hello: { ja: 'こんにちは', en: 'Hello' },
316
+ goodbye: { ja: 'さようなら', en: 'Goodbye' },
317
+ });
318
+
319
+ // i18n/user.ts
320
+ import { createI18n } from 'canopy-i18n';
321
+ import { LOCALES } from './locales';
178
322
 
179
- ## Example: Next.js App Router
323
+ export const user = createI18n(LOCALES).addTemplates<{ name: string }>()({
324
+ welcome: {
325
+ ja: (ctx) => `ようこそ、${ctx.name}さん`,
326
+ en: (ctx) => `Welcome, ${ctx.name}`,
327
+ },
328
+ });
180
329
 
181
- An example Next.js App Router project lives under `examples/next-app`.
330
+ // i18n/index.ts
331
+ export { common } from './common';
332
+ export { user } from './user';
182
333
 
183
- - Server-side usage: `/{locale}/server` renders messages using `applyLocale` in a server component
184
- - Client-side usage: `/{locale}/client` renders messages using hooks (`useLocale`, `useApplyLocale`)
334
+ // app.ts
335
+ import { bindLocale } from 'canopy-i18n';
336
+ import * as i18n from './i18n';
185
337
 
186
- How to run:
338
+ const messages = bindLocale(i18n, 'en');
339
+ console.log(messages.common.hello.render()); // "Hello"
340
+ console.log(messages.user.welcome.render({ name: 'John' })); // "Welcome, John"
341
+ ```
187
342
 
188
- ```bash
189
- git clone https://github.com/mohhh-ok/canopy-i18n
190
- cd canopy-i18n/examples/next-app
191
- pnpm install
192
- pnpm dev
343
+ ### Dynamic Locale Switching
344
+
345
+ ```ts
346
+ const builder = createI18n(['ja', 'en'] as const)
347
+ .add({
348
+ title: { ja: 'タイトル', en: 'Title' },
349
+ });
350
+
351
+ // Build different locale versions from the same builder
352
+ const jaMessages = builder.build('ja');
353
+ const enMessages = builder.build('en');
354
+
355
+ console.log(jaMessages.title.render()); // "タイトル"
356
+ console.log(enMessages.title.render()); // "Title"
357
+
358
+ // Or use bindLocale to switch locale dynamically
359
+ const messages = builder.build();
360
+ const localizedJa = bindLocale(messages, 'ja');
361
+ const localizedEn = bindLocale(messages, 'en');
362
+ ```
363
+
364
+ ### Deep Nested Structures
365
+
366
+ ```ts
367
+ const structure = {
368
+ header: createI18n(['ja', 'en'] as const)
369
+ .add({ title: { ja: 'ヘッダー', en: 'Header' } }),
370
+ content: {
371
+ main: createI18n(['ja', 'en'] as const)
372
+ .add({ body: { ja: '本文', en: 'Body' } }),
373
+ sidebar: createI18n(['ja', 'en'] as const)
374
+ .add({ widget: { ja: 'ウィジェット', en: 'Widget' } }),
375
+ },
376
+ };
377
+
378
+ const localized = bindLocale(structure, 'en');
379
+ console.log(localized.header.title.render()); // "Header"
380
+ console.log(localized.content.main.body.render()); // "Body"
381
+ console.log(localized.content.sidebar.widget.render()); // "Widget"
193
382
  ```
194
383
 
195
- Open `http://localhost:3000` and you will be redirected to `/{locale}` based on `Accept-Language`.
384
+
385
+ ## Repository
386
+
387
+ https://github.com/MOhhh-ok/canopy-i18n
@@ -0,0 +1,10 @@
1
+ import { ChainBuilder } from "./chainBuilder";
2
+ import { I18nMessage } from "./message";
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[] ? {
5
+ [K in keyof T]: DeepUnwrapChainBuilders<T[K]>;
6
+ } : T extends object ? {
7
+ [K in keyof T]: DeepUnwrapChainBuilders<T[K]>;
8
+ } : T;
9
+ export declare function bindLocale<T extends object>(obj: T, locale: string): DeepUnwrapChainBuilders<T>;
10
+ export {};
@@ -1,9 +1,17 @@
1
1
  "use strict";
2
2
  Object.defineProperty(exports, "__esModule", { value: true });
3
- exports.applyLocale = applyLocale;
3
+ exports.isChainBuilder = isChainBuilder;
4
+ exports.bindLocale = bindLocale;
5
+ const chainBuilder_1 = require("./chainBuilder");
4
6
  const message_1 = require("./message");
5
- function applyLocale(obj, locale) {
7
+ function isChainBuilder(x) {
8
+ return x instanceof chainBuilder_1.ChainBuilder;
9
+ }
10
+ function bindLocale(obj, locale) {
6
11
  function visit(v) {
12
+ if (isChainBuilder(v)) {
13
+ return v.build(locale);
14
+ }
7
15
  if ((0, message_1.isI18nMessage)(v)) {
8
16
  v.setLocale(locale);
9
17
  return v;
@@ -11,7 +19,7 @@ function applyLocale(obj, locale) {
11
19
  if (Array.isArray(v)) {
12
20
  return v.map(visit);
13
21
  }
14
- if (v && typeof v === 'object') {
22
+ if (v && typeof v === "object") {
15
23
  const out = {};
16
24
  for (const k of Object.keys(v)) {
17
25
  out[k] = visit(v[k]);
@@ -0,0 +1,29 @@
1
+ import { I18nMessage, LocalizedMessage } from "./message";
2
+ export declare class ChainBuilder<const Ls extends readonly string[], Messages extends Record<string, I18nMessage<Ls, any>> = {}> {
3
+ private readonly locales;
4
+ private messages;
5
+ constructor(locales: Ls, messages?: Messages);
6
+ /**
7
+ * 文字列指定版: 複数のメッセージを一度に追加
8
+ */
9
+ add<Entries extends Record<string, Record<Ls[number], string>>>(entries: {
10
+ [K in keyof Entries]: K extends keyof Messages ? never : Entries[K];
11
+ }): ChainBuilder<Ls, Messages & {
12
+ [K in keyof Entries]: LocalizedMessage<Ls, void>;
13
+ }>;
14
+ /**
15
+ * 関数指定版: 複数のテンプレート関数を一度に追加(型は統一)
16
+ */
17
+ addTemplates<C>(): <Entries extends Record<string, Record<Ls[number], (ctx: C) => string>>>(entries: {
18
+ [K in keyof Entries]: K extends keyof Messages ? never : Entries[K];
19
+ }) => ChainBuilder<Ls, Messages & {
20
+ [K in keyof Entries]: LocalizedMessage<Ls, C>;
21
+ }>;
22
+ private deepCloneWithLocale;
23
+ build(locale?: Ls[number]): Messages;
24
+ /**
25
+ * 現在のChainBuilderの状態をコピーした新しいインスタンスを返す
26
+ */
27
+ clone(): ChainBuilder<Ls, Messages>;
28
+ }
29
+ export declare function createI18n<const Ls extends readonly string[]>(locales: Ls): ChainBuilder<Ls, {}>;
@@ -0,0 +1,84 @@
1
+ "use strict";
2
+ Object.defineProperty(exports, "__esModule", { value: true });
3
+ exports.ChainBuilder = void 0;
4
+ exports.createI18n = createI18n;
5
+ const message_1 = require("./message");
6
+ class ChainBuilder {
7
+ locales;
8
+ messages;
9
+ constructor(locales, messages) {
10
+ this.locales = locales;
11
+ this.messages = (messages ?? {});
12
+ }
13
+ /**
14
+ * 文字列指定版: 複数のメッセージを一度に追加
15
+ */
16
+ add(entries) {
17
+ const newMessages = { ...this.messages };
18
+ for (const [key, data] of Object.entries(entries)) {
19
+ const msg = new message_1.I18nMessage(this.locales, this.locales[0]).setData(data);
20
+ newMessages[key] = msg;
21
+ }
22
+ return new ChainBuilder(this.locales, newMessages);
23
+ }
24
+ /**
25
+ * 関数指定版: 複数のテンプレート関数を一度に追加(型は統一)
26
+ */
27
+ addTemplates() {
28
+ return (entries) => {
29
+ const newMessages = { ...this.messages };
30
+ for (const [key, data] of Object.entries(entries)) {
31
+ const msg = new message_1.I18nMessage(this.locales, this.locales[0]).setData(data);
32
+ newMessages[key] = msg;
33
+ }
34
+ return new ChainBuilder(this.locales, newMessages);
35
+ };
36
+ }
37
+ deepCloneWithLocale(obj, locale) {
38
+ if ((0, message_1.isI18nMessage)(obj)) {
39
+ const cloned = Object.create(Object.getPrototypeOf(obj));
40
+ Object.assign(cloned, obj);
41
+ cloned.setLocale(locale);
42
+ return cloned;
43
+ }
44
+ if (Array.isArray(obj)) {
45
+ return obj.map(v => this.deepCloneWithLocale(v, locale));
46
+ }
47
+ if (obj && typeof obj === "object") {
48
+ const out = {};
49
+ for (const k of Object.keys(obj)) {
50
+ out[k] = this.deepCloneWithLocale(obj[k], locale);
51
+ }
52
+ return out;
53
+ }
54
+ return obj;
55
+ }
56
+ build(locale) {
57
+ if (locale !== undefined) {
58
+ return this.deepCloneWithLocale(this.messages, locale);
59
+ }
60
+ return this.messages;
61
+ }
62
+ /**
63
+ * 現在のChainBuilderの状態をコピーした新しいインスタンスを返す
64
+ */
65
+ clone() {
66
+ const clonedMessages = {};
67
+ for (const [key, msg] of Object.entries(this.messages)) {
68
+ if ((0, message_1.isI18nMessage)(msg)) {
69
+ // I18nMessageをクローン
70
+ const cloned = Object.create(Object.getPrototypeOf(msg));
71
+ Object.assign(cloned, msg);
72
+ clonedMessages[key] = cloned;
73
+ }
74
+ else {
75
+ clonedMessages[key] = msg;
76
+ }
77
+ }
78
+ return new ChainBuilder(this.locales, clonedMessages);
79
+ }
80
+ }
81
+ exports.ChainBuilder = ChainBuilder;
82
+ function createI18n(locales) {
83
+ return new ChainBuilder(locales);
84
+ }
package/dist/index.d.ts CHANGED
@@ -1,5 +1,5 @@
1
- export { applyLocale } from './applyLocale';
2
- export { createI18n } from './createI18n';
3
- export { I18nMessage, isI18nMessage } from './message';
4
- export type { LocalizedMessage } from './message';
5
- export type { Template } from './types';
1
+ export { bindLocale, isChainBuilder } from "./bindLocale";
2
+ export { ChainBuilder, createI18n } from "./chainBuilder";
3
+ export { I18nMessage, isI18nMessage } from "./message";
4
+ export type { LocalizedMessage } from "./message";
5
+ export type { Template } from "./types";
package/dist/index.js CHANGED
@@ -1,10 +1,12 @@
1
1
  "use strict";
2
2
  Object.defineProperty(exports, "__esModule", { value: true });
3
- exports.isI18nMessage = exports.I18nMessage = exports.createI18n = exports.applyLocale = void 0;
4
- var applyLocale_1 = require("./applyLocale");
5
- Object.defineProperty(exports, "applyLocale", { enumerable: true, get: function () { return applyLocale_1.applyLocale; } });
6
- var createI18n_1 = require("./createI18n");
7
- Object.defineProperty(exports, "createI18n", { enumerable: true, get: function () { return createI18n_1.createI18n; } });
3
+ exports.isI18nMessage = exports.I18nMessage = exports.createI18n = exports.ChainBuilder = exports.isChainBuilder = exports.bindLocale = void 0;
4
+ var bindLocale_1 = require("./bindLocale");
5
+ Object.defineProperty(exports, "bindLocale", { enumerable: true, get: function () { return bindLocale_1.bindLocale; } });
6
+ Object.defineProperty(exports, "isChainBuilder", { enumerable: true, get: function () { return bindLocale_1.isChainBuilder; } });
7
+ var chainBuilder_1 = require("./chainBuilder");
8
+ Object.defineProperty(exports, "ChainBuilder", { enumerable: true, get: function () { return chainBuilder_1.ChainBuilder; } });
9
+ Object.defineProperty(exports, "createI18n", { enumerable: true, get: function () { return chainBuilder_1.createI18n; } });
8
10
  var message_1 = require("./message");
9
11
  Object.defineProperty(exports, "I18nMessage", { enumerable: true, get: function () { return message_1.I18nMessage; } });
10
12
  Object.defineProperty(exports, "isI18nMessage", { enumerable: true, get: function () { return message_1.isI18nMessage; } });
package/dist/message.d.ts CHANGED
@@ -1,18 +1,15 @@
1
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>;
2
4
  export declare class I18nMessage<Ls extends readonly string[], C> {
3
5
  readonly locales: Ls;
4
6
  private _locale;
5
- private _fallbackLocale;
6
7
  private _data;
7
- constructor(locales: Ls, fallbackLocale: Ls[number]);
8
+ constructor(locales: Ls, locale: Ls[number]);
8
9
  get locale(): Ls[number];
9
- get fallbackLocale(): Ls[number];
10
10
  setLocale(locale: Ls[number]): this;
11
- setFallbackLocale(locale: Ls[number]): this;
12
11
  get data(): Record<Ls[number], Template<C>>;
13
12
  setData(data: Record<Ls[number], Template<C>>): this;
14
13
  render(this: I18nMessage<Ls, void>): string;
15
14
  render(ctx: C): string;
16
15
  }
17
- export type LocalizedMessage<Ls extends readonly string[], C> = I18nMessage<Ls, C>;
18
- export declare function isI18nMessage(x: unknown): x is I18nMessage<any, any>;
package/dist/message.js CHANGED
@@ -2,33 +2,25 @@
2
2
  Object.defineProperty(exports, "__esModule", { value: true });
3
3
  exports.I18nMessage = void 0;
4
4
  exports.isI18nMessage = isI18nMessage;
5
- function isTemplateFunction(t) {
6
- return typeof t === 'function';
5
+ const types_1 = require("./types");
6
+ function isI18nMessage(x) {
7
+ return x instanceof I18nMessage;
7
8
  }
8
9
  class I18nMessage {
9
10
  locales;
10
11
  _locale;
11
- _fallbackLocale;
12
12
  _data;
13
- constructor(locales, fallbackLocale) {
13
+ constructor(locales, locale) {
14
14
  this.locales = locales;
15
- this._fallbackLocale = fallbackLocale;
16
- this._locale = fallbackLocale;
15
+ this._locale = locale;
17
16
  }
18
17
  get locale() {
19
18
  return this._locale;
20
19
  }
21
- get fallbackLocale() {
22
- return this._fallbackLocale;
23
- }
24
20
  setLocale(locale) {
25
21
  this._locale = locale;
26
22
  return this;
27
23
  }
28
- setFallbackLocale(locale) {
29
- this._fallbackLocale = locale;
30
- return this;
31
- }
32
24
  get data() {
33
25
  return this._data;
34
26
  }
@@ -37,11 +29,8 @@ class I18nMessage {
37
29
  return this;
38
30
  }
39
31
  render(ctx) {
40
- const v = this._data[this._locale] ?? this._data[this._fallbackLocale];
41
- return isTemplateFunction(v) ? v(ctx) : v;
32
+ const v = this._data[this._locale];
33
+ return (0, types_1.isTemplateFunction)(v) ? v(ctx) : v;
42
34
  }
43
35
  }
44
36
  exports.I18nMessage = I18nMessage;
45
- function isI18nMessage(x) {
46
- return x instanceof I18nMessage;
47
- }
package/dist/types.d.ts CHANGED
@@ -1 +1,2 @@
1
1
  export type Template<C> = string | ((ctx: C) => string);
2
+ export declare function isTemplateFunction<C>(t: Template<C>): t is (ctx: C) => string;
package/dist/types.js CHANGED
@@ -1,2 +1,6 @@
1
1
  "use strict";
2
2
  Object.defineProperty(exports, "__esModule", { value: true });
3
+ exports.isTemplateFunction = isTemplateFunction;
4
+ function isTemplateFunction(t) {
5
+ return typeof t === "function";
6
+ }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "canopy-i18n",
3
- "version": "0.1.1",
3
+ "version": "0.2.0",
4
4
  "description": "A tiny, type-safe i18n helper",
5
5
  "type": "commonjs",
6
6
  "main": "dist/index.js",
@@ -20,12 +20,20 @@
20
20
  "publishConfig": {
21
21
  "access": "public"
22
22
  },
23
+ "scripts": {
24
+ "dev": "tsc --watch",
25
+ "build": "tsc",
26
+ "prepublishOnly": "pnpm run build",
27
+ "type-check": "tsc -p . --noEmit",
28
+ "test": "vitest run",
29
+ "test:watch": "vitest"
30
+ },
23
31
  "devDependencies": {
24
- "@tsconfig/node20": "^20.1.7",
25
- "@types/node": "^24.10.1",
26
- "release-it": "^19.0.6",
32
+ "@tsconfig/node20": "^20.1.8",
33
+ "@types/node": "^24.10.4",
34
+ "release-it": "^19.2.2",
27
35
  "typescript": "^5.9.3",
28
- "vitest": "^4.0.9"
36
+ "vitest": "^4.0.16"
29
37
  },
30
38
  "keywords": [
31
39
  "i18n",
@@ -42,12 +50,5 @@
42
50
  "repository": {
43
51
  "type": "git",
44
52
  "url": "https://github.com/MOhhh-ok/canopy-i18n"
45
- },
46
- "scripts": {
47
- "dev": "tsc --watch",
48
- "build": "tsc",
49
- "type-check": "tsc -p . --noEmit",
50
- "test": "vitest run",
51
- "test:watch": "vitest"
52
53
  }
53
- }
54
+ }
@@ -1 +0,0 @@
1
- export declare function applyLocale<T extends object>(obj: T, locale: string): T;
@@ -1 +0,0 @@
1
- export declare function applyLocaleDeep<T extends object>(obj: T, locale: string): T;
@@ -1,24 +0,0 @@
1
- "use strict";
2
- Object.defineProperty(exports, "__esModule", { value: true });
3
- exports.applyLocaleDeep = applyLocaleDeep;
4
- const message_1 = require("./message");
5
- function applyLocaleDeep(obj, locale) {
6
- function visit(v) {
7
- if ((0, message_1.isI18nMessage)(v)) {
8
- v.setLocale(locale);
9
- return v;
10
- }
11
- if (Array.isArray(v)) {
12
- return v.map(visit);
13
- }
14
- if (v && typeof v === 'object') {
15
- const out = {};
16
- for (const k of Object.keys(v)) {
17
- out[k] = visit(v[k]);
18
- }
19
- return out;
20
- }
21
- return v;
22
- }
23
- return visit(obj);
24
- }
package/dist/builder.d.ts DELETED
@@ -1,6 +0,0 @@
1
- import { Template } from "./types";
2
- import { LocalizedMessage } from "./message";
3
- export declare function createMessageBuilder<const Ls extends readonly string[]>(locales: Ls, fallbackLocale: Ls[number]): {
4
- <C>(data: Record<Ls[number], Template<C>>): LocalizedMessage<Ls, C>;
5
- (data: Record<Ls[number], string>): LocalizedMessage<Ls, void>;
6
- };
package/dist/builder.js DELETED
@@ -1,10 +0,0 @@
1
- "use strict";
2
- Object.defineProperty(exports, "__esModule", { value: true });
3
- exports.createMessageBuilder = createMessageBuilder;
4
- const message_1 = require("./message");
5
- function createMessageBuilder(locales, fallbackLocale) {
6
- function builder(data) {
7
- return new message_1.I18nMessage(locales, fallbackLocale).setData(data);
8
- }
9
- return builder;
10
- }
@@ -1,29 +0,0 @@
1
- export type Template<C> = string | ((ctx: C) => string);
2
- export declare class Li18nMessage<Ls extends readonly string[], C> {
3
- readonly locales: Ls;
4
- private _locale;
5
- private _fallbackLocale;
6
- readonly data: Record<Ls[number], Template<C>>;
7
- constructor(locales: Ls, _locale: Ls[number], _fallbackLocale: Ls[number], data: Record<Ls[number], Template<C>>);
8
- get locale(): Ls[number];
9
- get fallbackLocale(): Ls[number];
10
- setLocale(locale: Ls[number]): this;
11
- setFallbackLocale(locale: Ls[number]): this;
12
- render(this: Li18nMessage<Ls, void>): string;
13
- render(ctx: C): string;
14
- }
15
- export type LocalizedMessage<Ls extends readonly string[], C> = Li18nMessage<Ls, C>;
16
- export declare function createMessageBuilder<const Ls extends readonly string[]>(locales: Ls, locale: Ls[number], fallbackLocale: Ls[number]): {
17
- <C>(data: Record<Ls[number], Template<C>>): LocalizedMessage<Ls, C>;
18
- (data: Record<Ls[number], string>): LocalizedMessage<Ls, void>;
19
- };
20
- /**
21
- * Recursively apply locale for any Li18nMessage instances found in the given value.
22
- * Always deep; does not change fallback locale (keeps builder defaults).
23
- *
24
- * Example:
25
- * import * as trs from './testData';
26
- * applyLocaleDeep(trs, 'en');
27
- */
28
- export declare function applyLocaleDeep<T extends Record<string, unknown>>(obj: T, locale: string): T;
29
- export declare function applyLocaleDeep<T extends unknown[]>(obj: T, locale: string): T;
@@ -1,70 +0,0 @@
1
- "use strict";
2
- Object.defineProperty(exports, "__esModule", { value: true });
3
- exports.Li18nMessage = void 0;
4
- exports.createMessageBuilder = createMessageBuilder;
5
- exports.applyLocaleDeep = applyLocaleDeep;
6
- function isTemplateFunction(t) {
7
- return typeof t === 'function';
8
- }
9
- class Li18nMessage {
10
- locales;
11
- _locale;
12
- _fallbackLocale;
13
- data;
14
- constructor(locales, _locale, _fallbackLocale, data) {
15
- this.locales = locales;
16
- this._locale = _locale;
17
- this._fallbackLocale = _fallbackLocale;
18
- this.data = data;
19
- }
20
- get locale() {
21
- return this._locale;
22
- }
23
- get fallbackLocale() {
24
- return this._fallbackLocale;
25
- }
26
- setLocale(locale) {
27
- this._locale = locale;
28
- return this;
29
- }
30
- setFallbackLocale(locale) {
31
- this._fallbackLocale = locale;
32
- return this;
33
- }
34
- render(ctx) {
35
- const v = this.data[this._locale] ?? this.data[this._fallbackLocale];
36
- return isTemplateFunction(v) ? v(ctx) : v;
37
- }
38
- }
39
- exports.Li18nMessage = Li18nMessage;
40
- function createMessageBuilder(locales, locale, fallbackLocale) {
41
- function builder(data) {
42
- return new Li18nMessage(locales, locale, fallbackLocale, data);
43
- }
44
- return builder;
45
- }
46
- function isLi18nMessage(x) {
47
- return x instanceof Li18nMessage;
48
- }
49
- function applyLocaleDeep(obj, locale) {
50
- function visit(v) {
51
- if (isLi18nMessage(v)) {
52
- v.setLocale(locale);
53
- return v;
54
- }
55
- if (Array.isArray(v)) {
56
- return v.map(visit);
57
- }
58
- // Traverse any non-null object (including module namespace objects),
59
- // but avoid special handling already covered above.
60
- if (v && typeof v === 'object') {
61
- const out = {};
62
- for (const k of Object.keys(v)) {
63
- out[k] = visit(v[k]);
64
- }
65
- return out;
66
- }
67
- return v;
68
- }
69
- return visit(obj);
70
- }
@@ -1,6 +0,0 @@
1
- import { Template } from "./types";
2
- import { LocalizedMessage } from "./message";
3
- export declare function createI18n<const Ls extends readonly string[]>(locales: Ls, fallbackLocale: Ls[number]): {
4
- <C>(data: Record<Ls[number], Template<C>>, fb?: Ls[number]): LocalizedMessage<Ls, C>;
5
- (data: Record<Ls[number], string>, fb?: Ls[number]): LocalizedMessage<Ls, void>;
6
- };
@@ -1,10 +0,0 @@
1
- "use strict";
2
- Object.defineProperty(exports, "__esModule", { value: true });
3
- exports.createI18n = createI18n;
4
- const message_1 = require("./message");
5
- function createI18n(locales, fallbackLocale) {
6
- function builder(data, fb) {
7
- return new message_1.I18nMessage(locales, fb ?? fallbackLocale).setData(data);
8
- }
9
- return builder;
10
- }
package/dist/test.d.ts DELETED
@@ -1 +0,0 @@
1
- export {};
package/dist/test.js DELETED
@@ -1,12 +0,0 @@
1
- "use strict";
2
- Object.defineProperty(exports, "__esModule", { value: true });
3
- const index_1 = require("./index");
4
- const trs = require("./testData");
5
- for (const locale of ['ja', 'en']) {
6
- const { msg, title, nested } = (0, index_1.applyLocaleDeep)(trs, locale);
7
- console.log(msg.render({ name: '田中', age: 20 }));
8
- console.log(title.render());
9
- console.log(nested.hello.render());
10
- const hs = (0, index_1.applyLocaleDeep)(trs.hasSentMsg, locale);
11
- console.log(hs.render({ email: 'test@example.com' }));
12
- }
@@ -1,12 +0,0 @@
1
- export declare const title: import("./message").LocalizedMessage<readonly ["ja", "en"], unknown>;
2
- export declare const msg: import("./message").LocalizedMessage<readonly ["ja", "en"], {
3
- name: string;
4
- age: number;
5
- }>;
6
- export declare const nested: {
7
- hello: import("./message").LocalizedMessage<readonly ["ja", "en"], unknown>;
8
- world: import("./message").LocalizedMessage<readonly ["ja", "en"], unknown>;
9
- };
10
- export declare const hasSentMsg: import("./message").LocalizedMessage<readonly ["ja", "en"], {
11
- email: string;
12
- }>;
package/dist/testData.js DELETED
@@ -1,27 +0,0 @@
1
- "use strict";
2
- Object.defineProperty(exports, "__esModule", { value: true });
3
- exports.hasSentMsg = exports.nested = exports.msg = exports.title = void 0;
4
- const index_1 = require("./index");
5
- const builder = (0, index_1.createI18n)(['ja', 'en'], 'ja');
6
- exports.title = builder({
7
- ja: 'タイトルテスト',
8
- en: 'Title Test'
9
- });
10
- exports.msg = builder({
11
- ja: c => `こんにちは、${c.name}さん、あなたは${c.age}歳です。来年は${c.age + 1}歳です。`,
12
- en: c => `Hello, ${c.name}. You are ${c.age} years old. Next year you will be ${c.age + 1} years old.`
13
- });
14
- exports.nested = {
15
- hello: builder({
16
- ja: 'こんにちは',
17
- en: 'Hello'
18
- }),
19
- world: builder({
20
- ja: '世界',
21
- en: 'World'
22
- })
23
- };
24
- exports.hasSentMsg = builder({
25
- ja: o => `${o.email}にメールを送信しました。確認の上、処理を進めて下さい。`,
26
- en: o => `Email sent to ${o.email}. Please check and proceed with the process.`
27
- });