canopy-i18n 0.4.2 → 0.5.1

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/LICENSE ADDED
@@ -0,0 +1,21 @@
1
+ MIT License
2
+
3
+ Copyright (c) 2026 Masaaki Ota
4
+
5
+ Permission is hereby granted, free of charge, to any person obtaining a copy
6
+ of this software and associated documentation files (the "Software"), to deal
7
+ in the Software without restriction, including without limitation the rights
8
+ to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9
+ copies of the Software, and to permit persons to whom the Software is
10
+ furnished to do so, subject to the following conditions:
11
+
12
+ The above copyright notice and this permission notice shall be included in all
13
+ copies or substantial portions of the Software.
14
+
15
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17
+ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18
+ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19
+ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20
+ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21
+ SOFTWARE.
package/README.md CHANGED
@@ -2,6 +2,9 @@
2
2
 
3
3
  A tiny, type-safe i18n library for building localized messages with builder pattern and applying locales across nested data structures.
4
4
 
5
+ ![スクリーンショット](https://raw.githubusercontent.com/mohhh-ok/canopy-i18n/main/docs/images/hero.gif)
6
+
7
+
5
8
  ## Features
6
9
  - **AI-friendly**: Full type safety and single-file colocation give AI assistants complete context for accurate code generation.
7
10
  - **Type-safe**: Compile-time safety for locale keys with full TypeScript IntelliSense support.
@@ -36,7 +39,7 @@ const messages = createI18n(['en', 'ja'] as const).add({
36
39
  farewell: { en: 'Goodbye', ja: 'さようなら' }
37
40
  }).build('en');
38
41
 
39
- console.log(messages.greeting); // Fully type-safe, autocomplete works
42
+ console.log(messages.greeting()); // Fully type-safe, autocomplete works
40
43
  ```
41
44
 
42
45
  **With template functions:**
@@ -64,8 +67,8 @@ const menu = createI18n(['en', 'ja'] as const)
64
67
  }
65
68
  }).build('ja');
66
69
 
67
- console.log(menu.home.label); // "ホーム"
68
- console.log(menu.home.url); // "/ja"
70
+ console.log(menu.home().label); // "ホーム"
71
+ console.log(menu.home().url); // "/ja"
69
72
  ```
70
73
 
71
74
  **Benefits:**
@@ -118,15 +121,15 @@ const builder = baseBuilder
118
121
  const enMessages = builder.build('en');
119
122
  const jaMessages = builder.build('ja');
120
123
 
121
- // 4) Render messages (English)
122
- console.log(enMessages.title.render()); // "Title Test"
123
- console.log(enMessages.greeting.render()); // "Hello"
124
- console.log(enMessages.welcome.render({ name: 'Tanaka', age: 20 })); // "Hello, Tanaka. You are 20 years old."
124
+ // 4) Use messages (English)
125
+ console.log(enMessages.title()); // "Title Test"
126
+ console.log(enMessages.greeting()); // "Hello"
127
+ console.log(enMessages.welcome({ name: 'Tanaka', age: 20 })); // "Hello, Tanaka. You are 20 years old."
125
128
 
126
- // 5) Render messages (Japanese)
127
- console.log(jaMessages.title.render()); // "タイトルテスト"
128
- console.log(jaMessages.greeting.render()); // "こんにちは"
129
- console.log(jaMessages.welcome.render({ name: 'Tanaka', age: 20 })); // "こんにちは、Tanakaさん。あなたは20歳です。"
129
+ // 5) Use messages (Japanese)
130
+ console.log(jaMessages.title()); // "タイトルテスト"
131
+ console.log(jaMessages.greeting()); // "こんにちは"
132
+ console.log(jaMessages.welcome({ name: 'Tanaka', age: 20 })); // "こんにちは、Tanakaさん。あなたは20歳です。"
130
133
  ```
131
134
 
132
135
  ## API
@@ -163,8 +166,8 @@ const builder = createI18n(['ja', 'en'] as const)
163
166
  const messages = createI18n(['ja', 'en'] as const)
164
167
  .add<JSX.Element>({
165
168
  badge: {
166
- ja: <span style={{ color: 'red' }}>新着</span>,
167
- en: <span style={{ color: 'red' }}>NEW</span>,
169
+ ja: <span style={{ background: '#ff4444', color: 'white', padding: '2px 6px', borderRadius: '2px' }}>🔴 新着</span>,
170
+ en: <span style={{ background: '#4caf50', color: 'white', padding: '4px 12px', borderRadius: '16px' }}>✨ NEW</span>,
168
171
  },
169
172
  });
170
173
 
@@ -172,13 +175,14 @@ const messages = createI18n(['ja', 'en'] as const)
172
175
  type MenuItem = {
173
176
  label: string;
174
177
  url: string;
178
+ icon: string;
175
179
  };
176
180
 
177
181
  const menu = createI18n(['ja', 'en'] as const)
178
182
  .add<MenuItem>({
179
183
  home: {
180
- ja: { label: 'ホーム', url: '/' },
181
- en: { label: 'Home', url: '/' },
184
+ ja: { label: 'ホーム', url: '/ja', icon: '🏠' },
185
+ en: { label: 'Home', url: '/en', icon: '🏡' },
182
186
  },
183
187
  });
184
188
  ```
@@ -226,55 +230,6 @@ const japaneseMessages = builder.build('ja');
226
230
 
227
231
  **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.
228
232
 
229
- #### `.clone()`
230
- Creates an independent copy of the current builder with all its messages.
231
-
232
- - Returns: `ChainBuilder<Locales, Messages>` — A new builder instance with cloned messages
233
-
234
- ```ts
235
- const builder1 = createI18n(['ja', 'en'] as const)
236
- .add({
237
- title: { ja: 'タイトル', en: 'Title' },
238
- });
239
-
240
- const builder2 = builder1.clone().add({
241
- greeting: { ja: 'こんにちは', en: 'Hello' },
242
- });
243
-
244
- const messages1 = builder1.build('ja');
245
- const messages2 = builder2.build('ja');
246
-
247
- console.log(messages1.title.render()); // "タイトル"
248
- console.log(messages1.greeting); // undefined
249
-
250
- console.log(messages2.title.render()); // "タイトル"
251
- console.log(messages2.greeting.render()); // "こんにちは"
252
- ```
253
-
254
- **Note**: `clone()` creates a deep copy of all messages, allowing you to branch off from a base builder and add different messages independently.
255
-
256
- ### `I18nMessage<Locales, Context, ReturnType = string>`
257
- Represents a single localized message.
258
-
259
- #### Properties
260
- - `locales: Locales` — Readonly array of allowed locales
261
- - `locale: Locale` — Current active locale (getter)
262
- - `data: Record<Locale, Template<Context, ReturnType>>` — Message data for all locales (getter)
263
-
264
- #### Methods
265
- - `setLocale(locale: Locale): this` — Sets the active locale
266
- - `setData(data: Record<Locale, Template<Context, ReturnType>>): this` — Sets the message data
267
- - `render(ctx?: Context): ReturnType` — Renders the message for the active locale and returns the specified type
268
-
269
- ```ts
270
- const message = messages.title;
271
- console.log(message.locale); // Current locale
272
- console.log(message.render()); // Rendered string
273
-
274
- message.setLocale('ja');
275
- console.log(message.render()); // Japanese version
276
- ```
277
-
278
233
  ### `bindLocale(obj, locale)`
279
234
  Recursively traverses objects/arrays and sets the given locale on all `I18nMessage` instances and builds all `ChainBuilder` instances encountered.
280
235
 
@@ -291,8 +246,8 @@ const data = {
291
246
  };
292
247
 
293
248
  const localized = bindLocale(data, 'en');
294
- console.log(localized.common.title.render()); // English version
295
- console.log(localized.nested.special.msg.render()); // English version
249
+ console.log(localized.common.title()); // English version
250
+ console.log(localized.nested.special.msg()); // English version
296
251
  ```
297
252
 
298
253
  **Note**: `bindLocale` works with both `ChainBuilder` instances (automatically building them with the specified locale) and already-built message objects (updating their locale).
@@ -315,91 +270,6 @@ export type { Template, LocalizedMessage } from 'canopy-i18n';
315
270
 
316
271
  ## Usage Patterns
317
272
 
318
- ### React Components as Messages
319
-
320
- ```ts
321
- import { createI18n } from 'canopy-i18n';
322
-
323
- // Static React components
324
- const messages = createI18n(['ja', 'en'] as const)
325
- .add<JSX.Element>({
326
- badge: {
327
- ja: <span style={{ background: '#4caf50', color: 'white', padding: '4px 8px', borderRadius: '4px' }}>新着</span>,
328
- en: <span style={{ background: '#4caf50', color: 'white', padding: '4px 8px', borderRadius: '4px' }}>NEW</span>,
329
- },
330
- alert: {
331
- ja: <div style={{ background: '#fff3cd', padding: '12px', borderRadius: '4px' }}>⚠️ これは警告です</div>,
332
- en: <div style={{ background: '#fff3cd', padding: '12px', borderRadius: '4px' }}>⚠️ This is a warning</div>,
333
- },
334
- })
335
- .build('en');
336
-
337
- // Render in React
338
- function MyComponent() {
339
- return (
340
- <div>
341
- {messages.badge.render()}
342
- {messages.alert.render()}
343
- </div>
344
- );
345
- }
346
-
347
- // Dynamic React components with context
348
- type ButtonContext = {
349
- onClick: () => void;
350
- text: string;
351
- };
352
-
353
- const dynamicMessages = createI18n(['ja', 'en'] as const)
354
- .addTemplates<ButtonContext, JSX.Element>()({
355
- button: {
356
- ja: (ctx) => (
357
- <button onClick={ctx.onClick} style={{ background: '#2196f3', color: 'white', padding: '8px 16px' }}>
358
- {ctx.text}
359
- </button>
360
- ),
361
- en: (ctx) => (
362
- <button onClick={ctx.onClick} style={{ background: '#2196f3', color: 'white', padding: '8px 16px' }}>
363
- {ctx.text}
364
- </button>
365
- ),
366
- },
367
- })
368
- .build('en');
369
-
370
- // Use with context
371
- function AnotherComponent() {
372
- return <div>{dynamicMessages.button.render({ onClick: () => alert('Clicked!'), text: 'Click me' })}</div>;
373
- }
374
- ```
375
-
376
- ### Custom Object Types
377
-
378
- ```ts
379
- type MenuItem = {
380
- label: string;
381
- url: string;
382
- icon: string;
383
- };
384
-
385
- const menuMessages = createI18n(['ja', 'en'] as const)
386
- .add<MenuItem>({
387
- home: {
388
- ja: { label: 'ホーム', url: '/', icon: '🏠' },
389
- en: { label: 'Home', url: '/', icon: '🏠' },
390
- },
391
- settings: {
392
- ja: { label: '設定', url: '/settings', icon: '⚙️' },
393
- en: { label: 'Settings', url: '/settings', icon: '⚙️' },
394
- },
395
- })
396
- .build('en');
397
-
398
- const homeMenu = menuMessages.home.render();
399
- console.log(homeMenu.label); // "Home"
400
- console.log(homeMenu.url); // "/"
401
- console.log(homeMenu.icon); // "🏠"
402
- ```
403
273
 
404
274
  ### Basic String Messages
405
275
 
@@ -412,8 +282,8 @@ const messages = createI18n(['ja', 'en'] as const)
412
282
  })
413
283
  .build('en');
414
284
 
415
- console.log(messages.title.render()); // "Title"
416
- console.log(messages.greeting.render()); // "Hello"
285
+ console.log(messages.title()); // "Title"
286
+ console.log(messages.greeting()); // "Hello"
417
287
  ```
418
288
 
419
289
  ### Template Functions with Context
@@ -428,7 +298,7 @@ const messages = createI18n(['ja', 'en'] as const)
428
298
  })
