Skip to content

feat: localized emails (es, pt, de, fr, ja)#7

Open
FedericoMusa wants to merge 1 commit intopyoneerC:mainfrom
FedericoMusa:feat/localized-emails
Open

feat: localized emails (es, pt, de, fr, ja)#7
FedericoMusa wants to merge 1 commit intopyoneerC:mainfrom
FedericoMusa:feat/localized-emails

Conversation

@FedericoMusa
Copy link

@FedericoMusa FedericoMusa commented Mar 16, 2026

What this PR does

Adds multi-language support for all transactional emails sent by Deadhand.

Previously, all emails were hardcoded in English inside main.py.
This PR extracts them into a dedicated app/emails.py module and adds
translations for 5 languages.

Changes

  • app/emails.py (new file): contains all 5 email templates in 6 languages
  • app/models.py: adds preferred_language column to the User model
  • app/main.py: refactored to use the new email functions

Languages supported

🇺🇸 English · 🇦🇷 Spanish · 🇧🇷 Portuguese · 🇩🇪 German · 🇫🇷 French · 🇯🇵 Japanese

How language is detected

Language is inferred from the user's email TLD at registration time
(e.g. .ar → Spanish, .br → Portuguese) and stored in preferred_language.
Falls back to English if the TLD is unknown.

Emails covered

  • Welcome email (on vault creation)
  • Cancellation email
  • 30-day heartbeat reminder
  • 60-day warning
  • 90-day death trigger (to beneficiary)

Testing

Tested locally with a .com.ar email — confirmed Spanish email generated correctly.

Notes

Requires a DB migration to add the preferred_language column:
ALTER TABLE users ADD COLUMN preferred_language VARCHAR DEFAULT 'en';

Summary by CodeRabbit

Release Notes

New Features

  • Added multilingual email support spanning 6 languages (English, Spanish, Portuguese, German, French, and Japanese)
  • Implemented automatic language detection based on user email domain
  • Users receive all system emails in their preferred language
  • All transactional emails (welcome, reminders, cancellations, and death notifications) are now localized

@vercel
Copy link

vercel bot commented Mar 16, 2026

@FedericoMusa is attempting to deploy a commit to the pyoneerc's projects Team on Vercel.

A member of the Team first needs to authorize it.

@coderabbitai
Copy link

coderabbitai bot commented Mar 16, 2026

📝 Walkthrough

Walkthrough

This PR introduces a multilingual email templating system for the Deadhand Protocol application. A new emails module provides language utilities, localized email builders, and HTML composition helpers supporting six languages. The User model gains a preferred_language field, and main application routes refactor to use modular, localized email generation instead of hard-coded HTML strings.

Changes

Cohort / File(s) Summary
Email Templating Module
app/emails.py
New module with language support (English, Spanish, Portuguese, German, French, Japanese), language detection from email domain, and five email builder functions that render localized HTML templates with subject mappings and shared styling.
Integration & Refactoring
app/main.py
Imports email utilities and refactors all email generation paths (welcome, cancellation, 30/60/90-day reminders, death notification) to use localized templates via language inference. User creation updated to include preferred_language parameter.
Data Model
app/models.py
Adds preferred_language column to User model with default value "en" for storing user language preference.

Estimated code review effort

🎯 3 (Moderate) | ⏱️ ~20 minutes

Poem

🐰 From warren to inbox, translations take flight,
Six tongues now whisper through emails so bright!
With hearts that beat steady across every shore,
Our messages speak in the language you adore. 💌✨

🚥 Pre-merge checks | ✅ 2 | ❌ 1

❌ Failed checks (1 warning)

Check name Status Explanation Resolution
Docstring Coverage ⚠️ Warning Docstring coverage is 27.27% which is insufficient. The required threshold is 80.00%. Write docstrings for the functions missing them to satisfy the coverage threshold.
✅ Passed checks (2 passed)
Check name Status Explanation
Description Check ✅ Passed Check skipped - CodeRabbit’s high-level summary is enabled.
Title check ✅ Passed The title directly and clearly summarizes the main change: introducing localized email support for multiple languages (Spanish, Portuguese, German, French, Japanese).

✏️ Tip: You can configure your own custom pre-merge checks in the settings.

