packages/canopy-i18n/skills/SKILL.md
Use this skill when writing code that uses the canopy-i18n package — a type-safe, zero-dependency i18n library with a builder pattern API. Covers createI18n, add (static and template), build, bindLocale, React integration, and common gotchas like required `as const`.
npx skillsauth add mohhh-ok/canopy-i18n canopy-i18nInstall this skill globally with one command. Works with Claude Code, Cursor, and Windsurf.
3 of 9 scanners reported clean
Some scanners were skipped, did not run, or reported a non-clean status. Review each row below.
A type-safe i18n library using the builder pattern. This reference helps AI assistants generate accurate code for this package.
"type": "module" in package.jsonnpm install canopy-i18n
package.json must include "type": "module".
createI18n(locales)Creates a builder. as const is required for type inference.
import { createI18n } from 'canopy-i18n';
const builder = createI18n(['en', 'ja'] as const);
.add(entries)Adds messages. Each entry can be a static locale record or a template function. Static and template can be mixed in a single .add().
const builder = createI18n(['en', 'ja'] as const)
.add({
title: { en: 'Title', ja: 'タイトル' },
greeting: (ctx: { name: string; age: number }) => ({
en: `Hello, ${ctx.name}. You are ${ctx.age}.`,
ja: `こんにちは、${ctx.name}さん。${ctx.age}歳です。`,
}),
});
Returns a new ChainBuilder (immutable).
.build(locale)Builds the final messages object. Does not mutate the builder — you can build multiple locales from one builder. All messages are called as functions.
const enMessages = builder.build('en');
console.log(enMessages.title()); // "Title"
console.log(enMessages.greeting({ name: 'Taro', age: 25 })); // "Hello, Taro. You are 25."
bindLocale(obj, locale)Recursively traverses an object/array and calls .build(locale) on every ChainBuilder it finds. Used for the namespace pattern.
import { bindLocale } from 'canopy-i18n';
const data = { common: commonBuilder, user: userBuilder };
const messages = bindLocale(data, 'en');
console.log(messages.common.hello());
| Mistake | Fix |
|---------|-----|
| createI18n(['en', 'ja']) | createI18n(['en', 'ja'] as const) — without as const, locale keys become string and inference breaks |
| messages.title | messages.title() — all messages are functions, not strings |
| Mutating builder via build() | .build() is immutable; build multiple locales from one builder |
| CommonJS require() | ESM only; use import |
// i18n/locales.ts
export const LOCALES = ['en', 'ja'] as const;
// i18n/common.ts
import { createI18n } from 'canopy-i18n';
import { LOCALES } from './locales';
export const common = createI18n(LOCALES).add({
hello: { en: 'Hello', ja: 'こんにちは' },
});
// i18n/user.ts
export const user = createI18n(LOCALES).add({
welcome: (ctx: { name: string }) => ({
en: `Welcome, ${ctx.name}`,
ja: `ようこそ、${ctx.name}さん`,
}),
});
// app.ts
import { bindLocale } from 'canopy-i18n';
import * as i18n from './i18n';
const messages = bindLocale(i18n, 'en');
console.log(messages.common.hello());
console.log(messages.user.welcome({ name: 'John' }));
canopy-i18n/react exposes createI18nReact(LOCALES), returning a Provider, hooks, and a pre-bound i18n shorthand.
// i18n.ts
import { createI18nReact } from 'canopy-i18n/react';
export const LOCALES = ['en', 'ja'] as const;
export const { i18n, LocaleProvider, useLocale, useBindLocale } =
createI18nReact(LOCALES);
// `i18n(...)` is `ChainBuilder.add(...)` pre-bound to LOCALES.
export const appI18n = i18n({
title: { en: 'My App', ja: 'マイアプリ' },
greeting: (ctx: { name: string }) => ({
en: `Hello, ${ctx.name}!`,
ja: `こんにちは、${ctx.name}さん!`,
}),
});
Uncontrolled (in-memory, no persistence):
<LocaleProvider defaultLocale="en">
<App />
</LocaleProvider>
Controlled (locale lives outside React):
<LocaleProvider locale={currentLocale} onLocaleChange={setCurrentLocale}>
<App />
</LocaleProvider>
Source-driven (factory option useLocaleSource). The Provider reads locale from the hook on every render; setLocale calls onLocaleChange.
export const { LocaleProvider, useLocale } = createI18nReact(LOCALES, {
useLocaleSource: () => useMyStore((s) => s.locale),
onLocaleChange: (l) => useMyStore.getState().setLocale(l),
});
<LocaleProvider><App /></LocaleProvider>
Ready-made factories for common sources. Each returns the same shape as createI18nReact and operates in source-driven mode (<LocaleProvider> with no props).
import {
createHashI18nReact, // URL hash (#ja)
createSearchI18nReact, // URL search param (?lang=ja)
createPathnameI18nReact, // URL pathname prefix (/ja/...)
createStorageI18nReact, // localStorage
createCookieI18nReact, // Cookie
} from 'canopy-i18n/react';
export const { LocaleProvider, useLocale, useBindLocale } =
createHashI18nReact(LOCALES);
Options:
createSearchI18nReact(LOCALES, { param }) — defaults to langcreatePathnameI18nReact(LOCALES, { basePath }) — defaults to ""createStorageI18nReact(LOCALES, { key }) — defaults to canopy-i18n-localecreateCookieI18nReact(LOCALES, { key, maxAge, path, sameSite }) — defaults to canopy-i18n-locale / 1 year / / / Laximport { appI18n, useBindLocale, useLocale } from './i18n';
export default function App() {
const m = useBindLocale({ appI18n });
const { locale, setLocale } = useLocale();
return (
<div>
<h1>{m.appI18n.title()}</h1>
<p>{m.appI18n.greeting({ name: 'Taro' })}</p>
<button onClick={() => setLocale(locale === 'en' ? 'ja' : 'en')}>{locale}</button>
</div>
);
}
useBindLocale(msgsDef) is memoized per (msgsDef, locale). The Locale of every nested ChainBuilder must match the Provider's LOCALES; mismatches fail at compile time.
React is a peerDependency (>=18).
canopy-i18n/ai provides a runtime translator with a pluggable adapter (bring any AI backend). Static strings only — template functions are not translated. See README for details.
import { createAITranslator, memoryCache, openAIAdapter } from 'canopy-i18n/ai';
const translator = createAITranslator({
// Built-in adapters: openAIAdapter / anthropicAdapter / geminiAdapter
// (fetch-based; model & apiKey required; baseURL/instructions optional).
// Or implement AIAdapter yourself: { async translate({ texts, from, to }) {...} }
// (`from` is undefined when the source language is unknown — auto-detect it)
adapter: openAIAdapter({ model: 'gpt-4o-mini', apiKey: process.env.OPENAI_API_KEY! }),
sourceLocale: 'ja', // default `from` (optional; required for completeEntries)
cache: memoryCache(), // optional; custom { get, set } for DB persistence
// onError: 'fallback' // default: return original text on failure ('throw' to propagate)
});
// Custom adapters can reuse the built-in prompt logic:
// buildTranslatePrompt(request, { instructions }) / parseTranslatedTexts(raw, expected)
// Dynamic texts (e.g. user input) — cached, deduplicated, batched
await translator.translate(userInput, { to: 'en' }); // from = sourceLocale
await translator.translate(userInput, { to: 'en', from: 'fr' });
// Without sourceLocale and `from`, the adapter auto-detects the source language
// Fill missing locales of entries, then pass to ChainBuilder.add()
const entries = await translator.completeEntries(['ja', 'en'] as const, {
title: { ja: 'タイトル' }, // en is AI-translated; existing values are kept
});
const messages = createI18n(['ja', 'en'] as const).add(entries).build('en');
// Core
export { createI18n, ChainBuilder, bindLocale } from 'canopy-i18n';
export { I18nMessage, isI18nMessage, isChainBuilder } from 'canopy-i18n';
export type { Template, LocalizedMessage } from 'canopy-i18n';
// React subpath
export {
createI18nReact,
createHashI18nReact,
createSearchI18nReact,
createPathnameI18nReact,
createStorageI18nReact,
createCookieI18nReact,
} from 'canopy-i18n/react';
// AI subpath
export { createAITranslator, AITranslator, memoryCache } from 'canopy-i18n/ai';
export { openAIAdapter, anthropicAdapter, geminiAdapter } from 'canopy-i18n/ai';
export { buildTranslatePrompt, parseTranslatedTexts } from 'canopy-i18n/ai';
export type { AIAdapter, TranslationCache } from 'canopy-i18n/ai';
development
Use this skill when writing code that uses the canopy-i18n package — a type-safe, zero-dependency i18n library with a builder pattern API. Covers createI18n, add (static and template), build, bindLocale, React integration, and common gotchas like required `as const`.
development
Maintainer-only workflow for handling GitHub Secret Scanning alerts on OpenClaw. Use when Codex needs to triage, redact, clean up, and resolve secret leakage found in issue comments, issue bodies, PR comments, or other GitHub content.
development
Maintainer workflow for OpenClaw releases, prereleases, changelog release notes, and publish validation. Use when Codex needs to prepare or verify stable or beta release steps, align version naming, assemble release notes, check release auth requirements, or validate publish-time commands and artifacts.
development
Run, watch, debug, and extend OpenClaw QA testing with qa-lab and qa-channel. Use when Codex needs to execute the repo-backed QA suite, inspect live QA artifacts, debug failing scenarios, add new QA scenarios, or explain the OpenClaw QA workflow. Prefer the live OpenAI lane with regular openai/gpt-5.4 in fast mode; do not use gpt-5.4-pro or gpt-5.4-mini unless the user explicitly overrides that policy.