Skip to content

Latest commit

 

History

History
410 lines (302 loc) · 11.6 KB

File metadata and controls

410 lines (302 loc) · 11.6 KB

Internationalization (i18n) & Translation Contributor Guide

This guide covers how BlueCollar handles internationalization, how to contribute translations for the app UI and README files, and how to add support for a new locale.


Quick Start for Translation Contributors

  1. Check open i18n issues — someone may already be working on your language.
  2. Fork the repo and create a branch: git checkout -b i18n/add-{language}
  3. Translate the UI messages (see Adding a New Language).
  4. Optionally translate the README (see README Translations).
  5. Open a PR with title format: i18n: add {language} ({locale}) translations

next-intl Configuration

BlueCollar uses next-intl for all i18n functionality in the packages/app Next.js frontend.

Plugin Setup

packages/app/next.config.mjs wraps the Next.js config with the next-intl plugin:

import createNextIntlPlugin from 'next-intl/plugin'

const withNextIntl = createNextIntlPlugin()
export default withNextIntl(nextConfig)

The plugin automatically picks up packages/app/src/i18n.ts as the request config entry point.

Request Config

packages/app/src/i18n.ts loads the correct message file for each request based on the active locale:

import { getRequestConfig } from 'next-intl/server'

export default getRequestConfig(async ({ locale }) => ({
  messages: (await import(`./messages/${locale}.json`)).default
}))

Middleware

packages/app/src/middleware.ts handles locale detection and URL prefixing:

import createMiddleware from 'next-intl/middleware'

const intlMiddleware = createMiddleware({
  locales: ['en', 'fr', 'es'],
  defaultLocale: 'en',
  localePrefix: 'always',
})

The localePrefix: 'always' strategy means every URL includes the locale segment. A request to /workers will redirect to /en/workers.

URL Structure

/en/workers          → English workers page
/fr/workers          → French workers page
/es/dashboard        → Spanish dashboard
/en/auth/login       → English login page

Translation File Structure

Translation files live in packages/app/src/messages/ as JSON files, one per locale.

packages/app/src/messages/
├── en.json    # English (default)
├── es.json    # Spanish
└── fr.json    # French

File Format

Each file is a flat or nested JSON object. BlueCollar uses a two-level namespace structure: a top-level namespace key followed by message keys.

{
  "common": {
    "home": "Home",
    "save": "Save",
    "cancel": "Cancel"
  },
  "workers": {
    "title": "Find Skilled Workers",
    "noResults": "No workers found"
  },
  "auth": {
    "email": "Email",
    "loginTitle": "Login to BlueCollar"
  },
  "dashboard": {
    "title": "Dashboard",
    "myWorkers": "My Workers"
  }
}

Current Namespaces

Namespace Purpose
common Shared UI labels (nav, buttons, status words)
workers Workers listing and profile page strings
auth Authentication forms and labels
dashboard Dashboard page strings

Using Translations in Components

In Server Components:

import { getTranslations } from 'next-intl/server'

export default async function WorkersPage() {
  const t = await getTranslations('workers')
  return <h1>{t('title')}</h1>
}

In Client Components:

'use client'
import { useTranslations } from 'next-intl'

export default function WorkerCard() {
  const t = useTranslations('workers')
  return <span>{t('verified')}</span>
}

Adding a New Language

Follow these steps to add a new locale.

1. Create the translation file

Copy en.json as a starting point and translate all values. Keep all keys identical to the English file.

cp packages/app/src/messages/en.json packages/app/src/messages/{locale}.json

Then translate the values in {locale}.json:

{
  "common": {
    "home": "Início",
    "save": "Salvar",
    "cancel": "Cancelar"
  },
  "workers": {
    "title": "Encontrar Trabalhadores Qualificados",
    "noResults": "Nenhum trabalhador encontrado"
  }
}

2. Register the locale in middleware

Add the new locale code to the locales array in packages/app/src/middleware.ts:

const intlMiddleware = createMiddleware({
  locales: ['en', 'fr', 'es', 'pt'],  // add 'pt'
  defaultLocale: 'en',
  localePrefix: 'always',
})

3. Update the LanguageSwitcher component

packages/app/src/components/LanguageSwitcher.tsx renders the locale options. Add the new locale with its display name:

const locales = [
  { code: 'en', label: 'English' },
  { code: 'fr', label: 'Français' },
  { code: 'es', label: 'Español' },
  { code: 'pt', label: 'Português' },
]

4. Verify the route structure

The [locale] dynamic segment in packages/app/src/app/[locale]/ handles all localized routes automatically. No additional route changes are needed.

5. Test the new locale

Start the dev server and navigate to http://localhost:3000/{locale} to verify the new locale loads correctly.

README Translations

The project also accepts translated README files alongside the English README.md. These are standalone Markdown files at the repo root.

Existing Translations

File Language
README.md English
README.pt.md Portuguese