✨ Finishing Touches
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Post copyable unit tests in a comment
📝 Coding Plan
  • Generate coding plan for human review comments

Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out.

❤️ Share

Comment @coderabbitai help to get the list of available commands and usage tips.

Tip

CodeRabbit can approve the review once all CodeRabbit's comments are resolved.

Enable the reviews.request_changes_workflow setting to automatically approve the review once all CodeRabbit's comments are resolved.

Copy link

@coderabbitai coderabbitai bot left a comment

Choose a reason for hiding this comment

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

Actionable comments posted: 4

🧹 Nitpick comments (1)
app/emails.py (1)

123-350: Avoid rebuilding all localized template dictionaries on every call.

Each get_*_email call recreates large bodies maps. Move them to module-level constants and keep runtime work to lookup + formatting.

Also applies to: 357-432, 439-592

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@app/emails.py` around lines 123 - 350, Currently get_welcome_email,
get_cancellation_email and get_reminder_30d_email rebuild large localized
`bodies` dicts on every call; extract those per-language templates into
module-level constants (e.g., WELCOME_BODIES, CANCELLATION_BODIES,
REMINDER_30D_BODIES) and have the functions simply lookup the template by lang,
apply formatting (inject email/beneficiary/heartbeat_link) and pass the result
to _html_wrap; ensure you preserve the same string keys
("en","es","pt","de","fr","ja"), keep the same formatting placeholders used in
the f-strings, and fall back to the "en" template when lang is missing.
🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.

Inline comments:
In `@app/emails.py`:
- Around line 123-228: The email templates in get_welcome_email are
interpolating unescaped dynamic values (email, beneficiary_email,
heartbeat_link) into HTML which can lead to broken layout or XSS-like injection
in emails; update get_welcome_email to escape these values before formatting
(e.g., call an HTML-escape helper and use the escaped variables when building
bodies and passing into _html_wrap) and apply the same fix in get_death_email
for owner_email and shard_c_value so all dynamic fields are HTML-escaped prior
to interpolation.

In `@app/main.py`:
- Around line 923-926: When creating or loading a User, ensure inferred locale
is persisted: call get_language_from_email(email) and assign the result to
user.preferred_language for both newly constructed User objects and for the case
where a shell User already exists; update the block that creates
User(email=email, preferred_language=lang) to also set user.preferred_language =
lang when user is truthy, and make the same change in the other similar block
around the code identified by the second occurrence (the block currently at
lines 960-963) so existing Stripe-created shell users get their
preferred_language updated.
- Line 466: The SUBJECTS lookup uses direct indexing which can KeyError if a
persisted preferred_language is unexpected; update the send sites (where
send_email is called with SUBJECTS["cancellation"][lang]) to use a safe lookup
like SUBJECTS["cancellation"].get(lang, SUBJECTS["cancellation"]["en"]) or a
small helper function (e.g., resolve_subject(key, lang)) and pass the resolved
string into send_email; change the call in send_email(...) where
SUBJECTS["cancellation"][lang] is used and apply the same pattern to the other
similar send_email usages.

In `@app/models.py`:
- Line 32: The preferred_language Column currently only has an ORM-level default
("en") and must be hardened: update the Column definition for preferred_language
to include nullable=False and a database-level default (server_default) matching
the existing convention (e.g., server_default="'en'" or using text("...")), then
create a migration that first updates any existing NULL preferred_language rows
to 'en' and then alters the column to set NOT NULL and the server default;
reference the model symbol preferred_language and follow the same pattern used
for last_heartbeat when writing the migration so raw SQL/direct DB clients
cannot insert NULLs.

---

Nitpick comments:
In `@app/emails.py`:
- Around line 123-350: Currently get_welcome_email, get_cancellation_email and
get_reminder_30d_email rebuild large localized `bodies` dicts on every call;
extract those per-language templates into module-level constants (e.g.,
WELCOME_BODIES, CANCELLATION_BODIES, REMINDER_30D_BODIES) and have the functions
simply lookup the template by lang, apply formatting (inject
email/beneficiary/heartbeat_link) and pass the result to _html_wrap; ensure you
preserve the same string keys ("en","es","pt","de","fr","ja"), keep the same
formatting placeholders used in the f-strings, and fall back to the "en"
template when lang is missing.

ℹ️ Review info
⚙️ Run configuration

Configuration used: defaults

Review profile: CHILL

Plan: Pro

Run ID: c9af8e7d-c009-404c-9ab2-2093ede1e2f4

📥 Commits

Reviewing files that changed from the base of the PR and between fb7d7b2 and 559b58d.

📒 Files selected for processing (3)
  • app/emails.py
  • app/main.py
  • app/models.py

Comment on lines +123 to +228
def get_welcome_email(lang: str, email: str, beneficiary_email: str, heartbeat_link: str) -> str:
bodies = {
"en": f"""
<h1>it's not just a welcome email.</h1>
<p>hey,</p>
<p>you just did something most people never do: you protected your crypto for the people you love.</p>
<p>your vault is now active. here's what happens next:</p>
<ul>
<li>every 30 days, we'll send you a heartbeat link.</li>
<li>click it. that's it. takes 5 seconds.</li>
<li>if you miss 90 days in a row, shard c goes to your beneficiary.</li>
</ul>
<p><strong>vault active for: {email}</strong><br>
beneficiary: {beneficiary_email}</p>
<p>your first heartbeat link: <a href="{heartbeat_link}" class="heartbeat-link">click here to confirm you're alive</a></p>
<p>if you have any questions, just reply to this email. i read them all.</p>
<p>with care,<br><strong>max</strong><br><i>(the guy who sends you crayon drawings)</i></p>
<div class="footer"><p>sent by deadhand - built with care in argentina.</p></div>
""",
"es": f"""
<h1>no es solo un email de bienvenida.</h1>
<p>hola,</p>
<p>acabás de hacer algo que la mayoría de la gente nunca hace: protegiste tus cripto para las personas que querés.</p>
<p>tu vault ya está activo. esto es lo que pasa ahora:</p>
<ul>
<li>cada 30 días te enviamos un link de heartbeat.</li>
<li>hacé clic. eso es todo. tarda 5 segundos.</li>
<li>si pasás 90 días sin responder, el shard c se envía a tu beneficiario.</li>
</ul>
<p><strong>vault activo para: {email}</strong><br>
beneficiario: {beneficiary_email}</p>
<p>tu primer link de heartbeat: <a href="{heartbeat_link}" class="heartbeat-link">hacé clic para confirmar que estás vivo</a></p>
<p>si tenés alguna pregunta, respondé este email. los leo todos.</p>
<p>con cuidado,<br><strong>max</strong><br><i>(el que te manda los dibujos con crayones)</i></p>
<div class="footer"><p>enviado por deadhand - hecho con cuidado en argentina.</p></div>
""",
"pt": f"""
<h1>não é só um e-mail de boas-vindas.</h1>
<p>olá,</p>
<p>você acabou de fazer algo que a maioria das pessoas nunca faz: protegeu suas criptos para as pessoas que ama.</p>
<p>seu vault já está ativo. veja o que acontece agora:</p>
<ul>
<li>a cada 30 dias, vamos te enviar um link de heartbeat.</li>
<li>clique nele. só isso. leva 5 segundos.</li>
<li>se você ficar 90 dias sem responder, o shard c vai para seu beneficiário.</li>
</ul>
<p><strong>vault ativo para: {email}</strong><br>
beneficiário: {beneficiary_email}</p>
<p>seu primeiro link: <a href="{heartbeat_link}" class="heartbeat-link">clique aqui para confirmar que está vivo</a></p>
<p>se tiver dúvidas, responda este e-mail. eu leio todos.</p>
<p>com cuidado,<br><strong>max</strong></p>
<div class="footer"><p>enviado pelo deadhand - feito com carinho na argentina.</p></div>
""",
"de": f"""
<h1>das ist nicht nur eine Willkommens-E-Mail.</h1>
<p>hey,</p>
<p>du hast gerade etwas getan, das die meisten Menschen nie tun: du hast deine Krypto für deine Lieben gesichert.</p>
<p>dein Vault ist jetzt aktiv. so geht es weiter:</p>
<ul>
<li>alle 30 Tage senden wir dir einen Heartbeat-Link.</li>
<li>klick drauf. das ist alles. dauert 5 Sekunden.</li>
<li>wenn du 90 Tage nicht antwortest, wird Shard C an deinen Begünstigten gesendet.</li>
</ul>
<p><strong>vault aktiv für: {email}</strong><br>
begünstigte(r): {beneficiary_email}</p>
<p>dein erster Heartbeat-Link: <a href="{heartbeat_link}" class="heartbeat-link">hier klicken um zu bestätigen, dass du am Leben bist</a></p>
<p>bei Fragen einfach auf diese E-Mail antworten.</p>
<p>mit Sorgfalt,<br><strong>max</strong></p>
<div class="footer"><p>gesendet von deadhand - mit Sorgfalt in Argentinien gebaut.</p></div>
""",
"fr": f"""
<h1>ce n'est pas juste un e-mail de bienvenue.</h1>
<p>bonjour,</p>
<p>vous venez de faire quelque chose que la plupart des gens ne font jamais : protéger vos cryptos pour les personnes que vous aimez.</p>
<p>votre vault est maintenant actif. voici ce qui se passe :</p>
<ul>
<li>tous les 30 jours, nous vous envoyons un lien heartbeat.</li>
<li>cliquez dessus. c'est tout. ça prend 5 secondes.</li>
<li>si vous ne répondez pas pendant 90 jours, le shard c est envoyé à votre bénéficiaire.</li>
</ul>
<p><strong>vault actif pour : {email}</strong><br>
bénéficiaire : {beneficiary_email}</p>
<p>votre premier lien heartbeat : <a href="{heartbeat_link}" class="heartbeat-link">cliquez ici pour confirmer que vous êtes en vie</a></p>
<p>pour toute question, répondez à cet e-mail. je les lis tous.</p>
<p>avec soin,<br><strong>max</strong></p>
<div class="footer"><p>envoyé par deadhand - fait avec soin en argentine.</p></div>
""",
"ja": f"""
<h1>これはただの歓迎メールではありません。</h1>
<p>こんにちは、</p>
<p>あなたは多くの人がしないことをしました:愛する人のために暗号資産を守ったのです。</p>
<p>vaultが有効になりました。これからの流れ:</p>
<ul>
<li>30日ごとにハートビートリンクをお送りします。</li>
<li>クリックするだけ。5秒で完了します。</li>
<li>90日間応答がない場合、shard cが受取人に送られます。</li>
</ul>
<p><strong>vault有効:{email}</strong><br>
受取人:{beneficiary_email}</p>
<p>最初のハートビートリンク:<a href="{heartbeat_link}" class="heartbeat-link">生存確認のためここをクリック</a></p>
<p>ご質問はこのメールに返信してください。すべて読んでいます。</p>
<p>大切に、<br><strong>max</strong></p>
<div class="footer"><p>deadhandより - アルゼンチンで心を込めて作られました。</p></div>
""",
}
return _html_wrap(bodies.get(lang, bodies["en"]))
Copy link

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🟠 Major

Escape interpolated dynamic fields before embedding in HTML.

On Line 135/136/137 and Line 444/447 (and equivalent localized lines), raw values are injected into HTML. If any value contains markup, email rendering can be altered unexpectedly. Escape all dynamic fields before interpolation.

🔐 Proposed fix pattern
+from html import escape
+
+def _esc(value: str) -> str:
+    return escape(value or "", quote=True)

 def get_welcome_email(lang: str, email: str, beneficiary_email: str, heartbeat_link: str) -> str:
+    safe_email = _esc(email)
+    safe_beneficiary_email = _esc(beneficiary_email)
+    safe_heartbeat_link = _esc(heartbeat_link)
     bodies = {
         "en": f"""
             ...
-            <p><strong>vault active for: {email}</strong><br>
-            beneficiary: {beneficiary_email}</p>
-            <p>your first heartbeat link: <a href="{heartbeat_link}" class="heartbeat-link">click here to confirm you're alive</a></p>
+            <p><strong>vault active for: {safe_email}</strong><br>
+            beneficiary: {safe_beneficiary_email}</p>
+            <p>your first heartbeat link: <a href="{safe_heartbeat_link}" class="heartbeat-link">click here to confirm you're alive</a></p>
             ...

Apply the same pattern to owner_email and shard_c_value in get_death_email.

Also applies to: 439-592

🧰 Tools
🪛 Ruff (0.15.6)

[warning] 213-213: String contains ambiguous (FULLWIDTH COLON). Did you mean : (COLON)?

(RUF001)


[warning] 214-214: String contains ambiguous (FULLWIDTH COLON). Did you mean : (COLON)?

(RUF001)


[warning] 220-220: String contains ambiguous (FULLWIDTH COLON). Did you mean : (COLON)?

(RUF001)


[warning] 221-221: String contains ambiguous (FULLWIDTH COLON). Did you mean : (COLON)?

(RUF001)


[warning] 222-222: String contains ambiguous (FULLWIDTH COLON). Did you mean : (COLON)?

(RUF001)

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@app/emails.py` around lines 123 - 228, The email templates in
get_welcome_email are interpolating unescaped dynamic values (email,
beneficiary_email, heartbeat_link) into HTML which can lead to broken layout or
XSS-like injection in emails; update get_welcome_email to escape these values
before formatting (e.g., call an HTML-escape helper and use the escaped
variables when building bodies and passing into _html_wrap) and apply the same
fix in get_death_email for owner_email and shard_c_value so all dynamic fields
are HTML-escaped prior to interpolation.

