Skip to main content
RapidDev - Software Development Agency
weweb-tutorial

How to Create a Multi-Language Website in WeWeb

WeWeb has built-in multi-language support via More → Languages in the editor. Add language locales, translate text elements directly in the editor, and use the Change Language workflow action or Language Switcher component for user-controlled switching. Key limitations: no URL-based locale routing, no hreflang support, and button text requires workarounds. Advanced RTL and scale translation management are covered here.

What you'll learn

  • How to add language locales in WeWeb and translate text elements in the editor
  • How to detect the browser language and switch languages programmatically with a workflow action
  • How to add a Language Switcher component and handle button text translation limitations
  • How to manage translations at scale and integrate with third-party translation services
  • The SEO and RTL limitations of WeWeb's i18n system and how to work around them
Book a free consultation
4.9Clutch rating
600+Happy partners
17+Countries served
190+Team members
Advanced13 min read60–90 minWeWeb Free plan and aboveMarch 2026RapidDev Engineering Team
TL;DR

WeWeb has built-in multi-language support via More → Languages in the editor. Add language locales, translate text elements directly in the editor, and use the Change Language workflow action or Language Switcher component for user-controlled switching. Key limitations: no URL-based locale routing, no hreflang support, and button text requires workarounds. Advanced RTL and scale translation management are covered here.

Building a Multi-Language Website in WeWeb

WeWeb's built-in internationalization (i18n) system lets you translate your app's text content into multiple languages without custom code. When you add a second language, every Text and Heading element in your project gains a translation field alongside the default language content. WeWeb stores translations per element and serves the correct language based on the user's current language selection. This tutorial covers the complete setup: adding languages, translating content, detecting browser language on load, building a language switcher, handling the limitation around button text translation, managing large translation sets efficiently, and understanding the important SEO caveats of WeWeb's approach to multilingual content.

Prerequisites

  • A WeWeb project with at least one page of completed content in your primary language
  • Knowledge of the secondary language(s) you want to add, or access to a translation tool
  • Understanding of what i18n (internationalization) means conceptually
  • Basic familiarity with WeWeb variables and workflow actions

Step-by-step guide

1

Add language locales to your project

In the WeWeb editor, click the More button (three dots or 'More' in the top navigation bar) → Languages. A language management panel opens showing your current default language. Click + Add language → a dropdown of locale codes appears. Select your target language (e.g., 'fr' for French, 'de' for German, 'ar' for Arabic, 'ja' for Japanese). You can add multiple languages — there is no documented hard limit. Each language is identified by its locale code (ISO 639-1 two-letter code, sometimes with region: 'pt-BR' for Brazilian Portuguese). The first language you added when creating the project is the default — users who do not match any other language fall back to this. After adding languages, click Save. WeWeb now tracks a 'current language' state for your app. All text elements now have per-language fields when you select them.

Expected result: Your project shows multiple languages in More → Languages. Text elements now show language tabs when selected.

2

Translate text elements in the editor

With multiple languages added, select any Text, Heading, or Button element in the canvas. In the Settings panel, you now see language tabs at the top: one tab per language. The default language tab contains the original text. Click a secondary language tab (e.g., 'fr') and type or paste the translated text. The element's display updates to show the translation when that language is active. Work through each page systematically: select each text element → click the secondary language tab → enter translation → move to the next element. WeWeb does not auto-translate — all translations must be entered manually or pasted from a translation tool. To quickly identify untranslated elements: switch the editor to your secondary language (More → Languages → set as active/preview language). Any element showing its default language content is not yet translated. Tip: use a spreadsheet to track translation progress, collecting the default text and getting translations from a human translator or DeepL/Google Translate, then pasting back into WeWeb.

Expected result: Each text element has translations entered for all target languages. Switching the editor preview language shows the translated content.

3

Detect browser language and set it on page load

