canopy-i18n
v0.1.1
Published
A tiny, type-safe i18n helper
Maintainers
Readme
canopy-i18n
A tiny, type-safe i18n helper for building localized messages and applying a locale across nested data structures.
Features
- Type-safe locales: Compile-time safety for allowed locale keys.
- Per-message fallback: Each message knows its default and fallback locale.
- Template or string: Use plain strings or
(ctx) => stringtemplates. - Flexible templating: Since templates are plain functions, you can freely use JavaScript template literals, conditionals, helpers, or any formatting library. This library does not provide a tagged template literal API.
- Deep locale application: Switch locale across entire object/array trees.
Installation
npm install canopy-i18n
# or
pnpm add canopy-i18n
# or
yarn add canopy-i18nQuick start
import { createI18n, applyLocale } from 'canopy-i18n';
// 1) Declare allowed locales and fallback
const defineMessage = createI18n(['ja', 'en'] as const, 'ja');
// 2) Define messages
const title = defineMessage({
ja: 'タイトルテスト',
en: 'Title Test',
});
const msg = defineMessage<{ name: string; age: number }>({
ja: c => `こんにちは、${c.name}さん。あなたは${c.age}歳です。`,
en: c => `Hello, ${c.name}. You are ${c.age} years old.`,
});
// 3) Compose nested data structures
const data = {
title,
nested: {
hello: defineMessage({ ja: 'こんにちは', en: 'Hello' }),
},
};
// 4) Apply locale across the tree
const localized = applyLocale(data, 'en');
console.log(localized.title.render()); // "Title Test"
console.log(localized.nested.hello.render()); // "Hello"
console.log(msg.setLocale('en').render({ name: 'Tanaka', age: 20 }));API
createI18n(locales, fallbackLocale)
Returns a defineMessage function to create localized messages.
- locales:
readonly string[]— Allowed locale keys (e.g.['ja', 'en'] as const). - fallbackLocale: fallback locale when the active locale value is missing. New messages start with this locale active.
Overloads:
defineMessage<Record<L[number], string>>() -> I18nMessage<L, void>defineMessage<Record<L[number], Template<C>>>() -> I18nMessage<L, C>
I18nMessage<L, C>
Represents a single localized message.
- properties
locales: Llocale: L[number](getter)fallbackLocale: L[number](getter)data: Record<L[number], Template<C>>
- methods
setLocale(locale: L[number]): thissetFallbackLocale(locale: L[number]): thisrender(ctx?: C): string— If the value for the active locale is a function, it’s invoked withctx; otherwise the string is returned. Falls back tofallbackLocaleif needed.
applyLocale(obj, locale)
Recursively traverses arrays/objects and sets the given locale on all I18nMessage instances encountered.
- Returns a new container (arrays/objects are cloned), but reuses the same message instances after updating their locale.
Types
export type Template<C> = string | ((ctx: C) => string);Exports
export { I18nMessage, isI18nMessage } from 'canopy-i18n';
export { createI18n } from 'canopy-i18n';
export { applyLocale } from 'canopy-i18n';
export type { Template } from 'canopy-i18n';
export type { LocalizedMessage } from 'canopy-i18n';Notes
- CommonJS build (
main: dist/index.js) with TypeScript type declarations (types: dist/index.d.ts). - Works in Node or bundlers; recommended usage is TypeScript/ESM import via your build tool.
- License: MIT.
- Not a tagged template library: you write plain functions (examples use JS template literals inside those functions).
Split files example (namespace import)
Import all message exports as a namespace and set the locale across the whole tree.
// messages.ts
import { createI18n } from 'canopy-i18n';
const defineMessage = createI18n(['ja', 'en'] as const, 'ja');
export const title = defineMessage({
ja: 'タイトルテスト',
en: 'Title Test',
});
export const msg = defineMessage<{ name: string; age: number }>({
ja: c => `こんにちは、${c.name}さん。あなたは${c.age}歳です。`,
en: c => `Hello, ${c.name}. You are ${c.age} years old.`,
});// usage.ts
import * as messages from './messages';
import { applyLocale } from 'canopy-i18n';
const m = applyLocale(messages, 'en');
console.log(m.title.render()); // "Title Test"
console.log(m.msg.render({ name: 'Tanaka', age: 20 }));Multi-file structure
// i18n/defineMessage.ts
import { createI18n } from 'canopy-i18n';
export const defineMessage = createI18n(['ja', 'en'] as const, 'ja');// i18n/messages/common.ts
import { defineMessage } from '../defineMessage';
export const hello = defineMessage({ ja: 'こんにちは', en: 'Hello' });// i18n/messages/home.ts
import { defineMessage } from '../defineMessage';
export const title = defineMessage({ ja: 'タイトル', en: 'Title' });// i18n/messages/index.ts
export * as common from './common';
export * as home from './home';// usage.ts
import * as msgs from './i18n/messages';
import { applyLocale } from 'canopy-i18n';
const m = applyLocale(msgs, 'en');
console.log(m.common.hello.render()); // "Hello"
console.log(m.home.title.render()); // "Title"Note: Module namespace objects are read-only; applyLocale returns a cloned plain object while updating each I18nMessage instance's locale in place.
Example: Next.js App Router
An example Next.js App Router project lives under examples/next-app.
- Server-side usage:
/{locale}/serverrenders messages usingapplyLocalein a server component - Client-side usage:
/{locale}/clientrenders messages using hooks (useLocale,useApplyLocale)
How to run:
git clone https://github.com/mohhh-ok/canopy-i18n
cd canopy-i18n/examples/next-app
pnpm install
pnpm devOpen http://localhost:3000 and you will be redirected to /{locale} based on Accept-Language.