429
299
  .build('en');
430
300
 
431
- console.log(messages.profile.render({ name: 'Taro', age: 25 }));
301
+ console.log(messages.profile({ name: 'Taro', age: 25 }));
432
302
  // "Name: Taro, Age: 25"
433
303
  ```
434
304
 
@@ -447,39 +317,11 @@ const messages = createI18n(['ja', 'en'] as const)
447
317
  })
448
318
  .build('ja');
449
319
 
450
- console.log(messages.title.render()); // "タイトル"
451
- console.log(messages.items.render({ count: 5 })); // "5個のアイテム"
320
+ console.log(messages.title()); // "タイトル"
321
+ console.log(messages.items({ count: 5 })); // "5個のアイテム"
452
322
  ```
453
323
 
454
- ### Using Clone for Shared Base Messages
455
-
456
- ```ts
457
- // Create a base builder with common messages
458
- const baseBuilder = createI18n(['ja', 'en'] as const)
459
- .add({
460
- common: { ja: '共通', en: 'Common' },
461
- error: { ja: 'エラー', en: 'Error' },
462
- });
463
-
464
- // Clone and extend for admin pages
465
- const adminMessages = baseBuilder.clone()
466
- .add({
467
- adminTitle: { ja: '管理画面', en: 'Admin Panel' },
468
- })
469
- .build('en');
470
324
 
471
- // Clone and extend for user pages
472
- const userMessages = baseBuilder.clone()
473
- .add({
474
- userTitle: { ja: 'ユーザー画面', en: 'User Panel' },
475
- })
476
- .build('en');
477
-
478
- console.log(adminMessages.common.render()); // "Common"
479
- console.log(adminMessages.adminTitle.render()); // "Admin Panel"
480
- console.log(userMessages.common.render()); // "Common"
481
- console.log(userMessages.userTitle.render()); // "User Panel"
482
- ```
483
325
 
