0%
balqalam Logo
balqalam
FeaturesCommunityPricingDocumentation
Login
  • Introduction
  • Pitch
  • Hogwarts
  • Live Demo
  • MVP
  • Roadmap
  • Launch Sprint
  • PRD
  • Get Started
  • Localhost
  • Architecture
  • Structure
  • Pattern
  • Page
  • Layout
  • Content
  • Types
  • Config
  • Actions
  • Queries
  • Authorization
  • Validation
  • Form
  • Table
  • Detail
  • Card
  • Util
  • Hooks
  • List Params
  • Views
  • README.md
  • ISSUE.md
  • Technology Stack
  • Database
  • File
  • CDN Assets
  • Entry Points
  • Dashboard
  • Authentication
  • Credentials
  • OAuth
  • Flow Diagrams
  • Multi-Tenancy
  • Offline
  • Onboarding
  • Onboarding Videos
  • Add Values
  • Admission
  • Application
  • Attendance
  • Compliance
  • Profile
  • Exams
  • Exam Wizard
  • Timetable
  • Classrooms
  • Notifications
  • Conference
  • LMS (Lumos)
Finance
  • Finance
  • Fee Management
  • Invoice
  • Wallet
  • Salary
  • Payroll
  • Timesheet
  • Expenses
  • Budget
  • Receipt
  • Accounts
  • Banking
  • Reports
  • Dashboard
  • Permissions
  • Messages
  • Integration Flow
  • Provision
  • AI Document Processing
  • Document Intelligence
  • Internationalization
  • Translation
  • Translation Guide
  • Icons
  • Docs Factory
  • Inspiration
  • Listings
  • Teachers
  • Students
  • Catalog
  • Library
  • Contributing
  • Code of conduct
  • GitHub Workflow
  • Database Seeds
  • Database Safety
  • Test Accounts
  • Playwright
  • Prettier
  • Block Rebound
Sales & GTM
  • Marketing Brief
  • Sales
  • Go-to-market
  • Marketing
  • Admission — Feature Spotlight
  • Pilot Program
  • Leads
  • Proposal
  • Outreach Templates
  • Case Study
  • Competitors
  • Landing-page teardown
  • Competitor FAQ
  • Business model
  • Shared economy
  • Traction
Fundraising & Ecosystem
  • Get Support
  • Investor Deck
  • Data Room
  • Investors
  • Accelerators
  • Incubators
  • Grants
  • Sponsors
  • Partners
  • Competitions & Hackathons
  • Universities & Training Centers

Internationalization

PreviousNext

Architecture reference for i18n with Arabic (RTL) and English (LTR) — locales, routing, single-language storage, RTL CSS, client hooks.

Overview

This is the architecture reference for the i18n system. For adoption patterns (form labels, selects, validation, toasts, server-action errors) see Translation.

The application supports Arabic (RTL, default) and English (LTR). The system handles static UI translations via dictionary JSON and single-language user-generated content with on-demand translation via Google Translate + DB cache.

Supported Languages

LocaleNameDirectionFlagCurrencyDefault
arArabicRTLSASARYes
enEnglishLTRUSUSDNo

Key Features

  • URL-based routing: /[lang]/path (e.g., /ar/dashboard, /en/dashboard)
  • Single-language storage: One lang field, translated on demand via Google Translate API
  • Logical CSS properties: ms-, me-, ps-, pe- for auto-mirroring
  • Direction switching: dir attribute on <html> element
  • Smart fallbacks: Falls back to available language if preferred is empty

Quick Start

Server Components

import type { Locale } from "@/components/internationalization/config"
import { getDictionary } from "@/components/internationalization/dictionaries"
 
export default async function Page({ params }: { params: { lang: string } }) {
  const { lang } = await params
  const dictionary = await getDictionary(lang as Locale)
 
  return (
    <div>
      <h1>{dictionary.common.home}</h1>
      <p>{dictionary.common.loading}</p>
    </div>
  )
}

Client Components

"use client"
 
import { useLocale } from "@/components/internationalization/use-locale"
 
export function LocaleIndicator() {
  const { locale, isRTL, localeConfig } = useLocale()
 
  return (
    <span className={isRTL ? "text-end" : "text-start"}>
      {localeConfig.flag} {localeConfig.nativeName}
    </span>
  )
}

User Content Storage

All user-generated content uses single-language storage with a lang field. Content is stored in one language only. Translation happens on-demand via Google Translate API with database caching through the Translation model.

