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

How to Use Localization in Retool Apps

Retool has a built-in Component Localization feature (public beta) that translates component labels, placeholders, and error messages into 7 languages. For full app text translation, store a JSON translations object in a Transformer or Temporary State and reference it with {{ translations[locale.value].key }}. Switch locale with a language Select dropdown that updates a locale Temporary State variable.

What you'll learn

  • Enable Retool's built-in Component Localization for supported languages via App Settings
  • Build a JSON translation lookup table and switch languages using a Temporary State locale variable
  • Reference translated strings with {{ translations[locale.value].key }} in component text properties
  • Switch locale dynamically with a language Select dropdown that calls await locale.setValue()
  • Format dates and numbers for different locales using Intl.DateTimeFormat and Intl.NumberFormat
Book a free consultation
4.9Clutch rating
600+Happy partners
17+Countries served
190+Team members
Beginner9 min read20-25 minRetool Cloud and Self-hostedLast updated March 2026RapidDev Engineering Team
TL;DR

Retool has a built-in Component Localization feature (public beta) that translates component labels, placeholders, and error messages into 7 languages. For full app text translation, store a JSON translations object in a Transformer or Temporary State and reference it with {{ translations[locale.value].key }}. Switch locale with a language Select dropdown that updates a locale Temporary State variable.

Quick facts about this guide
FactValue
ToolRetool
DifficultyBeginner
Time required20-25 min
CompatibilityRetool Cloud and Self-hosted
Last updatedMarch 2026

Two Approaches to Localization in Retool Apps

Retool supports localization through two separate mechanisms. The first is the built-in Component Localization feature (public beta as of March 2026), which automatically translates component UI elements like validation error messages, date picker labels, and button text into one of 7 supported languages. This is configured at the app level in App Settings.

The second approach is a developer-built JSON translation strategy: you define a translations object mapping locale codes to key-value pairs for all your custom text strings, store it in a Transformer, and reference keys with {{ translations[locale.value].heading }}. A language Select dropdown updates a Temporary State locale variable, and all text expressions update reactively.

This tutorial covers both approaches. The built-in localization handles component chrome (static framework text). The JSON strategy handles your app's custom content. Production multi-language apps use both together.

Prerequisites

  • A Retool app with text-heavy components (Text, Button, Form labels)
  • Familiarity with Temporary State variables and Transformers
  • Understanding of {{ }} expression syntax

Step-by-step guide

1

Enable built-in Component Localization in App Settings

Open your Retool app. Click the Settings gear icon in the top toolbar and navigate to App Settings → Localization. In the Language dropdown, select the language you want the component UI to use: English, Spanish, French, German, Portuguese, Japanese, or Chinese (Simplified). This changes the language of built-in component text — validation error messages like 'This field is required', date picker month/day names, and select component placeholder text. It does not translate your custom labels or text content.

Expected result: Form validation messages, date picker labels, and component placeholders appear in the selected language for all app users.

2

Create a translations Transformer with your app's text strings

For translating your own text content, create a Transformer named translations. This transformer returns a JavaScript object keyed by locale code (e.g., 'en', 'es', 'fr') where each locale key maps to an object of translation strings. The transformer re-executes whenever its dependencies change — it has no dependencies here, so it evaluates once at load time as a static lookup table.

typescript
1// Transformer: translations
2// Returns a static lookup object — no dependencies, evaluates once
3// Transformers are read-only: cannot trigger queries or setValue()
4
5return {
6 en: {
7 welcomeHeading: 'Welcome to the Dashboard',
8 customerSection: 'Customer Management',
9 newCustomer: 'New Customer',
10 saveButton: 'Save',
11 cancelButton: 'Cancel',
12 confirmDelete: 'Are you sure you want to delete this record?',
13 searchPlaceholder: 'Search by name or email...',
14 noResults: 'No results found',
15 loadingMessage: 'Loading...',
16 },
17 es: {
18 welcomeHeading: 'Bienvenido al Panel',
19 customerSection: 'Gestión de Clientes',
20 newCustomer: 'Nuevo Cliente',
21 saveButton: 'Guardar',
22 cancelButton: 'Cancelar',
23 confirmDelete: '¿Estás seguro de que quieres eliminar este registro?',
24 searchPlaceholder: 'Buscar por nombre o correo...',
25 noResults: 'No se encontraron resultados',
26 loadingMessage: 'Cargando...',
27 },
28 fr: {
29 welcomeHeading: 'Bienvenue sur le Tableau de Bord',
30 customerSection: 'Gestion des Clients',
31 newCustomer: 'Nouveau Client',
32 saveButton: 'Enregistrer',
33 cancelButton: 'Annuler',
34 confirmDelete: 'Êtes-vous sûr de vouloir supprimer cet enregistrement ?',
35 searchPlaceholder: 'Rechercher par nom ou email...',
36 noResults: 'Aucun résultat trouvé',
37 loadingMessage: 'Chargement...',
38 },
39};

Expected result: {{ translations.value }} is an object accessible app-wide. {{ translations.value.en.saveButton }} returns 'Save'.

3

Create a locale Temporary State variable