<p>if you ever want to protect your family again, you know where to find me.</p>
lang = user.preferred_language or get_language_from_email(user.email)
cancellation_html = get_cancellation_email(lang)
send_email(user.email, SUBJECTS["cancellation"][lang], cancellation_html)
Copy link

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🟠 Major

Harden subject lookup with fallback language.

Direct indexing (SUBJECTS[...][lang]) can raise KeyError if persisted preferred_language is unexpected. Use .get(lang, SUBJECTS[...]["en"]) (or a helper) at each send site.

🛡️ Proposed fix pattern
-            send_email(user.email, SUBJECTS["cancellation"][lang], cancellation_html)
+            subject = SUBJECTS["cancellation"].get(lang, SUBJECTS["cancellation"]["en"])
+            send_email(user.email, subject, cancellation_html)

-    send_email(email, SUBJECTS["welcome"][lang], welcome_html)
+    send_email(email, SUBJECTS["welcome"].get(lang, SUBJECTS["welcome"]["en"]), welcome_html)

-                     send_email(user.email, SUBJECTS["reminder_30d"][lang], reminder_html)
+                     send_email(user.email, SUBJECTS["reminder_30d"].get(lang, SUBJECTS["reminder_30d"]["en"]), reminder_html)

-                    send_email(user.email, SUBJECTS["warning_60d"][lang], warning_html)
+                    send_email(user.email, SUBJECTS["warning_60d"].get(lang, SUBJECTS["warning_60d"]["en"]), warning_html)

