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:
| App | Translation files | Files per language |
|---|---|---|
| Student site | frontend/messages/<namespace>/<locale>.json | 22 |
| Admin dashboard | admin-dashboard/messages/<namespace>/<locale>.json | 24 |
| API | back-end/src/i18n/translations/<locale>/<namespace>.json | 18 |
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:
/coursesgoes 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-Languageheader, 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.
frontendgrep -rn "All rights reserved" messagesThat finds the footer's copyright line, key footer.copyright in messages/layouts/en.json. Change the same key in ar.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 devpicks 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:
{ "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-Languageheader. - 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>.jsonthroughI18nServiceinsrc/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,enas 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 buildcopies them intodist/.
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/.
Add the code to the locale list
src/config/locales.tsis the one list the student site reads: the routes, the redirect, the message loader, the script that setslanganddirbefore 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 tolocales, then give it an entry in the three maps beside it. The compiler points at any you miss.frontend/src/config/locales.tsexport 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" };Entry What 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. Create the message files
Copy every English file next to itself under the new code, then translate the values. Keep the keys.
Terminalinfrontendfind messages -name en.json -exec sh -c 'cp "$1" "$(dirname "$1")/fr.json"' _ {} \;In PowerShell:
TerminalinfrontendGet-ChildItem messages -Recurse -Filter en.json | ForEach-Object { Copy-Item $_.FullName (Join-Path $_.DirectoryName 'fr.json') }Check the fonts
Inter is loaded with the
latinsubset insrc/app/layout.tsx, which covers French, Spanish or German. Addlatin-extfor languages such as Polish, Czech or Turkish. For another script, load a font for it there and add a rule for itslanginsrc/styles/base.css, as the Arabic one does.Open the new language
Restart
yarn devand 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
hreflangalternates 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/.
Add the code to the three lists
localesinsrc/config/i18n.ts, which loads the messages.localesinsrc/proxy.ts, which redirects addresses without a language.localesinsrc/app/[locale]/layout.tsx, which decides which addresses exist.
Create the message files
Copy every English file under the new code, then translate the values.
Terminalinadmin-dashboardfind messages -name en.json -exec sh -c 'cp "$1" "$(dirname "$1")/fr.json"' _ {} \;Update the helpers
src/hooks/locale/useLocale.tsacceptsen,fr,esandar. Add your code if it is not one of them.src/hooks/locale/useDirection.tstreats onlyaras right to left. Extend the check for another right-to-left language.src/lib/api/client-utils/locale.tsandsrc/lib/api/client-utils/auth.tsrecognise onlyenandarin the address. Add your code.
Add it to the language switcher
In
src/components/LanguageSwitcher.tsx, import the flag fromcountry-flag-icons/react/3x2and add an entry tolanguages.admin-dashboard/src/components/LanguageSwitcher.tsximport { 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 }, ];Check the fonts
The fonts are loaded in
src/app/layout.tsx, andsrc/styles/base.cssswitches to the Arabic face forhtml[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/.
Copy the translation folder
Then translate the 18 files in it.
Terminalinback-endcp -R src/i18n/translations/en src/i18n/translations/frIn PowerShell:
Terminalinback-endCopy-Item -Recurse src/i18n/translations/en src/i18n/translations/frAdd the code to the supported list
SUPPORTED_LOCALESinsrc/i18n/locales.tsis 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.tsexport const SUPPORTED_LOCALES = ['en', 'ar', 'fr'] as const;Check the two places that test for Arabic
src/modules/mail/templates/layout.tslays an email out right to left only forar; extend it for another right-to-left language. The two searches,src/modules/search/search.service.tsandsrc/modules/public/student-search.service.ts, read a stored title in Arabic or English only, because content has two languages. List them:Terminalinback-endgrep -rn "=== 'ar'" srcRestart the API
It reads the translation files at start-up. For production,
yarn buildcopies them intodist/.