Create a Temporary State variable named locale with a default value of 'en' (or whichever locale you want on first load). This variable is the single source of truth for which language is currently active. All translation expressions will reference {{ locale.value }} to pick the right locale from the translations object.

typescript
1// Temporary State: locale
2// Default value: 'en'
3// Type: String
4
5// Usage in component text:
6{{ translations.value[locale.value].welcomeHeading }}
7
8// Fallback to English if locale key not found:
9{{ translations.value[locale.value]?.welcomeHeading || translations.value.en.welcomeHeading }}
10
11// Read current locale anywhere:
12{{ locale.value }}
13// Returns: 'en' | 'es' | 'fr' etc.

Expected result: locale.value is 'en' on load. Changing it causes all {{ translations.value[locale.value].key }} expressions to update.

4

Add a language Select dropdown for locale switching

Add a Select component named languageSelect to your app header or settings area. Configure it with static options: Label 'English', Value 'en'; Label 'Español', Value 'es'; Label 'Français', Value 'fr'. Set the Default Value to {{ locale.value }}. Add a Change event handler: trigger a JS Query named switchLocale that calls await locale.setValue(languageSelect.value).

typescript
1// languageSelect static options:
2// Label: English Value: en
3// Label: Español Value: es
4// Label: Français Value: fr
5// Label: Deutsch Value: de
6
7// Default Value: {{ locale.value }}
8
9// JS Query: switchLocale
10// Trigger: languageSelect Change event
11const selectedLocale = languageSelect.value;
12if (selectedLocale && translations.value[selectedLocale]) {
13 await locale.setValue(selectedLocale);
14} else {
15 console.warn(`Locale '${selectedLocale}' not found in translations`);
16 await locale.setValue('en'); // fallback
17}

Expected result: Selecting Español from the dropdown immediately updates all {{ translations.value[locale.value].key }} expressions to Spanish.

5

Reference translations in component text properties

For each Text component, Button label, or Form field label in your app, replace hard-coded strings with translation expressions. In a Text component's Value field, enter {{ translations.value[locale.value].welcomeHeading }}. For Button components, set the Label to {{ translations.value[locale.value].saveButton }}. The component re-renders whenever locale.value changes.

typescript
1// Text component Value:
2{{ translations.value[locale.value].welcomeHeading }}
3
4// Button Label:
5{{ translations.value[locale.value].saveButton }}
6
7// Text Input Placeholder:
8{{ translations.value[locale.value].searchPlaceholder }}
9
10// Confirmation text with variable interpolation:
11{{ translations.value[locale.value].confirmDelete.replace('{name}', table1.selectedRow.data.full_name) }}
12
13// Safe access with English fallback:
14{{ translations.value[locale.value]?.noResults ?? translations.value.en.noResults }}

Expected result: All component text updates to the selected language when the locale Select is changed.

6

Format dates and numbers for the active locale

Locale-aware number and date formatting uses the browser's built-in Intl API. Reference {{ locale.value }} in formatting expressions to display values in the correct regional format. Use Intl.NumberFormat for currency and number formatting, and Intl.DateTimeFormat for dates.

typescript
1// Number formatting — currency:
2{{ new Intl.NumberFormat(locale.value, {
3 style: 'currency',
4 currency: locale.value === 'en' ? 'USD' : locale.value === 'es' ? 'EUR' : 'EUR'
5}).format(amount) }}
6
7// Date formatting:
8{{ new Intl.DateTimeFormat(locale.value, {
9 year: 'numeric', month: 'long', day: 'numeric'
10}).format(new Date(datePicker1.value)) }}
11
12// Examples:
13// locale = 'en': March 25, 2026
14// locale = 'fr': 25 mars 2026
15// locale = 'de': 25. März 2026
16
17// Relative time (last seen):
18{{ new Intl.RelativeTimeFormat(locale.value, { numeric: 'auto' })
19 .format(-2, 'day') }}
20// en: '2 days ago' fr: 'il y a 2 jours'

Expected result: Dates and numbers format according to the active locale — '25 mars 2026' for French, 'March 25, 2026' for English.

Complete working example

