Skip to the article
Aniq-UI

LearnioLanguages

Languages

How Learnio picks a language, where every translation lives, and how to change a string or add a new language to each app.

For the Full Stack package

The languages that ship

Every app ships in English, the default, and Arabic, which is laid out right to left. Each app keeps its own translations:

AppTranslation filesFiles per language
Student sitefrontend/messages/<namespace>/<locale>.json22
Admin dashboardadmin-dashboard/messages/<namespace>/<locale>.json24
APIback-end/src/i18n/translations/<locale>/<namespace>.json18

A namespace is one area of the app, such as layouts, auth or courses. Some are nested folders, for example messages/dashboard/courses/en.json.

How the language is chosen

The language is the first part of the address: /en/courses or /ar/courses. There is no language cookie and no detection from the browser.

  • An address without a language is redirected to the default, English: /courses goes to /en/courses. In the admin, / goes to /en/dashboard.
  • The language switcher opens the same page under the other prefix.
  • Every request to the API carries the page's language in the Accept-Language header, so the API answers in it.

To open in Arabic by default, set defaultLocale to "ar" in src/config/locales.ts on the student site, and in src/proxy.ts in the admin.

Change a string

Every word on screen comes from a message file. Search for the text you see to find its key, then change it in every language.

Terminalin frontend
grep -rn "All rights reserved" messages

That finds the footer's copyright line, key footer.copyright in messages/layouts/en.json. Change the same key in ar.json:

frontend/messages/layouts/en.json
"footer": {
  "copyright": "© {year} Learnio. All rights reserved."
}
  • Keep placeholders such as {year} and tags such as <brand> exactly as they are. The code fills them in.
  • Change the key in every language file. A key missing from one language shows its path instead of text.
  • yarn dev picks the change up. A production build includes the messages, so rebuild it.

Notifications are the same: the API stores only a type, such as enrollment_created, and each app words it from its own files. In the admin they are under header.notifications.types in messages/dashboard/<locale>.json; on the student site under notifications.types in the same file.

Courses, posts and other content

Text you write in the admin, such as course titles, lessons, categories, blog posts and instructor profiles, is stored once with both languages, as a JSON value with an en and an ar key:

A category name in the database
{ "en": "Project Management", "ar": "إدارة المشاريع" }
  • The admin's forms show an English and an Arabic tab for each of these fields.
  • The API returns both values, and each frontend shows the one for the page's language. If that one is empty, it shows the other, so a course is never nameless.
  • Search results are already resolved by the API, from the Accept-Language header.
  • In the demo data from yarn seed, categories, blog posts, instructor profiles, quizzes and course section titles have Arabic text. Course titles, course descriptions and lesson titles carry the English text under both keys, so translate them in the admin.

Content has exactly two languages

The API's validation expects an en and an ar value on these fields and rejects any other key, and the admin's editors have two tabs. Adding a third content language means changing the entities and the DTOs in the API, the admin's multi-language inputs and its localizedText helper. The student site's localizedText already reads the key that matches the page's language. Without those changes, a page in a new interface language shows the English content.

What the API translates

  • Response messages, such as success and error text the admin shows in its alerts, come from src/i18n/translations/<locale>/<namespace>.json through I18nService in src/i18n/i18n.service.ts.
  • Emails are worded in mail.json. Verification and password-reset emails use the language of the request. Receipts and enrolment confirmations use the language the order was placed in. Arabic emails are laid out right to left.
  • Country names are in countries.json.

The language comes from the Accept-Language header. A regional code such as ar-SA counts as ar. A request with no header, or in a language the API does not support, gets the default language. A key missing from a language falls back to the default language's text, then to the key itself.

  • The default language is the Default Language entry (default_locale) in the admin's Settings, App Settings tab, en as seeded. A change applies to the next request, with no restart. The API refuses a value that is not one of its languages, so a typo cannot leave it answering in a language with no translations.
  • The API reads its translation files once, when it starts: restart it after editing one. yarn build copies them into dist/.

Add a language to the student site

The steps use French, fr, as the example. Use a two-letter lowercase code: several helpers recognise the language as a two-letter first segment of the address. Paths are inside frontend/.

  1. Add the code to the locale list

    src/config/locales.ts is the one list the student site reads: the routes, the redirect, the message loader, the script that sets lang and dir before the first paint, the language switcher, the API requests, the sign-in redirect, the page metadata, the sitemap, and every number and date it formats. Add the code to locales, then give it an entry in the three maps beside it. The compiler points at any you miss.

    frontend/src/config/locales.ts
    export const locales = ["en", "ar", "fr"] as const;
    
    export const OG_LOCALES: Record<Locale, string> = { en: "en_US", ar: "ar_AE", fr: "fr_FR" };
    export const LOCALE_FLAGS: Record<Locale, FlagIconCode> = { en: "US", ar: "AE", fr: "FR" };
    export const LOCALE_FORMAT_TAGS: Record<Locale, string> = { en: "en-US", ar: "ar-EG", fr: "fr-FR" };
    EntryWhat it sets
    OG_LOCALESThe language of the page's sharing card (og:locale)
    LOCALE_FLAGSThe flag the language switcher shows, as a country code
    LOCALE_FORMAT_TAGSHow numbers and dates are written. Use a tag with a region, such as fr-FR, so the digits and month names do not depend on the browser.
    RTL_LOCALESAdd the code for a language written right to left.
    JOINING_SCRIPT_LOCALESAdd the code for a script whose letters join, as Arabic's do. The home page's wordmark is then drawn as outlines, because Safari leaves such letters unjoined in SVG text.
  2. Create the message files

    Copy every English file next to itself under the new code, then translate the values. Keep the keys.

    Terminalin frontend
    find messages -name en.json -exec sh -c 'cp "$1" "$(dirname "$1")/fr.json"' _ {} \;

    In PowerShell:

    Terminalin frontend
    Get-ChildItem messages -Recurse -Filter en.json | ForEach-Object { Copy-Item $_.FullName (Join-Path $_.DirectoryName 'fr.json') }
  3. Check the fonts

    Inter is loaded with the latin subset in src/app/layout.tsx, which covers French, Spanish or German. Add latin-ext for languages such as Polish, Czech or Turkish. For another script, load a font for it there and add a rule for its lang in src/styles/base.css, as the Arabic one does.

  4. Open the new language

    Restart yarn dev and open localhost:3030/frLocal.

    Expected result: The page shows your translated text, the switcher lists the new language, and every page names it in its hreflang alternates and in the sitemap.

The checkout needs the API to know the language

The checkout sends the page's language with the order, and the API accepts only the languages in its own list there. Until the code is added to the API as well (Add a language to the API), a purchase made from a page in the new language is refused.

Add a language to the admin dashboard

The admin keeps its language list in three files. The steps use fr; keep to a two-letter lowercase code. Paths are inside admin-dashboard/.

  1. Add the code to the three lists

    • locales in src/config/i18n.ts, which loads the messages.
    • locales in src/proxy.ts, which redirects addresses without a language.
    • locales in src/app/[locale]/layout.tsx, which decides which addresses exist.
  2. Create the message files

    Copy every English file under the new code, then translate the values.

    Terminalin admin-dashboard
    find messages -name en.json -exec sh -c 'cp "$1" "$(dirname "$1")/fr.json"' _ {} \;
  3. Update the helpers

    • src/hooks/locale/useLocale.ts accepts en, fr, es and ar. Add your code if it is not one of them.
    • src/hooks/locale/useDirection.ts treats only ar as right to left. Extend the check for another right-to-left language.
    • src/lib/api/client-utils/locale.ts and src/lib/api/client-utils/auth.ts recognise only en and ar in the address. Add your code.
  4. Add it to the language switcher

    In src/components/LanguageSwitcher.tsx, import the flag from country-flag-icons/react/3x2 and add an entry to languages.

    admin-dashboard/src/components/LanguageSwitcher.tsx
    import { US, SA, FR } from "country-flag-icons/react/3x2";
    
    const languages = [
      { code: "en", country: "US" as const, Flag: US },
      { code: "ar", country: "SA" as const, Flag: SA },
      { code: "fr", country: "FR" as const, Flag: FR },
    ];
  5. Check the fonts

    The fonts are loaded in src/app/layout.tsx, and src/styles/base.css switches to the Arabic face for html[lang="ar"]. A language in another script needs its own font and a matching rule.

The brand names in src/config/brand.config.ts have only en and ar values, and the English one is shown for any other language.

Add a language to the API

Paths are inside back-end/.

  1. Copy the translation folder

    Then translate the 18 files in it.

    Terminalin back-end
    cp -R src/i18n/translations/en src/i18n/translations/fr

    In PowerShell:

    Terminalin back-end
    Copy-Item -Recurse src/i18n/translations/en src/i18n/translations/fr
  2. Add the code to the supported list

    SUPPORTED_LOCALES in src/i18n/locales.ts is the API's one list. The translations, the checkout's validation, the language frozen on each order, the payment pages, the emails and their links into both frontends all read it. Until the code is there, requests in the new language get the default language.

    back-end/src/i18n/locales.ts
    export const SUPPORTED_LOCALES = ['en', 'ar', 'fr'] as const;
  3. Check the two places that test for Arabic

    src/modules/mail/templates/layout.ts lays an email out right to left only for ar; extend it for another right-to-left language. The two searches, src/modules/search/search.service.ts and src/modules/public/student-search.service.ts, read a stored title in Arabic or English only, because content has two languages. List them:

    Terminalin back-end
    grep -rn "=== 'ar'" src
  4. Restart the API

    It reads the translation files at start-up. For production, yarn build copies them into dist/.

Stuck on a step?

Find a fix before you start over.

Troubleshooting

Cookie Preferences

We use cookies to enhance your browsing experience, analyze site traffic, and personalize content. By clicking "Accept All", you consent to our use of cookies for analytics and personalized advertising.