Skip to content

fix(l10n): disambiguate duration "m" label from millions abbreviation with msgctxt - #124246

Open
parh0m2007 wants to merge 3 commits into
getsentry:masterfrom
parh0m2007:fix/duration-m-msgctxt-collision
Open

fix(l10n): disambiguate duration "m" label from millions abbreviation with msgctxt#124246
parh0m2007 wants to merge 3 commits into
getsentry:masterfrom
parh0m2007:fix/duration-m-msgctxt-collision

Conversation

@parh0m2007

Copy link
Copy Markdown

Problem

The single-character msgid "m" was shared by three different meanings:

  1. Millionssmall_count() in src/sentry/templatetags/sentry_helpers.py (_("m"))
  2. Extra-short month labelDURATION_LABELS in static/app/utils/duration/getDuration.tsx
  3. Extra-short minute labelDURATION_LABELS in static/app/utils/duration/getDuration.tsx

Because gettext maps one msgid to one msgstr per locale, the Russian catalog carries the millions translation («млн») for all three. With unitStyle="extraShort" (TimeSincegetRelativeDategetDuration), durations under a month rendered as «5млн» ("5 millions") instead of «5м» ("5 min").

Fixes #124240

Solution

Route the extra-short month/minute labels through gettext translation contexts (msgctxt), keeping small_count()'s millions meaning as the context-less "m". Jed already supports pgettext lookups at runtime (msgctxt\u0004msgid keys), and the extraction script already knows the pgettext signature — the pipeline pieces just weren't wired up for frontend use.

Changes

  • static/app/locale.tsx: add tp (a pgettext wrapper) alongside t/tn, so ambiguous single-letter msgids can be disambiguated with a context.
  • static/app/utils/duration/getDuration.tsx: the extra-short month/minute labels now use tp('duration', 'm').
  • build-utils/ts-extract-gettext.ts: register tp: ['msgctxt', 'msgid'] so context-bearing calls are extracted with their msgctxt.
  • build-utils/po-catalog-loader.ts: previously only read translations[''], silently dropping all context-specific entries. Now iterates all contexts and emits entries in the msgctxt\u0004msgid key format Jed looks up.
  • bin/merge-catalogs: catalog.get(frontend_msg.id) matched context-specific msgids against the context-less entry (the source of the collision). Now looks up with catalog.get(id, context); the context travels with the Message when written back.
  • en/ru catalogs: surgically add the new msgctxt "duration" entry (ru: «м»; other locales fill in via Transifex). The next make merge-locale-catalogs + Transifex sync will refresh references across the remaining locales.

small_count() and its «млн» translation are untouched.

Verification

  • pnpm run typecheck — clean
  • pnpm run lint:js (oxlint) on changed files — 0 errors
  • Jest: all duration/locale/timeSince/relativeTime suites pass (126 tests), including two new regression tests asserting that with a Russian-style catalog getDuration(122, 0, false, true)'2м' and months → '3м' (not «млн»)
  • End-to-end: pnpm run build-js-po extracts the msgctxt "duration" entry; bin/merge-catalogs verified to keep plain "m" → «млн» and create the separate context entry; po-catalog-loader output verified to resolve via Jed.pgettext('duration', 'm')

… with msgctxt

The single-character msgid "m" was shared by three different meanings:
`small_count()` millions, the extra-short month label, and the
extra-short minute label in `getDuration()`. Since gettext maps one
msgid to one msgstr per locale, Russian durations under a month
rendered as «5млн» ("5 millions") instead of «5м» ("5 min").

Route the extra-short month/minute labels through a new `tp()`
(pgettext) helper with a "duration" translation context, and thread
msgctxt support through the i18n pipeline:

- `locale.tsx`: add `tp` (pgettext wrapper) alongside `t`/`tn`
- `getDuration.tsx`: use `tp('duration', 'm')` for the extra-short
  month/minute labels
- `ts-extract-gettext.ts`: register `tp` so contexts are extracted
- `po-catalog-loader.ts`: emit context-specific entries in the
  `msgctxt\u0004msgid` key format Jed looks up, instead of dropping
  them
- `bin/merge-catalogs`: look up messages with their context so
  context-specific msgids stay separate from context-less ones
- en/ru catalogs: add the new `msgctxt "duration"` entry (ru: «м»)

Fixes getsentry#124240
@parh0m2007
parh0m2007 requested review from a team as code owners September 13, 2026 08:10
@github-actions github-actions Bot added Scope: Frontend Automatically applied to PRs that change frontend components Scope: Backend Automatically applied to PRs that change backend components labels Sep 13, 2026
@github-actions

Copy link
Copy Markdown
Contributor

🚨 Warning: This pull request contains Frontend and Backend changes!

It's discouraged to make changes to Sentry's Frontend and Backend in a single pull request. The Frontend and Backend are not atomically deployed. If the changes are interdependent of each other, they must be separated into two pull requests and be made forward or backwards compatible, such that the Backend or Frontend can be safely deployed independently.

Have questions? Please ask in the #discuss-dev-infra channel.

Comment thread bin/merge-catalogs
merge_message() assigned from frontend_msg.usr_comments instead of
user_comments, which would raise an AttributeError whenever a frontend
message carried translator comments.

Spotted by Sentry's bug-prediction bot on getsentry#124246.
@parh0m2007

Copy link
Copy Markdown
Author

The usr_commentsuser_comments typo is fixed in e0dc82c. It pre-dated this PR (present in upstream/master), but it was a real crash path: any frontend message carrying translator comments (babel's user_comments) would raise an AttributeError in merge_message(). Verified the previously-crashing path now works and the comment merges into the catalog.

@parh0m2007
parh0m2007 requested review from a team as code owners September 13, 2026 09:14
@parh0m2007
parh0m2007 force-pushed the fix/duration-m-msgctxt-collision branch from d51a7ae to e0dc82c Compare September 13, 2026 09:20
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

Scope: Backend Automatically applied to PRs that change backend components Scope: Frontend Automatically applied to PRs that change frontend components

Projects

None yet

Development

Successfully merging this pull request may close these issues.

Russian locale: extraShort durations render 5m as «5млн» (millions) due to msgid "m" collision

1 participant