Skip to content

feat(email): add DE email templates + locale-based template resolver#2623

Closed
8emk10 wants to merge 29 commits intoseerr-team:developfrom
8emk10:feature/email-i18n
Closed

feat(email): add DE email templates + locale-based template resolver#2623
8emk10 wants to merge 29 commits intoseerr-team:developfrom
8emk10:feature/email-i18n

Conversation

@8emk10
Copy link
Copy Markdown

@8emk10 8emk10 commented Mar 3, 2026

Description

  • Adds German email templates (server/templates/email/de/*) for:
    • media-request
    • media-issue
    • generatedpassword
    • resetpassword
    • test-email
  • Introduces locale-based template resolution:
    recipient locale → global locale → EN fallback (with existence check).
  • Ensures required locals for media-issue templates are passed from email.ts.
  • Fixes DE issue email subjects (Audio/Video/Untertitel formatting; no prefix for OTHER).
  • Fixes EN “Requested Season / Requested Seasons” singular/plural in media-request extras via __extraLabel.

No changes to business logic outside of email rendering.

How Has This Been Tested?

  • Local Pug compilation for EN + DE templates:
    • media-request (EN/DE)
    • media-issue (EN/DE)
  • Manual functional testing of email notifications in local environment.

Summary by CodeRabbit

  • New Features

    • Implemented locale-aware email system that delivers notifications in user-preferred languages
    • Added full German email template support with intelligent fallback to English when needed
    • Enhanced email formatting for account setup, password resets, media requests, and issue notifications
  • Bug Fixes

    • Fixed GitHub Actions workflow to properly preserve environment variables in commit messages

root added 29 commits March 3, 2026 15:20
- add required locals for media-issue templates (EN+DE)
- localize DE issue subjects (Audio/Video/Untertitel; no prefix for OTHER)
- fix grammar in DE issue body (Dativ media label)
- fix colon + plural handling in media-request extras (EN+DE)
@8emk10 8emk10 requested a review from a team as a code owner March 3, 2026 14:38
@coderabbitai
Copy link
Copy Markdown

coderabbitai Bot commented Mar 3, 2026

📝 Walkthrough

Walkthrough

This pull request implements German language localization for email templates with locale-aware template resolution. It adds helper functions in TypeScript and Pug to select appropriate email templates based on user locale, modifies the email agent to pass locale information, and introduces German email templates for password, media request, and media issue notifications. A GitHub Actions workflow fix corrects commit message quoting to enable variable expansion.

Changes

Cohort / File(s) Summary
GitHub Actions Workflow
.github/workflows/create-tag.yml
Changed commit message quoting from double to single quotes, preventing shell variable expansion of ${TAG_VERSION}.
Locale Resolution Logic
server/entity/User.ts, server/lib/notifications/agents/email.ts
Added locale-aware template directory resolution with fallback to EN/default. Extended buildMessage signature to accept recipientLocale parameter. Introduced __normalizeLocale and __templateDirFor/\__userEmailTemplateDir helpers to check template existence and select locale-specific paths.
Template Helper Functions
server/templates/email/_helpers.pug, server/templates/email/de/_helpers.pug
Added English helper functions for media type detection, subject generation, label mapping, and grammar support. Introduced German-specific helpers with localized subject templates, label mappings, and string/function wrapper utilities for grammatically correct German phrasing.
German Email Templates
server/templates/email/de/generatedpassword/*, server/templates/email/de/resetpassword/*, server/templates/email/de/test-email/*, server/templates/email/de/media-request/*, server/templates/email/de/media-issue/*
Introduced complete German email templates (subject and HTML) for account creation, password reset, test notifications, media requests, and media issues with localized content and responsive styling.
English Email Templates
server/templates/email/media-request/*, server/templates/email/media-issue/*, server/templates/email/test-email/html.pug
Updated English templates to use helper-based subject generation, restructured body content into separate files, replaced static labels with __extraLabel helper calls, and added dynamic issue type handling.

Estimated code review effort

🎯 4 (Complex) | ⏱️ ~75 minutes

Possibly related PRs

Suggested reviewers

  • gauthier-th
  • fallenbagel
  • M0NsTeRRR

Poem

🐰 Hoppy locales and templates so fine,
German greetings in every design,
With helpers that guide and resolve with grace,
Emails now bloom in each language's space! 🌍✉️

🚥 Pre-merge checks | ✅ 2
✅ Passed checks (2 passed)
Check name Status Explanation
Description Check ✅ Passed Check skipped - CodeRabbit’s high-level summary is enabled.
Title check ✅ Passed The pull request title accurately summarizes the main changes: adding German email templates and implementing a locale-based template resolver to select templates based on user/global locale with English fallback.

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

Tip

Try Coding Plans. Let us write the prompt for your AI agent so you can ship faster (with fewer bugs).
Share your feedback on Discord.


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.

Copy link
Copy Markdown

@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: 9

Caution

Some comments are outside the diff and can’t be posted inline due to platform limitations.

⚠️ Outside diff range comments (1)
server/templates/email/media-issue/html.pug (1)

59-63: ⚠️ Potential issue | 🟡 Minor

CTA button appears to be missing its text content.

The action URL button template at lines 59-63 ends without any text content inside the span. The button would render empty. Compare with other templates that include text like | View Issue in #{applicationTitle}.

🔧 Suggested fix
           a(href=actionUrl style='display: block; margin: 1.5rem 3rem 0; text-decoration: none; font-size: 1.0em; line-height: 2.25em;')
             span(style='padding: 0.2rem; font-weight: 500; text-align: center; border-radius: 10px; background-color: rgb(99,102,241); color: `#fff`; display: block; border: 1px solid rgba(255,255,255,0.2);')
+              | View Issue in #{applicationTitle}
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@server/templates/email/media-issue/html.pug` around lines 59 - 63, The CTA
button span inside the media-issue HTML pug template is empty so the button
renders with no label; update the conditional block that uses actionUrl to
include visible text in the span (for example: "View Issue in
#{applicationTitle}" or another appropriate label), ensuring the span sits
between the opening and closing tags and references the existing
applicationTitle variable so the button displays a proper call-to-action.
🧹 Nitpick comments (2)
server/entity/User.ts (1)

33-72: Consider consolidating locale-template resolution into one shared utility.

This logic now exists in both server/entity/User.ts and server/lib/notifications/agents/email.ts, which is easy to desync.

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

In `@server/entity/User.ts` around lines 33 - 72, Extract the locale/template
resolution logic currently implemented in __normalizeLocale and
__userEmailTemplateDir into a single shared utility (e.g.,
server/lib/emailTemplateResolver.ts) and replace the duplicate implementations
in User.ts and server/lib/notifications/agents/email.ts with calls to that
utility; ensure the utility exposes functions with the same semantics
(normalizeLocale(locale?: string) and resolveUserEmailTemplateDir(templateName:
string, userLocale?: string, globalLocale?: string)) and preserves the existing
candidate search order, existence checks (existsSync), and fallback behavior so
both callers can import and use the same implementation.
server/lib/notifications/agents/email.ts (1)

18-60: Recommend reusing the resolver from one shared module.

Locale resolution + existence probing is now duplicated here and in server/entity/User.ts. Centralizing it will prevent behavior drift.

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

In `@server/lib/notifications/agents/email.ts` around lines 18 - 60, Replace the
duplicated locale-resolution and template-probing logic in __normalizeLocale and
__templateDirFor by importing and using the shared resolver from the common
module used by server/entity/User.ts; remove the local __normalizeLocale,
__templateDirFor and tryLoc implementations and call the shared function (the
one that handles candidate paths, EN fallback, and existence checks) so the same
behavior is used across User.ts and email.ts, preserving the existing behavior
of probing built vs source templates and checking for html.pug existence.
🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.

Inline comments:
In @.github/workflows/create-tag.yml:
- Line 81: The commit uses single quotes so ${TAG_VERSION} won't expand; update
the git commit invocation (the line with git commit -m 'chore(release): prepare
${TAG_VERSION}') to use double quotes instead so the TAG_VERSION environment
variable expands (e.g., change the commit message quoting around chore(release):
prepare ${TAG_VERSION}).

In `@server/entity/User.ts`:
- Around line 41-55: The base-selection loop in __userEmailTemplateDir currently
checks for 'generatedpassword/html.pug' and should instead look for the
requested template; update the candidates probing to check path.join(c,
templateName, 'html.pug') (so the chosen base actually contains the requested
templateName), preserve the existing try/catch behavior and the same fallback
return, and ensure references to templateName, userLocale, and globalLocale
remain unchanged elsewhere in __userEmailTemplateDir.

In `@server/lib/notifications/agents/email.ts`:
- Around line 31-59: The template directory resolution can return non-existent
paths because the initial base detection only checks 'test-email/html.pug' and
the final fallback returns path.join(base, templateName) without verifying it
exists; update __templateDirFor's base candidate check to look for path.join(c,
templateName, 'html.pug') (so the chosen base actually contains the requested
template), modify tryLoc to only return the 'en' path when
existsSync(path.join(base, templateName, 'html.pug')) is true, and change the
final return to verify existence (e.g., return the first existing of tryLoc(u),
tryLoc(g), or path.join(base, templateName) only if existsSync(...), otherwise
return '' or throw) referencing the existing symbols base, tryLoc, templateName,
__normalizeLocale, u, and g.

In `@server/templates/email/de/_helpers.pug`:
- Around line 5-7: The media-type detection incorrectly checks mediaType ===
'tv' and uses self-referential fallbacks; update the __isSeries branch to check
mediaType === 'series' (to match the sender locals) and replace the
self-referential fallback (typeof __isSeries !== 'undefined' && __isSeries) with
a non-self fallback flag passed in the locals (e.g., typeof series !==
'undefined' && series or a distinct flag like typeof isSeriesFallback !==
'undefined' && isSeriesFallback); similarly ensure __isMovie uses a reliable
non-self fallback (e.g., typeof movie !== 'undefined' && movie or a dedicated
fallback flag) so both __isMovie and __isSeries derive from actual provided
locals and not from their own prior values.

In `@server/templates/email/de/media-request/body.pug`:
- Line 16: The template references __isMovie in the line that builds the
descriptive phrase but never defines it; add a local definition (as done in
subject.pug) that derives __isMovie from the available context (e.g., const
__isMovie = typeof isMovie !== "undefined" ? isMovie : (__mediaType ===
"movie")) before the line using it so the conditional expression ((typeof is4k
!== "undefined" && is4k) ? (__isMovie ? "dem 4K Film" : "der 4K Serie") :
__mediaLabelDat()) always evaluates reliably.

In `@server/templates/email/de/media-request/html.pug`:
- Line 42: The div that builds the inline style currently always injects
imageUrl into url(...), causing url(undefined) when no poster exists; update the
template so the background image is only appended when imageUrl is truthy (e.g.,
produce background: linear-gradient(...) + (imageUrl ? ", url('"+imageUrl+"')
center 25%/cover" : "") ), or alternatively switch to two separate style/class
paths: one with the gradient+image and one with only the gradient/fallback;
refer to the div that constructs the style string and the imageUrl variable to
implement the conditional.
- Around line 40-66: The media-card table row (the top-level "tr" that contains
the div with imageUrl, actionUrl, mediaName, requestedBy and mediaExtra) was
dedented and sits outside the main table, which causes email clients to drop it;
move that "tr" so it is nested inside the parent table element (the table with
style='color: `#fff`; width: 100%;') so the row and its child td blocks (including
the conditional td for imageUrl and the timestamp row) are properly wrapped by
the table; ensure the existing child nodes (a with class 'title', the spans
using requestedBy.replace, the each mediaExtra loop, the conditional if imageUrl
block, and the timestamp td) keep their relative structure and indentation under
that table.

In `@server/templates/email/media-request/body.pug`:
- Around line 4-18: The notification templates use __prefix4k (either "4K " or
"") and currently interpolate as "#{__followingAcc()} #{__prefix4k}is..."
causing double spaces when __prefix4k is empty; update every branch (the lines
for MEDIA_AUTO_REQUESTED, MEDIA_APPROVED, MEDIA_AUTO_APPROVED, MEDIA_DECLINED,
MEDIA_AVAILABLE, the default branch, and the initial line) to use the same
conditional spacing logic used in the MEDIA_FAILED case so spacing is only added
when __prefix4k is non-empty (i.e., replace the "#{__followingAcc()}
#{__prefix4k}..." pattern with the conditional approach that trims and prefixes
a single space when needed, referencing __prefix4k and __followingAcc()).

In `@server/templates/email/media-request/html.pug`:
- Line 38: The second table row in the email template has its tr token
de-indented to column 0 which breaks table nesting; restore proper indentation
by moving that tr back to the same nesting level as the first tr (indent by 4
spaces so its child td lines remain at 6 spaces) in
server/templates/email/media-request/html.pug so the tr/td hierarchy is
consistent.

---

Outside diff comments:
In `@server/templates/email/media-issue/html.pug`:
- Around line 59-63: The CTA button span inside the media-issue HTML pug
template is empty so the button renders with no label; update the conditional
block that uses actionUrl to include visible text in the span (for example:
"View Issue in #{applicationTitle}" or another appropriate label), ensuring the
span sits between the opening and closing tags and references the existing
applicationTitle variable so the button displays a proper call-to-action.

---

Nitpick comments:
In `@server/entity/User.ts`:
- Around line 33-72: Extract the locale/template resolution logic currently
implemented in __normalizeLocale and __userEmailTemplateDir into a single shared
utility (e.g., server/lib/emailTemplateResolver.ts) and replace the duplicate
implementations in User.ts and server/lib/notifications/agents/email.ts with
calls to that utility; ensure the utility exposes functions with the same
semantics (normalizeLocale(locale?: string) and
resolveUserEmailTemplateDir(templateName: string, userLocale?: string,
globalLocale?: string)) and preserves the existing candidate search order,
existence checks (existsSync), and fallback behavior so both callers can import
and use the same implementation.

In `@server/lib/notifications/agents/email.ts`:
- Around line 18-60: Replace the duplicated locale-resolution and
template-probing logic in __normalizeLocale and __templateDirFor by importing
and using the shared resolver from the common module used by
server/entity/User.ts; remove the local __normalizeLocale, __templateDirFor and
tryLoc implementations and call the shared function (the one that handles
candidate paths, EN fallback, and existence checks) so the same behavior is used
across User.ts and email.ts, preserving the existing behavior of probing built
vs source templates and checking for html.pug existence.

ℹ️ Review info

Configuration used: Organization UI

Review profile: CHILL

Plan: Pro

📥 Commits

Reviewing files that changed from the base of the PR and between a16d046 and 5597cc2.

📒 Files selected for processing (22)
  • .github/workflows/create-tag.yml
  • server/entity/User.ts
  • server/lib/notifications/agents/email.ts
  • server/templates/email/_helpers.pug
  • server/templates/email/de/_helpers.pug
  • server/templates/email/de/generatedpassword/html.pug
  • server/templates/email/de/generatedpassword/subject.pug
  • server/templates/email/de/media-issue/html.pug
  • server/templates/email/de/media-issue/subject.pug
  • server/templates/email/de/media-request/body.pug
  • server/templates/email/de/media-request/html.pug
  • server/templates/email/de/media-request/subject.pug
  • server/templates/email/de/resetpassword/html.pug
  • server/templates/email/de/resetpassword/subject.pug
  • server/templates/email/de/test-email/html.pug
  • server/templates/email/de/test-email/subject.pug
  • server/templates/email/media-issue/html.pug
  • server/templates/email/media-issue/subject.pug
  • server/templates/email/media-request/body.pug
  • server/templates/email/media-request/html.pug
  • server/templates/email/media-request/subject.pug
  • server/templates/email/test-email/html.pug

run: |
git add package.json
git commit -m "chore(release): prepare ${TAG_VERSION}"
git commit -m 'chore(release): prepare ${TAG_VERSION}'
Copy link
Copy Markdown

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🟡 Minor

🧩 Analysis chain

🏁 Script executed:

# Check the actual file content at line 81
sed -n '75,85p' .github/workflows/create-tag.yml

Repository: seerr-team/seerr

Length of output: 381


🏁 Script executed:

# Run the provided test script to confirm variable expansion behavior
#!/bin/bash
set -euo pipefail

TAG_VERSION="1.2.3"
printf 'single-quoted -> %s\n' 'chore(release): prepare ${TAG_VERSION}'
printf 'double-quoted -> %s\n' "chore(release): prepare ${TAG_VERSION}"

Repository: seerr-team/seerr

Length of output: 162


🏁 Script executed:

# Check the full workflow file to see where TAG_VERSION is defined
cat .github/workflows/create-tag.yml

Repository: seerr-team/seerr

Length of output: 2705


Use double quotes to expand TAG_VERSION in the commit message.

Single quotes prevent the variable from expanding, resulting in a commit message containing the literal text ${TAG_VERSION} instead of the actual version number. This is inconsistent with other uses of TAG_VERSION in the same job (lines 76, 85).

💡 Proposed fix
-          git commit -m 'chore(release): prepare ${TAG_VERSION}'
+          git commit -m "chore(release): prepare ${TAG_VERSION}"
📝 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
git commit -m 'chore(release): prepare ${TAG_VERSION}'
git commit -m "chore(release): prepare ${TAG_VERSION}"
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In @.github/workflows/create-tag.yml at line 81, The commit uses single quotes
so ${TAG_VERSION} won't expand; update the git commit invocation (the line with
git commit -m 'chore(release): prepare ${TAG_VERSION}') to use double quotes
instead so the TAG_VERSION environment variable expands (e.g., change the commit
message quoting around chore(release): prepare ${TAG_VERSION}).

Comment thread server/entity/User.ts
Comment on lines +41 to +55
function __userEmailTemplateDir(templateName: string, userLocale?: string, globalLocale?: string) {
const base = (() => {
// Prefer built templates (prod), then source templates (dev)
const candidates = [
path.join(process.cwd(), 'dist/templates/email'),
path.join(process.cwd(), 'server/templates/email'),
path.join(__dirname, '../templates/email'),
];
for (const c of candidates) {
try {
if (existsSync(path.join(c, 'generatedpassword', 'html.pug'))) return c;
} catch (_) {}
}
return path.join(process.cwd(), 'server/templates/email');
})();
Copy link
Copy Markdown

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🟠 Major

Probe base directories using the requested templateName, not generatedpassword.

At Line 51, base selection is tied to generatedpassword/html.pug. This can pick a base where templateName doesn’t exist, causing later render failures.

Suggested fix
-        if (existsSync(path.join(c, 'generatedpassword', 'html.pug'))) return c;
+        if (existsSync(path.join(c, templateName, 'html.pug'))) return c;
📝 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
function __userEmailTemplateDir(templateName: string, userLocale?: string, globalLocale?: string) {
const base = (() => {
// Prefer built templates (prod), then source templates (dev)
const candidates = [
path.join(process.cwd(), 'dist/templates/email'),
path.join(process.cwd(), 'server/templates/email'),
path.join(__dirname, '../templates/email'),
];
for (const c of candidates) {
try {
if (existsSync(path.join(c, 'generatedpassword', 'html.pug'))) return c;
} catch (_) {}
}
return path.join(process.cwd(), 'server/templates/email');
})();
function __userEmailTemplateDir(templateName: string, userLocale?: string, globalLocale?: string) {
const base = (() => {
// Prefer built templates (prod), then source templates (dev)
const candidates = [
path.join(process.cwd(), 'dist/templates/email'),
path.join(process.cwd(), 'server/templates/email'),
path.join(__dirname, '../templates/email'),
];
for (const c of candidates) {
try {
if (existsSync(path.join(c, templateName, 'html.pug'))) return c;
} catch (_) {}
}
return path.join(process.cwd(), 'server/templates/email');
})();
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@server/entity/User.ts` around lines 41 - 55, The base-selection loop in
__userEmailTemplateDir currently checks for 'generatedpassword/html.pug' and
should instead look for the requested template; update the candidates probing to
check path.join(c, templateName, 'html.pug') (so the chosen base actually
contains the requested templateName), preserve the existing try/catch behavior
and the same fallback return, and ensure references to templateName, userLocale,
and globalLocale remain unchanged elsewhere in __userEmailTemplateDir.

Comment on lines +31 to +59
const base = (() => {
// Prefer built templates (prod), then source templates (dev)
const candidates = [
path.join(process.cwd(), 'dist/templates/email'),
path.join(process.cwd(), 'server/templates/email'),
];
for (const c of candidates) {
try {
if (existsSync(path.join(c, 'test-email', 'html.pug'))) return c;
} catch (_) {}
}
return path.join(process.cwd(), 'server/templates/email');
})();
const tryLoc = (loc: string) => {
// EN lives at base/<templateName>, localized at base/<loc>/<templateName>
if (!loc) return '';
if (loc === 'en') return path.join(base, templateName);
const candidate = path.join(base, loc, templateName);
// Check html.pug existence as "template exists" signal
if (existsSync(path.join(candidate, 'html.pug'))) return candidate;
return '';
};

const u = __normalizeLocale(userLocale);
const g = __normalizeLocale(globalLocale);

return (
tryLoc(u) || tryLoc(g) || path.join(base, templateName) // EN fallback
);
Copy link
Copy Markdown

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🟠 Major

__templateDirFor can return template paths that don’t exist.

Line 39 probes only test-email/html.pug, then Line 58 falls back to path.join(base, templateName) without existence checks. This can resolve to a missing template directory for non-test templates.

Suggested fix
-        if (existsSync(path.join(c, 'test-email', 'html.pug'))) return c;
+        if (existsSync(path.join(c, templateName, 'html.pug'))) return c;
...
-  return (
-    tryLoc(u) || tryLoc(g) || path.join(base, templateName) // EN fallback
-  );
+  const enFallback = path.join(base, templateName);
+  if (tryLoc(u)) return tryLoc(u);
+  if (tryLoc(g)) return tryLoc(g);
+  if (existsSync(path.join(enFallback, 'html.pug'))) return enFallback;
+  return path.join(process.cwd(), 'server/templates/email', templateName);
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@server/lib/notifications/agents/email.ts` around lines 31 - 59, The template
directory resolution can return non-existent paths because the initial base
detection only checks 'test-email/html.pug' and the final fallback returns
path.join(base, templateName) without verifying it exists; update
__templateDirFor's base candidate check to look for path.join(c, templateName,
'html.pug') (so the chosen base actually contains the requested template),
modify tryLoc to only return the 'en' path when existsSync(path.join(base,
templateName, 'html.pug')) is true, and change the final return to verify
existence (e.g., return the first existing of tryLoc(u), tryLoc(g), or
path.join(base, templateName) only if existsSync(...), otherwise return '' or
throw) referencing the existing symbols base, tryLoc, templateName,
__normalizeLocale, u, and g.

Comment on lines +5 to +7
- var __isMovie = (typeof mediaType !== 'undefined' && mediaType === 'movie') || (typeof __isMovie !== 'undefined' && __isMovie)
- var __isSeries = (typeof mediaType !== 'undefined' && mediaType === 'tv') || (typeof __isSeries !== 'undefined' && __isSeries)

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🟡 Minor

Align media-type detection with provided locals (series) and non-self fallback flags.

Line 6 checks mediaType === 'tv', but sender locals use 'series'. Also, typeof __isSeries !== 'undefined' && __isSeries is self-referential and not a reliable fallback.

Suggested fix
- var __isMovie = (typeof mediaType !== 'undefined' && mediaType === 'movie') || (typeof __isMovie !== 'undefined' && __isMovie)
- var __isSeries = (typeof mediaType !== 'undefined' && mediaType === 'tv') || (typeof __isSeries !== 'undefined' && __isSeries)
+ var __isMovie = (typeof mediaType !== 'undefined' && mediaType === 'movie') || (typeof isMovie !== 'undefined' && isMovie)
+ var __isSeries = (typeof mediaType !== 'undefined' && (mediaType === 'series' || mediaType === 'tv')) || (typeof isSeries !== 'undefined' && isSeries)
📝 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
- var __isMovie = (typeof mediaType !== 'undefined' && mediaType === 'movie') || (typeof __isMovie !== 'undefined' && __isMovie)
- var __isSeries = (typeof mediaType !== 'undefined' && mediaType === 'tv') || (typeof __isSeries !== 'undefined' && __isSeries)
- var __isMovie = (typeof mediaType !== 'undefined' && mediaType === 'movie') || (typeof isMovie !== 'undefined' && isMovie)
- var __isSeries = (typeof mediaType !== 'undefined' && (mediaType === 'series' || mediaType === 'tv')) || (typeof isSeries !== 'undefined' && isSeries)
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@server/templates/email/de/_helpers.pug` around lines 5 - 7, The media-type
detection incorrectly checks mediaType === 'tv' and uses self-referential
fallbacks; update the __isSeries branch to check mediaType === 'series' (to
match the sender locals) and replace the self-referential fallback (typeof
__isSeries !== 'undefined' && __isSeries) with a non-self fallback flag passed
in the locals (e.g., typeof series !== 'undefined' && series or a distinct flag
like typeof isSeriesFallback !== 'undefined' && isSeriesFallback); similarly
ensure __isMovie uses a reliable non-self fallback (e.g., typeof movie !==
'undefined' && movie or a dedicated fallback flag) so both __isMovie and
__isSeries derive from actual provided locals and not from their own prior
values.

when "MEDIA_AVAILABLE"
| #{__followingAcc()} #{__prefix4k}ist jetzt verfügbar:
when "MEDIA_FAILED"
| Bei #{((typeof is4k !== "undefined" && is4k) ? (__isMovie ? "dem 4K Film" : "der 4K Serie") : __mediaLabelDat())} ist ein Fehler aufgetreten:
Copy link
Copy Markdown

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🟠 Major

__isMovie is referenced but not defined in this template.

Line 16 uses __isMovie in the conditional expression, but unlike subject.pug which defines const __isMovie = ..., this template doesn't define it. This could result in a runtime error or incorrect behavior if __isMovie is not provided by the included helpers.

🔧 Suggested fix - add __isMovie definition
 - const __nt = (typeof notificationType !== "undefined" ? String(notificationType) : "")
+- const __isMovie = (typeof isMovie !== "undefined" ? !!isMovie : false)
 case __nt
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@server/templates/email/de/media-request/body.pug` at line 16, The template
references __isMovie in the line that builds the descriptive phrase but never
defines it; add a local definition (as done in subject.pug) that derives
__isMovie from the available context (e.g., const __isMovie = typeof isMovie !==
"undefined" ? isMovie : (__mediaType === "movie")) before the line using it so
the conditional expression ((typeof is4k !== "undefined" && is4k) ? (__isMovie ?
"dem 4K Film" : "der 4K Serie") : __mediaLabelDat()) always evaluates reliably.

Comment on lines +40 to +66
tr
td
div(style='box-sizing: border-box; margin: 1.5rem 0 0; width: 100%; color: #fff; border-radius: .75rem; padding: 1rem; border: 1px solid rgb(100,100,100); background: linear-gradient(135deg, rgba(17,24,39,0.47) 0%, rgb(17,24,39) 75%), url(' + imageUrl + ') center 25%/cover')
table(style='color: #fff; width: 100%;')
tr
td(style='vertical-align: top;')
a(href=actionUrl style='display: block; max-width: 20rem; color: #fff; font-weight: 700; text-decoration: none; margin: 0 1rem 0.25rem 0; font-size: 1.3em; line-height: 1.25em; margin-bottom: 5px;' class='title')
| #{mediaName}
div(style='overflow: hidden; text-overflow: ellipsis; white-space: nowrap; color: #d1d5db; font-size: .975em; line-height: 1.45em; padding-top: .25rem; padding-bottom: .25rem;')
span(style='display: block;')
b(style='color: #9ca3af; font-weight: 700;')
| Angefragt von&nbsp;
| #{requestedBy.replace(/\.|@/g, ((x) => x + '\ufeff'))}
each extra in mediaExtra
span(style='display: block;')
b(style='color: #9ca3af; font-weight: 700;')
| #{__extraLabel(extra.name, extra.value)}:&nbsp;
| #{extra.value}
if imageUrl
td(rowspan='2' style='width: 7rem;')
a(style='display: block; width: 7rem; overflow: hidden; border-radius: .375rem;' href=actionUrl)
div(style='overflow: hidden; box-sizing: border-box; margin: 0px;')
img(alt='' src=imageUrl style='box-sizing: border-box; padding: 0px; border: none; margin: auto; display: block; min-width: 100%; max-width: 100%; min-height: 100%; max-height: 100%;')
tr
td(style='font-size: .85em; color: #9ca3af; line-height: 1em; vertical-align: bottom; margin-right: 1rem')
span
| #{timestamp}
Copy link
Copy Markdown

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🟠 Major

Keep the media card row inside the main table.

At Line 40, tr is dedented out of the table created at Line 22. In email clients, orphan table rows are often dropped, so this section may not render.

Suggested fix
-tr
-      td
-        div(style='box-sizing: border-box; margin: 1.5rem 0 0; width: 100%; color: `#fff`; border-radius: .75rem; padding: 1rem; border: 1px solid rgb(100,100,100); background: linear-gradient(135deg, rgba(17,24,39,0.47) 0%, rgb(17,24,39) 75%), url(' + imageUrl + ') center 25%/cover')
+    tr
+      td
+        div(style='box-sizing: border-box; margin: 1.5rem 0 0; width: 100%; color: `#fff`; border-radius: .75rem; padding: 1rem; border: 1px solid rgb(100,100,100); background: linear-gradient(135deg, rgba(17,24,39,0.47) 0%, rgb(17,24,39) 75%), url(' + imageUrl + ') center 25%/cover')
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@server/templates/email/de/media-request/html.pug` around lines 40 - 66, The
media-card table row (the top-level "tr" that contains the div with imageUrl,
actionUrl, mediaName, requestedBy and mediaExtra) was dedented and sits outside
the main table, which causes email clients to drop it; move that "tr" so it is
nested inside the parent table element (the table with style='color: `#fff`;
width: 100%;') so the row and its child td blocks (including the conditional td
for imageUrl and the timestamp row) are properly wrapped by the table; ensure
the existing child nodes (a with class 'title', the spans using
requestedBy.replace, the each mediaExtra loop, the conditional if imageUrl
block, and the timestamp td) keep their relative structure and indentation under
that table.

include ./body.pug
tr
td
div(style='box-sizing: border-box; margin: 1.5rem 0 0; width: 100%; color: #fff; border-radius: .75rem; padding: 1rem; border: 1px solid rgb(100,100,100); background: linear-gradient(135deg, rgba(17,24,39,0.47) 0%, rgb(17,24,39) 75%), url(' + imageUrl + ') center 25%/cover')
Copy link
Copy Markdown

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🟡 Minor

Avoid rendering url(undefined) in the card background.

Line 42 always appends imageUrl into url(...). When no poster is available, this becomes url(undefined).

Suggested fix
+        - const cardBg = imageUrl
+        -   ? 'box-sizing: border-box; margin: 1.5rem 0 0; width: 100%; color: `#fff`; border-radius: .75rem; padding: 1rem; border: 1px solid rgb(100,100,100); background: linear-gradient(135deg, rgba(17,24,39,0.47) 0%, rgb(17,24,39) 75%), url(' + imageUrl + ') center 25%/cover'
+        -   : 'box-sizing: border-box; margin: 1.5rem 0 0; width: 100%; color: `#fff`; border-radius: .75rem; padding: 1rem; border: 1px solid rgb(100,100,100); background: linear-gradient(135deg, rgba(17,24,39,0.47) 0%, rgb(17,24,39) 75%)'
-        div(style='box-sizing: border-box; margin: 1.5rem 0 0; width: 100%; color: `#fff`; border-radius: .75rem; padding: 1rem; border: 1px solid rgb(100,100,100); background: linear-gradient(135deg, rgba(17,24,39,0.47) 0%, rgb(17,24,39) 75%), url(' + imageUrl + ') center 25%/cover')
+        div(style=cardBg)
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@server/templates/email/de/media-request/html.pug` at line 42, The div that
builds the inline style currently always injects imageUrl into url(...), causing
url(undefined) when no poster exists; update the template so the background
image is only appended when imageUrl is truthy (e.g., produce background:
linear-gradient(...) + (imageUrl ? ", url('"+imageUrl+"') center 25%/cover" :
"") ), or alternatively switch to two separate style/class paths: one with the
gradient+image and one with only the gradient/fallback; refer to the div that
constructs the style string and the imageUrl variable to implement the
conditional.

Comment on lines +4 to +18
| A new request for #{__followingAcc()} #{__prefix4k}is pending approval:
when "MEDIA_AUTO_REQUESTED"
| A new request for #{__followingAcc()} #{__prefix4k}is pending approval:
when "MEDIA_APPROVED"
| A request for #{__followingAcc()} #{__prefix4k}was approved:
when "MEDIA_AUTO_APPROVED"
| A request for #{__followingAcc()} #{__prefix4k}was auto-approved:
when "MEDIA_DECLINED"
| A request for #{__followingAcc()} #{__prefix4k}was declined:
when "MEDIA_AVAILABLE"
| #{__followingAcc()} #{__prefix4k}is now available:
when "MEDIA_FAILED"
| There was an error processing #{__followingAcc()}#{__prefix4k ? " " + String(__prefix4k).trim() : ""}:
default
| Notification for #{__followingAcc()} #{__prefix4k}:
Copy link
Copy Markdown

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🟡 Minor

🧩 Analysis chain

🏁 Script executed:

rg "__prefix4k" -n --type pug,js,typescript

Repository: seerr-team/seerr

Length of output: 101


🏁 Script executed:

rg "__prefix4k" -n

Repository: seerr-team/seerr

Length of output: 4833


Fix spacing inconsistency in notification messages.

__prefix4k is defined as either '4K ' (with trailing space) or '' (empty string). Lines 4, 6, 8, 10, 12, 14, and 18 use the pattern #{__followingAcc()} #{__prefix4k}is..., which produces double spaces when __prefix4k is empty (e.g., "A new request for the movie is pending").

Line 16 correctly handles this with #{__prefix4k ? " " + String(__prefix4k).trim() : ""}, but the other cases don't. Apply consistent spacing logic across all cases by either:

  • Always using the conditional approach from line 16, or
  • Adjusting the pattern to handle empty __prefix4k consistently
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@server/templates/email/media-request/body.pug` around lines 4 - 18, The
notification templates use __prefix4k (either "4K " or "") and currently
interpolate as "#{__followingAcc()} #{__prefix4k}is..." causing double spaces
when __prefix4k is empty; update every branch (the lines for
MEDIA_AUTO_REQUESTED, MEDIA_APPROVED, MEDIA_AUTO_APPROVED, MEDIA_DECLINED,
MEDIA_AVAILABLE, the default branch, and the initial line) to use the same
conditional spacing logic used in the MEDIA_FAILED case so spacing is only added
when __prefix4k is non-empty (i.e., replace the "#{__followingAcc()}
#{__prefix4k}..." pattern with the conditional approach that trims and prefixes
a single space when needed, referencing __prefix4k and __followingAcc()).

| #{body}
tr
include ./body.pug
tr
Copy link
Copy Markdown

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
set -euo pipefail

FILE="server/templates/email/media-request/html.pug"

echo "Lines 34-42 with numbers:"
nl -ba "$FILE" | sed -n '34,42p'

echo
echo "Leading-space counts (expect Line 38 to align with other rows under table):"
python - <<'PY'
from pathlib import Path
p = Path("server/templates/email/media-request/html.pug")
for i, line in enumerate(p.read_text().splitlines(), 1):
    if 34 <= i <= 42:
        indent = len(line) - len(line.lstrip(" "))
        print(f"Line {i}: indent={indent} | {line.lstrip()}")
PY

Repository: seerr-team/seerr

Length of output: 145


🏁 Script executed:

#!/bin/bash
set -euo pipefail

FILE="server/templates/email/media-request/html.pug"

echo "Lines 34-42 with line numbers:"
cat -n "$FILE" | sed -n '34,42p'

echo
echo "Indentation analysis:"
python3 - <<'PY'
from pathlib import Path
p = Path("server/templates/email/media-request/html.pug")
lines = p.read_text().splitlines()
for i in range(33, min(42, len(lines))):  # 0-indexed, so 33 = line 34
    line = lines[i]
    indent = len(line) - len(line.lstrip(" "))
    line_num = i + 1
    print(f"Line {line_num}: indent={indent:2d} | {repr(line)}")
PY

Repository: seerr-team/seerr

Length of output: 1371


Line 38's tr element is de-indented to the root level (0 spaces) and breaks the table structure.

The second tr should be indented to 4 spaces to align with the first tr on line 34, matching the nesting pattern where child td elements follow at indent 6 (line 39).

Proposed fix
-tr
+    tr
📝 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
tr
tr
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@server/templates/email/media-request/html.pug` at line 38, The second table
row in the email template has its tr token de-indented to column 0 which breaks
table nesting; restore proper indentation by moving that tr back to the same
nesting level as the first tr (indent by 4 spaces so its child td lines remain
at 6 spaces) in server/templates/email/media-request/html.pug so the tr/td
hierarchy is consistent.

@github-actions
Copy link
Copy Markdown

github-actions Bot commented Mar 3, 2026

👋 @8emk10, thank you for your contribution!
However, this pull request has been closed because it appears to contain a significant amount of AI-generated code without sufficient human review or supervision.
AI-generated code can often introduce subtle bugs, poor design patterns, or inconsistent styles that make long-term maintenance difficult and reduce overall code quality. For the sake of the project's future stability and readability, we require that all contributions meet our established coding standards and demonstrate clear developer oversight.
This pull request is also too large for effective human review. Please discuss with us on how to break down these changes into smaller, more focused PRs to ensure a thorough and efficient review process. If you'd like to revise and resubmit your changes with careful review and cleanup, we'd be happy to take another look.

@github-actions github-actions Bot closed this Mar 3, 2026
@github-actions github-actions Bot locked as spam and limited conversation to collaborators Mar 3, 2026
Sign up for free to subscribe to this conversation on GitHub. Already have an account? Sign in.

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants