canopy-i18n 0.1.1 → 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 +421 -114
- package/dist/bindLocale.d.ts +10 -0
- package/dist/{applyLocale.js → bindLocale.js} +11 -3
- package/dist/chainBuilder.d.ts +31 -0
- package/dist/chainBuilder.js +85 -0
- package/dist/index.d.ts +5 -5
- package/dist/index.js +7 -5
- package/dist/message.d.ts +9 -12
- package/dist/message.js +10 -18
- package/dist/types.d.ts +2 -1
- package/dist/types.js +4 -0
- package/package.json +14 -13
- package/dist/applyLocale.d.ts +0 -1
- package/dist/applyLocaleDeep.d.ts +0 -1
- package/dist/applyLocaleDeep.js +0 -24
- package/dist/builder.d.ts +0 -6
- package/dist/builder.js +0 -10
- package/dist/createBuilder.d.ts +0 -29
- package/dist/createBuilder.js +0 -70
- package/dist/createI18n.d.ts +0 -6
- package/dist/createI18n.js +0 -10
- package/dist/test.d.ts +0 -1
- package/dist/test.js +0 -12
- package/dist/testData.d.ts +0 -12
- package/dist/testData.js +0 -27
package/README.md
CHANGED
|
@@ -1,13 +1,14 @@
|
|
|
1
|
-
|
|
1
|
+
# canopy-i18n
|
|
2
2
|
|
|
3
|
-
A tiny, type-safe i18n
|
|
3
|
+
A tiny, type-safe i18n library for building localized messages with builder pattern and applying locales across nested data structures.
|
|
4
4
|
|
|
5
|
-
|
|
5
|
+
## Features
|
|
6
6
|
- **Type-safe locales**: Compile-time safety for allowed locale keys.
|
|
7
|
-
- **
|
|
8
|
-
- **
|
|
9
|
-
- **Flexible templating**:
|
|
10
|
-
- **
|
|
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
|
+
- **Generic return types**: Return any type (string, React components, etc.) from your messages.
|
|
11
|
+
- **Deep locale application**: Switch locale across entire object/array trees, including nested builders.
|
|
11
12
|
|
|
12
13
|
## Installation
|
|
13
14
|
|
|
@@ -17,179 +18,485 @@ npm install canopy-i18n
|
|
|
17
18
|
pnpm add canopy-i18n
|
|
18
19
|
# or
|
|
19
20
|
yarn add canopy-i18n
|
|
21
|
+
# or
|
|
22
|
+
bun add canopy-i18n
|
|
20
23
|
```
|
|
21
24
|
|
|
22
|
-
## Quick
|
|
25
|
+
## Quick Start
|
|
23
26
|
|
|
24
27
|
```ts
|
|
25
|
-
import { createI18n,
|
|
28
|
+
import { createI18n, bindLocale } from 'canopy-i18n';
|
|
29
|
+
|
|
30
|
+
// 1) Create a builder with allowed locales
|
|
31
|
+
const baseBuilder = createI18n(['ja', 'en'] as const);
|
|
32
|
+
|
|
33
|
+
// 2) Define messages using method chaining (store in a variable)
|
|
34
|
+
const builder = baseBuilder
|
|
35
|
+
.add({
|
|
36
|
+
title: {
|
|
37
|
+
ja: 'タイトルテスト',
|
|
38
|
+
en: 'Title Test',
|
|
39
|
+
},
|
|
40
|
+
greeting: {
|
|
41
|
+
ja: 'こんにちは',
|
|
42
|
+
en: 'Hello',
|
|
43
|
+
},
|
|
44
|
+
})
|
|
45
|
+
.addTemplates<{ name: string; age: number }>()({
|
|
46
|
+
welcome: {
|
|
47
|
+
ja: (ctx) => `こんにちは、${ctx.name}さん。あなたは${ctx.age}歳です。`,
|
|
48
|
+
en: (ctx) => `Hello, ${ctx.name}. You are ${ctx.age} years old.`,
|
|
49
|
+
},
|
|
50
|
+
});
|
|
51
|
+
|
|
52
|
+
// 3) Reuse the builder to create messages for different locales
|
|
53
|
+
const enMessages = builder.build('en');
|
|
54
|
+
const jaMessages = builder.build('ja');
|
|
55
|
+
|
|
56
|
+
// 4) Render messages (English)
|
|
57
|
+
console.log(enMessages.title.render()); // "Title Test"
|
|
58
|
+
console.log(enMessages.greeting.render()); // "Hello"
|
|
59
|
+
console.log(enMessages.welcome.render({ name: 'Tanaka', age: 20 })); // "Hello, Tanaka. You are 20 years old."
|
|
60
|
+
|
|
61
|
+
// 5) Render messages (Japanese)
|
|
62
|
+
console.log(jaMessages.title.render()); // "タイトルテスト"
|
|
63
|
+
console.log(jaMessages.greeting.render()); // "こんにちは"
|
|
64
|
+
console.log(jaMessages.welcome.render({ name: 'Tanaka', age: 20 })); // "こんにちは、Tanakaさん。あなたは20歳です。"
|
|
65
|
+
```
|
|
26
66
|
|
|
27
|
-
|
|
28
|
-
const defineMessage = createI18n(['ja', 'en'] as const, 'ja');
|
|
67
|
+
## API
|
|
29
68
|
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
ja: 'タイトルテスト',
|
|
33
|
-
en: 'Title Test',
|
|
34
|
-
});
|
|
69
|
+
### `createI18n(locales)`
|
|
70
|
+
Creates a `ChainBuilder` instance to build localized messages.
|
|
35
71
|
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
en: c => `Hello, ${c.name}. You are ${c.age} years old.`,
|
|
39
|
-
});
|
|
72
|
+
- **locales**: `readonly string[]` — Allowed locale keys (e.g. `['ja', 'en'] as const`).
|
|
73
|
+
- Returns: `ChainBuilder<Locales, {}>` — A builder instance to chain message definitions.
|
|
40
74
|
|
|
41
|
-
|
|
42
|
-
const
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
75
|
+
```ts
|
|
76
|
+
const builder = createI18n(['ja', 'en', 'fr'] as const);
|
|
77
|
+
```
|
|
78
|
+
|
|
79
|
+
### `ChainBuilder`
|
|
80
|
+
A builder class for creating multiple localized messages with method chaining.
|
|
81
|
+
|
|
82
|
+
#### `.add<ReturnType = string>(entries)`
|
|
83
|
+
Adds multiple messages at once. By default, returns `string`, but you can specify a custom return type.
|
|
84
|
+
|
|
85
|
+
- **ReturnType**: (optional) Type parameter for the return value (defaults to `string`)
|
|
86
|
+
- **entries**: `Record<string, Record<Locale, ReturnType>>`
|
|
87
|
+
- Returns: `ChainBuilder` with added messages
|
|
88
|
+
|
|
89
|
+
```ts
|
|
90
|
+
// String messages (default)
|
|
91
|
+
const builder = createI18n(['ja', 'en'] as const)
|
|
92
|
+
.add({
|
|
93
|
+
title: { ja: 'タイトル', en: 'Title' },
|
|
94
|
+
greeting: { ja: 'こんにちは', en: 'Hello' },
|
|
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;
|
|
47
110
|
};
|
|
48
111
|
|
|
49
|
-
|
|
50
|
-
|
|
112
|
+
const menu = createI18n(['ja', 'en'] as const)
|
|
113
|
+
.add<MenuItem>({
|
|
114
|
+
home: {
|
|
115
|
+
ja: { label: 'ホーム', url: '/' },
|
|
116
|
+
en: { label: 'Home', url: '/' },
|
|
117
|
+
},
|
|
118
|
+
});
|
|
119
|
+
```
|
|
120
|
+
|
|
121
|
+
#### `.addTemplates<Context, ReturnType = string>()(entries)`
|
|
122
|
+
Adds multiple template function messages at once with a unified context type and custom return type.
|
|
123
|
+
|
|
124
|
+
Note: This uses a curried API for better type inference. Call `addTemplates<Context, ReturnType>()` first, then call the returned function with entries.
|
|
125
|
+
|
|
126
|
+
- **Context**: Type parameter for the template function context
|
|
127
|
+
- **ReturnType**: (optional) Type parameter for the return value (defaults to `string`)
|
|
128
|
+
- **entries**: `Record<string, Record<Locale, (ctx: Context) => ReturnType>>`
|
|
129
|
+
- Returns: `ChainBuilder` with added template messages
|
|
130
|
+
|
|
131
|
+
```ts
|
|
132
|
+
const builder = createI18n(['ja', 'en'] as const)
|
|
133
|
+
.addTemplates<{ name: string; age: number }>()({
|
|
134
|
+
greet: {
|
|
135
|
+
ja: (ctx) => `こんにちは、${ctx.name}さん。${ctx.age}歳ですね。`,
|
|
136
|
+
en: (ctx) => `Hello, ${ctx.name}. You are ${ctx.age}.`,
|
|
137
|
+
},
|
|
138
|
+
farewell: {
|
|
139
|
+
ja: (ctx) => `さようなら、${ctx.name}さん。`,
|
|
140
|
+
en: (ctx) => `Goodbye, ${ctx.name}.`,
|
|
141
|
+
},
|
|
142
|
+
});
|
|
143
|
+
```
|
|
144
|
+
|
|
145
|
+
|
|
146
|
+
|
|
147
|
+
#### `.build(locale?)`
|
|
148
|
+
Builds the final messages object.
|
|
149
|
+
|
|
150
|
+
- **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.
|
|
151
|
+
- Returns: `Messages` — An object containing all defined messages
|
|
51
152
|
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
153
|
+
```ts
|
|
154
|
+
// Build with default locale (first in array)
|
|
155
|
+
const defaultMessages = builder.build();
|
|
156
|
+
|
|
157
|
+
// Build with specific locale
|
|
158
|
+
const englishMessages = builder.build('en');
|
|
159
|
+
const japaneseMessages = builder.build('ja');
|
|
55
160
|
```
|
|
56
161
|
|
|
57
|
-
|
|
162
|
+
**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.
|
|
58
163
|
|
|
59
|
-
|
|
60
|
-
|
|
164
|
+
#### `.clone()`
|
|
165
|
+
Creates an independent copy of the current builder with all its messages.
|
|
61
166
|
|
|
62
|
-
-
|
|
63
|
-
- **fallbackLocale**: fallback locale when the active locale value is missing. New messages start with this locale active.
|
|
167
|
+
- Returns: `ChainBuilder<Locales, Messages>` — A new builder instance with cloned messages
|
|
64
168
|
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
169
|
+
```ts
|
|
170
|
+
const builder1 = createI18n(['ja', 'en'] as const)
|
|
171
|
+
.add({
|
|
172
|
+
title: { ja: 'タイトル', en: 'Title' },
|
|
173
|
+
});
|
|
68
174
|
|
|
69
|
-
|
|
175
|
+
const builder2 = builder1.clone().add({
|
|
176
|
+
greeting: { ja: 'こんにちは', en: 'Hello' },
|
|
177
|
+
});
|
|
178
|
+
|
|
179
|
+
const messages1 = builder1.build('ja');
|
|
180
|
+
const messages2 = builder2.build('ja');
|
|
181
|
+
|
|
182
|
+
console.log(messages1.title.render()); // "タイトル"
|
|
183
|
+
console.log(messages1.greeting); // undefined
|
|
184
|
+
|
|
185
|
+
console.log(messages2.title.render()); // "タイトル"
|
|
186
|
+
console.log(messages2.greeting.render()); // "こんにちは"
|
|
187
|
+
```
|
|
188
|
+
|
|
189
|
+
**Note**: `clone()` creates a deep copy of all messages, allowing you to branch off from a base builder and add different messages independently.
|
|
190
|
+
|
|
191
|
+
### `I18nMessage<Locales, Context, ReturnType = string>`
|
|
70
192
|
Represents a single localized message.
|
|
71
193
|
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
194
|
+
#### Properties
|
|
195
|
+
- `locales: Locales` — Readonly array of allowed locales
|
|
196
|
+
- `locale: Locale` — Current active locale (getter)
|
|
197
|
+
- `data: Record<Locale, Template<Context, ReturnType>>` — Message data for all locales (getter)
|
|
198
|
+
|
|
199
|
+
#### Methods
|
|
200
|
+
- `setLocale(locale: Locale): this` — Sets 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
|
|
203
|
+
|
|
204
|
+
```ts
|
|
205
|
+
const message = messages.title;
|
|
206
|
+
console.log(message.locale); // Current locale
|
|
207
|
+
console.log(message.render()); // Rendered string
|
|
208
|
+
|
|
209
|
+
message.setLocale('ja');
|
|
210
|
+
console.log(message.render()); // Japanese version
|
|
211
|
+
```
|
|
212
|
+
|
|
213
|
+
### `bindLocale(obj, locale)`
|
|
214
|
+
Recursively traverses objects/arrays and sets the given locale on all `I18nMessage` instances and builds all `ChainBuilder` instances encountered.
|
|
81
215
|
|
|
82
|
-
|
|
83
|
-
|
|
216
|
+
- **obj**: Any object/array structure containing messages or builders
|
|
217
|
+
- **locale**: The locale to apply
|
|
218
|
+
- Returns: A new structure with locale applied (containers are cloned, message instances are updated in place)
|
|
219
|
+
|
|
220
|
+
```ts
|
|
221
|
+
const data = {
|
|
222
|
+
common: builder1,
|
|
223
|
+
nested: {
|
|
224
|
+
special: builder2,
|
|
225
|
+
},
|
|
226
|
+
};
|
|
227
|
+
|
|
228
|
+
const localized = bindLocale(data, 'en');
|
|
229
|
+
console.log(localized.common.title.render()); // English version
|
|
230
|
+
console.log(localized.nested.special.msg.render()); // English version
|
|
231
|
+
```
|
|
84
232
|
|
|
85
|
-
|
|
233
|
+
**Note**: `bindLocale` works with both `ChainBuilder` instances (automatically building them with the specified locale) and already-built message objects (updating their locale).
|
|
86
234
|
|
|
87
235
|
## Types
|
|
88
236
|
|
|
89
237
|
```ts
|
|
90
|
-
export type Template<C
|
|
238
|
+
export type Template<C, R = string> = R | ((ctx: C) => R);
|
|
239
|
+
export type LocalizedMessage<Locales, Context, ReturnType = string> = I18nMessage<Locales, Context, ReturnType>;
|
|
91
240
|
```
|
|
92
241
|
|
|
93
242
|
## Exports
|
|
94
243
|
|
|
95
244
|
```ts
|
|
245
|
+
export { createI18n, ChainBuilder } from 'canopy-i18n';
|
|
96
246
|
export { I18nMessage, isI18nMessage } from 'canopy-i18n';
|
|
97
|
-
export {
|
|
98
|
-
export {
|
|
99
|
-
export type { Template } from 'canopy-i18n';
|
|
100
|
-
export type { LocalizedMessage } from 'canopy-i18n';
|
|
247
|
+
export { bindLocale, isChainBuilder } from 'canopy-i18n';
|
|
248
|
+
export type { Template, LocalizedMessage } from 'canopy-i18n';
|
|
101
249
|
```
|
|
102
250
|
|
|
103
|
-
##
|
|
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).
|
|
251
|
+
## Usage Patterns
|
|
108
252
|
|
|
109
|
-
###
|
|
110
|
-
|
|
111
|
-
Import all message exports as a namespace and set the locale across the whole tree.
|
|
253
|
+
### React Components as Messages
|
|
112
254
|
|
|
113
255
|
```ts
|
|
114
|
-
// messages.ts
|
|
115
256
|
import { createI18n } from 'canopy-i18n';
|
|
116
|
-
const defineMessage = createI18n(['ja', 'en'] as const, 'ja');
|
|
117
257
|
|
|
118
|
-
|
|
119
|
-
|
|
120
|
-
|
|
121
|
-
|
|
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
|
+
};
|
|
122
287
|
|
|
123
|
-
|
|
124
|
-
|
|
125
|
-
|
|
126
|
-
|
|
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
|
+
}
|
|
127
309
|
```
|
|
128
310
|
|
|
129
|
-
|
|
130
|
-
// usage.ts
|
|
131
|
-
import * as messages from './messages';
|
|
132
|
-
import { applyLocale } from 'canopy-i18n';
|
|
311
|
+
### Custom Object Types
|
|
133
312
|
|
|
134
|
-
|
|
313
|
+
```ts
|
|
314
|
+
type MenuItem = {
|
|
315
|
+
label: string;
|
|
316
|
+
url: string;
|
|
317
|
+
icon: string;
|
|
318
|
+
};
|
|
135
319
|
|
|
136
|
-
|
|
137
|
-
|
|
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); // "🏠"
|
|
138
337
|
```
|
|
139
338
|
|
|
140
|
-
|
|
339
|
+
### Basic String Messages
|
|
141
340
|
|
|
142
341
|
```ts
|
|
143
|
-
|
|
144
|
-
|
|
145
|
-
|
|
342
|
+
const messages = createI18n(['ja', 'en'] as const)
|
|
343
|
+
.add({
|
|
344
|
+
title: { ja: 'タイトル', en: 'Title' },
|
|
345
|
+
greeting: { ja: 'こんにちは', en: 'Hello' },
|
|
346
|
+
farewell: { ja: 'さようなら', en: 'Goodbye' },
|
|
347
|
+
})
|
|
348
|
+
.build('en');
|
|
349
|
+
|
|
350
|
+
console.log(messages.title.render()); // "Title"
|
|
351
|
+
console.log(messages.greeting.render()); // "Hello"
|
|
146
352
|
```
|
|
147
353
|
|
|
354
|
+
### Template Functions with Context
|
|
355
|
+
|
|
148
356
|
```ts
|
|
149
|
-
|
|
150
|
-
|
|
151
|
-
|
|
357
|
+
const messages = createI18n(['ja', 'en'] as const)
|
|
358
|
+
.addTemplates<{ name: string; age: number }>()({
|
|
359
|
+
profile: {
|
|
360
|
+
ja: (ctx) => `名前: ${ctx.name}、年齢: ${ctx.age}歳`,
|
|
361
|
+
en: (ctx) => `Name: ${ctx.name}, Age: ${ctx.age}`,
|
|
362
|
+
},
|
|
363
|
+
})
|
|
364
|
+
.build('en');
|
|
365
|
+
|
|
366
|
+
console.log(messages.profile.render({ name: 'Taro', age: 25 }));
|
|
367
|
+
// "Name: Taro, Age: 25"
|
|
152
368
|
```
|
|
153
369
|
|
|
370
|
+
### Mixing String and Template Messages
|
|
371
|
+
|
|
154
372
|
```ts
|
|
155
|
-
|
|
156
|
-
|
|
157
|
-
|
|
373
|
+
const messages = createI18n(['ja', 'en'] as const)
|
|
374
|
+
.add({
|
|
375
|
+
title: { ja: 'タイトル', en: 'Title' },
|
|
376
|
+
})
|
|
377
|
+
.addTemplates<{ count: number }>()({
|
|
378
|
+
items: {
|
|
379
|
+
ja: (ctx) => `${ctx.count}個のアイテム`,
|
|
380
|
+
en: (ctx) => `${ctx.count} items`,
|
|
381
|
+
},
|
|
382
|
+
})
|
|
383
|
+
.build('ja');
|
|
384
|
+
|
|
385
|
+
console.log(messages.title.render()); // "タイトル"
|
|
386
|
+
console.log(messages.items.render({ count: 5 })); // "5個のアイテム"
|
|
158
387
|
```
|
|
159
388
|
|
|
389
|
+
### Using Clone for Shared Base Messages
|
|
390
|
+
|
|
160
391
|
```ts
|
|
161
|
-
//
|
|
162
|
-
|
|
163
|
-
|
|
392
|
+
// Create a base builder with common messages
|
|
393
|
+
const baseBuilder = createI18n(['ja', 'en'] as const)
|
|
394
|
+
.add({
|
|
395
|
+
common: { ja: '共通', en: 'Common' },
|
|
396
|
+
error: { ja: 'エラー', en: 'Error' },
|
|
397
|
+
});
|
|
398
|
+
|
|
399
|
+
// Clone and extend for admin pages
|
|
400
|
+
const adminMessages = baseBuilder.clone()
|
|
401
|
+
.add({
|
|
402
|
+
adminTitle: { ja: '管理画面', en: 'Admin Panel' },
|
|
403
|
+
})
|
|
404
|
+
.build('en');
|
|
405
|
+
|
|
406
|
+
// Clone and extend for user pages
|
|
407
|
+
const userMessages = baseBuilder.clone()
|
|
408
|
+
.add({
|
|
409
|
+
userTitle: { ja: 'ユーザー画面', en: 'User Panel' },
|
|
410
|
+
})
|
|
411
|
+
.build('en');
|
|
412
|
+
|
|
413
|
+
console.log(adminMessages.common.render()); // "Common"
|
|
414
|
+
console.log(adminMessages.adminTitle.render()); // "Admin Panel"
|
|
415
|
+
console.log(userMessages.common.render()); // "Common"
|
|
416
|
+
console.log(userMessages.userTitle.render()); // "User Panel"
|
|
164
417
|
```
|
|
165
418
|
|
|
419
|
+
### Namespace Pattern (Split Files)
|
|
420
|
+
|
|
166
421
|
```ts
|
|
167
|
-
//
|
|
168
|
-
|
|
169
|
-
import { applyLocale } from 'canopy-i18n';
|
|
422
|
+
// i18n/locales.ts
|
|
423
|
+
export const LOCALES = ['ja', 'en'] as const;
|
|
170
424
|
|
|
171
|
-
|
|
425
|
+
// i18n/common.ts
|
|
426
|
+
import { createI18n } from 'canopy-i18n';
|
|
427
|
+
import { LOCALES } from './locales';
|
|
172
428
|
|
|
173
|
-
|
|
174
|
-
|
|
175
|
-
|
|
429
|
+
export const common = createI18n(LOCALES).add({
|
|
430
|
+
hello: { ja: 'こんにちは', en: 'Hello' },
|
|
431
|
+
goodbye: { ja: 'さようなら', en: 'Goodbye' },
|
|
432
|
+
});
|
|
176
433
|
|
|
177
|
-
|
|
434
|
+
// i18n/user.ts
|
|
435
|
+
import { createI18n } from 'canopy-i18n';
|
|
436
|
+
import { LOCALES } from './locales';
|
|
178
437
|
|
|
179
|
-
|
|
438
|
+
export const user = createI18n(LOCALES).addTemplates<{ name: string }>()({
|
|
439
|
+
welcome: {
|
|
440
|
+
ja: (ctx) => `ようこそ、${ctx.name}さん`,
|
|
441
|
+
en: (ctx) => `Welcome, ${ctx.name}`,
|
|
442
|
+
},
|
|
443
|
+
});
|
|
180
444
|
|
|
181
|
-
|
|
445
|
+
// i18n/index.ts
|
|
446
|
+
export { common } from './common';
|
|
447
|
+
export { user } from './user';
|
|
182
448
|
|
|
183
|
-
|
|
184
|
-
|
|
449
|
+
// app.ts
|
|
450
|
+
import { bindLocale } from 'canopy-i18n';
|
|
451
|
+
import * as i18n from './i18n';
|
|
185
452
|
|
|
186
|
-
|
|
453
|
+
const messages = bindLocale(i18n, 'en');
|
|
454
|
+
console.log(messages.common.hello.render()); // "Hello"
|
|
455
|
+
console.log(messages.user.welcome.render({ name: 'John' })); // "Welcome, John"
|
|
456
|
+
```
|
|
187
457
|
|
|
188
|
-
|
|
189
|
-
|
|
190
|
-
|
|
191
|
-
|
|
192
|
-
|
|
458
|
+
### Dynamic Locale Switching
|
|
459
|
+
|
|
460
|
+
```ts
|
|
461
|
+
const builder = createI18n(['ja', 'en'] as const)
|
|
462
|
+
.add({
|
|
463
|
+
title: { ja: 'タイトル', en: 'Title' },
|
|
464
|
+
});
|
|
465
|
+
|
|
466
|
+
// Build different locale versions from the same builder
|
|
467
|
+
const jaMessages = builder.build('ja');
|
|
468
|
+
const enMessages = builder.build('en');
|
|
469
|
+
|
|
470
|
+
console.log(jaMessages.title.render()); // "タイトル"
|
|
471
|
+
console.log(enMessages.title.render()); // "Title"
|
|
472
|
+
|
|
473
|
+
// Or use bindLocale to switch locale dynamically
|
|
474
|
+
const messages = builder.build();
|
|
475
|
+
const localizedJa = bindLocale(messages, 'ja');
|
|
476
|
+
const localizedEn = bindLocale(messages, 'en');
|
|
477
|
+
```
|
|
478
|
+
|
|
479
|
+
### Deep Nested Structures
|
|
480
|
+
|
|
481
|
+
```ts
|
|
482
|
+
const structure = {
|
|
483
|
+
header: createI18n(['ja', 'en'] as const)
|
|
484
|
+
.add({ title: { ja: 'ヘッダー', en: 'Header' } }),
|
|
485
|
+
content: {
|
|
486
|
+
main: createI18n(['ja', 'en'] as const)
|
|
487
|
+
.add({ body: { ja: '本文', en: 'Body' } }),
|
|
488
|
+
sidebar: createI18n(['ja', 'en'] as const)
|
|
489
|
+
.add({ widget: { ja: 'ウィジェット', en: 'Widget' } }),
|
|
490
|
+
},
|
|
491
|
+
};
|
|
492
|
+
|
|
493
|
+
const localized = bindLocale(structure, 'en');
|
|
494
|
+
console.log(localized.header.title.render()); // "Header"
|
|
495
|
+
console.log(localized.content.main.body.render()); // "Body"
|
|
496
|
+
console.log(localized.content.sidebar.widget.render()); // "Widget"
|
|
193
497
|
```
|
|
194
498
|
|
|
195
|
-
|
|
499
|
+
|
|
500
|
+
## Repository
|
|
501
|
+
|
|
502
|
+
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, 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.
|
|
3
|
+
exports.isChainBuilder = isChainBuilder;
|
|
4
|
+
exports.bindLocale = bindLocale;
|
|
5
|
+
const chainBuilder_1 = require("./chainBuilder");
|
|
4
6
|
const message_1 = require("./message");
|
|
5
|
-
function
|
|
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 ===
|
|
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,31 @@
|
|
|
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>> = {}> {
|
|
4
|
+
private readonly locales;
|
|
5
|
+
private messages;
|
|
6
|
+
constructor(locales: Ls, messages?: Messages);
|
|
7
|
+
/**
|
|
8
|
+
* 複数のメッセージを一度に追加
|
|
9
|
+
* 型パラメータRでカスタム型も指定可能(デフォルトはstring)
|
|
10
|
+
*/
|
|
11
|
+
add<R = string, Entries extends Record<string, Record<Ls[number], R>> = Record<string, Record<Ls[number], R>>>(entries: {
|
|
12
|
+
[K in keyof Entries]: K extends keyof Messages ? never : Entries[K];
|
|
13
|
+
}): ChainBuilder<Ls, Messages & {
|
|
14
|
+
[K in keyof Entries]: LocalizedMessage<Ls, void, R>;
|
|
15
|
+
}>;
|
|
16
|
+
/**
|
|
17
|
+
* 関数指定版: 複数のテンプレート関数を一度に追加(型は統一)
|
|
18
|
+
*/
|
|
19
|
+
addTemplates<C, R = string>(): <Entries extends Record<string, Record<Ls[number], (ctx: C) => R>>>(entries: {
|
|
20
|
+
[K in keyof Entries]: K extends keyof Messages ? never : Entries[K];
|
|
21
|
+
}) => ChainBuilder<Ls, Messages & {
|
|
22
|
+
[K in keyof Entries]: LocalizedMessage<Ls, C, R>;
|
|
23
|
+
}>;
|
|
24
|
+
private deepCloneWithLocale;
|
|
25
|
+
build(locale?: Ls[number]): Messages;
|
|
26
|
+
/**
|
|
27
|
+
* 現在のChainBuilderの状態をコピーした新しいインスタンスを返す
|
|
28
|
+
*/
|
|
29
|
+
clone(): ChainBuilder<Ls, Messages>;
|
|
30
|
+
}
|
|
31
|
+
export declare function createI18n<const Ls extends readonly string[]>(locales: Ls): ChainBuilder<Ls, {}>;
|
|
@@ -0,0 +1,85 @@
|
|
|
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
|
+
* 型パラメータRでカスタム型も指定可能(デフォルトはstring)
|
|
16
|
+
*/
|
|
17
|
+
add(entries) {
|
|
18
|
+
const newMessages = { ...this.messages };
|
|
19
|
+
for (const [key, data] of Object.entries(entries)) {
|
|
20
|
+
const msg = new message_1.I18nMessage(this.locales, this.locales[0]).setData(data);
|
|
21
|
+
newMessages[key] = msg;
|
|
22
|
+
}
|
|
23
|
+
return new ChainBuilder(this.locales, newMessages);
|
|
24
|
+
}
|
|
25
|
+
/**
|
|
26
|
+
* 関数指定版: 複数のテンプレート関数を一度に追加(型は統一)
|
|
27
|
+
*/
|
|
28
|
+
addTemplates() {
|
|
29
|
+
return (entries) => {
|
|
30
|
+
const newMessages = { ...this.messages };
|
|
31
|
+
for (const [key, data] of Object.entries(entries)) {
|
|
32
|
+
const msg = new message_1.I18nMessage(this.locales, this.locales[0]).setData(data);
|
|
33
|
+
newMessages[key] = msg;
|
|
34
|
+
}
|
|
35
|
+
return new ChainBuilder(this.locales, newMessages);
|
|
36
|
+
};
|
|
37
|
+
}
|
|
38
|
+
deepCloneWithLocale(obj, locale) {
|
|
39
|
+
if ((0, message_1.isI18nMessage)(obj)) {
|
|
40
|
+
const cloned = Object.create(Object.getPrototypeOf(obj));
|
|
41
|
+
Object.assign(cloned, obj);
|
|
42
|
+
cloned.setLocale(locale);
|
|
43
|
+
return cloned;
|
|
44
|
+
}
|
|
45
|
+
if (Array.isArray(obj)) {
|
|
46
|
+
return obj.map(v => this.deepCloneWithLocale(v, locale));
|
|
47
|
+
}
|
|
48
|
+
if (obj && typeof obj === "object") {
|
|
49
|
+
const out = {};
|
|
50
|
+
for (const k of Object.keys(obj)) {
|
|
51
|
+
out[k] = this.deepCloneWithLocale(obj[k], locale);
|
|
52
|
+
}
|
|
53
|
+
return out;
|
|
54
|
+
}
|
|
55
|
+
return obj;
|
|
56
|
+
}
|
|
57
|
+
build(locale) {
|
|
58
|
+
if (locale !== undefined) {
|
|
59
|
+
return this.deepCloneWithLocale(this.messages, locale);
|
|
60
|
+
}
|
|
61
|
+
return this.messages;
|
|
62
|
+
}
|
|
63
|
+
/**
|
|
64
|
+
* 現在のChainBuilderの状態をコピーした新しいインスタンスを返す
|
|
65
|
+
*/
|
|
66
|
+
clone() {
|
|
67
|
+
const clonedMessages = {};
|
|
68
|
+
for (const [key, msg] of Object.entries(this.messages)) {
|
|
69
|
+
if ((0, message_1.isI18nMessage)(msg)) {
|
|
70
|
+
// I18nMessageをクローン
|
|
71
|
+
const cloned = Object.create(Object.getPrototypeOf(msg));
|
|
72
|
+
Object.assign(cloned, msg);
|
|
73
|
+
clonedMessages[key] = cloned;
|
|
74
|
+
}
|
|
75
|
+
else {
|
|
76
|
+
clonedMessages[key] = msg;
|
|
77
|
+
}
|
|
78
|
+
}
|
|
79
|
+
return new ChainBuilder(this.locales, clonedMessages);
|
|
80
|
+
}
|
|
81
|
+
}
|
|
82
|
+
exports.ChainBuilder = ChainBuilder;
|
|
83
|
+
function createI18n(locales) {
|
|
84
|
+
return new ChainBuilder(locales);
|
|
85
|
+
}
|
package/dist/index.d.ts
CHANGED
|
@@ -1,5 +1,5 @@
|
|
|
1
|
-
export {
|
|
2
|
-
export { createI18n } from
|
|
3
|
-
export { I18nMessage, isI18nMessage } from
|
|
4
|
-
export type { LocalizedMessage } from
|
|
5
|
-
export type { Template } from
|
|
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.
|
|
4
|
-
var
|
|
5
|
-
Object.defineProperty(exports, "
|
|
6
|
-
|
|
7
|
-
|
|
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
|
-
import { Template } from "./types";
|
|
2
|
-
export
|
|
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> {
|
|
3
5
|
readonly locales: Ls;
|
|
4
6
|
private _locale;
|
|
5
|
-
private _fallbackLocale;
|
|
6
7
|
private _data;
|
|
7
|
-
constructor(locales: Ls,
|
|
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
|
-
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
render(
|
|
15
|
-
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;
|
|
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
|
-
|
|
6
|
-
|
|
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,
|
|
13
|
+
constructor(locales, locale) {
|
|
14
14
|
this.locales = locales;
|
|
15
|
-
this.
|
|
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,11 @@ class I18nMessage {
|
|
|
37
29
|
return this;
|
|
38
30
|
}
|
|
39
31
|
render(ctx) {
|
|
40
|
-
const v = this._data[this._locale]
|
|
41
|
-
|
|
32
|
+
const v = this._data[this._locale];
|
|
33
|
+
if ((0, types_1.isTemplateFunction)(v)) {
|
|
34
|
+
return v(ctx);
|
|
35
|
+
}
|
|
36
|
+
return v;
|
|
42
37
|
}
|
|
43
38
|
}
|
|
44
39
|
exports.I18nMessage = I18nMessage;
|
|
45
|
-
function isI18nMessage(x) {
|
|
46
|
-
return x instanceof I18nMessage;
|
|
47
|
-
}
|
package/dist/types.d.ts
CHANGED
|
@@ -1 +1,2 @@
|
|
|
1
|
-
export type Template<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;
|
package/dist/types.js
CHANGED
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "canopy-i18n",
|
|
3
|
-
"version": "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",
|
|
@@ -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.
|
|
25
|
-
"@types/node": "^24.10.
|
|
26
|
-
"release-it": "^19.
|
|
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.
|
|
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
|
+
}
|
package/dist/applyLocale.d.ts
DELETED
|
@@ -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;
|
package/dist/applyLocaleDeep.js
DELETED
|
@@ -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
|
-
}
|
package/dist/createBuilder.d.ts
DELETED
|
@@ -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;
|
package/dist/createBuilder.js
DELETED
|
@@ -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
|
-
}
|
package/dist/createI18n.d.ts
DELETED
|
@@ -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
|
-
};
|
package/dist/createI18n.js
DELETED
|
@@ -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
|
-
}
|
package/dist/testData.d.ts
DELETED
|
@@ -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
|
-
});
|