Rules

  1. Generic field names only — title, body, name, description (never titleAr, nameEn)
  2. lang field — Every content model has lang String @default("ar") indicating the stored language
  3. On-demand translation — Use getText() from @/components/translation/display to translate at display time
  4. Translation — Translated strings are cached in the database to avoid repeated API calls
  5. School's preferred language — School.preferredLanguage determines the default storage language

Schema Pattern

model Announcement {
  id       String  @id @default(cuid())
  schoolId String
  title    String?          // Generic field name (NOT titleEn/titleAr)
  body     String? @db.Text // Generic field name (NOT bodyEn/bodyAr)
  lang     String  @default("ar") // Language of stored content
}

Display with Translation

Lists — batched localize() (preferred). One DB query for the whole page, three-tier resolution (in-memory LRU → Translation cache → provider chain: Google → Groq LLM fallback, each behind a circuit breaker):

import { localize } from "@/components/translation/localize"
// names / arbitrary short labels — batched + transliteration fallback
import { getLabels, getNames } from "@/components/translation/person"
 
// rows of a registered model (see registry.ts TRANSLATABLE map)
const rows = await localize("Announcement", announcements, { schoolId, lang })
 
const labels = await getLabels(
  rows.map((r) => r.name),
  lang,
  schoolId
)

Single values — getText() (LRU-backed; never inside .map()):

import { getText } from "@/components/translation/display"
 
// Stored in Arabic, user views in English -> translates via API
const title = await getText(announcement.title, "ar", "en", schoolId)
 
// Same language -> returns directly, no API call
const title = await getText(announcement.title, "ar", "ar", schoolId)

Writes — prewarm the cache so the first reader in the other language never waits:

import { after } from "next/server"
 
import { prewarm } from "@/components/translation/prewarm"
 
after(() => prewarm("Announcement", created, { schoolId }))

Migration from legacy bilingual fields

The migration off titleEn/titleAr patterns to single-language storage is complete. New code must use generic field names (title, body, name) plus a lang field. CLAUDE.md gotcha #12 enforces this. For the historical SQL and adoption tracker, see Translation.

Infrastructure

FilePurpose
src/components/translation/localize.tsBatched localize()/localizeOne() (preferred)
src/components/translation/registry.tsTRANSLATABLE map: model → translatable fields
src/components/translation/prewarm.tsprewarm() — cache-fill on write via after()
src/components/translation/person.tsgetNames()/getLabels() — batched names/labels
src/components/translation/memory-cache.tsProcess LRU (hot short terms, zero DB round-trips)
src/components/translation/display.tsgetText(), getFields() — single-value path
src/components/translation/actions.tstranslate() core (LRU → DB → provider chain)
src/components/translation/engine.tsProvider chain: Google → Groq, circuit breakers
src/components/translation/google.tsGoogle client: timeout, chunking, retry policy
src/components/translation/groq.tsGroq LLM fallback provider (free tier)
src/components/translation/sweep.tsCache sweep core (i18n:backfill + daily cron)
src/components/translation/util.tswithLang, detectScript
prisma/models/translation.prismaTranslation model (per-school cache)

Configuration

Locale Config

Location: src/components/internationalization/config.ts

export const i18n = {
  defaultLocale: "ar",
  locales: ["en", "ar"],
} as const
 
export type Locale = (typeof i18n)["locales"][number]
 
export const localeConfig = {
  en: {
    name: "English",
    nativeName: "English",
    dir: "ltr",
    flag: "🇺🇸",
    dateFormat: "MM/dd/yyyy",
    currency: "USD",
  },
  ar: {
    name: "Arabic",
    nativeName: "العربية",
    dir: "rtl",
    flag: "🇸🇦",
    dateFormat: "dd/MM/yyyy",
    currency: "SAR",
  },
} as const
 
export function isRTL(locale: Locale): boolean {
  return localeConfig[locale]?.dir === "rtl"
}

Dictionary Loaders

Every namespace is registered ONCE in src/components/internationalization/namespaces.ts — the server loader (dictionaries.ts) and the client loader (get-dictionary-client.ts) both derive from it, so their shapes can never drift (guarded by dictionary-loader-sync.test.ts). All loaders are wrapped in React cache(): one merge per request regardless of call-site count.

Route-scoped loaders keep the RSC payload down — use them in DictionaryProvider layouts:

import {
  getDictionary, // full merge (23 namespaces)
  getExamDictionary, // core + marking + generate + results
  getMessagingDictionary, // core + messages + messaging
  getSaasDashboardDictionary, // core + sales + messages
} from "@/components/internationalization/dictionaries"

