Internationalization
FORGE generates full multi-language support across both frontend applications. Translations are loaded from static JSON files (published from the admin panel), the useTranslation hook provides a t() function with interpolation, and RTL layouts are handled automatically based on the active language's direction.
Architecture
┌────────────────────────────────────────────────────────────┐
│ Admin Panel │
│ ┌──────────────────────────────────────────────────────┐ │
│ │ Translation Management Page │ │
│ │ key: "auth.login_title" │ │
│ │ en: "Login to your account" │ │
│ │ ar: "تسجيل الدخول إلى حسابك" │ │
│ │ │ │
│ │ [Publish] → exports /messages/en.json │ │
│ │ /messages/ar.json │ │
│ └──────────────────────────────────────────────────────┘ │
└────────────────────────────────────────────────────────────┘
│
▼
┌────────────────────────────────────────────────────────────┐
│ I18n Provider │
│ │
│ 1. Read locale from cookie/localStorage │
│ 2. Fetch /messages/{locale}.json │
│ 3. Fetch /api/v1/languages (for direction info) │
│ 4. Provide t() function, locale, direction │
│ 5. Set <html lang="..." dir="..."> │
└────────────────────────────────────────────────────────────┘
│
▼
┌────────────────────────────────────────────────────────────┐
│ Components │
│ │
│ const { t } = useTranslation(); │
│ <h1>{t("auth.login_title")}</h1> │
│ // → "Login to your account" or "تسجيل الدخول إلى حسابك" │
└────────────────────────────────────────────────────────────┘I18n Provider
Both applications wrap their component tree with an I18nProvider that manages locale state, loads translations, and tracks text direction:
// app/layout.tsx
import { I18nProvider } from "@/components/providers/i18n-provider";
export default function RootLayout({
children,
}: {
children: React.ReactNode;
}) {
return (
<html>
<body>
<I18nProvider defaultLocale="en" fallbackLocale="en">
{children}
</I18nProvider>
</body>
</html>
);
}Props
| Prop | Type | Default | Description |
|---|---|---|---|
defaultLocale | string | "en" | Initial locale if no preference is stored |
fallbackLocale | string | "en" | Locale to use if translation file fails to load |
How It Works
On mount, the provider:
- Reads the locale from the
localecookie, falls back tolocalStorage, then todefaultLocale - Reads the direction from the
directioncookie (set during language switch) - Fetches the language list from
/api/v1/languagesto get direction metadata - Loads translations from
/messages/{locale}.json(a static JSON file) - Sets
<html>attributes --langanddir-- to match the active locale
Translation Hook
The useTranslation hook is the primary interface for accessing translations in components:
import { useTranslation } from "@/components/providers/i18n-provider";
function LoginPage() {
const { t, locale, direction, isLoading } = useTranslation();
return (
<div>
<h1>{t("auth.login_title")}</h1>
<p>{t("auth.login_subtitle")}</p>
</div>
);
}Return Values
| Property | Type | Description |
|---|---|---|
t | (key: string, params?: Record<string, string | number>) => string | Translation function |
locale | string | Current locale code (e.g., "en", "ar") |
direction | "ltr" | "rtl" | Current text direction |
isLoading | boolean | Whether translations are still loading |
currentLanguage | string | Alias for locale |
Interpolation
The t() function supports parameter interpolation using syntax:
// Translation: "Welcome, {{name}}! You have {{count}} messages."
t("dashboard.welcome", { name: "Ahmed", count: 5 });
// → "Welcome, Ahmed! You have 5 messages."Key Convention
Translation keys follow a dot-separated namespace convention:
{module}.{section}.{key}Examples:
| Key | English | Arabic |
|---|---|---|
auth.login_title | Login to your account | تسجيل الدخول إلى حسابك |
admin.users.Users | Users | المستخدمون |
admin.users.Add_User | Add User | إضافة مستخدم |
admin.nav.Dashboard | Dashboard | لوحة التحكم |
common.Delete | Delete | حذف |
common.Save | Save | حفظ |
TIP
If a translation key is not found, the t() function returns the key itself. This makes it easy to identify missing translations during development -- untranslated text appears as its key path.
Language Switcher
The LanguageSwitcher component displays a dropdown with available languages. When a language is selected:
- Sets
localeanddirectioncookies (shared across subdomains) - Updates
localStorageas a fallback - Sets
document.documentElement.diranddocument.documentElement.lang - Updates the I18n context state (triggers re-render of all translated content)
- Calls
router.refresh()to update server-rendered content
import { LanguageSwitcher } from "@/components/language-switcher";
// Renders a Globe icon button with dropdown
<LanguageSwitcher />The component automatically hides itself when only one language is configured. Each language is displayed using its native_name property (e.g., "English", "العربية", "Francais").
Cookie-Based Persistence
Language preference is persisted in cookies so that server-rendered pages can access the locale before JavaScript loads:
// Set during language switch
document.cookie = `locale=${langCode};path=/;max-age=31536000`;
document.cookie = `direction=${direction};path=/;max-age=31536000`;The middleware (or Server Component) reads these cookies to determine the correct locale for server-side rendering:
// In a Server Component
const headersList = await headers();
const locale = headersList.get("x-locale") || "en";RTL Support
RTL (Right-to-Left) support is built into every layer of the frontend.
1. HTML Direction
The <html> element's dir attribute is set by the I18n provider:
<!-- English -->
<html lang="en" dir="ltr">
<!-- Arabic -->
<html lang="ar" dir="rtl">2. Logical CSS Properties
FORGE templates use CSS logical properties (ms-, me-, ps-, pe-, start, end) instead of physical directional properties (ml-, mr-, pl-, pr-, left, right). This ensures layouts mirror correctly in RTL mode:
// Instead of ml-2 (always left margin)
<Icon className="me-2 h-4 w-4" /> // margin-end: adapts to direction
// Instead of pl-8 (always left padding)
<div className="ps-8"> // padding-start: adapts to direction
// Instead of text-left
<span className="text-start"> // start: adapts to directionWARNING
When adding custom CSS, always use logical properties (ms, me, ps, pe, start, end) instead of physical directional properties (ml, mr, pl, pr, left, right). This ensures your layout works correctly in both LTR and RTL modes.
3. Direction-Aware Components
Several components adapt their behavior based on direction:
- Sidebar -- Positioned on the left in LTR, right in RTL
- DirectionAwareToaster -- Toasts appear in the correct corner
- RichTextEditor -- TinyMCE content direction matches the active locale
- DataTable pagination -- Navigation arrows flip in RTL
Translatable Content
Resources like roles, content pages, menus, and lookups support per-field translations stored as JSONB in the database. The admin panel provides tabbed editing interfaces for these.
TranslationsInput
For simple resources (roles, permissions) with display_name and description:
<TranslationsInput
value={role.translations || {}}
onChange={(translations) => setRole({ ...role, translations })}
fields={["display_name", "description"]}
/>The data shape:
{
"en": { "display_name": "Admin", "description": "Full access" },
"ar": { "display_name": "مشرف", "description": "وصول كامل" }
}ContentTranslationsInput
For content pages with rich fields (title, summary, details, button text, SEO):
<ContentTranslationsInput
value={content.translations || {}}
onChange={(translations) =>
setContent({ ...content, translations })
}
/>The data shape:
{
"en": {
"title": "About Us",
"summary": "Learn about our company",
"details": "<p>We are a...</p>",
"button_text": "Contact Us",
"seo": {
"title": "About - MyApp",
"description": "Learn about our company"
}
},
"ar": {
"title": "من نحن",
"summary": "تعرف على شركتنا",
"details": "<p>نحن شركة...</p>",
"button_text": "تواصل معنا",
"seo": {
"title": "من نحن - تطبيقي",
"description": "تعرف على شركتنا"
}
}
}Translation Management
The admin panel includes a dedicated Translations page (/translations) where administrators can:
- View all translation keys grouped by module
- Edit translation values for each active language
- Search for specific translation keys
- Publish translations to static JSON files served by the frontend
The publish action exports all translations for each language into a /messages/{locale}.json file that the I18n provider loads at runtime.
Date and Number Formatting
For locale-aware date and number formatting, use the browser's built-in Intl APIs with the current locale:
import { useTranslation } from "@/components/providers/i18n-provider";
function FormattedDate({ date }: { date: string }) {
const { locale } = useTranslation();
const formatted = new Intl.DateTimeFormat(locale, {
year: "numeric",
month: "long",
day: "numeric",
}).format(new Date(date));
return <time dateTime={date}>{formatted}</time>;
}
// English: "January 15, 2025"
// Arabic: "١٥ يناير ٢٠٢٥"Number formatting:
function FormattedNumber({ value }: { value: number }) {
const { locale } = useTranslation();
const formatted = new Intl.NumberFormat(locale).format(value);
return <span>{formatted}</span>;
}
// English: "1,234,567"
// Arabic: "١٬٢٣٤٬٥٦٧"Currency formatting:
function FormattedCurrency({
value,
currency = "USD",
}: {
value: number;
currency?: string;
}) {
const { locale } = useTranslation();
const formatted = new Intl.NumberFormat(locale, {
style: "currency",
currency,
}).format(value);
return <span>{formatted}</span>;
}
// English: "$1,234.56"
// Arabic: "١٬٢٣٤٫٥٦ US$"Adding a New Language
To add a new language to your FORGE application:
- Add the language in admin -- Go to Settings > Languages and create a new language entry with the language code, name, native name, and direction:
Code: fr
Name: French
Native Name: Francais
Direction: ltr
Is Active: trueAdd translations -- Go to the Translations page and add translations for all keys in the new language. Use the bulk-create or auto-translate features to speed this up.
Publish translations -- Click the "Publish" button to generate the
/messages/fr.jsonfile that the frontend will load.Add content translations -- For each content page, menu item, and role, add translations under the new language tab in the edit form.
TIP
The language switcher automatically picks up new languages from the /api/v1/languages endpoint. No code changes are needed -- just add the language through the admin panel and publish translations.
Additional Hooks
The I18n provider exports two additional hooks for specific use cases:
useI18n
Full access to the I18n context, including setLocale:
import { useI18n } from "@/components/providers/i18n-provider";
const { locale, direction, languages, isLoading, t, setLocale } = useI18n();useLanguages
Focused hook for language management:
import { useLanguages } from "@/components/providers/i18n-provider";
const { languages, locale, setLocale, isLoading } = useLanguages();What's Next
- Components -- LanguageSwitcher, TranslationsInput, and other i18n components
- Forms & Validation -- Multi-language form fields
- Web Application -- Locale-aware content pages and SEO