Skip to content

feat(gt-i18n): add runtime translation#34

Closed
ErnestM1234 wants to merge 5 commits intomainfrom
e/i18n/add-runtime-translation
Closed

feat(gt-i18n): add runtime translation#34
ErnestM1234 wants to merge 5 commits intomainfrom
e/i18n/add-runtime-translation

Conversation

@ErnestM1234
Copy link
Copy Markdown
Contributor

@ErnestM1234 ErnestM1234 commented Apr 17, 2026

Runtime translation port

Ports the runtime translation system from gt PR #1207 and PR #1217.

Changes

  • gt-i18n (minor) — replaces the monolithic TranslationsManager with a two-level LocalesCacheTranslationsCache hierarchy (batched runtime fetches via GT.translate_many, in-flight dedup, configurable concurrency cap, lifecycle hooks) and adds a public async tx() for on-demand string translation. New I18nManager methods: lookup_translation, lookup_translation_with_fallback, load_translations, get_lookup_translation. Deprecates get_translations, get_translation_resolver, resolve_translation_sync, and get_translation_loader with DeprecationWarning. hash_message now accepts format= and only applies index_vars() for ICU.
  • gt-fastapi / gt-flask (minor) — initialize_gt accepts new forwarded kwargs (lifecycle, batch_size, batch_interval_ms, max_concurrent_requests, translation_timeout_ms, cache_expiry_time); both packages now re-export tx alongside t.
  • generaltranslation (patch) — hash_source / hash_template pass ensure_ascii=False to json.dumps so non-ASCII content hashes stay in parity with JS JSON.stringify. Found by bulk hash-parity tests.

Greptile Summary

This PR ports the runtime translation system from the JS monorepo: replaces the monolithic TranslationsManager with a two-level LocalesCacheTranslationsCache hierarchy (batched translate_many calls, in-flight dedup, configurable concurrency cap, lifecycle hooks), adds the public async tx() function, extends hash_message with a format= parameter, fixes a cross-SDK hash divergence for non-ASCII content, and forwards new kwargs through gt-fastapi / gt-flask.

  • tx() documents that it "never raises or returns None" but is missing a try/except around lookup_translation_with_fallback — network errors from _send_batch propagate as exceptions rather than falling back to the interpolated source.

Confidence Score: 4/5

Safe to merge after fixing the tx() exception-fallback gap; all other changes are well-structured and well-tested.

One P1 issue: tx() propagates network exceptions instead of returning the interpolated source as documented and tested (success:False path works, exception path does not). The two-level cache hierarchy, batching logic, dedup, and deprecation wrappers are all correct. The non-ASCII hash fix and format= parameter on hash_message are clean and parity-tested.

packages/gt-i18n/src/gt_i18n/translation_functions/_tx.py — missing try/except for network error fallback

Important Files Changed

Filename Overview
packages/gt-i18n/src/gt_i18n/translation_functions/_tx.py New public tx() async translator — P1 bug: missing try/except means network errors propagate instead of falling back to source as documented.
packages/gt-i18n/src/gt_i18n/i18n_manager/_translations_cache.py New inner cache with batched runtime fetch, in-flight dedup, concurrency cap, and lifecycle hooks — logic is sound; API-failure path (success:False) correctly yields None, but network-exception path propagates (feeding the P1 in tx).
packages/gt-i18n/src/gt_i18n/i18n_manager/_locales_cache.py New outer cache keyed on locale with TTL, dedup, and lifecycle callbacks — implementation is clean, loader failures not silently swallowed (intentional change vs old TranslationsManager).
packages/gt-i18n/src/gt_i18n/i18n_manager/_cache.py Abstract base cache with Future-based in-flight dedup — correct exception propagation including CancelledError; safe to merge.
packages/gt-i18n/src/gt_i18n/i18n_manager/_i18n_manager.py Replaced TranslationsManager with two-level LocalesCache hierarchy; adds new lookup API and deprecates old methods. Minor issues: inline import time and direct _cache access in two methods.
packages/generaltranslation/src/generaltranslation/_id/_hash.py Adds ensure_ascii=False to json.dumps in hash_source and hash_template to match JS JSON.stringify for non-ASCII content — clean fix with good comment.

Sequence Diagram