-                    subject = SUBJECTS["death"][lang].format(owner_email=user.email)
+                    subject_tpl = SUBJECTS["death"].get(lang, SUBJECTS["death"]["en"])
+                    subject = subject_tpl.format(owner_email=user.email)

Also applies to: 963-963, 1065-1065, 1073-1073, 1099-1099

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@app/main.py` at line 466, The SUBJECTS lookup uses direct indexing which can
KeyError if a persisted preferred_language is unexpected; update the send sites
(where send_email is called with SUBJECTS["cancellation"][lang]) to use a safe
lookup like SUBJECTS["cancellation"].get(lang, SUBJECTS["cancellation"]["en"])
or a small helper function (e.g., resolve_subject(key, lang)) and pass the
resolved string into send_email; change the call in send_email(...) where
SUBJECTS["cancellation"][lang] is used and apply the same pattern to the other
similar send_email usages.

Comment on lines 923 to 926
if not user:
user = User(email=email)
lang = get_language_from_email(email)
user = User(email=email, preferred_language=lang)
db.add(user)
Copy link

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🟠 Major

Persist inferred language for existing shell users too.

At Line 924-925, language is only stored when creating a brand-new User. If a Stripe webhook already created a shell user, preferred_language stays stale (typically "en"), and later lifecycle emails won’t reflect inferred locale.

✅ Proposed fix
-    if not user:
-        lang = get_language_from_email(email)
-        user = User(email=email, preferred_language=lang)
+    lang = get_language_from_email(email)
+    if not user:
+        user = User(email=email, preferred_language=lang)
         db.add(user)
+    elif not user.shard_c:
+        # Stripe-created shell user completing setup now
+        user.preferred_language = lang
...
-    lang = get_language_from_email(email)
+    lang = user.preferred_language or get_language_from_email(email)

Also applies to: 960-963

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@app/main.py` around lines 923 - 926, When creating or loading a User, ensure
inferred locale is persisted: call get_language_from_email(email) and assign the
result to user.preferred_language for both newly constructed User objects and
for the case where a shell User already exists; update the block that creates
User(email=email, preferred_language=lang) to also set user.preferred_language =
lang when user is truthy, and make the same change in the other similar block
around the code identified by the second occurrence (the block currently at
lines 960-963) so existing Stripe-created shell users get their
preferred_language updated.

