Skip to content

i18n: 1,198 translations have broken placeholders (SQL Lab fails in Slovak, 500 errors, raw %s in the UI) #44551

Description

@glaterza

Bug description

In 1,198 translations, the placeholders (%s, %(name)s) do not match the English source. At runtime they:

Effect Count
Raise an exception (backend) 37
Show a raw template, such as the literal text %s SENHA 253
Drop a value: a name, a count, or an error detail 839
Show a wrong or blank value 69

Ask: I plan to fix this in three PRs, plus an optional fourth (see Proposed fix). PR 2 clears 1,190 fuzzy translations from 18 catalogs, so I would like a maintainer's view on that approach before I open it.

The most serious case: SQL Lab fails in Slovak. The Slovak catalog still has the old placeholder names that caused #38468 in French:

msgid  "Running block %(block_num)s out of %(block_count)s"
msgstr "Spouští sa príkaz %(statement_num)s z %(statement_count)s"

superset/sql_lab.py builds this progress message for every query. I ran SELECT name, qty FROM fruit on a local SQLite database, on master. In Slovak the query fails with the error 'statement_num', labelled as a SQLite error. The same query in English returns its rows.

Image Image

More cases I reproduced on a local master:

  • A 500 instead of a 400. GET /api/v1/chart/1/data/?filters_dashboard_id=424242 (a dashboard that does not exist) returns 400 {"message": "Dashboard 424242 not found"} in English. In pt_BR it returns 500 with the message 'id', because the translation is Gráfico %(id)s não encontrado.
  • A raw template in the UI. On the user info page in pt_BR, the Reset my password button reads %s SENHA.
Image

Other examples

Lang Source Translation Result
nl Dataset %(table)s already exists Dataset %(name)s bestaat al KeyError: 'name'
pl Not set Jeszcze brak %s UI shows Jeszcze brak %s
pt_BR Created by: %s Criado por author name is dropped
ko Could not load database driver for: %(engine)s 데이터베이스 드라이버를 로드할 수 없습니다 engine name is dropped

The problem affects 20 of the 29 translated languages. The most affected are ko (114), pl (100), nl (91), fa (89), ar (87) and pt_BR (87). It only reaches deployments that enable these languages (by default LANGUAGES is empty and BABEL_DEFAULT_LOCALE is en).

This bug class is not new: #38468 and #39566 (French) and #40165 (Czech) broke SQL Lab, and #33344 reported compile errors in zh_TW. Each was fixed by hand for the strings reported, with no check to stop the next one.

Why nothing catches it

  1. Superset serves fuzzy translations on purpose (docs/developer_docs/contributing/howtos.md). The release Dockerfiles in RELEASING/ compile with flask fab babel-compile (pybabel compile -f), the main Dockerfile uses --use-fuzzy when BUILD_TRANSLATIONS=true, and po2json.sh uses --fuzzy. So a broken fuzzy entry is as live as a confirmed one. 1,190 of the 1,198 are fuzzy.
  2. Compile errors are ignored. pybabel compile --use-fuzzy reports 1,090 errors and exits 1, but it still writes every .mo file. The Dockerfile ends the command with || true, and flask fab babel-compile does not check the exit status.
  3. CI checks translation counts and msgids, not placeholders. The standard gettext check, msgfmt -c, does not help here: it skips fuzzy entries and entries without a python-format flag (like Not set above).

The same pattern, errors in the translation tools that nobody sees, is behind #44395: babel_update.sh has skipped its normalization step since July 2024 (#29782), because a failing command did not stop the script.

Where they came from: 829 have the same translation as a different source string, so they most likely come from Babel's old fuzzy matching (the same problem as #42728). 97 came from scripts/translations/backfill_po.py. The script tells the model to keep placeholders, but nothing checks the output yet; the CI check in PR 1 can do that. For 272 I could not find the source.

