Skip to content

feat: mark core and api user-facing strings for translation - #602

Open
hamza-56 wants to merge 1 commit into
i18n-gettext-noopfrom
i18n-mark-core-api
Open

feat: mark core and api user-facing strings for translation#602
hamza-56 wants to merge 1 commit into
i18n-gettext-noopfrom
i18n-mark-core-api

Conversation

@hamza-56

@hamza-56 hamza-56 commented Aug 14, 2026

Copy link
Copy Markdown
Contributor

Part of: Sparkth UI, emails, API errors, and AI-generated content are English-only

Second layer of the i18n stack (#601#602#603#604#606#607).

What

Marks the user-facing strings owned by core, the API layer, and the services for translation, so API error details, domain-exception messages, and the verification email render in the request locale negotiated by LocaleMiddleware.

Changes

  • feat(api): wrap literal HTTPException details in _() across auth, user, user-plugins, file-parser, and llm routes; f-strings become translated str.format templates
  • feat(core): translate the permission domain-exception messages at raise time; mark the permission-denied detail and the plugin-service messages
  • feat(core): UserPluginResponse.for_plugin becomes the rendering boundary for the gettext_noop-marked frontend metadata (DisplayInfo/SidebarEntry)
  • feat(core): mark the LLM config exception/validation messages and the whitelist domain-exception messages
  • feat(core): render the verification email (subject, body sentences, link label) through _(), assembled from per-sentence templates
  • test: locale assertions through the real app (login 401 detail via Accept-Language, translated plugin metadata, exception templates, email content)

How to Test

  1. uv run pytest tests/api/v1/test_auth.py tests/api/v1/test_list_user_plugins.py tests/permissions/test_exception_messages.py tests/services/test_email_verification.py
  2. Full run: uv run pytest (1757 passed on this layer)
  3. make mypy and make lint.backend are clean

Notes

Deliberately not marked: machine-read codes (expired_token, email_not_verified), audit error_detail records, operator-facing configuration errors, and internal invariants ("is not persisted"). English output is unchanged until catalogs ship in #604.

This PR description was written with the assistance of an LLM (Claude).

Wraps request-time literals in _() and converts f-strings to translated
str.format templates across auth, permissions, plugins, llm, whitelist, and
the verification email, so they render in the request locale negotiated by
LocaleMiddleware. UserPluginResponse.for_plugin becomes the rendering
boundary for the gettext_noop-marked frontend metadata. Machine-read codes
(expired_token), operator-facing configuration errors, audit records, and
internal invariants deliberately stay English.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
@hamza-56

Copy link
Copy Markdown
Contributor Author

@claude-review

@claude

claude Bot commented Aug 14, 2026

Copy link
Copy Markdown
Contributor

Claude finished @hamza-56's task in 3m 41s —— View job


Review — PR #602 · mark core & api strings for translation

  • Read the full diff against origin/i18n-gettext-noop
  • Review API-layer marking (auth, file_parser, llm, user, user_plugins)
  • Review core marking (permissions, plugins, google_auth, whitelist, llm)
  • Review email verification rendering
  • Review test coverage
  • Post review

Solid, well-scoped mechanical PR. The marking patterns follow the i18n guide precisely, English output is preserved, and the risky bits (f-string → str.format, _ shadowing, HTML escaping) were handled carefully. No blocking issues.

What's done well

  • _ shadowing handledcore/google_auth.py correctly renames the throwaway _, to _client_secret now that _ means gettext. I checked every file that imports _; no remaining bare-_ throwaway collisions.
  • for_plugin rendering boundary is correct — positional reconstruction matches the dataclass field order (DisplayInfo(display_name, description, icon), SidebarEntry(label, icon, order) in sparkth/lib/frontend/hooks.py), and gettext_noop marks + gettext renders is exactly the pattern the guide prescribes for plain-str fields.
  • HTML escaping preserved/improved in the verification email — the whole translated greeting (which embeds name) is escaped as a unit, so it stays XSS-safe even if a translation introduces markup.
  • .format() placeholders all match their templates across the exception classes, adapter, and service.
  • Duplicate msgid dedupadapter.py and service.py share the identical "Model '{model}' not available…" template, which is correct for catalog reuse.
  • Layeringsparkth.core.* importing from sparkth.lib.i18n looks like an inversion, but the i18n module's docstring explicitly mandates it ("All modules … must import from here, never from sparkth.core.i18n directly"), so this is sanctioned, not a violation.

Suggestions (non-blocking)

Low — untested .format() templates. Tests representatively cover the auth 401, plugin metadata, two permission exceptions, and email. But the formatted templates in services/whitelist/service.py, llm/exceptions.py, llm/adapter.py, and the user_plugins plugin-name details have no locale test. English is unchanged so nothing breaks today, but a placeholder mismatch introduced by a future translation would only surface at runtime. One representative test through the API for a formatted template (e.g. Plugin '{name}' not found under Accept-Language: es) would lock the .format() contract. Given the mechanical uniformity, I'd call current coverage adequate — just calling out the gap.

Low — empty-msgid gotcha in for_plugin. See the inline note on core/plugins/service.py:61: gettext("") returns the PO header metadata; a plugin with an empty description would serialize that. Very low likelihood; a s if not s else translate(s) guard closes it.

Low — confirm intent in llm/adapter.py. The f"llm_config_id {config_id} not found or does not belong to this user." raise is left untranslated while the adjacent "This record is deactivated…" is marked. If that message is genuinely internal/machine-facing it's fine per your "Deliberately not marked" notes — just flagging the adjacency for a conscious call.

Consideration — localized operator logs. Translating domain-exception messages at raise time means str(exc) is localized wherever it lands, including any logger.exception/log path that captures it. Your notes already carve out audit error_detail; worth being aware the same effect reaches ordinary error logs (mixed-language operator logs). Inherent to "translate at raise" — no action needed unless log consistency matters to you.

Verdict

Approve-worthy from a review standpoint (I can't formally approve). No correctness or security concerns; the notes above are polish. I was unable to execute the test suite in this sandbox (network/DB commands are blocked here), so I relied on static analysis — your reported pytest / mypy / lint.backend runs stand as the green signal.
· base i18n-gettext-noop

"""
display = get_plugin_display_info(plugin_name)
if display is not None:
display = DisplayInfo(translate(display.display_name), translate(display.description), display.icon)

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Minor edge case: translate() is gettext, and GNUTranslations.gettext("") returns the catalog's header metadata rather than "" (the classic empty-msgid gotcha). DisplayInfo.description is a required str with no default, so a plugin registering DisplayInfo("Name", "") would serialize the PO header as its description under a non-English locale. Very low likelihood (no real plugin ships an empty description), but a cheap guard avoids it — only translate when the source string is non-empty.

Non-blocking — flagging for awareness.

@abdulrafey1 abdulrafey1 left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

fix the merge conflicts, rest looks good

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

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants