Skip to main content
Deltalytix uses next-international for internationalization (i18n), providing seamless multi-language support across the application.

Overview

The internationalization system supports:
  • Client-side translations: For dynamic React components
  • Server-side translations: For Next.js server components
  • Multiple locales: Currently English (en) and French (fr)
  • Type-safe translations: Full TypeScript support
  • Lazy loading: Translations loaded on-demand for performance

Current Supported Languages

English

Default language (en)

French

Secondary language (fr)

Setup

Client-Side Configuration

The client i18n setup is defined in /locales/client.ts:
import { createI18nClient } from 'next-international/client'

export const { 
  useI18n, 
  useScopedI18n, 
  I18nProviderClient, 
  useChangeLocale, 
  useCurrentLocale 
} = createI18nClient({
  en: () => import('./en'),
  fr: () => import('./fr'),
})

Server-Side Configuration

The server i18n setup is in /locales/server.ts:
import { createI18nServer } from 'next-international/server'

export const {
  getI18n,
  getScopedI18n,
  getCurrentLocale,
  getStaticParams,
} = createI18nServer({
  en: () => import('./en'),
  fr: () => import('./fr'),
})

Translation Files Structure

Translations are organized by feature:
locales/
├── client.ts
├── server.ts
├── en.ts
├── fr.ts
├── en/
│   ├── shared.ts
│   ├── landing.ts
│   ├── pricing.ts
│   ├── auth.ts
│   ├── dropzone.ts
│   ├── mindset.ts
│   ├── propfirm.ts
│   ├── chat.ts
│   ├── terms.ts
│   ├── embed.ts
│   ├── teams.ts
│   ├── referral.ts
│   ├── admin.ts
│   └── faq.ts
└── fr/
    ├── shared.ts
    ├── landing.ts
    └── ...

Main Translation File

The main file (en.ts) imports and exports all translations:
import shared from "./en/shared";
import landing from "./en/landing";
import pricing from "./en/pricing";
import auth from "./en/auth";
import dropzone from "./en/dropzone";
import mindset from "./en/mindset";
import propfirm from "./en/propfirm";
import chat from "./en/chat";
import terms from "./en/terms";
import embed from "./en/embed";
import teams from "./en/teams";
import referral from "./en/referral";
import admin from "./en/admin";
import faq from "./en/faq";

export default {
  ...shared,
  ...landing,
  ...pricing,
  ...auth,
  ...dropzone,
  ...mindset,
  ...propfirm,
  ...chat,
  ...terms,
  ...embed,
  ...teams,
  ...referral,
  ...admin,
  ...faq,
  // Additional inline translations
  "footer.heading": "Footer",
  "footer.description": "Advanced analytics for modern traders.",
  // More translations...
}

Using Translations

In Client Components

Use the useI18n hook for client-side translations:
'use client'

import { useI18n } from '@/locales/client'

export function MyComponent() {
  const t = useI18n()

  return (
    <div>
      <h1>{t('dashboard.title')}</h1>
      <p>{t('dashboard.description')}</p>
    </div>
  )
}

With Variables

Pass variables to translations:
const t = useI18n()

// Translation with count
t('import.successDescription', { 
  numberOfTradesAdded: 42 
})
// Output: "42 trades have been imported."

// Translation with multiple variables
t('rithmic.rateLimit.description', {
  max: 2,
  period: 15,
  wait: 8
})
Translation definition:
{
  "import.successDescription": "{numberOfTradesAdded} trades have been imported.",
  "rithmic.rateLimit.description": "Maximum {max} sync attempts per {period} minutes. Wait {wait} minutes before retrying."
}

Scoped Translations

Use scoped translations for feature-specific keys:
const scopedT = useScopedI18n('dashboard')

return (
  <div>
    <h1>{scopedT('title')}</h1>
    <p>{scopedT('description')}</p>
  </div>
)

In Server Components

Use the server-side API in server components:
import { getI18n } from '@/locales/server'

export default async function ServerComponent() {
  const t = await getI18n()

  return (
    <div>
      <h1>{t('dashboard.title')}</h1>
      <p>{t('dashboard.description')}</p>
    </div>
  )
}

Changing Locale

Allow users to change language:
import { useChangeLocale, useCurrentLocale } from '@/locales/client'

export function LanguageSelector() {
  const changeLocale = useChangeLocale()
  const currentLocale = useCurrentLocale()

  return (
    <Select value={currentLocale} onValueChange={changeLocale}>
      <SelectTrigger>
        <SelectValue />
      </SelectTrigger>
      <SelectContent>
        <SelectItem value="en">English</SelectItem>
        <SelectItem value="fr">Français</SelectItem>
      </SelectContent>
    </Select>
  )
}

Adding Translations

Step 1: Add English Translation

Add your key to the appropriate file in /locales/en/:
// locales/en/shared.ts
export default {
  shared: {
    loading: 'Loading your shared trades...',
    notFound: 'Share not found',
    // Add new translation
    newFeature: 'This is a new feature',
  }
}

Step 2: Add French Translation

Add the corresponding French translation:
// locales/fr/shared.ts
export default {
  shared: {
    loading: 'Chargement de vos trades partagés...',
    notFound: 'Partage introuvable',
    // Add French translation
    newFeature: 'Ceci est une nouvelle fonctionnalité',
  }
}

Step 3: Use the Translation

Use your new translation key in components:
const t = useI18n()

return <p>{t('shared.newFeature')}</p>

Translation Patterns

Nested Keys

Organize translations hierarchically:
{
  "dashboard": {
    "title": "Dashboard",
    "tabs": {
      "table": "Table",
      "accounts": "Accounts",
      "widgets": "Widgets"
    }
  }
}