Proposed fix

  1. A CI check that fails when a translation's placeholders cannot format correctly. It is small and has no new dependencies. On master it flags 1,227 entries, and 1,198 of those fail at runtime. Of the rest, 11 are one message whose singular and plural forms use different placeholder names (ParameterErrorMessage.tsx), which I would make consistent, and 17 fail only when a count is 0, which their call sites never pass. So PR 1 should land after PRs 2 and 3, with a short list of known exceptions, to keep CI green.

  2. Clear the 1,190 broken fuzzy entries. They become untranslated, so users see the English text with its values. This keeps the "serve fuzzy on purpose" policy and only removes entries that cannot work. I tested it on a copy of master:

    • All 37 exceptions go away, and the only remaining cases are the confirmed ones in PR 3.
    • Compile errors drop from 1,090 to 154.
    • check_translation_regression.py reports no regressions. Confirmed translations do not change; only the fuzzy count drops.

    I can split it by language if smaller PRs are easier to review. chore(i18n): update pt/pt_BR translations and rebuild translation index #43022 also edits pt_BR, so one of the two PRs will need a rebase.

  3. Fix the 8 broken confirmed translations manually: 4 in mi, 2 in es, 1 in fr, 1 in nl. Native speakers are welcome to review.

  4. Optional: fall back to English when a translation cannot be formatted. Today the backend raises and the frontend shows the raw template. With this change both show the English text with its values and log a warning. Errors in the English source still raise, so code bugs stay visible. I built and tested a prototype (a flask_babel.Domain subclass installed at startup, and a small change in Translator.ts):

    • Unit tests pass, and they fail without the change.
    • On a local master, Slovak SQL Lab runs its queries, the pt_BR API returns the proper 400, and the pt_BR Duplicate role dialog shows Duplicate role Admin instead of Duplicate role %(name)s.
    • It fixes 63 of the 1,198 cases: all 37 exceptions and 26 raw templates. The other 1,135 do not raise an error, so only PRs 1–3 fix them. This is a safety net for the future, including custom catalogs, not a replacement.

cc @rusackas @sadpandajoe (recent translation CI work in #44467, #44509)

Screenshots/recordings

Slovak vs. English, same query, same database, local master:

Image

Superset version

master / latest-dev

Python version

3.11

Node version

18 or greater

Browser

Chrome

Additional context

Measured on master at 269b9f99fe.

Reproduce without a running app

cp -r superset/translations /tmp/mo
pybabel compile --use-fuzzy -d /tmp/mo > /tmp/compile.log 2>&1; echo "exit=$?"   # exit=1
grep -c '^error:' /tmp/compile.log                                                 # 1090
ls /tmp/mo/*/LC_MESSAGES/messages.mo | wc -l                                       # 30

python -c '
from babel.support import Translations
t = Translations.load("/tmp/mo", ["sk"], "messages")
print(t.ugettext("Running block %(block_num)s out of %(block_count)s") % {"block_num": 1, "block_count": 1})
'
Traceback (most recent call last):
  File "<string>", line 4, in <module>
KeyError: 'statement_num'

Reproduce in the app: add "sk": {"flag": "sk", "name": "Slovak"} to LANGUAGES, compile the catalogs as above, switch the language to Slovak, and run any query in SQL Lab.

How the numbers were measured, and the limits
  • A static check compares each translated form with the source form it stands for (the singular with msgid, plural forms with msgid_plural). It follows gettext's plural rule: a form used for only one number may write that number out (as 1 or as a word), so Arabic's zero, one and two forms are not flagged.
  • Each flagged entry was then rendered the way users see it. Frontend: the catalog converted with po2json --fuzzy, then formatted with jed 1.1.1, using the same calls and the same fallback as Translator in superset-core. Backend: the .mo compiled with --use-fuzzy, then formatted as flask_babel 4.0 does (s % variables).
  • The test arguments follow the source string, not each call site. The one message with linked arguments (%(suggestion)s instead of …) was re-run with the real arguments; it renders correctly and is not counted.
  • 17 more entries fail only when the count is 0, which their call sites never pass. They are not counted and not cleared.
  • "Values present" does not mean "correct". Some entries that pass still say the wrong thing (on the same pt_BR page, First Name reads Nome do gráfico, "chart name"). A script cannot judge meaning, so those are not counted.
  • The 14 msgfmt -c errors in the committed fr, it, pt and sl files are mostly harmless: 8 are plural forms that write the number 1 literally (correct for it and pt), and 4 are the linked-arguments message above. Of the other 2 in fr, one is fixed in PR 3 and one fails only when the count is 0.

Checklist

  • I have searched Superset docs and Slack and didn't find a solution to my problem.
  • I have searched the GitHub issue tracker and didn't find a similar bug report.
  • I have checked Superset's logs for errors and if I found a relevant Python stacktrace, I included it here as text in the "additional context" section.

Activity

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Metadata

Metadata

Assignees

No one assigned

    Labels

    i18nNamespace | Anything related to localizationi18n:generalRelated to translationsi18n:slovaksqllabNamespace | Anything related to the SQL Lab🦾 ai-candidate

    Type

    No type

    Projects

    No projects

      Milestone

      No milestone

      Relationships

      None yet

      Development

      No branches or pull requests

      Issue actions