+1 (726) 227-2971

Localizing a LookML Model: One Semantic Layer, Many Languages

Sooner or later a Looker project outgrows English. A retailer rolls Explores out to country teams in Madrid and Munich. A SaaS company embeds dashboards in a product sold in nine markets. A finance group wants French labels on the same measures the group reporting team reads in English. The usual first attempt is to clone the model: orders_de.view.lkml next to orders.view.lkml, one Explore per language, and a maintenance bill that doubles with every new field.

There is a much better answer built into LookML. Looker can hold one model and swap the strings — labels, group labels, descriptions, dashboard titles — based on the viewer's locale. This tutorial covers how to set that up, what it does and does not translate, and how to keep the translation files honest once several developers are pushing changes.

What localization actually covers

Be precise about the scope before you promise anything to stakeholders. LookML localization swaps developer-authored metadata strings:

  • label, view_label, group_label, group_item_label
  • description on views, fields, Explores and models
  • Explore and model labels
  • Titles, subtitles and note text in LookML dashboards
  • Filter labels and, where supported, suggested value labels

It does not translate:

  • Data. If a product.category column contains "Footwear", it stays "Footwear". Translating data is a modeling job (a lookup/dimension table keyed by locale), not a localization-file job.
  • Looker's own UI chrome beyond the languages Looker itself ships.
  • User-created content — personal Looks, user-defined dashboards, custom fields. Only LookML-defined content is localized.
  • Number and date formatting automatically in every case; formats are driven by value_format, and a hard-coded value_format: "$#,##0.00" will show dollars in Munich. Plan formats alongside strings.

That last point is where most localization projects disappoint their sponsors. Say it out loud in the kickoff.

Step 1: add a locale_data folder

In your LookML project, create a folder named locale_data and add one strings file per locale. The filenames follow <locale>.strings.json:

my_project/
  locale_data/
    en.strings.json
    de.strings.json
    fr.strings.json
  views/
  models/

Each file is a flat JSON object mapping a key to the displayed string. Keys are yours to invent; treat them as a stable API:

{
  "orders.view_label": "Orders",
  "orders.order_id.label": "Order ID",
  "orders.order_id.description": "Primary key of the order, one row per order.",
  "orders.gross_revenue.label": "Gross Revenue",
  "orders.gross_revenue.description": "Sum of item price before discounts, excluding tax and shipping.",
  "group.finance": "Finance",
  "explore.orders.label": "Orders and Revenue"
}

And the German equivalent, with identical keys:

{
  "orders.view_label": "Aufträge",
  "orders.order_id.label": "Auftragsnummer",
  "orders.order_id.description": "Primärschlüssel des Auftrags, eine Zeile pro Auftrag.",
  "orders.gross_revenue.label": "Bruttoumsatz",
  "orders.gross_revenue.description": "Summe der Artikelpreise vor Rabatten, ohne Steuern und Versand.",
  "group.finance": "Finanzen",
  "explore.orders.label": "Aufträge und Umsatz"
}

A naming convention matters more than it looks. We use <view>.<field>.<property> for field metadata and group.* / explore.* for shared strings. Flat, predictable keys make the diff readable and make a missing-key script trivial to write.

Step 2: turn localization on in the model

In each model file, declare the default locale and how strict Looker should be about missing strings:

connection: "warehouse"

include: "/views/**/*.view.lkml"
include: "/dashboards/**/*.dashboard.lookml"

localization_settings: {
  default_locale: en
  localization_level: permissive
}
  • default_locale is the fallback used when the viewer's locale has no strings file.
  • localization_level: permissive renders the raw LookML value (or the fallback) when a key is missing. strict refuses to render un-localized strings, surfacing gaps loudly.

Start permissive during rollout so a missing German description does not break a dashboard for the whole DACH team, then move to strict once coverage is complete and you want CI to catch regressions. Flipping that one line is the cheapest quality gate in the project.

Step 3: reference keys instead of literals

Now replace literal strings in your views with keys, prefixed with "looker_localized" semantics — in practice, you point the property at the key:

view: orders {
  sql_table_name: analytics.orders ;;
  view_label: "orders.view_label"

  dimension: order_id {
    primary_key: yes
    type: number
    sql: ${TABLE}.order_id ;;
    label: "orders.order_id.label"
    description: "orders.order_id.description"
  }

  measure: gross_revenue {
    type: sum
    sql: ${TABLE}.item_price ;;
    label: "orders.gross_revenue.label"
    description: "orders.gross_revenue.description"
    group_label: "group.finance"
    value_format_name: decimal_2
  }
}

Two habits pay off immediately:

  1. Localize value_format by name, not by literal. value_format_name: decimal_2 plus a currency dimension beats value_format: "€#,##0.00" baked into the field. If you truly need per-locale currency symbols, drive them from a user attribute through Liquid rather than duplicating the measure.
  2. Never leave half a field localized. A dimension with a translated label and an English description looks sloppier than one that is entirely English. Localize per field, completely, and land it in one commit.

Explores and models

explore: orders {
  label: "explore.orders.label"
  description: "explore.orders.description"

  join: customers {
    view_label: "customers.view_label"
    relationship: many_to_one
    sql_on: ${orders.customer_id} = ${customers.id} ;;
  }
}

Note the view_label on the join: in a localized project, join-level labels are a common miss, because they live in the model file rather than the view file where the translator was looking.

LookML dashboards

LookML dashboards localize the same way, which is the real payoff for embedded analytics — one dashboard file, nine languages:

- dashboard: revenue_overview
  title: dashboard.revenue_overview.title
  layout: newspaper
  elements:
  - title: dashboard.revenue_overview.tile.revenue_by_month
    name: revenue_by_month
    model: ecommerce
    explore: orders
    type: looker_column
    fields: [orders.created_month, orders.gross_revenue]
  filters:
  - name: date_range
    title: dashboard.revenue_overview.filter.date_range
    type: field_filter
    explore: orders
    field: orders.created_date

User-defined dashboards cannot be localized. If a market team needs a translated dashboard, it has to live in LookML — which is a good argument for versioning your important dashboards as code anyway.

Step 4: decide how a viewer gets a locale

Three mechanisms, in increasing order of control:

  1. The user's Looker locale. Set per user in Admin, or synced from your identity provider. Fine for internal BI.
  2. A locale user attribute. The cleanest lever: set a default on the attribute, override per group, and let a market group inherit de without touching individual users. This also keeps locale available to Liquid, so you can use it in html, label_from_parameter-style patterns, or drill links.
  3. The embed URL / signed embed payload. For embedded analytics, pass the locale in the SSO embed payload along with the rest of the user's attributes. Your host application already knows the tenant's language; it should be the source of truth, not a Looker-side setting somebody has to remember to update.

A sketch of the third case, in the attributes you sign into an embed URL:

{
  "external_user_id": "tenant-3184-user-77",
  "permissions": ["access_data", "see_looks", "see_user_dashboards"],
  "models": ["ecommerce"],
  "group_ids": ["12"],
  "user_attributes": {
    "tenant_id": "3184",
    "locale": "de"
  }
}

If you are setting up embedding from scratch, pair this with our walkthrough of signed embedding and the Embed SDK.

One caution: locale and data access are different axes. locale: de must never be the thing that decides which rows a user sees. Row filtering belongs in access_filter and required_access_grants — see row-level security patterns. Mixing the two produces a model where translating a label accidentally changes a number, and that bug is miserable to find.

Step 5: keep translations from rotting

Localization decays quietly. A developer adds a measure on Tuesday, English-only, and nobody notices until a Frankfurt analyst files a ticket in March. Three controls prevent that:

A key-parity check in CI. The cheapest useful test: parse every *.strings.json, diff key sets against the default locale, fail on any missing or orphaned key.

#!/usr/bin/env bash
# ci/check-locales.sh — fail if any locale is missing keys present in en
set -euo pipefail
base=locale_data/en.strings.json
for f in locale_data/*.strings.json; do
  [ "$f" = "$base" ] && continue
  missing=$(jq -r --slurpfile o "$f" 'keys - ($o[0]|keys) | .[]' "$base")
  if [ -n "$missing" ]; then
    echo "FAIL $f is missing keys:"; echo "$missing"; exit 1
  fi
done
echo "All locales have full key coverage."

Wire it into the same pipeline that runs your LookML validation and data tests — see CI for LookML. A pull request that adds a field and forgets de.strings.json should go red before a human reviews it.

localization_level: strict in a validation branch. Even if production runs permissive, a branch with strict set will make LookML validation shout about anything unlocalized.

One owner per locale. Translation is a content job, not a developer job. Give each locale a named reviewer who signs off on the strings file diff. Because the files are flat JSON in Git, that review is genuinely readable — which is the whole reason this approach beats cloned views.

A migration path for an existing English project

Do not attempt a big-bang translation of a 400-field model. The sequence that works:

  1. Pick a beachhead. One Explore that one market actually uses. Usually 20–40 fields.
  2. Extract English first. Move that Explore's literals into en.strings.json with no translation at all and confirm nothing changed visually. This is a pure refactor and it should be invisible — if it is not, you have found a genuine bug in your key wiring, cheaply.
  3. Add the second locale. Now the diff is only translations, so the reviewer can actually review it.
  4. Turn on the CI parity check before locale three.
  5. Expand Explore by Explore, and make "new fields ship with all locales" a definition-of-done item in your team's PR template.

Because translated description text is also what AI features read when they interpret your model, this work compounds: a localized, well-described model answers better in Conversational Analytics too. If you are heading that direction, making your LookML model AI-ready covers the same metadata from the other angle.

Common failure modes

  • Duplicate keys across views. label: "name" reused in six views means one translator edit changes six places. Namespace keys by view.
  • Keys that leak into the UI. Seeing orders.gross_revenue.label in an Explore means the key is missing from the active locale and you are in permissive mode with no fallback string. The CI check above catches this before users do.
  • Translating the data instead of the metadata. If stakeholders want localized category names, add a locale-keyed lookup table and a Liquid- or user-attribute-driven dimension. That is a modeling change, scoped and estimated separately.
  • Fiscal and date assumptions. A German label on a US fiscal calendar is still confusing. Check fiscal_month_offset and week-start settings per market alongside strings — see dates, timezones, and fiscal calendars in LookML.
  • Forgetting the strings files exist. Six months later someone adds a field the old way. That is what the PR template and CI gate are for.

Checklist

  • locale_data/ folder with one <locale>.strings.json per supported locale
  • localization_settings block in every model file, with an explicit default_locale
  • Namespaced, flat keys (<view>.<field>.<property>) documented for translators
  • Labels and descriptions localized per field, never half
  • value_format_name preferred over hard-coded currency/number formats
  • Locale sourced from a user attribute (internal) or the signed embed payload (embedded)
  • Locale kept strictly separate from row-level access logic
  • CI key-parity check running on every pull request
  • A named reviewer per locale

Localization is one of those features where a day of structure saves a year of duplicated views. Vistelio's Looker developers do this work on multi-market and embedded deployments regularly — if you are weighing a cloned-model approach against a properly localized one, get in touch and we will walk through the trade-offs for your project.