// Usage
t('dashboard.tabs.table') // "Table"

Pluralization

Handle singular and plural forms:
{
  "calendar.trades": "trades",
  "calendar.trade": "trade"
}

// Usage
const count = trades.length
const label = count === 1 ? t('calendar.trade') : t('calendar.trades')

Complex Objects

Use objects for related translations:
{
  "authentication.termsAndPrivacy": {
    prefix: "By clicking continue, you agree to our",
    terms: "Terms of Service",
    and: "and",
    privacy: "Privacy Policy"
  }
}

// Usage
const terms = t('authentication.termsAndPrivacy')
return (
  <p>
    {terms.prefix} <a>{terms.terms}</a> {terms.and} <a>{terms.privacy}</a>
  </p>
)

Real-World Examples

Statistics Widget

import { useI18n } from '@/locales/client'

export default function StatisticsWidget() {
  const t = useI18n()

  return (
    <Card>
      <CardHeader>
        <CardTitle>{t('statistics.title')}</CardTitle>
      </CardHeader>
      <CardContent>
        <div>
          <h3>{t('statistics.profitLoss.title')}</h3>
          <span>{t('statistics.profitLoss.profits')}</span>
          <span>{t('statistics.profitLoss.losses')}</span>
          <span>{t('statistics.profitLoss.fees')}</span>
          <span>{t('statistics.profitLoss.net')}</span>
        </div>
      </CardContent>
    </Card>
  )
}
Translations:
{
  "statistics": {
    "title": "Statistics Overview",
    "profitLoss": {
      "title": "Profit/Loss",
      "profits": "Profits",
      "losses": "Losses",
      "fees": "Fees",
      "net": "Net Result"
    }
  }
}

Rithmic Sync

import { useI18n } from '@/locales/client'

export function RithmicSyncConnection() {
  const t = useI18n()

  return (
    <form>
      <h2>{t('rithmic.addNewCredentials')}</h2>
      
      <Label>{t('rithmic.usernameLabel')}</Label>
      <Input name="username" />
      
      <Label>{t('rithmic.passwordLabel')}</Label>
      <Input type="password" name="password" />
      
      <Button type="submit">
        {t('rithmic.getAccounts')}
      </Button>
    </form>
  )
}

Import Process

import { useI18n } from '@/locales/client'

export function ImportWizard() {
  const t = useI18n()

  return (
    <div>
      <h1>{t('import.title.selectType')}</h1>
      <p>{t('import.description.selectType')}</p>
      
      <Button onClick={handleNext}>
        {t('import.button.next')}
      </Button>
      
      <Button onClick={handleBack}>
        {t('import.button.back')}
      </Button>
    </div>
  )
}

Currency Formatting

Handle locale-specific number and currency formatting:
import { useCurrentLocale } from '@/locales/client'

const locale = useCurrentLocale()

const formatCurrency = (value: number) => {
  const formatted = new Intl.NumberFormat(
    locale === 'fr' ? 'fr-FR' : 'en-US',
    {
      minimumFractionDigits: 2,
      maximumFractionDigits: 2,
    }
  ).format(value)
  
  // Format with currency symbol
  if (locale === 'fr') {
    return `${formatted} $`  // French: "1 234,56 $"
  } else {
    return `$${formatted}`   // English: "$1,234.56"
  }
}

Best Practices

  1. Consistent Keys: Use dot notation for nested keys (e.g., dashboard.title)
  2. Descriptive Names: Make key names self-explanatory
  3. Feature Organization: Group translations by feature in separate files
  4. Complete Translations: Ensure all locales have matching keys
  5. Type Safety: Leverage TypeScript for translation key validation
  6. Context: Provide context in key names (e.g., button.save vs button.cancel)
  7. Reusability: Use common translations (like common.save) for repeated text

Common Translation Keys

common: {
  add: "Add",
  cancel: "Cancel",
  save: "Save",
  saving: "Saving...",
  clear: "Clear",
  done: "Done",
  delete: "Delete",
  deleting: "Deleting...",
  edit: "Edit",
  close: "Close",
  back: "Back",
  send: "Send",
  confirm: "Confirm",
  rename: "Rename",
  success: "Success",
  error: "Error",
  create: "Create",
  retry: "Retry",
  copy: "Copy",
  loading: "Loading"
}

Adding a New Language

1

Create Translation Files

Create new directories:
  • /locales/es.ts (main file)
  • /locales/es/ (feature files)
2

Update Client Configuration

// locales/client.ts
export const { useI18n } = createI18nClient({
  en: () => import('./en'),
  fr: () => import('./fr'),
  es: () => import('./es'), // Add Spanish
})
3

Update Server Configuration

// locales/server.ts
export const { getI18n } = createI18nServer({
  en: () => import('./en'),
  fr: () => import('./fr'),
  es: () => import('./es'), // Add Spanish
})
4

Translate All Keys

Copy all translations from English and translate them to the new language.
5

Add Language Selector

Update the language selector to include the new language:
<SelectItem value="es">Español</SelectItem>

Troubleshooting

Missing Translation Keys

If a key is missing, the system falls back to the key name:
t('nonexistent.key') // Returns: "nonexistent.key"

Type Errors

Ensure all translation files export objects with matching structure:
// Correct
export default {
  key: "value"
} as const

// Incorrect - missing 'as const'
export default {
  key: "value"
}

Hydration Mismatches

Ensure server and client translations match:
// Use client-side hook in client components
'use client'
const t = useI18n()

// Use server-side API in server components
const t = await getI18n()

next-international

Official next-international documentation

Request New Language

Contact us to request a new language

Build docs developers (and LLMs) love