# How to Use Localization in Retool Apps

- Tool: Retool
- Difficulty: Beginner
- Time required: 20-25 min
- Compatibility: Retool Cloud and Self-hosted
- Last updated: March 2026

## 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.

## 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.

## Before you start

- 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.

```
// Transformer: translations
// Returns a static lookup object — no dependencies, evaluates once
// Transformers are read-only: cannot trigger queries or setValue()

return {
  en: {
    welcomeHeading: 'Welcome to the Dashboard',
    customerSection: 'Customer Management',
    newCustomer: 'New Customer',
    saveButton: 'Save',
    cancelButton: 'Cancel',
    confirmDelete: 'Are you sure you want to delete this record?',
    searchPlaceholder: 'Search by name or email...',
    noResults: 'No results found',
    loadingMessage: 'Loading...',
  },
  es: {
    welcomeHeading: 'Bienvenido al Panel',
    customerSection: 'Gestión de Clientes',
    newCustomer: 'Nuevo Cliente',
    saveButton: 'Guardar',
    cancelButton: 'Cancelar',
    confirmDelete: '¿Estás seguro de que quieres eliminar este registro?',
    searchPlaceholder: 'Buscar por nombre o correo...',
    noResults: 'No se encontraron resultados',
    loadingMessage: 'Cargando...',
  },
  fr: {
    welcomeHeading: 'Bienvenue sur le Tableau de Bord',
    customerSection: 'Gestion des Clients',
    newCustomer: 'Nouveau Client',
    saveButton: 'Enregistrer',
    cancelButton: 'Annuler',
    confirmDelete: 'Êtes-vous sûr de vouloir supprimer cet enregistrement ?',
    searchPlaceholder: 'Rechercher par nom ou email...',
    noResults: 'Aucun résultat trouvé',
    loadingMessage: 'Chargement...',
  },
};
```

**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.

```
// Temporary State: locale
// Default value: 'en'
// Type: String

// Usage in component text:
{{ translations.value[locale.value].welcomeHeading }}

// Fallback to English if locale key not found:
{{ translations.value[locale.value]?.welcomeHeading || translations.value.en.welcomeHeading }}

// Read current locale anywhere:
{{ locale.value }}
// 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).

```
// languageSelect static options:
// Label: English    Value: en
// Label: Español    Value: es
// Label: Français   Value: fr
// Label: Deutsch    Value: de

// Default Value: {{ locale.value }}

// JS Query: switchLocale
// Trigger: languageSelect Change event
const selectedLocale = languageSelect.value;
if (selectedLocale && translations.value[selectedLocale]) {
  await locale.setValue(selectedLocale);
} else {
  console.warn(`Locale '${selectedLocale}' not found in translations`);
  await locale.setValue('en'); // fallback
}
```

**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.

```
// Text component Value:
{{ translations.value[locale.value].welcomeHeading }}

// Button Label:
{{ translations.value[locale.value].saveButton }}

// Text Input Placeholder:
{{ translations.value[locale.value].searchPlaceholder }}

// Confirmation text with variable interpolation:
{{ translations.value[locale.value].confirmDelete.replace('{name}', table1.selectedRow.data.full_name) }}

// Safe access with English fallback:
{{ 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.

```
// Number formatting — currency:
{{ new Intl.NumberFormat(locale.value, {
  style: 'currency',
  currency: locale.value === 'en' ? 'USD' : locale.value === 'es' ? 'EUR' : 'EUR'
}).format(amount) }}

// Date formatting:
{{ new Intl.DateTimeFormat(locale.value, {
  year: 'numeric', month: 'long', day: 'numeric'
}).format(new Date(datePicker1.value)) }}

// Examples:
// locale = 'en': March 25, 2026
// locale = 'fr': 25 mars 2026
// locale = 'de': 25. März 2026

// Relative time (last seen):
{{ new Intl.RelativeTimeFormat(locale.value, { numeric: 'auto' })
  .format(-2, 'day') }}
// 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 code example

File: `Transformer: translations`

```javascript
// Transformer: translations
// Static lookup object for all app text strings
// Transformers are read-only — no side effects, just returns data
// Extend this object for each new language you add

return {
  en: {
    // Navigation
    dashboardTitle: 'Dashboard',
    customersTitle: 'Customers',
    ordersTitle: 'Orders',

    // Actions
    save: 'Save',
    cancel: 'Cancel',
    delete: 'Delete',
    edit: 'Edit',
    create: 'Create New',
    search: 'Search',
    clearFilters: 'Clear Filters',

    // Messages
    loadingMessage: 'Loading...',
    noResultsFound: 'No results found',
    confirmDeleteMessage: 'This action cannot be undone. Are you sure?',
    saveSuccessMessage: 'Record saved successfully.',
    saveErrorMessage: 'Failed to save. Please try again.',

    // Form labels
    fullNameLabel: 'Full Name',
    emailLabel: 'Email Address',
    phoneLabel: 'Phone Number',
    statusLabel: 'Status',
  },

  es: {
    // Navigation
    dashboardTitle: 'Panel de Control',
    customersTitle: 'Clientes',
    ordersTitle: 'Pedidos',

    // Actions
    save: 'Guardar',
    cancel: 'Cancelar',
    delete: 'Eliminar',
    edit: 'Editar',
    create: 'Crear Nuevo',
    search: 'Buscar',
    clearFilters: 'Limpiar Filtros',

    // Messages
    loadingMessage: 'Cargando...',
    noResultsFound: 'No se encontraron resultados',
    confirmDeleteMessage: 'Esta acción no se puede deshacer. ¿Estás seguro?',
    saveSuccessMessage: 'Registro guardado exitosamente.',
    saveErrorMessage: 'Error al guardar. Por favor intenta de nuevo.',

    // Form labels
    fullNameLabel: 'Nombre Completo',
    emailLabel: 'Correo Electrónico',
    phoneLabel: 'Número de Teléfono',
    statusLabel: 'Estado',
  },
};
```

## Common mistakes

- **Trying to call setValue() or trigger queries from inside the translations Transformer — Transformers are read-only** — undefined Fix: 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.
- **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** — undefined Fix: Always reference the locale dynamically: translations.value[locale.value].key. The bracket notation reads the currently active locale from the Temporary State variable.
- **Forgetting to set the languageSelect Default Value to {{ locale.value }} — the dropdown shows the wrong language after a locale change triggered from elsewhere** — undefined Fix: Set languageSelect Default Value to {{ locale.value }} so it stays in sync with the current locale state, even if the locale is changed programmatically.
- **Expecting Component Localization to translate custom labels you wrote in the component Label field — it only translates framework-generated text** — undefined Fix: 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

## 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.

---

Source: https://www.rapidevelopers.com/retool-tutorial/how-to-use-localization-in-retool-apps
© RapidDev — https://www.rapidevelopers.com/retool-tutorial/how-to-use-localization-in-retool-apps
