auto-claude: subtask-1-3 - Document how to add new translation keys
Added comprehensive documentation for adding translation keys: - Step-by-step guide with namespace identification - Detailed JSON structure explanation with real examples - Namespace organization patterns (meta, common, navigation, pages, etc.) - Code examples using useTranslations() hook - Advanced usage: dynamic keys, string interpolation, rich content - Best practices for key naming and structure consistency - Examples for arrays, objects, and nested translations Co-Authored-By: Claude Sonnet 4.5 <noreply@anthropic.com>
This commit is contained in:
+445
-10
@@ -51,30 +51,465 @@ export default function MyComponent() {
|
||||
|
||||
### Adding a New Translation Key
|
||||
|
||||
1. Open all three locale files:
|
||||
- `src/messages/de.json`
|
||||
- `src/messages/en.json`
|
||||
- `src/messages/sr.json`
|
||||
#### Step-by-Step Guide
|
||||
|
||||
2. Add your key to the same namespace in each file:
|
||||
Follow this process to add new translation keys to the application:
|
||||
|
||||
##### 1. Identify the Appropriate Namespace
|
||||
|
||||
Before adding a new translation key, determine where it should live based on its purpose:
|
||||
|
||||
- **`meta`** - Site metadata, author information, SEO keywords
|
||||
- **`common`** - Shared UI elements (buttons, navigation, errors, cookies)
|
||||
- **`navigation`** - All navigation-related text (main menu, footer links, mobile menu)
|
||||
- **`footer`** - Footer content (sections, legal links, copyright)
|
||||
- **`pages`** - Page-specific content organized by route
|
||||
- `pages.home` - Homepage content
|
||||
- `pages.about` - About page content
|
||||
- `pages.contact` - Contact page content
|
||||
- `pages.privacy` - Privacy policy content
|
||||
- `pages.terms` - Terms and conditions content
|
||||
- `pages.notfound` - 404 page content
|
||||
- **`portfolio`** - Portfolio page content and filters
|
||||
- **`blog`** - Blog page content and UI elements
|
||||
- **`login`** - Login page content
|
||||
- **`dashboard`** - Dashboard content and metrics
|
||||
|
||||
**Best Practice:** Use `common` for elements that appear across multiple pages. Use page-specific namespaces for content unique to a single page.
|
||||
|
||||
##### 2. Open All Three Translation Files
|
||||
|
||||
You must update all three locale files to maintain consistency:
|
||||
|
||||
```bash
|
||||
# Open in your editor:
|
||||
src/messages/de.json # German (default)
|
||||
src/messages/en.json # English
|
||||
src/messages/sr.json # Serbian
|
||||
```
|
||||
|
||||
##### 3. Add the Translation Key
|
||||
|
||||
Add your key to the **exact same location** in each file. The JSON structure must be identical across all locales.
|
||||
|
||||
**Example: Adding a new action button**
|
||||
|
||||
```json
|
||||
// de.json
|
||||
{
|
||||
"common": {
|
||||
"actions": {
|
||||
"newAction": "Neue Aktion" // de
|
||||
"newAction": "New Action" // en
|
||||
"newAction": "Nova Akcija" // sr
|
||||
"readMore": "Weiterlesen",
|
||||
"viewMore": "Mehr anzeigen",
|
||||
"viewAll": "Alle anzeigen",
|
||||
"download": "Herunterladen" // ← New key
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// en.json
|
||||
{
|
||||
"common": {
|
||||
"actions": {
|
||||
"readMore": "Read More",
|
||||
"viewMore": "View More",
|
||||
"viewAll": "View All",
|
||||
"download": "Download" // ← New key
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// sr.json
|
||||
{
|
||||
"common": {
|
||||
"actions": {
|
||||
"readMore": "Procitaj vise",
|
||||
"viewMore": "Prikazi vise",
|
||||
"viewAll": "Prikazi sve",
|
||||
"download": "Preuzmi" // ← New key
|
||||
}
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
3. Use it in your component:
|
||||
**Example: Adding a new page section**
|
||||
|
||||
```json
|
||||
// de.json
|
||||
{
|
||||
"pages": {
|
||||
"services": { // ← New namespace
|
||||
"hero": {
|
||||
"title": "Unsere Dienstleistungen",
|
||||
"subtitle": "Maßgeschneiderte Lösungen für Ihr Unternehmen"
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// en.json
|
||||
{
|
||||
"pages": {
|
||||
"services": { // ← New namespace
|
||||
"hero": {
|
||||
"title": "Our Services",
|
||||
"subtitle": "Tailored solutions for your business"
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// sr.json
|
||||
{
|
||||
"pages": {
|
||||
"services": { // ← New namespace
|
||||
"hero": {
|
||||
"title": "Naše usluge",
|
||||
"subtitle": "Prilagođena rešenja za vaše poslovanje"
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
##### 4. Use the Translation in Your Component
|
||||
|
||||
Import the `useTranslations` hook and specify the namespace:
|
||||
|
||||
```tsx
|
||||
import { useTranslations } from 'next-intl';
|
||||
|
||||
export default function MyComponent() {
|
||||
// Option 1: Access top-level namespace
|
||||
const t = useTranslations('common.actions');
|
||||
return <button>{t('newAction')}</button>;
|
||||
|
||||
return (
|
||||
<button className="btn-primary">
|
||||
{t('download')}
|
||||
</button>
|
||||
);
|
||||
}
|
||||
```
|
||||
|
||||
**Multiple namespaces in one component:**
|
||||
|
||||
```tsx
|
||||
import { useTranslations } from 'next-intl';
|
||||
|
||||
export default function ServicesPage() {
|
||||
const tHero = useTranslations('pages.services.hero');
|
||||
const tCommon = useTranslations('common.actions');
|
||||
|
||||
return (
|
||||
<div>
|
||||
<h1>{tHero('title')}</h1>
|
||||
<p>{tHero('subtitle')}</p>
|
||||
<button>{tCommon('readMore')}</button>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
```
|
||||
|
||||
##### 5. Verify Your Changes
|
||||
|
||||
1. **Build the application** to ensure no TypeScript errors:
|
||||
```bash
|
||||
npm run build
|
||||
```
|
||||
|
||||
2. **Test in all three languages:**
|
||||
- German: `http://localhost:3000/de`
|
||||
- English: `http://localhost:3000/en`
|
||||
- Serbian: `http://localhost:3000/sr`
|
||||
|
||||
3. **Check for missing translations** - The browser console will warn if a key is missing
|
||||
|
||||
#### JSON Structure & Organization
|
||||
|
||||
##### Hierarchical Namespace Pattern
|
||||
|
||||
Translation files use a **deeply nested JSON structure** to organize translations logically:
|
||||
|
||||
```json
|
||||
{
|
||||
"namespace": { // Top-level category
|
||||
"subnamespace": { // Feature or section
|
||||
"key": "value", // Leaf node (actual translation)
|
||||
"nested": {
|
||||
"key": "value" // Deeper nesting allowed
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
**Real example from the codebase:**
|
||||
|
||||
```json
|
||||
{
|
||||
"pages": { // Namespace: Page content
|
||||
"home": { // Subnamespace: Homepage
|
||||
"hero": { // Section: Hero section
|
||||
"title": "FULLSTACK DEVELOPER",
|
||||
"subtitle": "AI AGENTS | VOICE AI",
|
||||
"navigation": { // Nested object
|
||||
"experience": "EXPERIENCE",
|
||||
"skills": "SKILLS"
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
**Access in code:**
|
||||
|
||||
```tsx
|
||||
const t = useTranslations('pages.home.hero');
|
||||
console.log(t('title')); // "FULLSTACK DEVELOPER"
|
||||
console.log(t('navigation.experience')); // "EXPERIENCE"
|
||||
```
|
||||
|
||||
##### Special Data Types
|
||||
|
||||
**Arrays** - For lists of items:
|
||||
|
||||
```json
|
||||
{
|
||||
"meta": {
|
||||
"site": {
|
||||
"keywords": [
|
||||
"Fullstack Developer",
|
||||
"AI Developer Germany",
|
||||
"Process Automation"
|
||||
]
|
||||
}
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
**Usage:**
|
||||
|
||||
```tsx
|
||||
const t = useTranslations('meta.site');
|
||||
const keywords = t.raw('keywords'); // Returns array
|
||||
```
|
||||
|
||||
**Objects** - For structured data:
|
||||
|
||||
```json
|
||||
{
|
||||
"pages": {
|
||||
"home": {
|
||||
"experience": {
|
||||
"positions": [
|
||||
{
|
||||
"role": "Process Automation Specialist",
|
||||
"company": "Everlast Consulting GmbH",
|
||||
"period": "12/2024 - PRESENT",
|
||||
"highlights": [
|
||||
"Development of AI agents",
|
||||
"Web scraping solutions"
|
||||
]
|
||||
}
|
||||
]
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
**Usage:**
|
||||
|
||||
```tsx
|
||||
const t = useTranslations('pages.home.experience');
|
||||
const positions = t.raw('positions'); // Returns array of objects
|
||||
|
||||
positions.map((position) => (
|
||||
<div key={position.role}>
|
||||
<h3>{position.role}</h3>
|
||||
<p>{position.company}</p>
|
||||
<span>{position.period}</span>
|
||||
<ul>
|
||||
{position.highlights.map((highlight) => (
|
||||
<li key={highlight}>{highlight}</li>
|
||||
))}
|
||||
</ul>
|
||||
</div>
|
||||
));
|
||||
```
|
||||
|
||||
#### Advanced Usage Examples
|
||||
|
||||
##### Dynamic Translation Keys
|
||||
|
||||
Access translations dynamically based on variables:
|
||||
|
||||
```tsx
|
||||
import { useTranslations } from 'next-intl';
|
||||
|
||||
export default function CategoryFilter({ categories }: { categories: string[] }) {
|
||||
const t = useTranslations('portfolio.filters.categories');
|
||||
|
||||
return (
|
||||
<div>
|
||||
{categories.map((category) => (
|
||||
<button key={category}>
|
||||
{t(category as any)} // Dynamic key lookup
|
||||
</button>
|
||||
))}
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
// If categories = ["Data Science", "AI Development"]
|
||||
// Output: "Data Science" → "Data Science" (en) or "Data Science" (de)
|
||||
// "AI Development" → "AI Development" (en) or "KI-Entwicklung" (de)
|
||||
```
|
||||
|
||||
##### String Interpolation
|
||||
|
||||
Use variables in translations:
|
||||
|
||||
```tsx
|
||||
import { useTranslations } from 'next-intl';
|
||||
|
||||
export default function Pagination({ start, end, total }: PaginationProps) {
|
||||
const t = useTranslations('blog.ui.pagination');
|
||||
|
||||
// Translation: "Showing {start}-{end} of {total} articles"
|
||||
return (
|
||||
<p>
|
||||
{t('showing', { start, end, total })}
|
||||
</p>
|
||||
);
|
||||
}
|
||||
```
|
||||
|
||||
**In JSON:**
|
||||
|
||||
```json
|
||||
{
|
||||
"blog": {
|
||||
"ui": {
|
||||
"pagination": {
|
||||
"showing": "Showing {start}-{end} of {total} articles"
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
##### Accessing Rich Content
|
||||
|
||||
For translations containing HTML or rich content:
|
||||
|
||||
```tsx
|
||||
import { useTranslations } from 'next-intl';
|
||||
|
||||
export default function RichText() {
|
||||
const t = useTranslations('pages.about');
|
||||
|
||||
return (
|
||||
<div>
|
||||
{/* Simple text */}
|
||||
<h1>{t('hero.title')}</h1>
|
||||
|
||||
{/* Raw access for objects/arrays */}
|
||||
<div>
|
||||
{t.raw('hero.expertise.processAutomation')}
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
```
|
||||
|
||||
#### Common Patterns & Best Practices
|
||||
|
||||
##### ✅ DO: Keep Structure Consistent
|
||||
|
||||
```json
|
||||
// Good: Same structure across all locales
|
||||
// de.json
|
||||
{ "common": { "actions": { "save": "Speichern" } } }
|
||||
|
||||
// en.json
|
||||
{ "common": { "actions": { "save": "Save" } } }
|
||||
|
||||
// sr.json
|
||||
{ "common": { "actions": { "save": "Sačuvaj" } } }
|
||||
```
|
||||
|
||||
##### ❌ DON'T: Mix Structures
|
||||
|
||||
```json
|
||||
// Bad: Different structures
|
||||
// de.json
|
||||
{ "common": { "actions": { "save": "Speichern" } } }
|
||||
|
||||
// en.json
|
||||
{ "common": { "save": "Save" } } // ❌ Missing "actions" level
|
||||
```
|
||||
|
||||
##### ✅ DO: Use Semantic Naming
|
||||
|
||||
```json
|
||||
{
|
||||
"common": {
|
||||
"actions": {
|
||||
"submit": "Submit", // ✅ Clear, semantic
|
||||
"cancel": "Cancel",
|
||||
"delete": "Delete"
|
||||
}
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
##### ❌ DON'T: Use Generic Names
|
||||
|
||||
```json
|
||||
{
|
||||
"common": {
|
||||
"button1": "Submit", // ❌ Non-descriptive
|
||||
"btn": "Cancel", // ❌ Abbreviated
|
||||
"text1": "Delete" // ❌ Generic
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
##### ✅ DO: Group Related Translations
|
||||
|
||||
```json
|
||||
{
|
||||
"pages": {
|
||||
"contact": {
|
||||
"contactForm": { // ✅ Grouped by feature
|
||||
"title": "Contact Me",
|
||||
"name": { "label": "Name", "placeholder": "Your name" },
|
||||
"email": { "label": "Email", "placeholder": "your@email.com" },
|
||||
"submit": "Send Message"
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
##### ✅ DO: Reuse Common Translations
|
||||
|
||||
```tsx
|
||||
// Good: Reuse common translations
|
||||
import { useTranslations } from 'next-intl';
|
||||
|
||||
export default function MyForm() {
|
||||
const tCommon = useTranslations('common.actions');
|
||||
|
||||
return (
|
||||
<form>
|
||||
<button type="submit">{tCommon('submit')}</button>
|
||||
<button type="button">{tCommon('cancel')}</button>
|
||||
</form>
|
||||
);
|
||||
}
|
||||
```
|
||||
|
||||
## Documentation Structure
|
||||
|
||||
Reference in New Issue
Block a user