Dev Playbook
Conventions

Internationalization (i18n)

Internationalization and localization approach.

Core Principle

Never hardcode user-facing strings. Even if you only support one language today, extracting strings into translation files costs almost nothing and makes adding languages trivial later.

Architecture

src/
├── locales/
│   ├── en/
│   │   ├── common.json        ← Shared strings (buttons, nav, errors)
│   │   ├── courses.json       ← Feature-specific strings
│   │   └── auth.json
│   ├── tr/
│   │   ├── common.json
│   │   ├── courses.json
│   │   └── auth.json
│   └── index.ts               ← i18n configuration

Translation File Format

{
  "courses": {
    "title": "My Courses",
    "empty": "You haven't enrolled in any courses yet.",
    "enrollButton": "Enroll Now",
    "capacity": "{{remaining}} of {{total}} spots remaining",
    "filters": {
      "all": "All",
      "active": "Active",
      "completed": "Completed"
    }
  }
}

Key Naming Convention

  • Dot notation for nested keys: courses.filters.active
  • Feature-scoped: courses.title, not title
  • Descriptive: courses.enrollButton, not courses.btn1
  • Consistent: Same structure across all languages

Implementation

Next.js — next-intl or next-i18next

// Using next-intl
import { useTranslations } from 'next-intl';

export default function CoursesPage() {
  const t = useTranslations('courses');

  return (
    <div>
      <h1>{t('title')}</h1>
      <p>{t('capacity', { remaining: 5, total: 30 })}</p>
      <button>{t('enrollButton')}</button>
    </div>
  );
}

.NET — Resource Files or JSON

// Using IStringLocalizer
public class CoursesController : ControllerBase
{
    private readonly IStringLocalizer<CoursesController> _localizer;

    [HttpGet]
    public IActionResult Get()
    {
        var message = _localizer["CourseFull"];
        return Ok(new { message });
    }
}

Python — gettext or custom

# Using gettext
from gettext import gettext as _

def get_enrollment_message(remaining: int, total: int) -> str:
    return _("{remaining} of {total} spots remaining").format(
        remaining=remaining, total=total
    )

Formatting

Dates and Times

Never format dates manually. Use Intl.DateTimeFormat or equivalent library.

// GOOD — locale-aware
const formatted = new Intl.DateTimeFormat('tr-TR', {
  year: 'numeric',
  month: 'long',
  day: 'numeric',
}).format(date);
// → "15 Mart 2025"

// BAD — hardcoded format
const formatted = `${date.getMonth()}/${date.getDate()}/${date.getFullYear()}`;
// → "3/15/2025" (always US format)

Numbers and Currency

// Numbers
new Intl.NumberFormat('tr-TR').format(1234567.89);
// → "1.234.567,89"

// Currency
new Intl.NumberFormat('tr-TR', { style: 'currency', currency: 'TRY' }).format(99.90);
// → "₺99,90"

Pluralization

Don't assume all languages pluralize like English (just add "s").

// English
{
  "studentCount": {
    "zero": "No students",
    "one": "1 student",
    "other": "{{count}} students"
  }
}

// Turkish (no grammatical plural for counted nouns)
{
  "studentCount": {
    "zero": "Ogrenci yok",
    "one": "1 ogrenci",
    "other": "{{count}} ogrenci"
  }
}
// next-intl handles ICU plural syntax
// common.json: "studentCount": "{count, plural, =0 {No students} one {1 student} other {# students}}"
t('studentCount', { count: 5 });

Content Rules

Do

  • Extract ALL user-facing strings — buttons, labels, headings, error messages, tooltips
  • Use variables for dynamic content: "Welcome, {{name}}" not "Welcome, " + name
  • Support RTL — Arabic, Hebrew need right-to-left layout
  • Handle text expansion — German text is ~30% longer than English, design with flexibility
  • Use Unicode — UTF-8 everywhere (database, files, API responses)
  • Store dates in UTC — convert to local timezone only in the UI

Don't

  • Don't concatenate strings: t('hello') + ' ' + name — word order differs across languages
  • Don't embed HTML in translations: "Click <b>here</b>" — use components with slots instead
  • Don't hardcode sort order — alphabetical order varies by locale
  • Don't assume text direction — design layouts that adapt to RTL
  • Don't use flags for languages — a flag represents a country, not a language

Language Detection

Priority order:

  1. User preference — stored in profile/settings
  2. URL parameter or path/en/courses or ?lang=en
  3. Accept-Language header — browser default
  4. Fallback — default language (English)
// Next.js middleware for locale detection
import createMiddleware from 'next-intl/middleware';

export default createMiddleware({
  locales: ['en', 'tr'],
  defaultLocale: 'en',
});

Translation Workflow

  1. Developer adds keys to the default language (English) file
  2. Missing translations fall back to the default language
  3. Translation files are reviewed in PRs like code
  4. Consider professional translation for user-facing content
  5. Use tools like Crowdin, Lokalise, or Phrase for team translation management

Anti-Patterns

  • Hardcoded strings in components — Always use translation functions
  • Date formatting with template literals — Use Intl.DateTimeFormat
  • Splitting sentences across multiple keys — Word order changes across languages
  • Ignoring text expansion in designs — German, Finnish, and other languages need more space
  • Using machine translation without review — Automated translations need human validation

On this page