Skip to content

feat(core): add gettext_noop marker and translation-catalog fixture - #601

Open
hamza-56 wants to merge 1 commit into
mainfrom
i18n-gettext-noop
Open

feat(core): add gettext_noop marker and translation-catalog fixture#601
hamza-56 wants to merge 1 commit into
mainfrom
i18n-gettext-noop

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

First layer of the i18n stack (#601#602#603#604#606#607) that follows up the translation foundations from #590.

What

Adds the missing marking primitive and the shared test tooling that the string-marking PRs stacked on top rely on: a deferred gettext_noop marker for strings that must stay plain str, and a fixture for asserting on real translations.

Changes

  • feat(core): gettext_noop marking function, exported through sparkth.lib.i18n and extracted via -k gettext_noop; for literals stored in plain-str fields (the DisplayInfo/SidebarEntry dataclasses embedded in Pydantic response models, where a LazyString cannot go), translated later by passing the stored value through gettext() at the rendering boundary
  • test(core): translation_catalog fixture in sparkth.lib.testing — compiles single-message catalogs onto the LOCALE_DIRS hook so any test (core or plugin) can assert on an actual translation
  • test(core): shipped_locale_dirs session fixture detaching the shipped catalog directories for the whole suite, so locally compiled .mo files (make i18n.compile) never leak into assertions written against the English source fallback
  • docs: translations guide gains the fourth marking case and the new testing workflow

How to Test

  1. uv run pytest tests/core/i18n (24 passed)
  2. make i18n.compile && uv run pytest — the suite stays green with compiled catalogs present (the detachment fixture at work); before this PR the compiled-catalog state broke test_gettext_returns_the_source_when_the_locale_has_no_catalog
  3. make mypy and make lint.backend are clean

Notes

No behavior change outside tests: gettext_noop has no call sites yet (they arrive in #602/#603).

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

The frontend metadata dataclasses (DisplayInfo, SidebarEntry) are embedded in
Pydantic response models, where a LazyString cannot go: their fields must stay
plain str. gettext_noop marks such literals for extraction while returning
them unchanged; the stored source string is translated with gettext() at the
rendering boundary. The translation_catalog fixture in sparkth.lib.testing
lets any test (core or plugin) assert on a real translation by compiling a
single-message catalog onto the LOCALE_DIRS hook.

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 2m 28s —— View job


Claude Code Review

  • Read the PR diff and changed files
  • Review correctness, tests, and docs
  • Post review feedback

Reviewed all 7 changed files against origin/main. This is a clean, well-scoped foundation PR — the marking primitive and the test tooling both land with docs and tests, no behavior change outside tests (as advertised). Nice work.

Overall assessment

Approve-worthy. No blocking or correctness issues found. A couple of low-priority nits below.

What's good

  • gettext_noop is a minimal, correct identity marker, exported symmetrically through core.i18n.__init__lib.i18n and covered by tests at both the core and façade layers.
  • shipped_locale_dirs is a genuinely nice fix: detaching the shipped catalog dirs for the whole session means a developer's locally-compiled .mo files can't leak into English-fallback assertions. This closes the real footgun the PR body calls out (test_gettext_returns_the_source_when_the_locale_has_no_catalog breaking under compiled catalogs).
  • translation_catalog correctly sidesteps the _load_catalogs lru_cache(maxsize=None) staleness trap by compiling each message into a fresh tmp_path subdir — every registration mutates the (locale, dirs) cache key, so no invalidation is needed. The Protocol return type and per-registration teardown are tidy.
  • Makefile: -k gettext_noop added to i18n.extract, which both i18n.init and i18n.update depend on, so extraction stays consistent across all targets. -k augments (not replaces) pybabel's default keyword set, so _/gettext/lazy_gettext still extract. 👍

Suggestions (Low)

  1. docs/guides/translations.md:127 — the testing example is a plain def but uses await client.post(...). Under asyncio_mode = "auto" this needs async def; as written it's a SyntaxError if copied. (inline comment + suggestion)

  2. tests/core/i18n/test_translate.py:83test_gettext_noop_returns_the_message_unchanged requests catalog_dir, but gettext_noop never consults a catalog, so the fixture is dead weight and slightly muddies intent. (inline comment)

  3. No consumer for translation_catalog yet (non-blocking) — the new fixture has no test exercising it in this PR (the two gettext_noop tests reuse the existing catalog_dir). That's reasonable for stacked tooling that feat: mark core and api user-facing strings for translation #602/feat(plugins): mark plugin user-facing strings for translation #603 will use, but it means CI doesn't validate the fixture's happy path here. Consider having one of the new gettext_noop tests route through translation_catalog to both cover it and retire the duplicated catalog_dir/write_catalog logic.

Notes

  • No frontend files touched — React anti-pattern checks N/A.
  • No security, async-safety, or exception-handling concerns; no bare except.
  • TDD respected: tests accompany the new marker and the façade export.
    · i18n-gettext-noop

`locale_context("<lang>")` or an `Accept-Language` header:

```python
def test_detail_is_translated(client, translation_catalog):

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.

The example body uses await client.post(...) but the function is declared with a plain def. The suite runs asyncio_mode = "auto", so this test would need to be an async def to actually await. As written, copying this pattern produces a SyntaxError (await outside an async function).

Suggested change
def test_detail_is_translated(client, translation_catalog):
async def test_detail_is_translated(client, translation_catalog):


def test_the_core_locale_dir_is_registered_at_import() -> None:
assert LOCALE_DIRS.get(LOCALE_DIR) == LOCALE_DIR
def test_gettext_noop_returns_the_message_unchanged(catalog_dir: Path) -> None:

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: this test requests catalog_dir, but gettext_noop never consults a catalog (it returns its argument verbatim), so the fixture is unused here — it registers a Spanish catalog that has no bearing on the assertion. Dropping the parameter makes the test's intent (pure identity, independent of any catalog) clearer:

def test_gettext_noop_returns_the_message_unchanged() -> None:
    with locale_context("es"):
        assert gettext_noop(TRANSLATED[0]) == TRANSLATED[0]

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.

1 participant