sequenceDiagram
    participant User as Caller
    participant tx as tx()
    participant Mgr as I18nManager
    participant LC as LocalesCache
    participant TC as TranslationsCache
    participant GT as GT.translate_many

    User->>tx: await tx("Hello", _locale="es")
    tx->>Mgr: lookup_translation_with_fallback("Hello", ...)
    Mgr->>LC: get("es") [sync, TTL check]
    alt locale not cached
        LC->>LC: miss("es") → _fallback → load_translations("es")
        LC-->>Mgr: TranslationsCache(locale="es")
    end
    Mgr->>TC: miss({message, options})
    alt hash in cache
        TC-->>Mgr: cached string
    else new miss
        TC->>TC: enqueue hash, schedule_batch / drain_queue
        TC->>GT: translate_many(sources, {target_locale: "es"})
        GT-->>TC: {hash: {success, translation}}
        TC-->>Mgr: resolved string (or None on failure)
    end
    Mgr-->>tx: str | None
    tx-->>User: interpolated result (source fallback if None)
Loading
Prompt To Fix All With AI
This is a comment left during a code review.
Path: packages/gt-i18n/src/gt_i18n/translation_functions/_tx.py
Line: 34-37

Comment:
**`tx()` propagates exceptions on network errors, violating its "never raises" contract**

`tx()` is documented to "return the interpolated source on any failure (never raises or returns None)" — a contract also stated explicitly in the test file comments. However, when `_translate_many` raises a `BaseException` (e.g., a network error or timeout), `_send_batch` propagates it via `fut.set_exception(exc)`. This exception then bubbles from `tc.miss()``lookup_translation_with_fallback()``tx()` with no interception.

The `success: False` path is correctly handled (`miss()` returns `None`, `tx()` falls back to `content`), but the exception path is not. A try/except is needed:

```python
    try:
        translation = await manager.lookup_translation_with_fallback(content, **kwargs)
    except Exception:
        translation = None
    target = translation if translation is not None else content
    locale = kwargs.get("_locale") or manager.get_locale()
    return interpolate_message(target, kwargs, locale)
```

How can I resolve this? If you propose a fix, please make it concise.

---

This is a comment left during a code review.
Path: packages/gt-i18n/src/gt_i18n/i18n_manager/_i18n_manager.py
Line: 196-201

Comment:
**`import time` inside method body**

`import time as _time` is deferred inside `get_translations_sync` rather than placed at the module level. Python caches imports so there's no performance issue, but the inline import is unconventional and inconsistent with how the same module (`time`) is imported at the top of `_locales_cache.py`. Add `import time` at the top of the file alongside the other stdlib imports and reference `time.monotonic()` directly.

How can I resolve this? If you propose a fix, please make it concise.

---

This is a comment left during a code review.
Path: packages/gt-i18n/src/gt_i18n/i18n_manager/_i18n_manager.py
Line: 154-159

Comment:
**Direct access to `_locales_cache._cache` bypasses TTL eviction**

Both `load_translations` (line 158) and `get_translations_sync` (line 193) read `self._locales_cache._cache` directly, bypassing `LocalesCache.get()` which enforces TTL by evicting expired entries. The race window is negligible in practice, but `_cache` is a protected attribute. `LocalesCache` could expose a `get_translations_snapshot(locale) -> dict[str, str]` helper to encapsulate the TTL logic in one place.

How can I resolve this? If you propose a fix, please make it concise.

Reviews (1): Last reviewed commit: "final touches" | Re-trigger Greptile

Greptile also left 3 inline comments on this PR.

Comment on lines +34 to +37
translation = await manager.lookup_translation_with_fallback(content, **kwargs)
target = translation if translation is not None else content
locale = kwargs.get("_locale") or manager.get_locale()
return interpolate_message(target, kwargs, locale)
Copy link
Copy Markdown

Choose a reason for hiding this comment

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

P1 tx() propagates exceptions on network errors, violating its "never raises" contract

tx() is documented to "return the interpolated source on any failure (never raises or returns None)" — a contract also stated explicitly in the test file comments. However, when _translate_many raises a BaseException (e.g., a network error or timeout), _send_batch propagates it via fut.set_exception(exc). This exception then bubbles from tc.miss()lookup_translation_with_fallback()tx() with no interception.

