canopy-i18n
3
总安装量
3
周安装量
#60600
全站排名
安装命令
npx skills add https://github.com/mohhh-ok/canopy-i18n --skill canopy-i18n
Agent 安装分布
opencode
3
gemini-cli
3
github-copilot
3
codex
3
kimi-cli
3
amp
3
Skill 文档
canopy-i18n â AI Code Generation Reference
A type-safe i18n library using the builder pattern. This reference helps AI assistants generate accurate code for this package.
Package Overview
- Type-safe: Compile-time detection of typos in locale keys via TypeScript inference
- Builder pattern: Define translations with method chaining
- Zero dependencies: Native TypeScript only
- ESM only: Requires
"type": "module"inpackage.json - Node.js 20+
Installation
npm install canopy-i18n
# or
pnpm add canopy-i18n
bun add canopy-i18n
package.json must include "type": "module":
{
"type": "module"
}
Core API
createI18n(locales)
Creates a builder instance. as const is required for type inference.
import { createI18n } from 'canopy-i18n';
// â
Correct: use as const
const builder = createI18n(['en', 'ja'] as const);
// â Wrong: without as const, type becomes string[] and type inference is lost
const builder = createI18n(['en', 'ja']);
- Argument:
readonly string[]â allowed locale keys - Returns:
ChainBuilder<Locales, {}>â a chain builder instance
.add<R, K>(entries)
Adds multiple static messages (string or custom type).
// Default (string type)
const builder = createI18n(['en', 'ja'] as const)
.add({
title: { en: 'Title', ja: 'ã¿ã¤ãã«' },
greeting: { en: 'Hello', ja: 'ããã«ã¡ã¯' },
});
// Custom return type (object)
type MenuItem = { label: string; url: string };
const menu = createI18n(['en', 'ja'] as const)
.add<MenuItem>({
home: {
en: { label: 'Home', url: '/en' },
ja: { label: 'Home', url: '/ja' },
},
});
- Type param
R: return value type (default:string) - Type param
K: key type for entries (usually omitted) - entries:
Record<K, Record<Locale, R>> - Returns: new
ChainBuilder(immutable)
.addTemplates<C, R, K>()(entries)
Curried API â two-step call required. Adds template functions that receive a context object of type C.
// â ï¸ Curried: two-step call ()() is mandatory
const builder = createI18n(['en', 'ja'] as const)
.addTemplates<{ name: string; age: number }>()({ // note: ()() two steps
greeting: {
en: (ctx) => `Hello, ${ctx.name}. You are ${ctx.age}.`,
ja: (ctx) => `ããã«ã¡ã¯ã${ctx.name}ããã${ctx.age}æ³ã§ãã`,
},
});
// Custom return type (JSX.Element)
const jsxBuilder = createI18n(['en', 'ja'] as const)
.addTemplates<{ name: string }, JSX.Element>()({
badge: {
en: ({ name }) => <strong>Welcome, {name}!</strong>,
ja: ({ name }) => <strong>ããããã{name}ããï¼</strong>,
},
});
- Type param
C: context object type (required) - Type param
R: return value type (default:string) - Type param
K: key type (usually omitted) - entries:
Record<K, Record<Locale, (ctx: C) => R>> - Returns: new
ChainBuilder(immutable)
.build(locale?)
Builds the final messages object.
const builder = createI18n(['en', 'ja'] as const)
.add({ title: { en: 'Title', ja: 'ã¿ã¤ãã«' } });
// With specific locale
const enMessages = builder.build('en');
const jaMessages = builder.build('ja');
// Without locale â defaults to first locale in array
const defaultMessages = builder.build(); // uses 'en'
// All messages are called as functions
console.log(enMessages.title()); // "Title"
console.log(jaMessages.title()); // "ã¿ã¤ãã«"
- Argument
locale: optional; defaults to first locale in array - Returns:
{ [key]: () => R }or{ [key]: (ctx: C) => R } - Immutable:
.build()does not mutate the builder â you can generate multiple locales from one builder
bindLocale(obj, locale)
Recursively traverses an object/array and calls .build(locale) on all ChainBuilder instances found. Used for the namespace pattern (split files).
import { bindLocale } from 'canopy-i18n';
const data = {
common: commonBuilder,
nested: {
user: userBuilder,
},
};
const messages = bindLocale(data, 'en');
console.log(messages.common.hello()); // "Hello"
console.log(messages.nested.user.welcome({ name: 'John' })); // "Welcome, John"
- Argument
obj: any object/array containingChainBuilderinstances - Argument
locale: locale string to apply - Returns: new structure with all builders resolved
Critical Gotchas
1. as const is required
// â
Correct
createI18n(['en', 'ja'] as const)
// â Type error â locale keys become string, inference breaks
createI18n(['en', 'ja'])
2. addTemplates is curried â two-step call
// â
Correct: ()() two steps
.addTemplates<{ name: string }>()({
key: { en: (ctx) => `Hello, ${ctx.name}` }
})
// â Wrong: one-step call causes type error
.addTemplates<{ name: string }>({
key: { en: (ctx) => `Hello, ${ctx.name}` }
})
3. .build() is immutable
const builder = createI18n(['en', 'ja'] as const).add({ ... });
// â
Multiple locales from one builder
const enMessages = builder.build('en');
const jaMessages = builder.build('ja');
4. ESM only
// Required in package.json
{ "type": "module" }
5. All messages must be called as functions
const m = builder.build('en');
// â
Call as a function
m.title()
m.greeting({ name: 'Alice' })
// â Do not access as property â it is a function object, not a string
m.title
Common Patterns
Basic String Messages
import { createI18n } from 'canopy-i18n';
const messages = createI18n(['en', 'ja'] as const)
.add({
title: { en: 'Title', ja: 'ã¿ã¤ãã«' },
greeting: { en: 'Hello', ja: 'ããã«ã¡ã¯' },
farewell: { en: 'Goodbye', ja: 'ããããªã' },
})
.build('en');
console.log(messages.title()); // "Title"
console.log(messages.greeting()); // "Hello"
Template Functions (Variable Interpolation)
import { createI18n } from 'canopy-i18n';
const messages = createI18n(['en', 'ja'] as const)
.addTemplates<{ name: string; age: number }>()({
profile: {
en: (ctx) => `Name: ${ctx.name}, Age: ${ctx.age}`,
ja: (ctx) => `åå: ${ctx.name}ãå¹´é½¢: ${ctx.age}æ³`,
},
})
.build('en');
console.log(messages.profile({ name: 'Taro', age: 25 }));
// "Name: Taro, Age: 25"
Mixing Static and Template Messages
import { createI18n } from 'canopy-i18n';
const messages = createI18n(['en', 'ja'] as const)
.add({
title: { en: 'Items', ja: 'ã¢ã¤ãã ' },
})
.addTemplates<{ count: number }>()({
count: {
en: (ctx) => `${ctx.count} items`,
ja: (ctx) => `${ctx.count}åã®ã¢ã¤ãã `,
},
})
.build('en');
console.log(messages.title()); // "Items"
console.log(messages.count({ count: 5 })); // "5 items"
Custom Return Type (Object)
import { createI18n } from 'canopy-i18n';
type MenuItem = { label: string; url: string; icon: string };
const menu = createI18n(['en', 'ja'] as const)
.add<MenuItem>({
home: {
en: { label: 'Home', url: '/en', icon: 'ð¡' },
ja: { label: 'ãã¼ã ', url: '/ja', icon: 'ð ' },
},
about: {
en: { label: 'About', url: '/en/about', icon: 'â¹ï¸' },
ja: { label: 'æ¦è¦', url: '/ja/about', icon: 'â¹ï¸' },
},
})
.build('en');
console.log(menu.home().label); // "Home"
console.log(menu.home().url); // "/en"
Custom Return Type (JSX)
import { createI18n } from 'canopy-i18n';
import type { JSX } from 'react';
const messages = createI18n(['en', 'ja'] as const)
.add<JSX.Element>({
badge: {
en: <span style={{ background: '#4caf50', color: 'white' }}>NEW</span>,
ja: <span style={{ background: '#ff4444', color: 'white' }}>æ°ç</span>,
},
})
.addTemplates<{ name: string }, JSX.Element>()({
greeting: {
en: ({ name }) => <strong>Welcome, {name}!</strong>,
ja: ({ name }) => <strong>ããããã{name}ããï¼</strong>,
},
})
.build('en');
const badge = messages.badge();
const greeting = messages.greeting({ name: 'Alice' });
Namespace Pattern (Split Files + bindLocale)
// i18n/locales.ts
export const LOCALES = ['en', 'ja'] as const;
export type Locale = (typeof LOCALES)[number];
// i18n/common.ts
import { createI18n } from 'canopy-i18n';
import { LOCALES } from './locales';
export const common = createI18n(LOCALES).add({
hello: { en: 'Hello', ja: 'ããã«ã¡ã¯' },
goodbye: { en: 'Goodbye', ja: 'ããããªã' },
});
// i18n/user.ts
import { createI18n } from 'canopy-i18n';
import { LOCALES } from './locales';
export const user = createI18n(LOCALES)
.addTemplates<{ name: string }>()({
welcome: {
en: (ctx) => `Welcome, ${ctx.name}`,
ja: (ctx) => `ããããã${ctx.name}ãã`,
},
});
// i18n/index.ts
export { common } from './common';
export { user } from './user';
// app.ts
import { bindLocale } from 'canopy-i18n';
import * as i18n from './i18n';
const messages = bindLocale(i18n, 'en');
console.log(messages.common.hello()); // "Hello"
console.log(messages.user.welcome({ name: 'John' })); // "Welcome, John"
Deep Nested Structures
import { createI18n, bindLocale } from 'canopy-i18n';
const structure = {
header: createI18n(['en', 'ja'] as const)
.add({ title: { en: 'Header', ja: 'ãããã¼' } }),
content: {
main: createI18n(['en', 'ja'] as const)
.add({ body: { en: 'Body', ja: 'æ¬æ' } }),
sidebar: createI18n(['en', 'ja'] as const)
.add({ widget: { en: 'Widget', ja: 'ã¦ã£ã¸ã§ãã' } }),
},
};
const localized = bindLocale(structure, 'en');
console.log(localized.header.title()); // "Header"
console.log(localized.content.main.body()); // "Body"
console.log(localized.content.sidebar.widget()); // "Widget"
React Integration
Locale Context
// LocaleContext.tsx
import { bindLocale } from 'canopy-i18n';
import { createContext, useContext, useState } from 'react';
type Locale = 'en' | 'ja';
type ContextType = {
locale: Locale;
setLocale: (locale: Locale) => void;
};
const LocaleContext = createContext<ContextType | undefined>(undefined);
export function LocaleProvider({ children }: { children: React.ReactNode }) {
const [locale, setLocale] = useState<Locale>('en');
return (
<LocaleContext.Provider value={{ locale, setLocale }}>
{children}
</LocaleContext.Provider>
);
}
export function useLocale() {
const ctx = useContext(LocaleContext);
if (!ctx) throw new Error('useLocale must be used within a LocaleProvider');
return ctx;
}
// Reactively applies bindLocale based on current locale
export function useBindLocale<T extends object>(msgsDef: T) {
const { locale } = useLocale();
return bindLocale(msgsDef, locale);
}
Usage in Components
// i18n.ts â export ChainBuilders (not yet built)
import { createI18n } from 'canopy-i18n';
const LOCALES = ['en', 'ja'] as const;
export const defineMessage = () => createI18n(LOCALES);
export const appI18n = defineMessage()
.add({
title: { en: 'My App', ja: 'ãã¤ã¢ããª' },
description: { en: 'Welcome!', ja: 'ããããï¼' },
})
.addTemplates<{ name: string }>()({
greeting: {
en: (ctx) => `Hello, ${ctx.name}!`,
ja: (ctx) => `ããã«ã¡ã¯ã${ctx.name}ããï¼`,
},
});
// App.tsx â apply locale with useBindLocale
import { useBindLocale } from './LocaleContext';
import { appI18n } from './i18n';
export default function App() {
const m = useBindLocale(appI18n);
return (
<div>
<h1>{m.title()}</h1>
<p>{m.description()}</p>
<p>{m.greeting({ name: 'Taro' })}</p>
</div>
);
}
Component-Local i18n (Colocation)
// ProfileCard.tsx â define and use i18n in the same file
import { createI18n } from 'canopy-i18n';
import type { JSX } from 'react';
import { useBindLocale } from './LocaleContext';
const profileI18n = createI18n(['en', 'ja'] as const)
.add({
title: { en: 'User Profile', ja: 'ã¦ã¼ã¶ã¼ãããã£ã¼ã«' },
editButton: { en: 'Edit Profile', ja: 'ãããã£ã¼ã«ç·¨é' },
})
.addTemplates<{ name: string }, JSX.Element>()({
greeting: {
en: ({ name }) => <strong>Welcome, {name}!</strong>,
ja: ({ name }) => <strong>ããããã{name}ããï¼</strong>,
},
});
export function ProfileCard({ name }: { name: string }) {
const m = useBindLocale(profileI18n);
return (
<div>
<h2>{m.title()}</h2>
<div>{m.greeting({ name })}</div>
<button>{m.editButton()}</button>
</div>
);
}
Language Switcher Component
// LanguageSwitcher.tsx
import { useLocale } from './LocaleContext';
export function LanguageSwitcher() {
const { locale, setLocale } = useLocale();
return (
<div>
<button onClick={() => setLocale('en')} disabled={locale === 'en'}>EN</button>
<button onClick={() => setLocale('ja')} disabled={locale === 'ja'}>JA</button>
</div>
);
}
Exports Reference
// Functions & Classes
export { createI18n } from 'canopy-i18n'; // create a builder
export { ChainBuilder } from 'canopy-i18n'; // builder class
export { I18nMessage } from 'canopy-i18n'; // message class
export { isI18nMessage } from 'canopy-i18n'; // type guard
export { bindLocale } from 'canopy-i18n'; // apply locale to nested structure
export { isChainBuilder } from 'canopy-i18n'; // type guard
// Types
export type { Template } from 'canopy-i18n'; // R | ((ctx: C) => R)
export type { LocalizedMessage } from 'canopy-i18n'; // built message function type
Type Details
// Template<C, R>: a static value or a function that receives context
type Template<C, R = string> = R | ((ctx: C) => R);
// LocalizedMessage<Ls, C, R>: the function type after build()
// - when C is void: () => R
// - when C is present: (ctx: C) => R
type LocalizedMessage<Ls, C, R = string> =
C extends void
? (() => R) & { __brand: "I18nMessage" }
: ((ctx: C) => R) & { __brand: "I18nTemplateMessage" };
Common Mistakes
| Mistake | Fix |
|---|---|
createI18n(['en', 'ja']) |
createI18n(['en', 'ja'] as const) |
.addTemplates<C>({ ... }) |
.addTemplates<C>()({ ... }) (two-step) |
messages.title |
messages.title() (call as function) |
CommonJS require() |
Use ESM import |
| Typo in locale key | TypeScript catches it at compile time |