Skip to content
SaaS4Builders
Internationalization

Internationalization Overview

How SaaS4Builders handles multi-language support: dual-layer translation architecture, included locales, adding or removing languages, locale detection, and what gets translated.

SaaS4Builders is fully internationalized. The boilerplate ships with four locales — English, French, Spanish, and Italian — complete with translations for every user-facing string. The locale list is entirely yours to customize: you can add new languages, remove ones you don't need, or replace the defaults altogether. The i18n system spans both the backend and frontend, using two independent but complementary translation layers.


Architecture at a Glance

The translation system is split into two layers, each using the conventions native to its stack:

LayerTechnologyStorageScope
BackendLaravel lang files + Astrotomic TranslatablePHP arrays in backend/lang/ + database translation tablesAPI messages, emails, PDF exports, model data (product names, plan descriptions)
FrontendNuxt i18n module (@nuxtjs/i18n)JSON files in frontend/i18n/locales/UI labels, navigation, form validation messages, error messages

These two layers are independent — the backend does not serve translations to the frontend. Each stack loads its own translations at its own level, which means:

  • Backend translations are used for API error messages, email content, invoice PDFs, and any server-rendered text
  • Frontend translations are used for everything the user sees in the browser
  • Catalog data (product names, plan descriptions, feature names) lives in the database with per-locale rows, managed by the Astrotomic Translatable package

Included Locales

The boilerplate ships with complete translations for four locales:

CodeLanguageBCP 47 TagDirection
enEnglishen-USLTR
frFrançaisfr-FRLTR
esEspañoles-ESLTR
itItalianoit-ITLTR

Both stacks use the same locale codes (en, fr, es, it). The BCP 47 tags (e.g., en-US) are used on the frontend for locale-aware formatting of numbers, currencies, and dates via the browser's Intl API.

Translation coverage per included locale:

LayerFilesApproximate Keys per Locale
Backend16 PHP files per locale (64 total)~320
Frontend1 JSON file per locale (4 total)~1,700
These four locales are a starting point, not a limit. The i18n system is designed to be extended. You can add any locale by providing the corresponding translation files and updating the configuration — no code changes required. Conversely, you can remove locales you don't need by deleting their translation files and updating the locale list. See Backend Translations — How to Add a 5th Locale and Frontend i18n — How to Add a 5th Locale for step-by-step instructions.

Backend Locale Detection

The backend determines the active locale through the SetLocaleFromHeader middleware, which runs on every API request:

backend/app/Http/Middleware/SetLocaleFromHeader.php
public function handle(Request $request, Closure $next): Response
{
    // Priority 1: Query parameter ?lang=xx
    $langParam = $request->query('lang');

    if ($langParam !== null && is_string($langParam)) {
        $supportedLocales = config('app.supported_locales', ['en']);
        $language = $this->extractLanguageCode($langParam);

        if (in_array($language, $supportedLocales, true)) {
            App::setLocale($language);
            return $next($request);
        }
    }

    // Priority 2: Accept-Language header
    $locale = $this->parseAcceptLanguage($request->header('Accept-Language'));
    App::setLocale($locale);

    return $next($request);
}

Resolution priority:

  1. Query parameter (?lang=fr) — explicit override, highest priority
  2. Accept-Language header — respects RFC quality values (e.g., fr;q=0.9, en;q=0.8)
  3. Fallback — defaults to en if no supported locale is found

The active locale list is configured via environment variable — add or remove codes here to control which languages your application accepts:

APP_SUPPORTED_LOCALES=en,fr,es,it
APP_LOCALE=en
APP_FALLBACK_LOCALE=en

Frontend Locale Detection

The frontend uses the @nuxtjs/i18n module with a URL prefix strategy — the locale appears in the URL path:

/en/dashboard    → English
/fr/dashboard    → French
/es/dashboard    → Spanish
/it/dashboard    → Italian
frontend/nuxt.config.ts
i18n: {
  defaultLocale: process.env.NUXT_PUBLIC_LOCALE || 'en',
  locales,
  strategy: 'prefix',
  detectBrowserLanguage: {
    useCookie: true,
    cookieKey: 'i18n_redirected',
    redirectOn: 'root',
  },
},

On first visit, the browser's preferred language is detected and stored in the i18n_redirected cookie. Subsequent visits use the cookie value to redirect from the root URL to the appropriate locale prefix. Users can switch locales at any time through the language selector in the UI.

The locale definitions live in a dedicated configuration file:

frontend/i18n/locales.config.ts
export const localeCodes = ['en', 'fr', 'es', 'it'] as const
export type LocaleCode = (typeof localeCodes)[number]

export const locales: LocaleObject[] = [
  { code: 'en', name: 'English', language: 'en-US', dir: 'ltr', file: 'en.json' },
  { code: 'fr', name: 'Français', language: 'fr-FR', dir: 'ltr', file: 'fr.json' },
  { code: 'es', name: 'Español', language: 'es-ES', dir: 'ltr', file: 'es.json' },
  { code: 'it', name: 'Italiano', language: 'it-IT', dir: 'ltr', file: 'it.json' },
]

The language field maps each short code to its BCP 47 tag, which the Intl API uses for locale-aware formatting of currencies, numbers, and dates.


What Gets Translated

Translation content falls into three categories, each handled differently:

System Messages (Backend PHP Files)

API error messages, email subjects and bodies, invoice PDF labels, notification text, and validation messages. These live in PHP array files under backend/lang/{locale}/, organized by domain.

backend/lang/en/team.php
'invitation_sent' => 'Invitation sent successfully.',
'seat_limit_reached' => 'Seat limit reached: :current/:max seats used.',

UI Strings (Frontend JSON Files)

Navigation labels, button text, form labels, page titles, and client-side error messages. These live in JSON files under frontend/i18n/locales/.

frontend/i18n/locales/en.json
{
  "navigation": {
    "sidebar": {
      "dashboard": "Dashboard",
      "billing": "Billing",
      "team": "Team"
    }
  }
}

Catalog Data (Database Translations)

Product names, plan descriptions, and feature names — content that is managed by platform administrators and may change at runtime. These are stored in dedicated translation tables in the database, managed by the astrotomic/laravel-translatable package.

ModelTranslatable Fields
Productname, description
Planname, description
Featurename, description
Catalog translations are stored in the database (not in PHP files) because they are managed through admin interfaces and may differ between deployments. System messages and UI strings are part of the codebase and ship with the boilerplate.

What's Next

  • Backend Translations — Laravel lang file structure, model translations with Astrotomic Translatable, and how to add new translation keys
  • Frontend i18n — Nuxt i18n module setup, JSON locale files, Zod validation integration, and currency formatting