plan_type = Column(String, default="lifetime") # "annual" or "lifetime"
is_active = Column(Boolean, default=True) # False if subscription cancelled

preferred_language = Column(String, default="en")
Copy link

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🟠 Major

🧩 Analysis chain

🏁 Script executed:

#!/bin/bash
# Verify schema migration references for users.preferred_language
rg -n "preferred_language|ALTER TABLE users ADD COLUMN preferred_language|op\\.add_column\\(.*users" -g "*.py" -g "*.sql"

# Discover migration directories if present
fd -t d "alembic|migrations"

Repository: pyoneerC/deadhand

Length of output: 637


🏁 Script executed:

# Read the model definition around line 32
cat -n app/models.py | head -40

# Check if there's a setup.py, pyproject.toml, or similar to understand the project structure
ls -la | grep -E "(setup|pyproject|alembic|migrations)"

# Search for any migration-related files or directories more broadly
fd -t f -i "migrat" .
fd -t f -i "alembic" .

Repository: pyoneerC/deadhand

Length of output: 1757


🏁 Script executed:

# Check database.py
cat -n app/database.py

# Look for schema initialization or SQL files
fd -t f -e sql .
fd -t f -e "*.schema" .

# Check for any README or documentation about database setup
cat README.md 2>/dev/null | head -100