484
326
  ### Namespace Pattern (Split Files)
485
327
 
@@ -516,8 +358,8 @@ import { bindLocale } from 'canopy-i18n';
516
358
  import * as i18n from './i18n';
517
359
 
518
360
  const messages = bindLocale(i18n, 'en');
519
- console.log(messages.common.hello.render()); // "Hello"
520
- console.log(messages.user.welcome.render({ name: 'John' })); // "Welcome, John"
361
+ console.log(messages.common.hello()); // "Hello"
362
+ console.log(messages.user.welcome({ name: 'John' })); // "Welcome, John"
521
363
  ```
522
364
 
523
365
  ### Dynamic Locale Switching
@@ -532,13 +374,8 @@ const builder = createI18n(['ja', 'en'] as const)
532
374
  const jaMessages = builder.build('ja');
533
375
  const enMessages = builder.build('en');
534
376
 
535
- console.log(jaMessages.title.render()); // "タイトル"
536
- console.log(enMessages.title.render()); // "Title"
537
-
538
- // Or use bindLocale to switch locale dynamically
539
- const messages = builder.build();
540
- const localizedJa = bindLocale(messages, 'ja');
541
- const localizedEn = bindLocale(messages, 'en');
377
+ console.log(jaMessages.title()); // "タイトル"
378
+ console.log(enMessages.title()); // "Title"
542
379
  ```