Transformer: translations
1// Transformer: translations
2// Static lookup object for all app text strings
3// Transformers are read-only — no side effects, just returns data
4// Extend this object for each new language you add
5
6return {
7 en: {
8 // Navigation
9 dashboardTitle: 'Dashboard',
10 customersTitle: 'Customers',
11 ordersTitle: 'Orders',
12
13 // Actions
14 save: 'Save',
15 cancel: 'Cancel',
16 delete: 'Delete',
17 edit: 'Edit',
18 create: 'Create New',
19 search: 'Search',
20 clearFilters: 'Clear Filters',
21
22 // Messages
23 loadingMessage: 'Loading...',
24 noResultsFound: 'No results found',
25 confirmDeleteMessage: 'This action cannot be undone. Are you sure?',
26 saveSuccessMessage: 'Record saved successfully.',
27 saveErrorMessage: 'Failed to save. Please try again.',
28
29 // Form labels
30 fullNameLabel: 'Full Name',
31 emailLabel: 'Email Address',
32 phoneLabel: 'Phone Number',
33 statusLabel: 'Status',
34 },
35
36 es: {
37 // Navigation
38 dashboardTitle: 'Panel de Control',
39 customersTitle: 'Clientes',
40 ordersTitle: 'Pedidos',
41
42 // Actions
43 save: 'Guardar',
44 cancel: 'Cancelar',
45 delete: 'Eliminar',
46 edit: 'Editar',
47 create: 'Crear Nuevo',
48 search: 'Buscar',
49 clearFilters: 'Limpiar Filtros',
50
51 // Messages
52 loadingMessage: 'Cargando...',
53 noResultsFound: 'No se encontraron resultados',
54 confirmDeleteMessage: 'Esta acción no se puede deshacer. ¿Estás seguro?',
55 saveSuccessMessage: 'Registro guardado exitosamente.',
56 saveErrorMessage: 'Error al guardar. Por favor intenta de nuevo.',
57
58 // Form labels
59 fullNameLabel: 'Nombre Completo',
60 emailLabel: 'Correo Electrónico',
61 phoneLabel: 'Número de Teléfono',
62 statusLabel: 'Estado',
63 },
64};

Common mistakes

Why it's a problem: Trying to call setValue() or trigger queries from inside the translations Transformer — Transformers are read-only

How to avoid: The translations Transformer only returns a static JavaScript object. All locale-switching logic belongs in a JS Query (switchLocale) triggered by the languageSelect Change event handler.

Why it's a problem: Hard-coding the locale code in expressions like {{ translations.value.en.key }} instead of {{ translations.value[locale.value].key }} — the language never changes when the user switches

How to avoid: Always reference the locale dynamically: translations.value[locale.value].key. The bracket notation reads the currently active locale from the Temporary State variable.

Why it's a problem: Forgetting to set the languageSelect Default Value to {{ locale.value }} — the dropdown shows the wrong language after a locale change triggered from elsewhere

How to avoid: Set languageSelect Default Value to {{ locale.value }} so it stays in sync with the current locale state, even if the locale is changed programmatically.

Why it's a problem: Expecting Component Localization to translate custom labels you wrote in the component Label field — it only translates framework-generated text

How to avoid: Custom labels, button text, and Text component content must use the JSON translations approach. Component Localization handles Retool's own UI chrome only.

Best practices

  • Use the built-in Component Localization for component framework text (validation messages, date picker labels) — it is maintained by Retool and handles edge cases
  • Keep the translations Transformer as the single source of truth for all custom text — avoid hard-coding strings in multiple components
  • Provide English as a fallback for every key: {{ translations.value[locale.value]?.key ?? translations.value.en.key }}
  • Store locale preference in a database if users should see their preferred language on next login — Temporary State resets on page refresh
  • Name translation keys by semantic purpose (save, cancel, confirmDelete) rather than by location (button1Text, section3Heading) so they are reusable across screens
  • Format dates and numbers using the Intl API referencing locale.value rather than hard-coding format strings
  • Test right-to-left (RTL) languages like Arabic or Hebrew early — Retool's layout engine is not fully RTL-aware in all components

Still stuck?

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

ChatGPT Prompt

I am building a Retool app that needs to support English and Spanish. I want: (1) a Transformer named translations that returns a JavaScript object with 'en' and 'es' keys, each containing translations for: dashboardTitle, save, cancel, search, noResultsFound, confirmDelete, (2) a Temporary State variable named locale defaulting to 'en', (3) a Select component named languageSelect with English/Spanish options and a Change event handler that calls a JS Query to switch the locale, (4) example expressions for using the translations in a Text component and a Button label with English fallback.

Retool Prompt

In my Retool app, I have a translations Transformer and a locale Temporary State variable. When I write {{ translations.value[locale.value].saveButton }} in a Button's Label field, it shows undefined. What is wrong? How do I debug which part is failing — is it the transformer, the locale variable, or the key name?

Frequently asked questions

Does Retool's Component Localization work for right-to-left (RTL) languages like Arabic or Hebrew?

As of March 2026, RTL languages are not included in Retool's 7 supported Component Localization languages. RTL layout adjustments (text direction, padding reversal) are not automatically applied by Retool. You can set text direction via custom CSS with !important on containers, but full RTL support requires manual layout adjustments.

Can I load translations from a database instead of a hard-coded Transformer?

Yes. Create a query that fetches translation records from a database table (locale, key, value columns) and use a Transformer to reshape the flat rows into the nested locale object format. This is better for content-heavy apps or when translations are managed by non-developers through a CMS or admin interface.

How do I persist the user's locale preference across sessions?

Temporary State resets on page refresh. To persist the preference, save the selected locale code to a user preferences table in your database (linked to the current user's ID via currentUser.sid) when they change it. On app load, run a query to fetch the user's preference and call await locale.setValue(preferenceResult) in a 'Run on page load' JS Query.

RapidDev

Talk to an Expert

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

Book a free consultation

Learning is great. Shipping is faster with help.

Our engineers have built 600+ apps on Retool and the tools around it. If your project needs to be live sooner than your learning curve allows — book a free consultation.

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.