canopy-i18n 0.0.4 → 0.1.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
@@ -22,18 +22,18 @@ yarn add canopy-i18n
22
22
  ## Quick start
23
23
 
24
24
  ```ts
25
- import { createMessageBuilder, applyLocaleDeep } from 'canopy-i18n';
25
+ import { createI18n, applyLocale } from 'canopy-i18n';
26
26
 
27
27
  // 1) Declare allowed locales and fallback
28
- const builder = createMessageBuilder(['ja', 'en'] as const, 'ja');
28
+ const defineMessage = createI18n(['ja', 'en'] as const, 'ja');
29
29
 
30
30
  // 2) Define messages
31
- const title = builder({
31
+ const title = defineMessage({
32
32
  ja: 'タイトルテスト',
33
33
  en: 'Title Test',
34
34
  });
35
35
 
36
- const msg = builder<{ name: string; age: number }>({
36
+ const msg = defineMessage<{ name: string; age: number }>({
37
37
  ja: c => `こんにちは、${c.name}さん。あなたは${c.age}歳です。`,
38
38
  en: c => `Hello, ${c.name}. You are ${c.age} years old.`,
39
39
  });
@@ -42,12 +42,12 @@ const msg = builder<{ name: string; age: number }>({
42
42
  const data = {
43
43
  title,
44
44
  nested: {
45
- hello: builder({ ja: 'こんにちは', en: 'Hello' }),
45
+ hello: defineMessage({ ja: 'こんにちは', en: 'Hello' }),
46
46
  },
47
47
  };
48
48
 
49
49
  // 4) Apply locale across the tree
50
- const localized = applyLocaleDeep(data, 'en');
50
+ const localized = applyLocale(data, 'en');
51
51
 
52
52
  console.log(localized.title.render()); // "Title Test"
53
53
  console.log(localized.nested.hello.render()); // "Hello"
@@ -56,15 +56,15 @@ console.log(msg.setLocale('en').render({ name: 'Tanaka', age: 20 }));
56
56
 
57
57
  ## API
58
58
 
59
- ### createMessageBuilder(locales, fallbackLocale)
60
- Returns a `builder` function to create localized messages.
59
+ ### createI18n(locales, fallbackLocale)
60
+ Returns a `defineMessage` function to create localized messages.
61
61
 
62
62
  - **locales**: `readonly string[]` — Allowed locale keys (e.g. `['ja', 'en'] as const`).
63
63
  - **fallbackLocale**: fallback locale when the active locale value is missing. New messages start with this locale active.
64
64
 
65
65
  Overloads:
66
- - `builder<Record<L[number], string>>() -> I18nMessage<L, void>`
67
- - `builder<Record<L[number], Template<C>>>() -> I18nMessage<L, C>`
66
+ - `defineMessage<Record<L[number], string>>() -> I18nMessage<L, void>`
67
+ - `defineMessage<Record<L[number], Template<C>>>() -> I18nMessage<L, C>`
68
68
 
69
69
  ### I18nMessage<L, C>
70
70
  Represents a single localized message.
@@ -79,7 +79,7 @@ Represents a single localized message.
79
79
  - `setFallbackLocale(locale: L[number]): this`
80
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.
81
81
 
82
- ### applyLocaleDeep(obj, locale)
82
+ ### applyLocale(obj, locale)
83
83
  Recursively traverses arrays/objects and sets the given `locale` on all `I18nMessage` instances encountered.
84
84
 
85
85
  - Returns a new container (arrays/objects are cloned), but reuses the same message instances after updating their locale.
@@ -94,8 +94,8 @@ export type Template<C> = string | ((ctx: C) => string);
94
94
 
95
95
  ```ts
96
96
  export { I18nMessage, isI18nMessage } from 'canopy-i18n';
97
- export { createMessageBuilder } from 'canopy-i18n';
98
- export { applyLocaleDeep } from 'canopy-i18n';
97
+ export { createI18n } from 'canopy-i18n';
98
+ export { applyLocale } from 'canopy-i18n';
99
99
  export type { Template } from 'canopy-i18n';