WeWeb does not auto-detect and apply the browser's language — you must implement this with a workflow. Create a workflow that fires on page load: go to the App workflows section (left panel → Workflows → Project workflows) or a page-level workflow → Trigger: On page load. Add a Custom JavaScript action to detect the browser language: read navigator.language, match it against your supported locales, and return the best match. Then add a Change language action using the returned locale. The Change language action takes a locale code string (e.g., 'fr', 'en'). Add this workflow to your app's On page load trigger (Project workflows) so it fires on every page. If you only add it to one page's load trigger, language switching won't persist when users navigate directly to other pages.

typescript
1// Injection point: Workflow → Custom JavaScript action
2// Place this action in a Project workflow triggered 'On app load'
3// to detect and set the browser language
4
5const supportedLocales = ['en', 'fr', 'de', 'es', 'pt-BR'];
6const defaultLocale = 'en';
7
8// Check if user has a saved preference first
9const savedLocale = localStorage.getItem('weweb_user_locale');
10if (savedLocale && supportedLocales.includes(savedLocale)) {
11 return { detectedLocale: savedLocale };
12}
13
14// Fall back to browser language detection
15const browserLang = navigator.language || navigator.userLanguage || defaultLocale;
16
17// Try exact match first (e.g., 'pt-BR')
18if (supportedLocales.includes(browserLang)) {
19 return { detectedLocale: browserLang };
20}
21
22// Try base language match (e.g., 'pt' from 'pt-BR' or 'pt-PT')
23const baseLang = browserLang.split('-')[0];
24const baseMatch = supportedLocales.find(l => l.split('-')[0] === baseLang);
25if (baseMatch) {
26 return { detectedLocale: baseMatch };
27}
28
29// Default to first supported locale
30return { detectedLocale: defaultLocale };

Expected result: On page load, the app reads the browser language and automatically switches to the matching locale if supported. Users see content in their language without manually selecting it.

4

Add a Language Switcher for user control