Adding a README Translation

  1. Copy the English README:
    cp README.md README.{locale}.md
  2. Translate all user-facing text while preserving code blocks, URLs, and file paths unchanged.
  3. Add a language switch link at the top below the title:
    **[English](./README.md) | [Português](./README.{locale}.md)**
  4. Add a "Translation" section at the bottom crediting yourself:
    ## Translation
    
    This translation was contributed by the community to make the project accessible to {language} speakers.
    
    **Translator:** Your Name / Community
  5. Update the English README.md to link to the new translation:
    **[English](./README.md) | [Português](./README.{locale}.md)**

Translators Section

When submitting a README translation, add your name (or GitHub handle) in the final ## Translation footer. If multiple people contributed, list them all.

Translation Contribution Workflow

For contributors adding or updating translations

  1. Fork the repository and create a branch: git checkout -b i18n/add-{language}
  2. Add or update the translation file in packages/app/src/messages/
  3. Ensure every key present in en.json also exists in your translation file — missing keys fall back to the key name, not the English value
  4. Run the validation script (see below) to check for missing keys
  5. Open a pull request with the title format: i18n: add {language} ({locale}) translations

Validating Translations

To find missing keys, compare your locale file against en.json:

# Deep comparison — finds missing keys at any nesting level
node -e "
const en = require('./packages/app/src/messages/en.json')
const locale = require('./packages/app/src/messages/{locale}.json')

function findMissing(base, target, path = '') {
  return Object.keys(base).flatMap(k => {
    const p = path ? path + '.' + k : k
    if (!(k in target)) return [p]
    if (typeof base[k] === 'object' && base[k] !== null && !Array.isArray(base[k]))
      return findMissing(base[k], target[k], p)
    return []
  })
}

const missing = findMissing(en, locale)
if (missing.length) {
  console.log('Missing keys:', missing.join('\n  '))
  process.exit(1)
} else {
  console.log('All keys present!')
}
"

You can also add this as a script in packages/app/package.json:

{
  "scripts": {
    "i18n:check": "node -e \"...\""
  }
}

PR Checklist for Translation Contributors

  • All keys from en.json are present in the translation file
  • Values are translated (not left as English)
  • Variables ({count}, {name}) are preserved — do not translate variable names
  • ICU plural syntax is correct for the target language
  • LanguageSwitcher component includes the new locale
  • Middleware locales array includes the new locale code
  • (Optional) README translation is included with language switcher link

Reviewer Checklist

  • Translation file is valid JSON
  • No keys are missing compared to en.json
  • Variable placeholders are intact
  • Language switcher and middleware are updated
  • README translation (if any) preserves URLs, code blocks, and file paths

Keeping translations in sync

When new features add strings to en.json, all other locale files must be updated. A missing key will render the raw key string (e.g., workers.newFeature) in the UI.

The CI pipeline will eventually check for missing keys automatically. In the meantime, run the validation script above before merging any PR that adds new UI strings.

Pluralization and Formatting Rules

next-intl uses the ICU message format for pluralization and interpolation.

Variable Interpolation

Pass dynamic values as the second argument to t():

{
  "workers": {
    "resultsCount": "Showing {count} workers"
  }
}
t('resultsCount', { count: 42 })
// → "Showing 42 workers"

Pluralization

Use ICU plural syntax to handle singular/plural forms:

{
  "workers": {
    "reviewCount": "{count, plural, =0 {No reviews} one {# review} other {# reviews}}"
  }
}
t('reviewCount', { count: 0 })   // → "No reviews"
t('reviewCount', { count: 1 })   // → "1 review"
t('reviewCount', { count: 5 })   // → "5 reviews"

Each locale file should provide the plural forms appropriate for that language. For example, Russian has more plural categories than English — consult the CLDR plural rules for the target language.

Date and Number Formatting

next-intl integrates with the Intl API for locale-aware formatting:

import { useFormatter } from 'next-intl'

function PriceDisplay({ amount }: { amount: number }) {
  const format = useFormatter()
  return <span>{format.number(amount, { style: 'currency', currency: 'USD' })}</span>
}
// Date formatting
format.dateTime(new Date(), { dateStyle: 'medium' })
// en: "Apr 23, 2026"
// fr: "23 avr. 2026"
// es: "23 abr 2026"

Select (Gender / Conditional)

Use ICU select for gender-aware or conditional strings:

{
  "workers": {
    "workerStatus": "{status, select, active {Available} inactive {Unavailable} other {Unknown}}"
  }
}

Translation Guidelines

What to translate

  • All user-facing UI strings in the message JSON files
  • README content (prose, descriptions, instructions)

What NOT to translate

  • Variable names and placeholders ({count}, {name}, {email})
  • Code blocks, commands, and file paths
  • URLs and links
  • Brand name "BlueCollar"
  • Technical terms that are universally understood (e.g., "API", "JSON", "JWT")

Language Quality

  • Use natural, idiomatic phrasing for the target language — avoid literal translations
  • Maintain consistent terminology throughout the file
  • Respect the tone of the original (professional, helpful, concise)
  • For regional variants (e.g., pt-BR vs pt-PT), add a note in the PR description