100
100
  export type { LocalizedMessage } from 'canopy-i18n';
101
101
  ```
@@ -112,15 +112,15 @@ Import all message exports as a namespace and set the locale across the whole tr
112
112
 
113
113
  ```ts
114
114
  // messages.ts
115
- import { createMessageBuilder } from 'canopy-i18n';
116
- const builder = createMessageBuilder(['ja', 'en'] as const, 'ja');
115
+ import { createI18n } from 'canopy-i18n';
116
+ const defineMessage = createI18n(['ja', 'en'] as const, 'ja');
117
117
 
118
- export const title = builder({
118
+ export const title = defineMessage({
119
119
  ja: 'タイトルテスト',
120
120
  en: 'Title Test',
121
121
  });
122
122
 
123
- export const msg = builder<{ name: string; age: number }>({
123
+ export const msg = defineMessage<{ name: string; age: number }>({
124
124
  ja: c => `こんにちは、${c.name}さん。あなたは${c.age}歳です。`,
125
125
  en: c => `Hello, ${c.name}. You are ${c.age} years old.`,
126
126
  });
@@ -129,9 +129,9 @@ export const msg = builder<{ name: string; age: number }>({
129
129
  ```ts
130
130
  // usage.ts
131
131
  import * as messages from './messages';
132
- import { applyLocaleDeep } from 'canopy-i18n';
132
+ import { applyLocale } from 'canopy-i18n';
133
133
 
134
- const m = applyLocaleDeep(messages, 'en');
134
+ const m = applyLocale(messages, 'en');
135
135
 
136
136
  console.log(m.title.render()); // "Title Test"
137
137
  console.log(m.msg.render({ name: 'Tanaka', age: 20 }));
@@ -140,21 +140,21 @@ console.log(m.msg.render({ name: 'Tanaka', age: 20 }));
140
140
  #### Multi-file structure
141
141
 
142
142
  ```ts
143
- // i18n/builder.ts
144
- import { createMessageBuilder } from 'canopy-i18n';
145
- export const builder = createMessageBuilder(['ja', 'en'] as const, 'ja');
143
+ // i18n/defineMessage.ts
144
+ import { createI18n } from 'canopy-i18n';
145
+ export const defineMessage = createI18n(['ja', 'en'] as const, 'ja');
146
146
  ```
147
147
 
148
148
  ```ts
149
149
  // i18n/messages/common.ts
150
- import { builder } from '../builder';
151
- export const hello = builder({ ja: 'こんにちは', en: 'Hello' });
150
+ import { defineMessage } from '../defineMessage';
151
+ export const hello = defineMessage({ ja: 'こんにちは', en: 'Hello' });
152
152
  ```
153
153
 
154
154
  ```ts
155
155
  // i18n/messages/home.ts
156
- import { builder } from '../builder';
157
- export const title = builder({ ja: 'タイトル', en: 'Title' });
156
+ import { defineMessage } from '../defineMessage';
157
+ export const title = defineMessage({ ja: 'タイトル', en: 'Title' });
158
158
  ```
159
159
 
160
160
  ```ts
@@ -166,12 +166,30 @@ export * as home from './home';
166
166
  ```ts
167
167
  // usage.ts
168
168
  import * as msgs from './i18n/messages';
169
- import { applyLocaleDeep } from 'canopy-i18n';
169
+ import { applyLocale } from 'canopy-i18n';
170
170
 
171
- const m = applyLocaleDeep(msgs, 'en');
171
+ const m = applyLocale(msgs, 'en');
172
172
 
173
173
  console.log(m.common.hello.render()); // "Hello"
174
174
  console.log(m.home.title.render()); // "Title"
175
175
  ```
176
176
 
177
- Note: Module namespace objects are read-only; `applyLocaleDeep` returns a cloned plain object while updating each `I18nMessage` instance's locale in place.
177
+ Note: Module namespace objects are read-only; `applyLocale` returns a cloned plain object while updating each `I18nMessage` instance's locale in place.
178
+
179
+ ## Example: Next.js App Router
180
+
181
+ An example Next.js App Router project lives under `examples/next-app`.
182
+
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`)
185
+
186
+ How to run:
187
+
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
193
+ ```
194
+
195
+ Open `http://localhost:3000` and you will be redirected to `/{locale}` based on `Accept-Language`.
@@ -0,0 +1 @@
1
+ export declare function applyLocale<T extends object>(obj: T, locale: string): T;
@@ -0,0 +1,24 @@
1
+ "use strict";
2
+ Object.defineProperty(exports, "__esModule", { value: true });
3
+ exports.applyLocale = applyLocale;
4
+ const message_1 = require("./message");
5
+ function applyLocale(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
+ }
@@ -0,0 +1,6 @@
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
+ };
@@ -0,0 +1,10 @@
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/index.d.ts CHANGED
@@ -1,5 +1,5 @@
1
- export type { Template } from './types';
1
+ export { applyLocale } from './applyLocale';
2
+ export { createI18n } from './createI18n';
2
3
  export { I18nMessage, isI18nMessage } from './message';
3
4
  export type { LocalizedMessage } from './message';
4
- export { createMessageBuilder } from './builder';
5
- export { applyLocaleDeep } from './applyLocaleDeep';
5
+ export type { Template } from './types';
package/dist/index.js CHANGED
@@ -1,10 +1,10 @@
1
1
  "use strict";
2
2
  Object.defineProperty(exports, "__esModule", { value: true });
3
- exports.applyLocaleDeep = exports.createMessageBuilder = exports.isI18nMessage = exports.I18nMessage = void 0;
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; } });
4
8
  var message_1 = require("./message");
5
9
  Object.defineProperty(exports, "I18nMessage", { enumerable: true, get: function () { return message_1.I18nMessage; } });
6
10
  Object.defineProperty(exports, "isI18nMessage", { enumerable: true, get: function () { return message_1.isI18nMessage; } });
7
- var builder_1 = require("./builder");
8
- Object.defineProperty(exports, "createMessageBuilder", { enumerable: true, get: function () { return builder_1.createMessageBuilder; } });
9
- var applyLocaleDeep_1 = require("./applyLocaleDeep");
10
- Object.defineProperty(exports, "applyLocaleDeep", { enumerable: true, get: function () { return applyLocaleDeep_1.applyLocaleDeep; } });
package/dist/testData.js CHANGED
@@ -2,7 +2,7 @@
2
2
  Object.defineProperty(exports, "__esModule", { value: true });
3
3
  exports.hasSentMsg = exports.nested = exports.msg = exports.title = void 0;
4
4
  const index_1 = require("./index");
5
- const builder = (0, index_1.createMessageBuilder)(['ja', 'en'], 'ja');
5
+ const builder = (0, index_1.createI18n)(['ja', 'en'], 'ja');
6
6
  exports.title = builder({
7
7
  ja: 'タイトルテスト',
8
8
  en: 'Title Test'
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "canopy-i18n",
3
- "version": "0.0.4",
3
+ "version": "0.1.0",
4
4
  "description": "A tiny, type-safe i18n helper",
5
5
  "type": "commonjs",
6
6
  "main": "dist/index.js",
@@ -23,7 +23,8 @@
23
23
  "devDependencies": {
24
24
  "@tsconfig/node20": "^20.1.6",
25
25
  "@types/node": "^24.3.3",
26
- "typescript": "^5.9.2"
26
+ "typescript": "^5.9.2",
27
+ "vitest": "^3.2.4"
27
28
  },
28
29
  "keywords": [
29
30
  "i18n",
@@ -44,6 +45,8 @@
44
45
  "scripts": {
45
46
  "dev": "tsc --watch",
46
47
  "build": "tsc",
47
- "check": "tsc -p . --noEmit"
48
+ "type-check": "tsc -p . --noEmit",
49
+ "test": "vitest run",
50
+ "test:watch": "vitest"
48
51
  }
49
52
  }