From 2daac9703b036b83c25642d7e2f3b5dad3442564 Mon Sep 17 00:00:00 2001 From: moss-bryophyta <261561981+moss-bryophyta@users.noreply.github.com> Date: Sat, 7 Mar 2026 21:19:53 -0800 Subject: [PATCH 1/5] examples: use gt.config.json and _gt/.json for translations Refactor all examples to: - Read configuration from gt.config.json - Store translations in _gt/.json files - Use a load_translations function that reads from _gt/ directory The custom load_translations function is still required for now but will be removed in a future release. --- examples/fastapi-eager/_gt/es.json | 4 +++ examples/fastapi-eager/_gt/fr.json | 4 +++ examples/fastapi-eager/app.py | 37 +++++++++++++-------------- examples/fastapi-eager/gt.config.json | 4 +++ examples/fastapi-lazy/_gt/es.json | 4 +++ examples/fastapi-lazy/_gt/fr.json | 4 +++ examples/fastapi-lazy/app.py | 34 ++++++++++++------------ examples/fastapi-lazy/gt.config.json | 4 +++ examples/flask-eager/_gt/es.json | 4 +++ examples/flask-eager/_gt/fr.json | 4 +++ examples/flask-eager/app.py | 34 ++++++++++++------------ examples/flask-eager/gt.config.json | 4 +++ examples/flask-lazy/_gt/es.json | 4 +++ examples/flask-lazy/_gt/fr.json | 4 +++ examples/flask-lazy/app.py | 33 ++++++++++++------------ examples/flask-lazy/gt.config.json | 4 +++ 16 files changed, 119 insertions(+), 67 deletions(-) create mode 100644 examples/fastapi-eager/_gt/es.json create mode 100644 examples/fastapi-eager/_gt/fr.json create mode 100644 examples/fastapi-eager/gt.config.json create mode 100644 examples/fastapi-lazy/_gt/es.json create mode 100644 examples/fastapi-lazy/_gt/fr.json create mode 100644 examples/fastapi-lazy/gt.config.json create mode 100644 examples/flask-eager/_gt/es.json create mode 100644 examples/flask-eager/_gt/fr.json create mode 100644 examples/flask-eager/gt.config.json create mode 100644 examples/flask-lazy/_gt/es.json create mode 100644 examples/flask-lazy/_gt/fr.json create mode 100644 examples/flask-lazy/gt.config.json diff --git a/examples/fastapi-eager/_gt/es.json b/examples/fastapi-eager/_gt/es.json new file mode 100644 index 0000000..84a3870 --- /dev/null +++ b/examples/fastapi-eager/_gt/es.json @@ -0,0 +1,4 @@ +{ + "8042e0a3d395c1fb": "Hola, mundo!", + "9b323e35e1a80c51": "Hola, {name}!" +} diff --git a/examples/fastapi-eager/_gt/fr.json b/examples/fastapi-eager/_gt/fr.json new file mode 100644 index 0000000..fba7e6d --- /dev/null +++ b/examples/fastapi-eager/_gt/fr.json @@ -0,0 +1,4 @@ +{ + "8042e0a3d395c1fb": "Bonjour, le monde!", + "9b323e35e1a80c51": "Bonjour, {name}!" +} diff --git a/examples/fastapi-eager/app.py b/examples/fastapi-eager/app.py index 8a1fae5..b636079 100644 --- a/examples/fastapi-eager/app.py +++ b/examples/fastapi-eager/app.py @@ -1,39 +1,38 @@ """FastAPI example with eager translation loading. -Translations are loaded at startup for all configured locales. +Translations are stored in _gt/.json and loaded at startup. +Configuration is read from gt.config.json. Run: uv run uvicorn app:app --port 8000 """ +import json +from pathlib import Path + from fastapi import FastAPI from gt_fastapi import initialize_gt, t app = FastAPI(title="FastAPI Eager Example") -# Pre-built translation dictionaries keyed by hash. -# hash_message("Hello, world!") -> "8042e0a3d395c1fb" -# hash_message("Hello, {name}!") -> "9b323e35e1a80c51" -TRANSLATIONS: dict[str, dict[str, str]] = { - "es": { - "8042e0a3d395c1fb": "Hola, mundo!", - "9b323e35e1a80c51": "Hola, {name}!", - }, - "fr": { - "8042e0a3d395c1fb": "Bonjour, le monde!", - "9b323e35e1a80c51": "Bonjour, {name}!", - }, -} +BASE_DIR = Path(__file__).parent +GT_DIR = BASE_DIR / "_gt" + +with open(BASE_DIR / "gt.config.json") as f: + config = json.load(f) def load_translations(locale: str) -> dict[str, str]: - """Return translations for a locale from the in-memory dictionary.""" - print(f"[eager] Loading translations for '{locale}'") - return TRANSLATIONS.get(locale, {}) + """Load translations from _gt/.json.""" + path = GT_DIR / f"{locale}.json" + if path.exists(): + with open(path) as f: + return json.load(f) + return {} initialize_gt( app, - default_locale="en", - locales=["en", "es", "fr"], + default_locale=config.get("defaultLocale", "en"), + locales=config.get("locales"), load_translations=load_translations, eager_loading=True, ) diff --git a/examples/fastapi-eager/gt.config.json b/examples/fastapi-eager/gt.config.json new file mode 100644 index 0000000..9940de2 --- /dev/null +++ b/examples/fastapi-eager/gt.config.json @@ -0,0 +1,4 @@ +{ + "defaultLocale": "en", + "locales": ["es", "fr"] +} diff --git a/examples/fastapi-lazy/_gt/es.json b/examples/fastapi-lazy/_gt/es.json new file mode 100644 index 0000000..84a3870 --- /dev/null +++ b/examples/fastapi-lazy/_gt/es.json @@ -0,0 +1,4 @@ +{ + "8042e0a3d395c1fb": "Hola, mundo!", + "9b323e35e1a80c51": "Hola, {name}!" +} diff --git a/examples/fastapi-lazy/_gt/fr.json b/examples/fastapi-lazy/_gt/fr.json new file mode 100644 index 0000000..fba7e6d --- /dev/null +++ b/examples/fastapi-lazy/_gt/fr.json @@ -0,0 +1,4 @@ +{ + "8042e0a3d395c1fb": "Bonjour, le monde!", + "9b323e35e1a80c51": "Bonjour, {name}!" +} diff --git a/examples/fastapi-lazy/app.py b/examples/fastapi-lazy/app.py index 64d91c3..0f95651 100644 --- a/examples/fastapi-lazy/app.py +++ b/examples/fastapi-lazy/app.py @@ -1,36 +1,38 @@ """FastAPI example with lazy translation loading. -Translations are loaded on first request per locale, not at startup. +Translations are stored in _gt/.json and loaded on first request per locale. +Configuration is read from gt.config.json. Run: uv run uvicorn app:app --port 8001 """ +import json +from pathlib import Path + from fastapi import Depends, FastAPI, Request from gt_fastapi import initialize_gt, t app = FastAPI(title="FastAPI Lazy Example") -TRANSLATIONS: dict[str, dict[str, str]] = { - "es": { - "8042e0a3d395c1fb": "Hola, mundo!", - "9b323e35e1a80c51": "Hola, {name}!", - }, - "fr": { - "8042e0a3d395c1fb": "Bonjour, le monde!", - "9b323e35e1a80c51": "Bonjour, {name}!", - }, -} +BASE_DIR = Path(__file__).parent +GT_DIR = BASE_DIR / "_gt" + +with open(BASE_DIR / "gt.config.json") as f: + config = json.load(f) async def load_translations(locale: str) -> dict[str, str]: - """Simulate loading translations from a remote source.""" - print(f"[lazy] Loading translations for '{locale}'") - return TRANSLATIONS.get(locale, {}) + """Load translations from _gt/.json.""" + path = GT_DIR / f"{locale}.json" + if path.exists(): + with open(path) as f: + return json.load(f) + return {} manager = initialize_gt( app, - default_locale="en", - locales=["en", "es", "fr"], + default_locale=config.get("defaultLocale", "en"), + locales=config.get("locales"), load_translations=load_translations, eager_loading=False, ) diff --git a/examples/fastapi-lazy/gt.config.json b/examples/fastapi-lazy/gt.config.json new file mode 100644 index 0000000..9940de2 --- /dev/null +++ b/examples/fastapi-lazy/gt.config.json @@ -0,0 +1,4 @@ +{ + "defaultLocale": "en", + "locales": ["es", "fr"] +} diff --git a/examples/flask-eager/_gt/es.json b/examples/flask-eager/_gt/es.json new file mode 100644 index 0000000..84a3870 --- /dev/null +++ b/examples/flask-eager/_gt/es.json @@ -0,0 +1,4 @@ +{ + "8042e0a3d395c1fb": "Hola, mundo!", + "9b323e35e1a80c51": "Hola, {name}!" +} diff --git a/examples/flask-eager/_gt/fr.json b/examples/flask-eager/_gt/fr.json new file mode 100644 index 0000000..fba7e6d --- /dev/null +++ b/examples/flask-eager/_gt/fr.json @@ -0,0 +1,4 @@ +{ + "8042e0a3d395c1fb": "Bonjour, le monde!", + "9b323e35e1a80c51": "Bonjour, {name}!" +} diff --git a/examples/flask-eager/app.py b/examples/flask-eager/app.py index bfe8948..4853e27 100644 --- a/examples/flask-eager/app.py +++ b/examples/flask-eager/app.py @@ -1,36 +1,38 @@ """Flask example with eager translation loading. -Translations are loaded at startup for all configured locales. +Translations are stored in _gt/.json and loaded at startup. +Configuration is read from gt.config.json. Run: uv run python app.py (serves on port 5050) """ +import json +from pathlib import Path + from flask import Flask from gt_flask import initialize_gt, t app = Flask(__name__) -TRANSLATIONS: dict[str, dict[str, str]] = { - "es": { - "8042e0a3d395c1fb": "Hola, mundo!", - "9b323e35e1a80c51": "Hola, {name}!", - }, - "fr": { - "8042e0a3d395c1fb": "Bonjour, le monde!", - "9b323e35e1a80c51": "Bonjour, {name}!", - }, -} +BASE_DIR = Path(__file__).parent +GT_DIR = BASE_DIR / "_gt" + +with open(BASE_DIR / "gt.config.json") as f: + config = json.load(f) def load_translations(locale: str) -> dict[str, str]: - """Return translations for a locale from the in-memory dictionary.""" - print(f"[eager] Loading translations for '{locale}'") - return TRANSLATIONS.get(locale, {}) + """Load translations from _gt/.json.""" + path = GT_DIR / f"{locale}.json" + if path.exists(): + with open(path) as f: + return json.load(f) + return {} initialize_gt( app, - default_locale="en", - locales=["en", "es", "fr"], + default_locale=config.get("defaultLocale", "en"), + locales=config.get("locales"), load_translations=load_translations, eager_loading=True, ) diff --git a/examples/flask-eager/gt.config.json b/examples/flask-eager/gt.config.json new file mode 100644 index 0000000..9940de2 --- /dev/null +++ b/examples/flask-eager/gt.config.json @@ -0,0 +1,4 @@ +{ + "defaultLocale": "en", + "locales": ["es", "fr"] +} diff --git a/examples/flask-lazy/_gt/es.json b/examples/flask-lazy/_gt/es.json new file mode 100644 index 0000000..84a3870 --- /dev/null +++ b/examples/flask-lazy/_gt/es.json @@ -0,0 +1,4 @@ +{ + "8042e0a3d395c1fb": "Hola, mundo!", + "9b323e35e1a80c51": "Hola, {name}!" +} diff --git a/examples/flask-lazy/_gt/fr.json b/examples/flask-lazy/_gt/fr.json new file mode 100644 index 0000000..fba7e6d --- /dev/null +++ b/examples/flask-lazy/_gt/fr.json @@ -0,0 +1,4 @@ +{ + "8042e0a3d395c1fb": "Bonjour, le monde!", + "9b323e35e1a80c51": "Bonjour, {name}!" +} diff --git a/examples/flask-lazy/app.py b/examples/flask-lazy/app.py index 4f87f9d..eae9192 100644 --- a/examples/flask-lazy/app.py +++ b/examples/flask-lazy/app.py @@ -1,38 +1,39 @@ """Flask example with lazy translation loading. -Translations are loaded on first request per locale, not at startup. +Translations are stored in _gt/.json and loaded on first request per locale. +Configuration is read from gt.config.json. Run: uv run python app.py (serves on port 5051) """ import asyncio +import json +from pathlib import Path from flask import Flask from gt_flask import initialize_gt, t app = Flask(__name__) -TRANSLATIONS: dict[str, dict[str, str]] = { - "es": { - "8042e0a3d395c1fb": "Hola, mundo!", - "9b323e35e1a80c51": "Hola, {name}!", - }, - "fr": { - "8042e0a3d395c1fb": "Bonjour, le monde!", - "9b323e35e1a80c51": "Bonjour, {name}!", - }, -} +BASE_DIR = Path(__file__).parent +GT_DIR = BASE_DIR / "_gt" + +with open(BASE_DIR / "gt.config.json") as f: + config = json.load(f) def load_translations(locale: str) -> dict[str, str]: - """Simulate loading translations from a remote source.""" - print(f"[lazy] Loading translations for '{locale}'") - return TRANSLATIONS.get(locale, {}) + """Load translations from _gt/.json.""" + path = GT_DIR / f"{locale}.json" + if path.exists(): + with open(path) as f: + return json.load(f) + return {} manager = initialize_gt( app, - default_locale="en", - locales=["en", "es", "fr"], + default_locale=config.get("defaultLocale", "en"), + locales=config.get("locales"), load_translations=load_translations, eager_loading=False, ) diff --git a/examples/flask-lazy/gt.config.json b/examples/flask-lazy/gt.config.json new file mode 100644 index 0000000..9940de2 --- /dev/null +++ b/examples/flask-lazy/gt.config.json @@ -0,0 +1,4 @@ +{ + "defaultLocale": "en", + "locales": ["es", "fr"] +} From aa4eb9ae0519e63aaf71dd00e5181cfe1c1eab80 Mon Sep 17 00:00:00 2001 From: moss-bryophyta <261561981+moss-bryophyta@users.noreply.github.com> Date: Sat, 7 Mar 2026 22:05:41 -0800 Subject: [PATCH 2/5] add guides for declare_static and custom get_locale --- guides/custom-get-locale.md | 133 ++++++++++++++++++++++++++++++++++++ guides/declare-static.md | 87 +++++++++++++++++++++++ 2 files changed, 220 insertions(+) create mode 100644 guides/custom-get-locale.md create mode 100644 guides/declare-static.md diff --git a/guides/custom-get-locale.md b/guides/custom-get-locale.md new file mode 100644 index 0000000..2e6129d --- /dev/null +++ b/guides/custom-get-locale.md @@ -0,0 +1,133 @@ +# Custom locale detection with `get_locale` + +## Overview + +Both `gt_fastapi` and `gt_flask` accept a `get_locale` callback in `initialize_gt()` that controls how the user's locale is determined on each request. If you don't provide one, the default behavior parses the `Accept-Language` header. + +## Default behavior + +When no `get_locale` is provided, GT automatically: + +1. Reads the `Accept-Language` HTTP header from the incoming request (e.g. `en-US,en;q=0.9,es;q=0.8`) +2. Parses it into a list of locales sorted by quality value +3. Calls `determine_locale()` to find the best match against your configured `locales` +4. Falls back to `default_locale` if no match is found + +This means out of the box, users get content in whatever language their browser requests — as long as you have translations for it. + +## Providing a custom `get_locale` + +Pass a callable that takes the request object and returns a locale string: + +### FastAPI + +```python +from fastapi import FastAPI, Request +from gt_fastapi import initialize_gt + +app = FastAPI() + +def get_locale(request: Request) -> str: + """Detect locale from a query parameter, cookie, or header.""" + # 1. Check query parameter + locale = request.query_params.get("lang") + if locale: + return locale + + # 2. Check cookie + locale = request.cookies.get("locale") + if locale: + return locale + + # 3. Fall back to default + return "en" + +initialize_gt( + app, + default_locale="en", + locales=["es", "fr"], + get_locale=get_locale, +) +``` + +### Flask + +```python +from flask import Flask, request +from gt_flask import initialize_gt + +app = Flask(__name__) + +def get_locale(req) -> str: + """Detect locale from a query parameter, cookie, or header.""" + # 1. Check query parameter + locale = req.args.get("lang") + if locale: + return locale + + # 2. Check cookie + locale = req.cookies.get("locale") + if locale: + return locale + + # 3. Fall back to default + return "en" + +initialize_gt( + app, + default_locale="en", + locales=["es", "fr"], + get_locale=get_locale, +) +``` + +## How it's used internally + +When a request comes in: + +- **FastAPI**: The `gt_middleware` runs on every request. If `get_locale` is provided, it calls `get_locale(request)`. Otherwise it parses `Accept-Language`. +- **Flask**: A `before_request` hook runs on every request with the same logic. + +The resolved locale is then set on the `I18nManager`, which `t()` reads from when translating strings. + +## Common patterns + +### URL path prefix + +```python +def get_locale(request) -> str: + """Extract locale from URL path like /es/about or /fr/home.""" + parts = request.url.path.strip("/").split("/") + if parts and parts[0] in ("es", "fr", "de"): + return parts[0] + return "en" +``` + +### User profile / database + +```python +def get_locale(request) -> str: + """Look up locale from authenticated user's profile.""" + user = get_current_user(request) # your auth logic + if user and user.preferred_locale: + return user.preferred_locale + return "en" +``` + +### Subdomain + +```python +def get_locale(request) -> str: + """Detect locale from subdomain like es.example.com.""" + host = request.headers.get("host", "") + subdomain = host.split(".")[0] + if subdomain in ("es", "fr", "de"): + return subdomain + return "en" +``` + +## Notes + +- Your `get_locale` function should always return a valid locale string +- If it returns a locale you don't have translations for, `t()` will fall back to the original (default locale) content +- The function receives the raw request object — `Request` for FastAPI, `flask.request` for Flask diff --git a/guides/declare-static.md b/guides/declare-static.md new file mode 100644 index 0000000..87bad84 --- /dev/null +++ b/guides/declare-static.md @@ -0,0 +1,87 @@ +# Using `declare_static` + +`declare_static` marks content as statically analyzable so that the GT CLI can extract all possible translation entries at build time. + +## Overview + +`declare_static` is an identity function — it returns exactly what you pass in. Its purpose is as a **marker** for static analysis tools. When the GT CLI scans your code, it recognizes `declare_static(...)` calls and determines all possible return values, creating a separate translation entry for each. + +This is useful for: +- **Preserving word agreement** across languages (gender, plurality, etc.) +- **Reusable content** with function calls inside translated strings +- **Fragmented sentences** where part of the string is dynamic but has a known set of outcomes + +## Installation + +`declare_static` is exported from all GT Python packages: + +```python +from gt_fastapi import declare_static +# or +from gt_flask import declare_static +# or +from gt_i18n import declare_static +``` + +## Basic usage + +```python +from gt_fastapi import declare_static, t + +def get_subject(gender: str) -> str: + return "boy" if gender == "male" else "girl" + +# Without declare_static — the CLI can't know the possible values +message = t(f"The {get_subject(gender)} is playing.") + +# With declare_static — the CLI extracts both outcomes +message = t(f"The {declare_static(get_subject(gender))} is playing.") +``` + +With `declare_static`, the CLI creates two translation entries: +- `"The boy is playing."` → `"El niño está jugando."` +- `"The girl is playing."` → `"La niña está jugando."` + +Notice how agreement is handled automatically — Spanish uses "El" vs "La" depending on the subject. + +## How it works + +1. **At build time**, the GT CLI analyzes functions wrapped by `declare_static` +2. It determines all possible return values (these must be statically analyzable) +3. It creates separate translation entries for each unique outcome +4. **At runtime**, `declare_static` is just an identity function — it returns its argument unchanged + +## Combining with `declare_var` + +Use `declare_var` for truly dynamic content (user input, API data) inside a `declare_static` call: + +```python +from gt_fastapi import declare_static, declare_var, t + +def get_greeting(name: str | None) -> str: + if name: + return f"Hello, {declare_var(name)}" + return "Hello, stranger" + +message = t(f"{declare_static(get_greeting(name))}! How are you?") +``` + +## Inline expressions + +You can embed logic directly: + +```python +from gt_fastapi import declare_static, t + +message = t(f"The {declare_static('boy' if gender == 'male' else 'girl')} is playing.") +``` + +## Performance considerations + +`declare_static` multiplies translation entries. Each call with N possible outcomes creates N entries, and multiple `declare_static` calls in the same string multiply exponentially. Use judiciously. + +## Notes + +- All possible outcomes must be statically analyzable at build time +- Dynamic content (variables, API calls) inside `declare_static` should be wrapped with `declare_var` +- `decode_vars` can be used to extract original values from declared variables From cb194cd8d4c296bf6faf3116c607458b9d0c005f Mon Sep 17 00:00:00 2001 From: moss-bryophyta <261561981+moss-bryophyta@users.noreply.github.com> Date: Sat, 7 Mar 2026 22:08:21 -0800 Subject: [PATCH 3/5] Revert "add guides for declare_static and custom get_locale" This reverts commit 877ec87ce56706c6f85ccf50adbd0fdade1bb11e. --- guides/custom-get-locale.md | 133 ------------------------------------ guides/declare-static.md | 87 ----------------------- 2 files changed, 220 deletions(-) delete mode 100644 guides/custom-get-locale.md delete mode 100644 guides/declare-static.md diff --git a/guides/custom-get-locale.md b/guides/custom-get-locale.md deleted file mode 100644 index 2e6129d..0000000 --- a/guides/custom-get-locale.md +++ /dev/null @@ -1,133 +0,0 @@ -# Custom locale detection with `get_locale` - -## Overview - -Both `gt_fastapi` and `gt_flask` accept a `get_locale` callback in `initialize_gt()` that controls how the user's locale is determined on each request. If you don't provide one, the default behavior parses the `Accept-Language` header. - -## Default behavior - -When no `get_locale` is provided, GT automatically: - -1. Reads the `Accept-Language` HTTP header from the incoming request (e.g. `en-US,en;q=0.9,es;q=0.8`) -2. Parses it into a list of locales sorted by quality value -3. Calls `determine_locale()` to find the best match against your configured `locales` -4. Falls back to `default_locale` if no match is found - -This means out of the box, users get content in whatever language their browser requests — as long as you have translations for it. - -## Providing a custom `get_locale` - -Pass a callable that takes the request object and returns a locale string: - -### FastAPI - -```python -from fastapi import FastAPI, Request -from gt_fastapi import initialize_gt - -app = FastAPI() - -def get_locale(request: Request) -> str: - """Detect locale from a query parameter, cookie, or header.""" - # 1. Check query parameter - locale = request.query_params.get("lang") - if locale: - return locale - - # 2. Check cookie - locale = request.cookies.get("locale") - if locale: - return locale - - # 3. Fall back to default - return "en" - -initialize_gt( - app, - default_locale="en", - locales=["es", "fr"], - get_locale=get_locale, -) -``` - -### Flask - -```python -from flask import Flask, request -from gt_flask import initialize_gt - -app = Flask(__name__) - -def get_locale(req) -> str: - """Detect locale from a query parameter, cookie, or header.""" - # 1. Check query parameter - locale = req.args.get("lang") - if locale: - return locale - - # 2. Check cookie - locale = req.cookies.get("locale") - if locale: - return locale - - # 3. Fall back to default - return "en" - -initialize_gt( - app, - default_locale="en", - locales=["es", "fr"], - get_locale=get_locale, -) -``` - -## How it's used internally - -When a request comes in: - -- **FastAPI**: The `gt_middleware` runs on every request. If `get_locale` is provided, it calls `get_locale(request)`. Otherwise it parses `Accept-Language`. -- **Flask**: A `before_request` hook runs on every request with the same logic. - -The resolved locale is then set on the `I18nManager`, which `t()` reads from when translating strings. - -## Common patterns - -### URL path prefix - -```python -def get_locale(request) -> str: - """Extract locale from URL path like /es/about or /fr/home.""" - parts = request.url.path.strip("/").split("/") - if parts and parts[0] in ("es", "fr", "de"): - return parts[0] - return "en" -``` - -### User profile / database - -```python -def get_locale(request) -> str: - """Look up locale from authenticated user's profile.""" - user = get_current_user(request) # your auth logic - if user and user.preferred_locale: - return user.preferred_locale - return "en" -``` - -### Subdomain - -```python -def get_locale(request) -> str: - """Detect locale from subdomain like es.example.com.""" - host = request.headers.get("host", "") - subdomain = host.split(".")[0] - if subdomain in ("es", "fr", "de"): - return subdomain - return "en" -``` - -## Notes - -- Your `get_locale` function should always return a valid locale string -- If it returns a locale you don't have translations for, `t()` will fall back to the original (default locale) content -- The function receives the raw request object — `Request` for FastAPI, `flask.request` for Flask diff --git a/guides/declare-static.md b/guides/declare-static.md deleted file mode 100644 index 87bad84..0000000 --- a/guides/declare-static.md +++ /dev/null @@ -1,87 +0,0 @@ -# Using `declare_static` - -`declare_static` marks content as statically analyzable so that the GT CLI can extract all possible translation entries at build time. - -## Overview - -`declare_static` is an identity function — it returns exactly what you pass in. Its purpose is as a **marker** for static analysis tools. When the GT CLI scans your code, it recognizes `declare_static(...)` calls and determines all possible return values, creating a separate translation entry for each. - -This is useful for: -- **Preserving word agreement** across languages (gender, plurality, etc.) -- **Reusable content** with function calls inside translated strings -- **Fragmented sentences** where part of the string is dynamic but has a known set of outcomes - -## Installation - -`declare_static` is exported from all GT Python packages: - -```python -from gt_fastapi import declare_static -# or -from gt_flask import declare_static -# or -from gt_i18n import declare_static -``` - -## Basic usage - -```python -from gt_fastapi import declare_static, t - -def get_subject(gender: str) -> str: - return "boy" if gender == "male" else "girl" - -# Without declare_static — the CLI can't know the possible values -message = t(f"The {get_subject(gender)} is playing.") - -# With declare_static — the CLI extracts both outcomes -message = t(f"The {declare_static(get_subject(gender))} is playing.") -``` - -With `declare_static`, the CLI creates two translation entries: -- `"The boy is playing."` → `"El niño está jugando."` -- `"The girl is playing."` → `"La niña está jugando."` - -Notice how agreement is handled automatically — Spanish uses "El" vs "La" depending on the subject. - -## How it works - -1. **At build time**, the GT CLI analyzes functions wrapped by `declare_static` -2. It determines all possible return values (these must be statically analyzable) -3. It creates separate translation entries for each unique outcome -4. **At runtime**, `declare_static` is just an identity function — it returns its argument unchanged - -## Combining with `declare_var` - -Use `declare_var` for truly dynamic content (user input, API data) inside a `declare_static` call: - -```python -from gt_fastapi import declare_static, declare_var, t - -def get_greeting(name: str | None) -> str: - if name: - return f"Hello, {declare_var(name)}" - return "Hello, stranger" - -message = t(f"{declare_static(get_greeting(name))}! How are you?") -``` - -## Inline expressions - -You can embed logic directly: - -```python -from gt_fastapi import declare_static, t - -message = t(f"The {declare_static('boy' if gender == 'male' else 'girl')} is playing.") -``` - -## Performance considerations - -`declare_static` multiplies translation entries. Each call with N possible outcomes creates N entries, and multiple `declare_static` calls in the same string multiply exponentially. Use judiciously. - -## Notes - -- All possible outcomes must be statically analyzable at build time -- Dynamic content (variables, API calls) inside `declare_static` should be wrapped with `declare_var` -- `decode_vars` can be used to extract original values from declared variables From 7011046237eeaed02ca4ba4ade85725cb4d27e67 Mon Sep 17 00:00:00 2001 From: moss-bryophyta <261561981+moss-bryophyta@users.noreply.github.com> Date: Mon, 9 Mar 2026 15:16:30 -0700 Subject: [PATCH 4/5] refactor(examples): let initialize_gt auto-read gt.config.json from CWD MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Remove manual gt.config.json parsing — initialize_gt reads it automatically from the current working directory. --- examples/fastapi-eager/app.py | 10 ++-------- examples/fastapi-lazy/app.py | 10 ++-------- examples/flask-eager/app.py | 10 ++-------- examples/flask-lazy/app.py | 10 ++-------- 4 files changed, 8 insertions(+), 32 deletions(-) diff --git a/examples/fastapi-eager/app.py b/examples/fastapi-eager/app.py index b636079..b05c14d 100644 --- a/examples/fastapi-eager/app.py +++ b/examples/fastapi-eager/app.py @@ -1,7 +1,7 @@ """FastAPI example with eager translation loading. Translations are stored in _gt/.json and loaded at startup. -Configuration is read from gt.config.json. +Configuration is read automatically from gt.config.json in the CWD. Run: uv run uvicorn app:app --port 8000 """ @@ -13,11 +13,7 @@ app = FastAPI(title="FastAPI Eager Example") -BASE_DIR = Path(__file__).parent -GT_DIR = BASE_DIR / "_gt" - -with open(BASE_DIR / "gt.config.json") as f: - config = json.load(f) +GT_DIR = Path(__file__).parent / "_gt" def load_translations(locale: str) -> dict[str, str]: @@ -31,8 +27,6 @@ def load_translations(locale: str) -> dict[str, str]: initialize_gt( app, - default_locale=config.get("defaultLocale", "en"), - locales=config.get("locales"), load_translations=load_translations, eager_loading=True, ) diff --git a/examples/fastapi-lazy/app.py b/examples/fastapi-lazy/app.py index 0f95651..a795c05 100644 --- a/examples/fastapi-lazy/app.py +++ b/examples/fastapi-lazy/app.py @@ -1,7 +1,7 @@ """FastAPI example with lazy translation loading. Translations are stored in _gt/.json and loaded on first request per locale. -Configuration is read from gt.config.json. +Configuration is read automatically from gt.config.json in the CWD. Run: uv run uvicorn app:app --port 8001 """ @@ -13,11 +13,7 @@ app = FastAPI(title="FastAPI Lazy Example") -BASE_DIR = Path(__file__).parent -GT_DIR = BASE_DIR / "_gt" - -with open(BASE_DIR / "gt.config.json") as f: - config = json.load(f) +GT_DIR = Path(__file__).parent / "_gt" async def load_translations(locale: str) -> dict[str, str]: @@ -31,8 +27,6 @@ async def load_translations(locale: str) -> dict[str, str]: manager = initialize_gt( app, - default_locale=config.get("defaultLocale", "en"), - locales=config.get("locales"), load_translations=load_translations, eager_loading=False, ) diff --git a/examples/flask-eager/app.py b/examples/flask-eager/app.py index 4853e27..daf42cf 100644 --- a/examples/flask-eager/app.py +++ b/examples/flask-eager/app.py @@ -1,7 +1,7 @@ """Flask example with eager translation loading. Translations are stored in _gt/.json and loaded at startup. -Configuration is read from gt.config.json. +Configuration is read automatically from gt.config.json in the CWD. Run: uv run python app.py (serves on port 5050) """ @@ -13,11 +13,7 @@ app = Flask(__name__) -BASE_DIR = Path(__file__).parent -GT_DIR = BASE_DIR / "_gt" - -with open(BASE_DIR / "gt.config.json") as f: - config = json.load(f) +GT_DIR = Path(__file__).parent / "_gt" def load_translations(locale: str) -> dict[str, str]: @@ -31,8 +27,6 @@ def load_translations(locale: str) -> dict[str, str]: initialize_gt( app, - default_locale=config.get("defaultLocale", "en"), - locales=config.get("locales"), load_translations=load_translations, eager_loading=True, ) diff --git a/examples/flask-lazy/app.py b/examples/flask-lazy/app.py index eae9192..d503937 100644 --- a/examples/flask-lazy/app.py +++ b/examples/flask-lazy/app.py @@ -1,7 +1,7 @@ """Flask example with lazy translation loading. Translations are stored in _gt/.json and loaded on first request per locale. -Configuration is read from gt.config.json. +Configuration is read automatically from gt.config.json in the CWD. Run: uv run python app.py (serves on port 5051) """ @@ -14,11 +14,7 @@ app = Flask(__name__) -BASE_DIR = Path(__file__).parent -GT_DIR = BASE_DIR / "_gt" - -with open(BASE_DIR / "gt.config.json") as f: - config = json.load(f) +GT_DIR = Path(__file__).parent / "_gt" def load_translations(locale: str) -> dict[str, str]: @@ -32,8 +28,6 @@ def load_translations(locale: str) -> dict[str, str]: manager = initialize_gt( app, - default_locale=config.get("defaultLocale", "en"), - locales=config.get("locales"), load_translations=load_translations, eager_loading=False, ) From ae67bd9795ff56d59a22d1bfb8599c201b2b7f3f Mon Sep 17 00:00:00 2001 From: moss-bryophyta <261561981+moss-bryophyta@users.noreply.github.com> Date: Thu, 19 Mar 2026 17:28:23 -0700 Subject: [PATCH 5/5] chore: update examples to latest API conventions - Remove redundant config params (read from gt.config.json) - Simplify load_translations helpers - Fix Flask eager loading bug: use resolved_locales instead of locales param - Clean up docstrings and imports - All four examples tested and working --- examples/fastapi-eager/app.py | 11 +++-------- examples/fastapi-lazy/app.py | 22 ++++++---------------- examples/flask-eager/app.py | 15 ++++----------- examples/flask-lazy/app.py | 24 ++++++------------------ packages/gt-flask/src/gt_flask/_setup.py | 2 +- 5 files changed, 20 insertions(+), 54 deletions(-) diff --git a/examples/fastapi-eager/app.py b/examples/fastapi-eager/app.py index b05c14d..cacef98 100644 --- a/examples/fastapi-eager/app.py +++ b/examples/fastapi-eager/app.py @@ -1,7 +1,7 @@ """FastAPI example with eager translation loading. -Translations are stored in _gt/.json and loaded at startup. -Configuration is read automatically from gt.config.json in the CWD. +Translations are loaded from _gt/.json at startup. +Configuration is read from gt.config.json. Run: uv run uvicorn app:app --port 8000 """ @@ -17,7 +17,6 @@ def load_translations(locale: str) -> dict[str, str]: - """Load translations from _gt/.json.""" path = GT_DIR / f"{locale}.json" if path.exists(): with open(path) as f: @@ -25,11 +24,7 @@ def load_translations(locale: str) -> dict[str, str]: return {} -initialize_gt( - app, - load_translations=load_translations, - eager_loading=True, -) +initialize_gt(app, load_translations=load_translations) @app.get("/") diff --git a/examples/fastapi-lazy/app.py b/examples/fastapi-lazy/app.py index a795c05..2fc013d 100644 --- a/examples/fastapi-lazy/app.py +++ b/examples/fastapi-lazy/app.py @@ -1,7 +1,7 @@ """FastAPI example with lazy translation loading. -Translations are stored in _gt/.json and loaded on first request per locale. -Configuration is read automatically from gt.config.json in the CWD. +Translations are loaded from _gt/.json on first request per locale. +Configuration is read from gt.config.json. Run: uv run uvicorn app:app --port 8001 """ @@ -17,7 +17,6 @@ async def load_translations(locale: str) -> dict[str, str]: - """Load translations from _gt/.json.""" path = GT_DIR / f"{locale}.json" if path.exists(): with open(path) as f: @@ -25,26 +24,17 @@ async def load_translations(locale: str) -> dict[str, str]: return {} -manager = initialize_gt( - app, - load_translations=load_translations, - eager_loading=False, -) +manager = initialize_gt(app, load_translations=load_translations, eager_loading=False) -async def _ensure_translations(request: Request) -> None: - """Load translations for the current locale if not already cached. - - t() only reads from cache (get_translations_sync), so we must - explicitly trigger a load for the current locale before t() runs. - """ +async def ensure_translations(request: Request) -> None: + """Load translations for the request locale before t() runs.""" locale = manager.get_locale() if manager.requires_translation(locale): await manager.get_translations(locale) -# Add dependency to all routes — runs inside route context after middleware -app.router.dependencies = [Depends(_ensure_translations)] +app.router.dependencies = [Depends(ensure_translations)] @app.get("/") diff --git a/examples/flask-eager/app.py b/examples/flask-eager/app.py index daf42cf..ac61549 100644 --- a/examples/flask-eager/app.py +++ b/examples/flask-eager/app.py @@ -1,14 +1,14 @@ """Flask example with eager translation loading. -Translations are stored in _gt/.json and loaded at startup. -Configuration is read automatically from gt.config.json in the CWD. +Translations are loaded from _gt/.json at startup. +Configuration is read from gt.config.json. Run: uv run python app.py (serves on port 5050) """ import json from pathlib import Path -from flask import Flask +from flask import Flask, request from gt_flask import initialize_gt, t app = Flask(__name__) @@ -17,7 +17,6 @@ def load_translations(locale: str) -> dict[str, str]: - """Load translations from _gt/.json.""" path = GT_DIR / f"{locale}.json" if path.exists(): with open(path) as f: @@ -25,11 +24,7 @@ def load_translations(locale: str) -> dict[str, str]: return {} -initialize_gt( - app, - load_translations=load_translations, - eager_loading=True, -) +initialize_gt(app, load_translations=load_translations) @app.get("/") @@ -39,8 +34,6 @@ def index() -> dict[str, str]: @app.get("/greet") def greet() -> dict[str, str]: - from flask import request - name = request.args.get("name", "World") return {"message": t("Hello, {name}!", name=name)} diff --git a/examples/flask-lazy/app.py b/examples/flask-lazy/app.py index d503937..e80beb2 100644 --- a/examples/flask-lazy/app.py +++ b/examples/flask-lazy/app.py @@ -1,7 +1,7 @@ """Flask example with lazy translation loading. -Translations are stored in _gt/.json and loaded on first request per locale. -Configuration is read automatically from gt.config.json in the CWD. +Translations are loaded from _gt/.json on first request per locale. +Configuration is read from gt.config.json. Run: uv run python app.py (serves on port 5051) """ @@ -9,7 +9,7 @@ import json from pathlib import Path -from flask import Flask +from flask import Flask, request from gt_flask import initialize_gt, t app = Flask(__name__) @@ -18,7 +18,6 @@ def load_translations(locale: str) -> dict[str, str]: - """Load translations from _gt/.json.""" path = GT_DIR / f"{locale}.json" if path.exists(): with open(path) as f: @@ -26,21 +25,12 @@ def load_translations(locale: str) -> dict[str, str]: return {} -manager = initialize_gt( - app, - load_translations=load_translations, - eager_loading=False, -) +manager = initialize_gt(app, load_translations=load_translations, eager_loading=False) @app.before_request -def _ensure_translations() -> None: - """Load translations for the current locale if not already cached. - - Registered after initialize_gt(), so this runs after the locale is set. - t() only reads from cache (get_translations_sync), so we must - explicitly trigger a load for the current locale before t() runs. - """ +def ensure_translations() -> None: + """Load translations for the request locale before t() runs.""" locale = manager.get_locale() if manager.requires_translation(locale): asyncio.run(manager.get_translations(locale)) @@ -53,8 +43,6 @@ def index() -> dict[str, str]: @app.get("/greet") def greet() -> dict[str, str]: - from flask import request - name = request.args.get("name", "World") return {"message": t("Hello, {name}!", name=name)} diff --git a/packages/gt-flask/src/gt_flask/_setup.py b/packages/gt-flask/src/gt_flask/_setup.py index 004ee73..f82570c 100644 --- a/packages/gt-flask/src/gt_flask/_setup.py +++ b/packages/gt-flask/src/gt_flask/_setup.py @@ -71,7 +71,7 @@ def initialize_gt( ) set_i18n_manager(manager) - if eager_loading and locales: + if eager_loading and resolved_locales: asyncio.run(manager.load_all_translations()) @app.before_request