Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
4 changes: 4 additions & 0 deletions examples/fastapi-eager/_gt/es.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,4 @@
{
"8042e0a3d395c1fb": "Hola, mundo!",
"9b323e35e1a80c51": "Hola, {name}!"
}
4 changes: 4 additions & 0 deletions examples/fastapi-eager/_gt/fr.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,4 @@
{
"8042e0a3d395c1fb": "Bonjour, le monde!",
"9b323e35e1a80c51": "Bonjour, {name}!"
}
40 changes: 14 additions & 26 deletions examples/fastapi-eager/app.py
Original file line number Diff line number Diff line change
@@ -1,42 +1,30 @@
"""FastAPI example with eager translation loading.

Translations are loaded at startup for all configured locales.
Translations are loaded from _gt/<locale>.json 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}!",
},
}
GT_DIR = Path(__file__).parent / "_gt"


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, {})


initialize_gt(
app,
default_locale="en",
locales=["en", "es", "fr"],
load_translations=load_translations,
eager_loading=True,
)
path = GT_DIR / f"{locale}.json"
Copy link

Choose a reason for hiding this comment

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

Unsanitized locale in file path

The locale string is taken directly from the Accept-Language header (after determine_locale resolves it) and used to construct a file path without any validation. A crafted header containing traversal sequences such as ../../secrets would build a path like _gt/../../secrets.json, potentially reaching files outside the _gt/ directory.

While the appended .json extension and the path.exists() guard reduce practical risk, a defence-in-depth approach would validate that the resolved locale only contains safe characters before using it in path construction:

def load_translations(locale: str) -> dict[str, str]:
    """Load translations from _gt/<locale>.json."""
    import re
    if not re.fullmatch(r"[a-zA-Z0-9_-]+", locale):
        return {}
    path = GT_DIR / f"{locale}.json"
    if path.exists():
        with open(path) as f:
            return json.load(f)
    return {}

The same pattern appears in all four examples:

  • examples/fastapi-lazy/app.py:25
  • examples/flask-eager/app.py:25
  • examples/flask-lazy/app.py:25
Prompt To Fix With AI
This is a comment left during a code review.
Path: examples/fastapi-eager/app.py
Line: 25

Comment:
**Unsanitized locale in file path**

The `locale` string is taken directly from the `Accept-Language` header (after `determine_locale` resolves it) and used to construct a file path without any validation. A crafted header containing traversal sequences such as `../../secrets` would build a path like `_gt/../../secrets.json`, potentially reaching files outside the `_gt/` directory.

While the appended `.json` extension and the `path.exists()` guard reduce practical risk, a defence-in-depth approach would validate that the resolved locale only contains safe characters before using it in path construction:

```python
def load_translations(locale: str) -> dict[str, str]:
    """Load translations from _gt/<locale>.json."""
    import re
    if not re.fullmatch(r"[a-zA-Z0-9_-]+", locale):
        return {}
    path = GT_DIR / f"{locale}.json"
    if path.exists():
        with open(path) as f:
            return json.load(f)
    return {}
```

The same pattern appears in all four examples:
- `examples/fastapi-lazy/app.py:25`
- `examples/flask-eager/app.py:25`
- `examples/flask-lazy/app.py:25`

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

if path.exists():
with open(path) as f:
return json.load(f)
return {}


initialize_gt(app, load_translations=load_translations)


@app.get("/")
Expand Down
4 changes: 4 additions & 0 deletions examples/fastapi-eager/gt.config.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,4 @@
{
"defaultLocale": "en",
"locales": ["es", "fr"]
}
4 changes: 4 additions & 0 deletions examples/fastapi-lazy/_gt/es.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,4 @@
{
"8042e0a3d395c1fb": "Hola, mundo!",
"9b323e35e1a80c51": "Hola, {name}!"
}
4 changes: 4 additions & 0 deletions examples/fastapi-lazy/_gt/fr.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,4 @@
{
"8042e0a3d395c1fb": "Bonjour, le monde!",
"9b323e35e1a80c51": "Bonjour, {name}!"
}
44 changes: 15 additions & 29 deletions examples/fastapi-lazy/app.py
Original file line number Diff line number Diff line change
@@ -1,54 +1,40 @@
"""FastAPI example with lazy translation loading.

Translations are loaded on first request per locale, not at startup.
Translations are loaded from _gt/<locale>.json 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}!",
},
}
GT_DIR = Path(__file__).parent / "_gt"


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, {})

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"],
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("/")
Expand Down
4 changes: 4 additions & 0 deletions examples/fastapi-lazy/gt.config.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,4 @@
{
"defaultLocale": "en",
"locales": ["es", "fr"]
}
4 changes: 4 additions & 0 deletions examples/flask-eager/_gt/es.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,4 @@
{
"8042e0a3d395c1fb": "Hola, mundo!",
"9b323e35e1a80c51": "Hola, {name}!"
}
4 changes: 4 additions & 0 deletions examples/flask-eager/_gt/fr.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,4 @@
{
"8042e0a3d395c1fb": "Bonjour, le monde!",
"9b323e35e1a80c51": "Bonjour, {name}!"
}
37 changes: 13 additions & 24 deletions examples/flask-eager/app.py
Original file line number Diff line number Diff line change
@@ -1,39 +1,30 @@
"""Flask example with eager translation loading.

Translations are loaded at startup for all configured locales.
Translations are loaded from _gt/<locale>.json at startup.
Configuration is read from gt.config.json.
Run: uv run python app.py (serves on port 5050)
"""

from flask import Flask
import json
from pathlib import Path

from flask import Flask, request
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}!",
},
}
GT_DIR = Path(__file__).parent / "_gt"


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, {})
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"],
load_translations=load_translations,
eager_loading=True,
)
initialize_gt(app, load_translations=load_translations)


@app.get("/")
Expand All @@ -43,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)}

Expand Down
4 changes: 4 additions & 0 deletions examples/flask-eager/gt.config.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,4 @@
{
"defaultLocale": "en",
"locales": ["es", "fr"]
}
4 changes: 4 additions & 0 deletions examples/flask-lazy/_gt/es.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,4 @@
{
"8042e0a3d395c1fb": "Hola, mundo!",
"9b323e35e1a80c51": "Hola, {name}!"
}
4 changes: 4 additions & 0 deletions examples/flask-lazy/_gt/fr.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,4 @@
{
"8042e0a3d395c1fb": "Bonjour, le monde!",
"9b323e35e1a80c51": "Bonjour, {name}!"
}
45 changes: 14 additions & 31 deletions examples/flask-lazy/app.py
Original file line number Diff line number Diff line change
@@ -1,51 +1,36 @@
"""Flask example with lazy translation loading.

Translations are loaded on first request per locale, not at startup.
Translations are loaded from _gt/<locale>.json 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 flask import Flask, request
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}!",
},
}
GT_DIR = Path(__file__).parent / "_gt"


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, {})
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"],
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))
Expand All @@ -58,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)}

Expand Down
4 changes: 4 additions & 0 deletions examples/flask-lazy/gt.config.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,4 @@
{
"defaultLocale": "en",
"locales": ["es", "fr"]
}
2 changes: 1 addition & 1 deletion packages/gt-flask/src/gt_flask/_setup.py
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
Loading