543
380
 
544
381
  ### Deep Nested Structures
@@ -556,9 +393,9 @@ const structure = {
556
393
  };
557
394
 
558
395
  const localized = bindLocale(structure, 'en');
559
- console.log(localized.header.title.render()); // "Header"
560
- console.log(localized.content.main.body.render()); // "Body"
561
- console.log(localized.content.sidebar.widget.render()); // "Widget"
396
+ console.log(localized.header.title()); // "Header"
397
+ console.log(localized.content.main.body()); // "Body"
398
+ console.log(localized.content.sidebar.widget()); // "Widget"
562
399
  ```
563
400
 
564
401
 
@@ -1,7 +1,9 @@
1
1
  import { ChainBuilder } from "./chainBuilder.js";
2
- import { I18nMessage } from "./message.js";
2
+ import { I18nMessage, type LocalizedMessage } from "./message.js";
3
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[] ? {
4
+ type DeepUnwrapChainBuilders<T> = T extends I18nMessage<infer Ls, infer C, infer R> ? LocalizedMessage<Ls, C, R> : T extends ChainBuilder<infer Ls, infer Messages> ? {
5
+ [K in keyof Messages]: Messages[K] extends I18nMessage<Ls, infer C, infer R> ? LocalizedMessage<Ls, C, R> : never;
6
+ } : T extends readonly any[] ? {
5
7
  [K in keyof T]: DeepUnwrapChainBuilders<T[K]>;
6
8
  } : T extends object ? {
7
9
  [K in keyof T]: DeepUnwrapChainBuilders<T[K]>;
@@ -9,8 +9,10 @@ export function bindLocale(obj, locale) {
9
9
  return v.build(locale);
10
10
  }
11
11
  if (isI18nMessage(v)) {
12
- v.setLocale(locale);
13
- return v;
12
+ const cloned = Object.create(Object.getPrototypeOf(v));
13
+ Object.assign(cloned, v);
14
+ cloned.setLocale(locale);
15
+ return cloned.toFunction();
14
16
  }
15
17
  if (Array.isArray(v)) {
16
18
  return v.map(visit);
@@ -11,7 +11,7 @@ export declare class ChainBuilder<const Ls extends readonly string[], Messages e
11
11
  add<R = string, Entries extends Record<string, Record<Ls[number], R>> = Record<string, Record<Ls[number], R>>>(entries: {
12
12
  [K in keyof Entries]: K extends keyof Messages ? never : Entries[K];
13
13
  }): ChainBuilder<Ls, Messages & {
14
- [K in keyof Entries]: LocalizedMessage<Ls, void, R>;
14
+ [K in keyof Entries]: I18nMessage<Ls, void, R>;
15
15
  }>;
16
16
  /**
17
17
  * 関数指定版: 複数のテンプレート関数を一度に追加(型は統一)
@@ -19,13 +19,11 @@ export declare class ChainBuilder<const Ls extends readonly string[], Messages e
19
19
  addTemplates<C, R = string>(): <Entries extends Record<string, Record<Ls[number], (ctx: C) => R>>>(entries: {
20
20
  [K in keyof Entries]: K extends keyof Messages ? never : Entries[K];
21
21
  }) => ChainBuilder<Ls, Messages & {
22
- [K in keyof Entries]: LocalizedMessage<Ls, C, R>;
22
+ [K in keyof Entries]: I18nMessage<Ls, C, R>;
23
23
  }>;
24
24
  private deepCloneWithLocale;
25
- build(locale?: Ls[number]): Messages;
26
- /**
27
- * 現在のChainBuilderの状態をコピーした新しいインスタンスを返す
28
- */
29
- clone(): ChainBuilder<Ls, Messages>;
25
+ build<M = {
26
+ [K in keyof Messages]: Messages[K] extends I18nMessage<Ls, infer C, infer R> ? LocalizedMessage<Ls, C, R> : never;
27
+ }>(locale?: Ls[number]): M;
30
28
  }
31
29
  export declare function createI18n<const Ls extends readonly string[]>(locales: Ls): ChainBuilder<Ls, {}>;
@@ -51,28 +51,19 @@ export class ChainBuilder {
51
51
  return obj;
52
52
  }
53
53
  build(locale) {
54
- if (locale !== undefined) {
55
- return this.deepCloneWithLocale(this.messages, locale);
56
- }
57
- return this.messages;
58
- }
59
- /**
60
- * 現在のChainBuilderの状態をコピーした新しいインスタンスを返す
61
- */
62
- clone() {
63
- const clonedMessages = {};
64
- for (const [key, msg] of Object.entries(this.messages)) {
54
+ const clonedMessages = locale !== undefined
55
+ ? this.deepCloneWithLocale(this.messages, locale)
56
+ : this.messages;
57
+ const result = {};
58
+ for (const [key, msg] of Object.entries(clonedMessages)) {
65
59
  if (isI18nMessage(msg)) {
66
- // I18nMessageをクローン
67
- const cloned = Object.create(Object.getPrototypeOf(msg));
68
- Object.assign(cloned, msg);
69
- clonedMessages[key] = cloned;
60
+ result[key] = msg.toFunction();
70
61
  }
71
62
  else {
72
- clonedMessages[key] = msg;
63
+ result[key] = msg;
73
64
  }
74
65
  }
75
- return new ChainBuilder(this.locales, clonedMessages);
66
+ return result;
76
67
  }
77
68
  }
78
69
  export function createI18n(locales) {
package/dist/message.d.ts CHANGED
@@ -1,5 +1,9 @@
1
1
  import type { Template } from "./types.js";
2
- export type LocalizedMessage<Ls extends readonly string[], C, R = string> = I18nMessage<Ls, C, R>;
2
+ export type LocalizedMessage<Ls extends readonly string[], C, R = string> = C extends void ? (() => R) & {
3
+ __brand: "I18nMessage";
4
+ } : ((ctx: C) => R) & {
5
+ __brand: "I18nTemplateMessage";
6
+ };
3
7
  export declare function isI18nMessage(x: unknown): x is I18nMessage<any, any, any>;
4
8
  export declare class I18nMessage<Ls extends readonly string[], C, R = string> {
5
9
  readonly locales: Ls;
@@ -10,6 +14,6 @@ export declare class I18nMessage<Ls extends readonly string[], C, R = string> {
10
14
  setLocale(locale: Ls[number]): this;
11
15
  get data(): Record<Ls[number], Template<C, R>>;
12
16
  setData(data: Record<Ls[number], Template<C, R>>): this;
13
- render(this: I18nMessage<Ls, void, R>): R;
14
- render(ctx: C): R;
17
+ private render;
18
+ toFunction(): LocalizedMessage<Ls, C, R>;
15
19
  }
package/dist/message.js CHANGED
@@ -31,4 +31,9 @@ export class I18nMessage {
31
31
  }
32
32
  return v;
33
33
  }
34
+ toFunction() {
35
+ const self = this;
36
+ const fn = ((ctx) => self.render(ctx));
37
+ return fn;
38
+ }
34
39
  }
@@ -0,0 +1 @@
1
+ export {};
@@ -0,0 +1,14 @@
1
+ import { createI18n } from "./chainBuilder.js";
2
+ const a = createI18n(["ja", "en"]);
3
+ const b = a
4
+ .add({ abc: { ja: "abc", en: "abc" } })
5
+ .addTemplates()({
6
+ bb: { ja: ({ aa }) => `${aa}aa`, en: ({ aa }) => `${aa}bb` },
7
+ });
8
+ const c = b.build("en");
9
+ const e = { ...c.abc, toString: () => "aaa" };
10
+ console.log(c.bb({ aa: "name" }));
11
+ console.log(`${c.bb({ aa: "name" })}`);
12
+ console.log("aaa" + c.bb({ aa: "name" }));
13
+ console.log(`${c.bb({ aa: "name" })}`);
14
+ console.log(`${c.abc()}`);
package/package.json CHANGED
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "name": "canopy-i18n",
3
- "version": "0.4.2",
4
- "description": "A tiny, type-safe i18n helper",
3
+ "version": "0.5.1",
4
+ "description": "The Type-Safe i18n library that your IDE will Love",
5
5
  "type": "module",
6
6
  "main": "dist/index.js",
7
7
  "types": "dist/index.d.ts",
@@ -25,14 +25,15 @@
25
25
  "prepublishOnly": "pnpm run build",
26
26
  "type-check": "tsc -p . --noEmit",
27
27
  "test": "vitest run",
28
- "test:watch": "vitest"
28
+ "test:watch": "vitest",
29
+ "release": "release-it"
29
30
  },
30
31
  "devDependencies": {
31
32
  "@tsconfig/node20": "^20.1.8",
32
- "@types/node": "^24.10.4",
33
- "release-it": "^19.2.2",
33
+ "@types/node": "^25.0.10",
34
+ "release-it": "^19.2.4",
34
35
  "typescript": "^5.9.3",
35
- "vitest": "^4.0.16"
36
+ "vitest": "^4.0.18"
36
37
  },
37
38
  "keywords": [
38
39
  "i18n",
@@ -48,6 +49,6 @@
48
49
  "license": "MIT",
49
50
  "repository": {
50
51
  "type": "git",
51
- "url": "https://github.com/MOhhh-ok/canopy-i18n"
52
+ "url": "https://github.com/mohhh-ok/canopy-i18n"
52
53
  }
53
54
  }