A lightweight i18n library with fragment collocation pattern that works across frameworks and runtimes
npm install colocaleA lightweight i18n library that works across frameworks and JavaScript runtimes.
Inspired by GraphQL's fragment collocation pattern, each component can declaratively define the translation keys it needs. Designed to work with any component-based framework including React, Vue, and others, it excels in both client-side and server-side rendering environments.
- π― Colocation: Define translation keys alongside your components
- π Type-safe: Full TypeScript support with auto-generated types
- π¦ Lightweight: Zero dependencies, simple API
- π Pluralization: Built on Intl.PluralRules for proper plural handling
- β‘ Fast: Extract and send only the translations needed by components
- π Universal: Works in Node.js, browsers, edge runtimes, and any JavaScript environment
- π¨ Framework-agnostic: Compatible with React, Vue, and other component-based frameworks
Colocale is designed to work across various JavaScript runtimes and frameworks:
Runtimes:
- Node.js
- Browsers
- Edge runtimes (Cloudflare Workers, Vercel Edge, etc.)
- Any JavaScript runtime with Intl support
Frameworks:
- Works with any component-based framework (React, Vue, Svelte, etc.)
- Particularly effective with frameworks that support server-side rendering
- Examples available for React and Vue in the example/ directory
Whether you're building a client-side SPA, a server-rendered application, or a hybrid app, colocale adapts to your architecture.
Colocale intentionally does not provide automatic runtime fallback to a default language when translations are missing. This is a deliberate design choice, not a limitation.
Why no automatic fallback?
Traditional i18n libraries often fall back to a default language (like English) when translations are missing. While convenient during development, this approach has significant downsides:
- β Silent failures in production: Missing translations go unnoticed until users report them
- β Inconsistent user experience: Users see a mix of their language and the fallback language
- β No accountability: Developers aren't forced to ensure complete translations before deployment
Our approach: Fail fast at build time, not runtime
Instead of hiding problems at runtime, colocale ensures translation completeness at build/CI time:
- β
npx colocale check validates all translations before they reach production
- β
CI/CD integration catches missing translations in pull requests
- β
Type-safe keys prevent typos and invalid references at compile time
- β
Consistent user experience - all translations are complete, or the build fails
Integrate into your CI pipeline:
``yaml`.github/workflows/ci.yml
- name: Validate translations
run: npx colocale check messages
This design philosophy ensures that translation issues are caught early in the development process, not by your users in production. When you do need runtime behavior for truly missing keys (edge cases), the library returns the key name itself, making the issue immediately visible during testing.
Unlike many i18n libraries, colocale intentionally avoids using React Context, Vue's provide/inject, or similar dependency injection mechanisms. This design choice enables several key benefits:
π True Framework Agnosticism
- Works identically in React, Vue, Svelte, or vanilla JavaScript
- No framework-specific runtime dependencies
- Same API across all environments
π Explicit Dependencies
- Component translation requirements are clearly visible in code
- Easy to trace which translations a component tree needs
- Facilitates static analysis and tree-shaking
βοΈ Universal Compatibility
- Works seamlessly in server components, client components, and hybrid scenarios
- No issues with framework-specific boundaries (like Next.js Server/Client Component boundary)
- Runs in any JavaScript environment (Node.js, Deno, Bun, browsers)
π― Predictable Data Flow
- Translations flow explicitly through props, following standard component patterns
- No hidden dependencies through context
- Easier to test and debug
Yes, colocale requires passing messages through propsβthis is intentional! While "prop drilling" is often seen as an anti-pattern, for i18n it provides significant advantages:
β Why It Works for i18n:
1. Single prop: Only one messages object needs to be passed down
2. Stable data: Translations rarely change during runtime
3. Clear contract: Component interfaces explicitly show i18n dependency
4. Performance: No context re-renders or provider overhead
5. Flexibility: Components can be used anywhere without provider setup
π GraphQL Inspiration:
This pattern is inspired by GraphQL's fragment collocation, where data requirements are defined alongside components and aggregated up the tree. Just as GraphQL fragments make data dependencies explicit, colocale makes translation dependencies explicit.
colocale is ideal when you want:
- A framework-agnostic i18n solution that works everywhere
- Explicit, traceable translation dependencies
- To work with server-side rendering and modern meta-frameworks
- Type-safe translations with minimal setup
- A simple, predictable API without magic
If you prefer context-based solutions or need framework-specific features, consider alternatives like react-i18next or vue-i18n.
`bash`
npm install colocaleor
bun add colocale
Note: If you want to use the codegen command to generate TypeScript types, you'll need TypeScript installed in your project:
`bash`
npm install -D typescriptor
bun add -d typescript
colocale provides 2 subcommands:
`bashShow help
npx colocale --help
$3
The
check command is your safety net for preventing translation issues in production:- Validates translation file structure: Ensures proper flat structure and valid JSON
- Checks key consistency across locales: Detects missing or extra keys between languages
- Validates plural forms: Ensures all required plural forms (
_one, _other) are present
- Verifies placeholder syntax: Catches malformed placeholders before they break at runtime
- Cross-locale validation: When checking multiple locales, ensures all have identical key structuresAdd to your CI/CD pipeline to enforce translation completeness:
`yaml
.github/workflows/ci.yml (or similar)
- name: Check translation completeness
run: npx colocale check messages
# Build will fail if any translations are missing or inconsistent
`This ensures no missing translations slip into production, maintaining a consistent user experience across all supported languages.
Quick Start
> π‘ Looking for complete examples? Check out the working examples in the
example/ directory:
>
> - React example - React 18 with React Router
> - Vue example - Vue 3 with Vue Router$3
Create JSON files for each namespace using flat structure (level 0).
Important: Translation files now use flat structure only. Nested objects are not allowed. Use dot notation for grouping (e.g.,
"profile.name" instead of nested {"profile": {"name": "..."}}).`json
// messages/en/common.json
{
"submit": "Submit",
"cancel": "Cancel",
"itemCount_one": "1 item",
"itemCount_other": "{{count}} items"
}
``json
// messages/en/user.json
{
"profile.name": "Name",
"profile.email": "Email"
}
`$3
Colocale supports dynamic placeholders in your translation strings using the
{{variableName}} syntax.Translation file:
`json
// messages/en/common.json
{
"greeting": "Hello, {{name}}!",
"welcome": "Welcome {{user}}, you have {{count}} new messages"
}
`Usage:
`typescript
const t = createTranslator(messages, commonTranslations);t("greeting", { name: "Alice" });
// Output: "Hello, Alice!"
t("welcome", { user: "Bob", count: 5 });
// Output: "Welcome Bob, you have 5 new messages"
`Placeholder rules:
- Placeholders use double curly braces:
{{variableName}}
- Variable names must contain only alphanumeric characters and underscores
- Values are automatically converted to strings
- Placeholders work seamlessly with pluralization (see pluralization examples in translation files)$3
`bash
npx colocale codegen messages
`This automatically generates a type-safe
defineRequirement function from your translation files. The generated file (default: defineRequirement.ts) includes:- TypeScript type definitions for your translation structure
- A ready-to-use
defineRequirement function with full type inference$3
Create a dedicated file for translation requirements:
`typescript
// translations.ts (or wherever you organize your i18n code)
import defineRequirement from "@/defineRequirement"; // Generated by codegen
import { mergeRequirements } from "colocale";// Component-specific translation requirements with full type safety
export const userProfileTranslations = defineRequirement("user", [
"profile.name",
"profile.email",
]);
export const commonTranslations = defineRequirement("common", [
"submit",
"cancel",
]);
// Page-level merged requirements
export const userPageTranslations = mergeRequirements(
commonTranslations,
userProfileTranslations
);
`Note: The
defineRequirement function generated by codegen provides full type inference and compile-time validation automatically.Use in your components:
`typescript
// UserProfile.tsx (React) or UserProfile.vue (Vue)
import { createTranslator, type Messages } from "colocale";
import { userProfileTranslations } from "./translations";export default function UserProfile({ messages }: { messages: Messages }) {
const t = createTranslator(messages, userProfileTranslations);
return (
);
}
`$3
`typescript
// UserPage.tsx (React) or UserPage.vue (Vue)
import { createTranslator, type Messages } from "colocale";
import { commonTranslations, userPageTranslations } from "./translations";
import UserProfile from "./UserProfile";export default function UserPage({ messages }: { messages: Messages }) {
const t = createTranslator(messages, commonTranslations);
return (
);
}
`$3
Colocale requires translations to be organized in a locale-grouped format. How you load translations depends on your framework and architecture:
Basic structure:
`typescript
// Compose translations into locale-grouped structure
const allMessages = {
ja: {
common: jaCommonTranslations,
user: jaUserTranslations,
},
en: {
common: enCommonTranslations,
user: enUserTranslations,
},
};// Use pickMessages to extract only the needed translations for a specific locale
import { pickMessages } from "colocale";
const messages = pickMessages(
allMessages,
userPageTranslations,
locale // e.g., "ja" or "en"
);
`Translation file structure:
`
messages/
βββ ja/
β βββ common.json
β βββ user.json
βββ en/
βββ common.json
βββ user.json
``json
// messages/ja/common.json
{
"submit": "ιδΏ‘",
"cancel": "γγ£γ³γ»γ«"
}
``json
// messages/en/common.json
{
"submit": "Submit",
"cancel": "Cancel"
}
`example/ directory for complete implementations:- React example: Client-side application with static imports for translations
- Vue example: Client-side application with dynamic imports for translations
Using with Next.js App Router
When using colocale with Next.js App Router, you can take advantage of server-side rendering for optimal performance. This pattern applies to any server-rendering framework, but Next.js is shown as a practical example.
$3
Separate translation requirements from Client Components to avoid bundler issues:
1. Create a dedicated
translations.ts file (without 'use client'):`typescript
// app/users/translations.ts
import defineRequirement from "@/defineRequirement";
import { mergeRequirements } from "colocale";export const userProfileTranslations = defineRequirement("user", [
"profile.name",
"profile.email",
]);
export const commonTranslations = defineRequirement("common", [
"submit",
"cancel",
]);
export const userPageTranslations = mergeRequirements(
commonTranslations,
userProfileTranslations
);
`2. Use in Client Components:
`typescript
// components/UserProfile.tsx
"use client";
import { createTranslator, type Messages } from "colocale";
import { userProfileTranslations } from "../app/users/translations";export default function UserProfile({ messages }: { messages: Messages }) {
const t = createTranslator(messages, userProfileTranslations);
return (
);
}
`β οΈ Why separate files? If you export translation requirements from a Client Component (with
'use client'), Next.js's bundler creates proxy functions instead of the actual values, breaking mergeRequirements and type safety.$3
Translations must be organized in locale-grouped format. Import translation files per locale and namespace, then compose them:
`typescript
// app/[locale]/users/page.tsx
import { pickMessages } from "colocale";
import { userPageTranslations } from "./translations";
import UserPage from "./UserPage";// Import translations per locale and namespace (static imports)
import jaCommonTranslations from "@/messages/ja/common.json";
import jaUserTranslations from "@/messages/ja/user.json";
import enCommonTranslations from "@/messages/en/common.json";
import enUserTranslations from "@/messages/en/user.json";
export default async function Page({ params }: { params: { locale: string } }) {
// Compose into locale-grouped structure
const allMessages = {
ja: {
common: jaCommonTranslations,
user: jaUserTranslations,
},
en: {
common: enCommonTranslations,
user: enUserTranslations,
},
};
// pickMessages filters by locale and extracts only the needed translations
const messages = pickMessages(
allMessages,
userPageTranslations,
params.locale
);
return ;
}
`For larger applications, you can use dynamic imports:
`typescript
// app/[locale]/users/page.tsx
import { pickMessages } from "colocale";
import { userPageTranslations } from "./translations";
import UserPage from "./UserPage";export default async function Page({ params }: { params: { locale: string } }) {
// Extract required namespaces from translation requirements
const namespaces = userPageTranslations.map((req) => req.namespace);
// Remove duplicates to avoid importing the same file multiple times
const uniqueNamespaces = Array.from(new Set(namespaces));
// Dynamically import only the needed locale's translations
const translations = await Promise.all(
uniqueNamespaces.map(async (namespace) => ({
namespace,
data: (
await import(
@/messages/${params.locale}/${namespace}.json)
).default,
}))
); // Compose into locale-grouped structure
const allMessages = {
[params.locale]: Object.fromEntries(
translations.map(({ namespace, data }) => [namespace, data])
),
};
// pickMessages filters to the specified locale
const messages = pickMessages(
allMessages,
userPageTranslations,
params.locale
);
return ;
}
`$3
- β
DO create a separate
translations.ts file (without 'use client') for translation requirements
- β
DO import translation requirements from this shared file in both Server and Client Components
- β
DO colocate translations.ts with the components that use them (e.g., per page or feature folder)
- β DON'T export translation requirements from files with 'use client' directive
- β DON'T define translation requirements inside Client Components if they need to be used in Server ComponentsAPI Reference
$3
Extracts only the needed translations from locale-grouped translation files.
`typescript
function pickMessages(
allMessages: LocaleTranslations,
requirements: TranslationRequirement[] | TranslationRequirement,
locale: Locale
): Messages;
`Parameters:
-
allMessages: Object containing translations grouped by locale: { [locale]: { [namespace]: { [key]: translation } } }
- requirements: Translation requirement(s) defining which keys to extract
- locale: Locale identifier (see Locale type) - used for filtering translations and proper pluralization with Intl.PluralRulesLocale type: The
Locale type provides autocomplete for supported locale codes ("en", "ja") while still accepting any BCP 47 language tag as a string.Automatic plural extraction: When you specify a base key (e.g.,
"itemCount"), keys with _one, _other suffixes are automatically extracted based on Intl.PluralRules.$3
Creates a translation function bound to a specific namespace from a TranslationRequirement.
`typescript
function createTranslator>(
messages: Messages,
requirement: R
): ConstrainedTranslatorFunction;
`Key constraint: The returned translator function is constrained to only accept keys defined in the
TranslationRequirement.$3
Merges multiple translation requirements into a single array.
`typescript
function mergeRequirements(
...requirements: TranslationRequirement[]
): TranslationRequirement[];
`$3
Helper function to create a TranslationRequirement with compile-time type validation.
Generated by codegen (Recommended):
The
codegen command generates a type-safe defineRequirement function that automatically validates namespaces and keys:`bash
npx colocale codegen messages # Generates defineRequirement.ts
``typescript
import defineRequirement from "./defineRequirement"; // Generated file// β
Full type safety with auto-completion
const req = defineRequirement("common", ["submit", "cancel"]);
// β Compile error - namespace doesn't exist
const req = defineRequirement("invalid", ["key"]);
// β Compile error - key doesn't exist in namespace
const req = defineRequirement("common", ["invalid"]);
`Manual usage (without codegen):
The recommended approach is to use the
codegen command to generate the type-safe defineRequirement function. If you need to create translation requirements manually without type safety, you can create them directly:`typescript
import type { TranslationRequirement } from "colocale";// Manually create a translation requirement (no compile-time type safety)
const req: TranslationRequirement = {
namespace: "common",
keys: ["submit", "cancel"],
};
`Note: Manual usage does not provide compile-time validation of namespaces and keys. Use the
codegen` command for full type safety.MIT