The success: False path is correctly handled (miss() returns None, tx() falls back to content), but the exception path is not. A try/except is needed:

    try:
        translation = await manager.lookup_translation_with_fallback(content, **kwargs)
    except Exception:
        translation = None
    target = translation if translation is not None else content
    locale = kwargs.get("_locale") or manager.get_locale()
    return interpolate_message(target, kwargs, locale)
Prompt To Fix With AI
This is a comment left during a code review.
Path: packages/gt-i18n/src/gt_i18n/translation_functions/_tx.py
Line: 34-37

Comment:
**`tx()` propagates exceptions on network errors, violating its "never raises" contract**

`tx()` is documented to "return the interpolated source on any failure (never raises or returns None)" — a contract also stated explicitly in the test file comments. However, when `_translate_many` raises a `BaseException` (e.g., a network error or timeout), `_send_batch` propagates it via `fut.set_exception(exc)`. This exception then bubbles from `tc.miss()``lookup_translation_with_fallback()``tx()` with no interception.

The `success: False` path is correctly handled (`miss()` returns `None`, `tx()` falls back to `content`), but the exception path is not. A try/except is needed:

```python
    try:
        translation = await manager.lookup_translation_with_fallback(content, **kwargs)
    except Exception:
        translation = None
    target = translation if translation is not None else content
    locale = kwargs.get("_locale") or manager.get_locale()
    return interpolate_message(target, kwargs, locale)
```

How can I resolve this? If you propose a fix, please make it concise.

Comment on lines +196 to +201
# Respect TTL — expired entries shouldn't leak stale data.
import time as _time

if entry.expires_at < _time.monotonic():
return {}
return dict(entry.translations)
Copy link
Copy Markdown

Choose a reason for hiding this comment

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

P2 import time inside method body

import time as _time is deferred inside get_translations_sync rather than placed at the module level. Python caches imports so there's no performance issue, but the inline import is unconventional and inconsistent with how the same module (time) is imported at the top of _locales_cache.py. Add import time at the top of the file alongside the other stdlib imports and reference time.monotonic() directly.

Prompt To Fix With AI
This is a comment left during a code review.
Path: packages/gt-i18n/src/gt_i18n/i18n_manager/_i18n_manager.py
Line: 196-201

Comment:
**`import time` inside method body**

`import time as _time` is deferred inside `get_translations_sync` rather than placed at the module level. Python caches imports so there's no performance issue, but the inline import is unconventional and inconsistent with how the same module (`time`) is imported at the top of `_locales_cache.py`. Add `import time` at the top of the file alongside the other stdlib imports and reference `time.monotonic()` directly.

How can I resolve this? If you propose a fix, please make it concise.

Note: If this suggestion doesn't match your team's coding style, reply to this and let me know. I'll remember it for next time!

Comment on lines +154 to +159
async def load_translations(self, locale: str | None = None) -> dict[str, str]:
"""Load (or return cached) translations dict for the locale."""
target = locale or self.get_locale()
return await self._translations.get_translations(target)
await self._locales_cache.miss(target)
entry = self._locales_cache._cache.get(target)
return dict(entry.translations) if entry else {}
Copy link
Copy Markdown

Choose a reason for hiding this comment

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

P2 Direct access to _locales_cache._cache bypasses TTL eviction

Both load_translations (line 158) and get_translations_sync (line 193) read self._locales_cache._cache directly, bypassing LocalesCache.get() which enforces TTL by evicting expired entries. The race window is negligible in practice, but _cache is a protected attribute. LocalesCache could expose a get_translations_snapshot(locale) -> dict[str, str] helper to encapsulate the TTL logic in one place.

Prompt To Fix With AI
This is a comment left during a code review.
Path: packages/gt-i18n/src/gt_i18n/i18n_manager/_i18n_manager.py
Line: 154-159

Comment:
**Direct access to `_locales_cache._cache` bypasses TTL eviction**

Both `load_translations` (line 158) and `get_translations_sync` (line 193) read `self._locales_cache._cache` directly, bypassing `LocalesCache.get()` which enforces TTL by evicting expired entries. The race window is negligible in practice, but `_cache` is a protected attribute. `LocalesCache` could expose a `get_translations_snapshot(locale) -> dict[str, str]` helper to encapsulate the TTL logic in one place.

How can I resolve this? If you propose a fix, please make it concise.

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