WeWeb includes a built-in Language Switcher component. Add it via Add element (+) → Utility → Language Switcher. Place it in your navigation bar or footer. The Language Switcher automatically shows a list of all languages you have added to the project and highlights the currently active one. Clicking a language calls the Change language action internally — no workflow setup required. To style the Language Switcher: select it → Styling panel → adjust font, colors, dropdown background, and border. If you prefer a custom language switcher design: add a Select element or a row of flag buttons. Add a workflow to each button or to the Select's On change trigger: Change language action → bind the locale to the button's value (bind each button's click to a specific locale string like 'fr'). After a language change, persist the user's choice to localStorage via a Custom JavaScript action: `localStorage.setItem('weweb_user_locale', selectedLocale);` so the On app load workflow reads it on next visit.

Expected result: Users can switch languages using the Language Switcher or custom buttons. The language persists across page navigations within the session.

5

Handle button text and input placeholder translations

Button text translation in WeWeb behaves differently from regular text elements. WeWeb's built-in Button element may not support per-language text binding the same way Text elements do — this is a documented limitation. Workaround 1: bind the button's text property to a formula that returns different text per language: `if(wwContext.lang === 'fr', 'Envoyer', 'Submit')`. For more languages: `{en: 'Submit', fr: 'Envoyer', de: 'Absenden'}[wwContext.lang] || 'Submit'`. Workaround 2: instead of using the built-in button text field, add a Text element inside a Container that acts as a button. Use the Text element (which does support i18n tabs) for the label, and add click workflows to the Container. For Input placeholders: bind the placeholder property to a language-conditional formula: `wwContext.lang === 'fr' ? 'Votre adresse e-mail' : 'Your email address'`. For all translatable strings, using a JSON-based translation object stored in an App-level Variable is the most maintainable approach at scale.

typescript
1// Injection point: App-level Variable (Data panel → Variables → New → Object)
2// Name: translations
3// Default value: paste the JSON object below
4// Then reference in bindings as: translations[wwContext.lang].button_submit
5
6// Example translations object structure:
7{
8 "en": {
9 "button_submit": "Submit",
10 "button_next": "Next",
11 "button_back": "Back",
12 "placeholder_email": "Your email address",
13 "placeholder_name": "Your full name",
14 "error_required": "This field is required",
15 "success_message": "Thank you! We'll be in touch.",
16 "nav_home": "Home",
17 "nav_about": "About",
18 "nav_contact": "Contact"
19 },
20 "fr": {
21 "button_submit": "Envoyer",
22 "button_next": "Suivant",
23 "button_back": "Retour",
24 "placeholder_email": "Votre adresse e-mail",
25 "placeholder_name": "Votre nom complet",
26 "error_required": "Ce champ est obligatoire",
27 "success_message": "Merci ! Nous vous contacterons bientôt.",
28 "nav_home": "Accueil",
29 "nav_about": "À propos",
30 "nav_contact": "Contact"
31 },
32 "de": {
33 "button_submit": "Absenden",
34 "button_next": "Weiter",
35 "button_back": "Zurück",
36 "placeholder_email": "Ihre E-Mail-Adresse",
37 "placeholder_name": "Ihr vollständiger Name",
38 "error_required": "Dieses Feld ist erforderlich",
39 "success_message": "Vielen Dank! Wir melden uns.",
40 "nav_home": "Startseite",
41 "nav_about": "Über uns",
42 "nav_contact": "Kontakt"
43 }
44}

Expected result: Button text, placeholders, and error messages display in the correct language. Strings not directly supported by WeWeb's i18n tabs are handled via the translations object.

6

Manage translations at scale with a spreadsheet export

For projects with hundreds of translatable strings, manually entering translations in the WeWeb editor is tedious and error-prone. A more scalable workflow: (1) Build your app fully in the default language. (2) Export all translatable strings to a spreadsheet: use a Custom JavaScript action in a development workflow that collects all visible text and outputs it as CSV — or manually copy strings from the editor. (3) Send the spreadsheet to a human translator or process it through DeepL API. (4) Import translations back: paste each translated string into the corresponding element's language tab. Alternatively, store all translations in a Supabase 'translations' table with columns: key, locale, value. Fetch this table on app load into an App-level translations variable. Bind all text elements to `translationsData[current_key][currentLang]`. This approach lets you update translations without republishing the WeWeb app — update the database and the translations update live. The tradeoff: more complex setup but zero-downtime translation updates.

Expected result: You have a repeatable process for collecting, translating, and importing strings. For large projects, translations are managed in a database rather than hardcoded in the editor.

7

Understand SEO and RTL limitations

WeWeb's i18n system has two significant limitations for international SEO and RTL languages. SEO: WeWeb does not support URL-based locale routing (/en/page, /fr/page). All languages are served at the same URL. This means search engines cannot index language-specific pages separately — you cannot have French content indexed at /fr/contact and English at /en/contact. There is also no hreflang tag support, which tells search engines about language variants. For SEO-critical multilingual sites, a dedicated CMS/SSR platform is a better choice than WeWeb. For app-style authenticated sections where SEO is less critical, WeWeb's approach is acceptable. RTL languages (Arabic, Hebrew, Farsi): WeWeb does not automatically apply dir='rtl' or switch text alignment. For RTL support, add a Custom JavaScript action on language change: when locale is 'ar', set `document.documentElement.setAttribute('dir', 'rtl')` and apply an RTL CSS class. Add RTL-specific styles in App Settings → Custom Code → Head with `[dir='rtl']` selectors to mirror flexbox direction, text alignment, and padding. This is a manual workaround — WeWeb does not natively handle RTL layout mirroring.

typescript
1// Injection point: Workflow → Custom JavaScript action
2// Add this action AFTER the Change language action in your language switcher workflow
3// It enables RTL layout when Arabic or Hebrew is selected
4
5const locale = variables.currentLocale; // or read from wwContext.lang
6const rtlLocales = ['ar', 'he', 'fa', 'ur'];
7
8if (rtlLocales.includes(locale)) {
9 document.documentElement.setAttribute('dir', 'rtl');
10 document.documentElement.setAttribute('lang', locale);
11 document.body.classList.add('rtl-layout');
12} else {
13 document.documentElement.setAttribute('dir', 'ltr');
14 document.documentElement.setAttribute('lang', locale);
15 document.body.classList.remove('rtl-layout');
16}
17
18// Also update the lang attribute for accessibility
19return { direction: rtlLocales.includes(locale) ? 'rtl' : 'ltr' };

Expected result: RTL languages apply dir='rtl' to the document. SEO limitations are documented and you have a strategy (separate subdomain for SEO pages, or accepting the limitation for app-only sections).

Complete working example

language-detection-and-rtl.js
1// Injection point: Project workflow → On app load → Custom JavaScript action
2// Full language detection, persistence, and RTL initialization
3
4const supportedLocales = ['en', 'fr', 'de', 'es', 'ar'];
5const rtlLocales = ['ar', 'he', 'fa', 'ur'];
6const defaultLocale = 'en';
7
8// Step 1: Determine locale to use
9let targetLocale = defaultLocale;
10
11// Check localStorage for saved preference
12const saved = localStorage.getItem('app_locale');
13if (saved && supportedLocales.includes(saved)) {
14 targetLocale = saved;
15} else {
16 // Detect from browser
17 const browser = (navigator.language || 'en').toLowerCase();
18 const exact = supportedLocales.find(l => l.toLowerCase() === browser);
19 const base = supportedLocales.find(l => l.split('-')[0] === browser.split('-')[0]);
20 targetLocale = exact || base || defaultLocale;
21}
22
23// Step 2: Apply RTL if needed
24const isRTL = rtlLocales.includes(targetLocale);
25document.documentElement.setAttribute('dir', isRTL ? 'rtl' : 'ltr');
26document.documentElement.setAttribute('lang', targetLocale);
27
28if (isRTL) {
29 document.body.classList.add('rtl-layout');
30} else {
31 document.body.classList.remove('rtl-layout');
32}
33
34// Step 3: Return the locale so the next workflow action
35// (Change language) can use it
36return {
37 locale: targetLocale,
38 isRTL: isRTL
39};
40
41// After this action, add:
42// Action: Change language → bind locale to workflowResult.locale

Common mistakes

Why it's a problem: Adding the language detection workflow only to one page's On page load instead of the app-level On app load trigger

How to avoid: Place language detection in a Project workflow triggered by 'On app load' (not a page-level trigger). This ensures the language is set before any page content renders, regardless of which page the user lands on. Page-level triggers only fire when the user visits that specific page.

Why it's a problem: Trying to use URL-based language routing (/fr/about) expecting WeWeb to serve different language content per path

How to avoid: WeWeb does not support URL-based locale routing natively. All languages are served from the same URL path. For truly URL-separated language content, you need SSR or a headless CMS with separate URL handling. For WeWeb, use language detection + the Change language action, accepting the single-URL limitation.

Why it's a problem: Leaving button text untranslated because the button's text field doesn't show language tabs

How to avoid: Button text fields sometimes don't expose per-language tabs. Use a binding formula instead: `{en: 'Submit', fr: 'Envoyer'}[wwContext.lang] || 'Submit'`. Or use the translations Object variable approach where all strings are keyed by locale in a single variable. Reference them as translations[wwContext.lang].button_submit.

Why it's a problem: Not persisting the user's language selection to localStorage, causing the language to reset on page refresh

How to avoid: After the Change language action in your language switcher workflow, add a Custom JavaScript action: `localStorage.setItem('app_locale', selectedLocale)`. In the On app load detection workflow, check localStorage first before falling back to browser detection. This ensures the user's chosen language persists across sessions.

Best practices

  • Add all target languages before starting translation so you translate each element once rather than going back multiple times
  • Use a translations Object variable for strings not supported by WeWeb's i18n tabs (button text, placeholders, error messages)
  • Persist the user's language choice to localStorage so it survives page refreshes and return visits
  • Place language detection in a Project-level 'On app load' workflow, not a page-level trigger, so all pages initialize with the correct language
  • For RTL language support, use a Custom JavaScript action to set dir='rtl' on the document and add RTL-specific CSS via App Settings → Custom Code
  • Accept that WeWeb's single-URL approach is unsuitable for SEO-critical multilingual content — use a separate subdomain/SSR platform for public SEO pages
  • Test each language on a real device — mobile keyboards and input methods behave differently per language

Still stuck?

Copy one of these prompts to get a personalized, step-by-step explanation.

ChatGPT Prompt

I'm building a multilingual WeWeb app (English, French, German). WeWeb's i18n is tag-based (no URL routing). I need: (1) browser language auto-detection on app load, (2) a localStorage save when user switches languages, (3) handling for strings in button text that don't support WeWeb's language tabs natively. Write the JavaScript for the detection workflow and the translations object structure I should use for a translation key approach.

WeWeb Prompt

In WeWeb, I've added French and German as languages. I have a language switcher button group in my nav (3 buttons: EN, FR, DE). I need each button to: (1) call the Change language action with its locale, (2) save the locale to localStorage, (3) update the document lang attribute. I also need the On app load workflow to detect the browser language and set it. Write the workflow configurations and Custom JavaScript action code for these two scenarios.

Frequently asked questions

Does WeWeb support URL-based language routing like /en/about and /fr/about?

No. As of March 2026, WeWeb does not support URL-based locale routing. All language variants are served from the same URL path — /about shows English or French depending on the active language state, not the URL. This is a fundamental limitation for multilingual SEO (no separate indexable URLs per language, no hreflang support). For SEO-critical multilingual content, consider a separate SSR platform. For authenticated app sections where SEO is less important, WeWeb's single-URL approach is sufficient.

How many languages can I add to a WeWeb project?

WeWeb does not publish a hard limit on the number of languages. In practice, projects with 5–10 languages are feasible. Performance impact is minimal since WeWeb only serves the active language's content. The main practical limit is translation management effort — with 10+ languages, a database-backed translations approach (storing strings in Supabase and fetching by locale) is much more maintainable than entering translations in the editor for every element.

Can WeWeb's i18n system handle right-to-left languages like Arabic?

WeWeb does not natively apply RTL layout mirroring when you add Arabic or Hebrew as a language. You must implement RTL support manually: use a Custom JavaScript action on language change to set dir='rtl' on the document.documentElement, add RTL-specific CSS in App Settings → Custom Code → Head using [dir='rtl'] selectors to mirror flexbox direction (row-reverse), flip padding/margin, and adjust text alignment. This requires testing every page layout in RTL mode.

How do I translate content that comes from my Supabase database, not from WeWeb text elements?

Database content requires a different approach from UI text: store translated versions in the database itself. Options: (1) separate columns per language (name_en, name_fr, name_de) and bind display elements to the column matching wwContext.lang: `product[`name_${wwContext.lang}`]`, (2) a separate translations table with columns: content_type, content_id, locale, field_name, value — join it in your queries, (3) a JSONB column storing all translations: {en: 'Product name', fr: 'Nom du produit'}. Approach 1 (separate columns) is simplest for WeWeb bindings. Approach 3 (JSONB) is most flexible.

RapidDev

Talk to an Expert

Our team has built 600+ apps. Get personalized help with your project.

Book a free consultation

Need help with your project?

Our experts have built 600+ apps and can accelerate your development. Book a free consultation — no strings attached.

Book a free consultation

We put the rapid in RapidDev

Need a dedicated strategic tech and growth partner? Discover what RapidDev can do for your business! Book a call with our team to schedule a free, no-obligation consultation. We'll discuss your project and provide a custom quote at no cost.