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 configurationTranslation 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, nottitle - Descriptive:
courses.enrollButton, notcourses.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:
- User preference — stored in profile/settings
- URL parameter or path —
/en/coursesor?lang=en - Accept-Language header — browser default
- 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
- Developer adds keys to the default language (English) file
- Missing translations fall back to the default language
- Translation files are reviewed in PRs like code
- Consider professional translation for user-facing content
- 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