10 KiB
i18n Translation Workflow Guide
This guide explains how to work with translations in the CommunityRule application. The app uses a hybrid approach combining globalized, shared UI elements with context-aware, localized content pages, making it easy for content creators and contributors to update text without modifying component code.
Overview
All UI text is stored in JSON files under messages/en/. The structure follows best practices:
- Page-specific content lives in
pages/(varies by page context) - Component defaults live in
components/(shared across pages) - Common strings live in
common.json(shared UI elements)
Components reference these translations using keys, allowing content to be edited independently of the codebase.
Directory Structure
messages/
en/
pages/
home.json # Home page specific content
learn.json # Learn page specific content
components/
heroBanner.json # Component defaults (aria-labels, alt texts)
numberedCards.json # Component defaults
askOrganizer.json # Component defaults
featureGrid.json # Component defaults
footer.json # Shared across pages
header.json # Shared across pages
common.json # Shared UI strings (buttons, links, labels)
navigation.json # Navigation items
metadata.json # Page metadata (title, description)
index.ts # Exports all messages
When to Use pages/ vs components/
Use pages/ for:
- Page-specific content: Titles, subtitles, descriptions that vary by page
- Context-aware text: Content that changes based on where the component is used
- User-facing content: All text that users see on a specific page
Example: The home page hero banner title "Collaborate" goes in pages/home.json, not components/heroBanner.json
Use components/ for:
- Component defaults: Aria-labels, alt text patterns, shared behavior text
- Shared across pages: Text that doesn't vary by page context
- Accessibility text: Aria-labels and alt texts that are component-level
Example: The hero banner image alt text "Hero illustration" stays in components/heroBanner.json because it's the same across all pages
Use common.json for:
- Shared UI strings: Buttons, links, labels used across multiple components
- Global strings: Text that appears in many places
Page-Specific Translations
For page-specific content, use the pages.* namespace pattern:
Server Components:
import messages from "../../messages/en/index";
import { getTranslation } from "../../lib/i18n/getTranslation";
export default function LearnPage() {
const t = (key: string) => getTranslation(messages, key);
const contentLockupData = {
title: t("pages.learn.contentLockup.title"),
subtitle: t("pages.learn.contentLockup.subtitle"),
};
return <ContentLockup {...contentLockupData} />;
}
Client Components:
"use client";
import { useTranslation } from "../../contexts/MessagesContext";
export default function MyComponent() {
const t = useTranslation("pages.home.heroBanner");
return <h1>{t("title")}</h1>;
}
Adding New Translation Keys
1. Identify the Component
Determine which component needs the translation. If it's a shared string (like a button label), add it to common.json. Otherwise, add it to the component-specific file.
2. Add the Key to the JSON File
Open the appropriate JSON file and add your translation key. Use descriptive, semantic keys:
Good:
{
"heroBanner": {
"title": "Collaborate",
"subtitle": "with clarity"
}
}
Bad:
{
"text1": "Collaborate",
"text2": "with clarity"
}
3. Use Nested Objects for Organization
Group related translations together:
{
"numberedCards": {
"title": "How CommunityRule works",
"buttons": {
"createCommunityRule": "Create CommunityRule",
"seeHowItWorks": "See how it works"
}
}
}
4. Update the Component or Page
For Page Components (Server Components):
import messages from "../../messages/en/index";
import { getTranslation } from "../../lib/i18n/getTranslation";
export default function MyPage() {
const t = (key: string) => getTranslation(messages, key);
// Use page-specific keys
const data = {
title: t("pages.home.heroBanner.title"),
subtitle: t("pages.home.heroBanner.subtitle"),
};
return <HeroBanner {...data} />;
}
For Client Components:
"use client";
import { useTranslation } from "../../contexts/MessagesContext";
export default function MyComponent() {
// For page-specific content
const t = useTranslation("pages.home.heroBanner");
return <h1>{t("title")}</h1>;
// For component defaults
const tDefault = useTranslation("heroBanner");
return <img alt={tDefault("imageAlt")} />;
}
Translation Key Naming Conventions
- Use camelCase for keys:
buttonText,ariaLabel - Use descriptive names:
createCommunityRulenotbtn1 - Group by component: Each component has its own namespace
- Use nested objects for related strings:
buttons.createCommunityRule - Include context in comments: Use
_commentfields for clarity
Example:
{
"_comment": "HeroBanner component translations",
"title": "Collaborate",
"subtitle": "with clarity",
"description": "Help your community make important decisions..."
}
Extracting Strings from Components
When migrating a component to use translations:
- Identify hardcoded strings in the component
- Create translation keys in the appropriate JSON file
- Replace hardcoded strings with
t("key.path")calls - Test the component to ensure translations load correctly
Example Migration
Before:
export default function HeroBanner() {
return (
<div>
<h1>Collaborate</h1>
<p>with clarity</p>
</div>
);
}
After:
"use client";
import { useTranslations } from "next-intl";
export default function HeroBanner() {
const t = useTranslations("heroBanner");
return (
<div>
<h1>{t("title")}</h1>
<p>{t("subtitle")}</p>
</div>
);
}
Adding a New Page
When creating a new page that needs translations:
- Create a page translation file:
messages/en/pages/about.json(for example) - Add page-specific content: All user-facing text for that page
- Import in index.ts: Add the import and export in
messages/en/index.ts - Use in page component: Use
t("pages.about.*")pattern in your page
Example:
// messages/en/pages/about.json
{
"hero": {
"title": "About Us",
"subtitle": "Learn more about our mission"
}
}
// app/about/page.tsx
const t = (key: string) => getTranslation(messages, key);
const title = t("pages.about.hero.title");
Adding New Languages (Future)
When adding support for a new language:
- Create a new locale directory:
messages/es/(for Spanish, for example) - Copy the English files as a starting point (including
pages/structure) - Translate all strings in the JSON files
- Test thoroughly to ensure all translations are present
Testing Translations
- Check for missing keys: Ensure all translation keys used in components exist in the JSON files
- Verify type safety: TypeScript will catch typos in translation keys at compile time
- Test in browser: Run the dev server and verify text displays correctly
- Check for fallbacks: Missing translations will show the key path (e.g.,
heroBanner.title)
Best Practices
For Content Creators
- Edit JSON files directly: No need to understand React or TypeScript
- Use descriptive comments: Add
_commentfields to explain context - Maintain consistency: Use the same terminology across components
- Test changes: Run the dev server to see your changes immediately
For Developers
- Use TypeScript: Translation keys are type-safe
- Namespace when possible: Use
useTranslations("namespace")for better organization - Server components first: Prefer server-side translations for better performance
- Extract incrementally: Migrate components one at a time
Common Patterns
Buttons and CTAs
{
"buttons": {
"createCommunityRule": "Create CommunityRule",
"seeHowItWorks": "See how it works"
}
}
Aria Labels
{
"ariaLabels": {
"followBluesky": "Follow us on Bluesky",
"followGitlab": "Follow us on GitLab"
}
}
Dynamic Content
For content that varies (like card text), use arrays or numbered keys:
{
"cards": {
"card1": { "text": "First step" },
"card2": { "text": "Second step" },
"card3": { "text": "Third step" }
}
}
Troubleshooting
Translation Key Not Found
If you see a key path like heroBanner.title instead of the text:
- Check the JSON file exists and has the key
- Verify the key path matches exactly (case-sensitive)
- Restart the dev server if you just added the key
TypeScript Errors
If TypeScript complains about translation keys:
- Ensure the key exists in the JSON file
- Check for typos in the key path
- Verify the namespace is correct if using
useTranslations("namespace")
Missing Translations
If text doesn't appear:
- Check the browser console for errors
- Verify the component is wrapped in
MessagesProvider(for client components) - Ensure
getTranslation()is called correctly in server components - Check if you're using the correct namespace (
pages.*vs component defaults)
Architecture: Hybrid Approach
This implementation follows the recognized best practice of combining:
- Globalized, shared UI elements: Component defaults in
components/(aria-labels, alt texts) - Context-aware, localized content pages: Page-specific content in
pages/(titles, descriptions)
This allows:
- Components to remain flexible and reusable
- Page content to be easily edited without code changes
- Clear separation between shared defaults and page-specific content
- Scalable structure for adding new pages
Resources
- Component defaults in
messages/en/components/ - Page-specific content in
messages/en/pages/ - Shared UI strings in
messages/en/common.json
Last Updated: January 2025
Maintained by: CommunityRule Development Team