Repository: pyoneerC/deadhand

Length of output: 5005


🏁 Script executed:

# Check main.py for database initialization
rg -n "create_all|metadata|Base" app/main.py | head -20

# Look for startup events or initialization
rg -n "startup|lifespan|on_event" app/main.py | head -20

# Check if there's any schema initialization or seed script
fd -t f -e "init*" .
fd -t f "schema|seed" .

Repository: pyoneerC/deadhand

Length of output: 684


🏁 Script executed:

# Check git history for preferred_language addition
git log -p --follow -S "preferred_language" -- app/models.py | head -100

# Check how preferred_language is actually used in the application
rg -B2 -A2 "preferred_language" app/main.py

Repository: pyoneerC/deadhand

Length of output: 1621


preferred_language needs a database-level default and NOT NULL constraint.

The column uses only an ORM-level default="en" without a server_default or nullable=False constraint. This means:

  • Raw SQL inserts or direct database operations bypass the ORM default and allow NULL
  • Existing records in the database (if this is a recent addition) may already have NULL values
  • The schema does not enforce consistency across all deployment paths

While the code defensively handles NULL with user.preferred_language or get_language_from_email(...), it should guarantee the constraint at the database level, matching the pattern already established for other fields like last_heartbeat.

🧩 Suggested model hardening
-from sqlalchemy import Column, Integer, String, DateTime, Boolean
+from sqlalchemy import Column, Integer, String, DateTime, Boolean, text
...
-    preferred_language = Column(String, default="en")
+    preferred_language = Column(String(5), nullable=False, server_default=text("'en'"), default="en")
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
preferred_language = Column(String, default="en")
from sqlalchemy import Column, Integer, String, DateTime, Boolean, text
preferred_language = Column(String(5), nullable=False, server_default=text("'en'"), default="en")
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@app/models.py` at line 32, The preferred_language Column currently only has
an ORM-level default ("en") and must be hardened: update the Column definition
for preferred_language to include nullable=False and a database-level default
(server_default) matching the existing convention (e.g., server_default="'en'"
or using text("...")), then create a migration that first updates any existing
NULL preferred_language rows to 'en' and then alters the column to set NOT NULL
and the server default; reference the model symbol preferred_language and follow
the same pattern used for last_heartbeat when writing the migration so raw
SQL/direct DB clients cannot insert NULLs.

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