For form / select / validation / toast / server-action adoption patterns, see Translation.

Translation Guard (CI)

en/ar drift cannot ship — these run in pnpm test and CI:

GuardCatches
src/tests/i18n/dictionary-parity.test.tsAny en/ar key drift + committed [AR]/[EN] stubs
src/tests/i18n/dictionary-loader-sync.test.tsServer vs client dictionary shape divergence
src/tests/i18n/hardcoded-ratchet.test.tsNEW hardcoded English strings (8 anti-patterns)
src/tests/i18n/rtl-physical-class.test.tsPhysical CSS classes (ml-, text-left…) — at 0
pnpm i18n:check (CI step)Same parity engine, fail-fast before tests

RTL Support with Logical Properties

CSS Logical Properties (Preferred)

Always use logical CSS properties instead of physical directions. These automatically adapt to RTL/LTR:

Physical (Avoid)Logical (Use)Description
ml-*ms-*Margin inline-start
mr-*me-*Margin inline-end
pl-*ps-*Padding inline-start
pr-*pe-*Padding inline-end
left-*start-*Position start
right-*end-*Position end
text-lefttext-startText alignment start
text-righttext-endText alignment end
border-l-*border-s-*Border start
border-r-*border-e-*Border end
rounded-l-*rounded-s-*Border radius start
rounded-r-*rounded-e-*Border radius end

Example:

// Wrong - physical properties don't adapt
<div className="ml-4 pr-2 text-left border-l-2">
 
// Correct - logical properties adapt to RTL/LTR
<div className="ms-4 pe-2 text-start border-s-2">

Layout Integration

The root layout sets direction based on locale:

// src/app/[lang]/layout.tsx
import {
  localeConfig,
  type Locale,
} from "@/components/internationalization/config"
 
export default async function RootLayout({
  children,
  params,
}: {
  children: React.ReactNode
  params: { lang: string }
}) {
  const { lang } = (await params) as { lang: Locale }
  const config = localeConfig[lang]
 
  return (
    <html lang={lang} dir={config.dir}>
      <body>{children}</body>
    </html>
  )
}

When to Use rtl: Variants

Only use rtl: variants for cases that cannot be solved with logical properties:

// Flex direction reversal (no logical equivalent)
<div className="flex flex-row rtl:flex-row-reverse">
 
// Icon mirroring for directional icons
<ChevronRight className="rtl:scale-x-[-1]" />
 
// Space reversal in flex
<div className="flex space-x-2 rtl:space-x-reverse">

Client-Side Hooks

useLocale

"use client"
 
import { useLocale } from "@/components/internationalization/use-locale"
 
export function MyComponent() {
  const { locale, isRTL, localeConfig } = useLocale()
 
  return (
    <div dir={isRTL ? "rtl" : "ltr"}>
      <p>Current: {localeConfig.nativeName}</p>
      <p>Direction: {localeConfig.dir}</p>
    </div>
  )
}

useDictionary

"use client"
 
import { useDictionary } from "@/components/internationalization/use-dictionary"
 
export function ClientComponent() {
  const { dictionary, isLoading } = useDictionary()
 
  if (isLoading) return <Spinner />
 
  return <h1>{dictionary?.common.welcome}</h1>
}

useSwitchLocaleHref

"use client"
 
import Link from "next/link"
 
import { useSwitchLocaleHref } from "@/components/internationalization/use-locale"
 
export function LanguageSwitcher() {
  const switchLocaleHref = useSwitchLocaleHref()
 
  return (
    <nav className="flex gap-2">
      <Link href={switchLocaleHref("ar")}>العربية</Link>
      <Link href={switchLocaleHref("en")}>English</Link>
    </nav>
  )
}

Arabic Number & Date Formatting

Number Formatting

const formatter = new Intl.NumberFormat(locale, {
  style: "decimal",
  maximumFractionDigits: 2,
})
formatter.format(1234567) // ar: "١٬٢٣٤٬٥٦٧" | en: "1,234,567"
 
const currencyFormatter = new Intl.NumberFormat(locale, {
  style: "currency",
  currency: locale === "ar" ? "SAR" : "USD",
})
currencyFormatter.format(99.99) // ar: "٩٩٫٩٩ ر.س" | en: "$99.99"

Date Formatting

const dateFormatter = new Intl.DateTimeFormat(locale, {
  year: "numeric",
  month: "long",
  day: "numeric",
})
dateFormatter.format(new Date()) // ar: "١ ديسمبر ٢٠٢٥" | en: "December 1, 2025"
 
const relativeFormatter = new Intl.RelativeTimeFormat(locale, {
  numeric: "auto",
})
relativeFormatter.format(-2, "day") // ar: "منذ يومين" | en: "2 days ago"

Arabic Pluralization

Arabic has three plural forms: singular (1), dual (2), and plural (3+).

function arabicPlural(
  count: number,
  singular: string,
  dual: string,
  plural: string
): string {
  if (count === 1) return singular
  if (count === 2) return dual
  return plural
}
 
// Usage
arabicPlural(count, "طالب", "طالبان", "طلاب") // student/students
arabicPlural(count, "يوم", "يومان", "أيام") // day/days

Best Practices

UI Guidelines

  1. Use logical properties — ms-, me-, ps-, pe- instead of ml-, mr-, etc.
  2. Mirror directional icons — Arrows and chevrons should flip in RTL
  3. Test both languages — Always verify layouts in ar and en
  4. Numbers stay Western — Unless explicitly localized for display

Content Guidelines

  1. Avoid string concatenation — Use full-sentence translations
  2. Generic field names only — title, body, name (never titleEn/titleAr)
  3. Single-language storage — Store in one language with a lang field
  4. On-demand translation — Use getText() for display-time translation

Typography

  • Arabic font: Rubik (system default)
  • English font: GeistSans
  • Line height: 1.8 recommended for Arabic
  • No font-size hardcoding — Use semantic HTML (h1-h6, p, small)

Folder Structure

src/
  components/internationalization/
    config.ts                 # Locale config, types, isRTL helper
    namespaces.ts             # SINGLE namespace registry (flat + 19 feature)
    dictionaries.ts           # Server loaders (cache()-wrapped, route-scoped)
    get-dictionary-client.ts  # Client loader (same registry)
    locale-detect.ts          # detectLocale/pathnameHasLocale (live proxy logic)
    use-locale.ts             # Client hooks: useLocale, useSwitchLocaleHref
    use-dictionary.ts         # useDictionary hook (context-first)
    dictionary-context.tsx    # DictionaryProvider
    language-switcher.tsx     # Language switcher component
    actions.ts                # setLocale server action
    helpers/index.ts          # ValidationHelper, ToastHelper, ErrorHelper
    lib/key-diff.ts           # Pure en/ar diff engine
    lib/parity.ts             # Real-file parity + placeholder scanner
    en.json / ar.json         # General translations (flat)
    school-{en,ar}.json       # School dashboard (flat, largest pair)
    lumos-{en,ar}.json       # Lumos/LMS (flat)
    operator-{en,ar}.json     # SaaS operator (flat)
    dictionaries/{en,ar}/     # 19 feature namespaces: admin, attendance,
                              # banking, compliance, finance, generate, lab,
                              # library, live-classes, marking, messages,
                              # messaging, notifications, parentPortal,
                              # profile, results, sales, transportation,
                              # whatsapp
  components/translation/     # System B: dynamic content engine (own README)

References

Internal

  • Translation Guide — deep guide to DB-content (on-demand) translation
  • Translation — adoption patterns, coverage tracker
  • src/components/translation/display.ts — getText() for on-demand translation
  • src/components/translation/google.ts — Google Translate API client
  • src/components/translation/actions.ts — Translation with database caching
  • src/components/internationalization/ — Full i18n implementation

External

  • Next.js Internationalization
  • Tailwind CSS Logical Properties
  • MDN CSS Logical Properties
  • W3C Authoring HTML & CSS for RTL
Document IntelligenceTranslation

On This Page

OverviewSupported LanguagesKey FeaturesQuick StartServer ComponentsClient ComponentsUser Content StorageRulesSchema PatternDisplay with TranslationMigration from legacy bilingual fieldsInfrastructureConfigurationLocale ConfigDictionary LoadersTranslation Guard (CI)RTL Support with Logical PropertiesCSS Logical Properties (Preferred)Layout IntegrationWhen to Use rtl: VariantsClient-Side HooksuseLocaleuseDictionaryuseSwitchLocaleHrefArabic Number & Date FormattingNumber FormattingDate FormattingArabic PluralizationBest PracticesUI GuidelinesContent GuidelinesTypographyFolder StructureReferencesInternalExternal

Built by Databayt ·

Welcome to balqalam.

A great journey is about to begin.