Skip to content

🌵 [SPIKE] Pass locale from portal to magic link endpoint, send localized magic links in multiple languages! #23380

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Open
wants to merge 10 commits into
base: main
Choose a base branch
from

Conversation

cathysarisky
Copy link
Member

@cathysarisky cathysarisky commented May 17, 2025

🌵 Welcome to my spike. :)

This PR adds being able to pass a locale to magic link requests, resulting in users receiving sign-in/subscribe/sign-up links in whatever language is passed.

Portal already accommodates multiple languages, accepting a data-locale attribute and using it to overwrite sitewide locale when passed. This PR causes Portal to also pass the locale to the magic link endpoint, and sets up the magic link endpoint to receive it and use it for sending localized text.

Where things are:
[X] Appears functional in manual testing
[X] More tests needed
[ ] Think about whether it should also be possible from data-attributes.

Copy link
Contributor

coderabbitai bot commented May 17, 2025

Walkthrough

This change introduces locale-awareness to the magic link email flow throughout the application. The locale property is now passed from the client through API calls and server controllers down to the email generation logic. The i18n module is refactored to export multiple functions, including locale-specific utilities, and the email subject, text, and HTML generation methods are updated to be asynchronous and locale-aware. The magic link service and its consumers now accept and propagate the locale parameter, enabling dynamic translation of email content. Related test files are updated to initialize the i18n service and adjust for signature changes. Additionally, new end-to-end tests verify correct localization behavior based on the provided or sitewide locale.

Suggested labels

affects:i18n, migration

✨ Finishing Touches
  • 📝 Generate Docstrings

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
🪧 Tips

Chat

There are 3 ways to chat with CodeRabbit:

  • Review comments: Directly reply to a review comment made by CodeRabbit. Example:
    • I pushed a fix in commit <commit_id>, please review it.
    • Explain this complex logic.
    • Open a follow-up GitHub issue for this discussion.
  • Files and specific lines of code (under the "Files changed" tab): Tag @coderabbitai in a new review comment at the desired location with your query. Examples:
    • @coderabbitai explain this code block.
    • @coderabbitai modularize this function.
  • PR comments: Tag @coderabbitai in a new PR comment to ask questions about the PR branch. For the best results, please provide a very specific query, as very limited context is provided in this mode. Examples:
    • @coderabbitai gather interesting stats about this repository and render them as a table. Additionally, render a pie chart showing the language distribution in the codebase.
    • @coderabbitai read src/utils.ts and explain its main purpose.
    • @coderabbitai read the files in the src/scheduler package and generate a class diagram using mermaid and a README in the markdown format.
    • @coderabbitai help me debug CodeRabbit configuration file.

Support

Need help? Create a ticket on our support page for assistance with any issues or questions.

Note: Be mindful of the bot's finite context window. It's strongly recommended to break down tasks such as reading entire modules into smaller chunks. For a focused discussion, use review comments to chat about specific files and their changes, instead of using the PR comments.

CodeRabbit Commands (Invoked using PR comments)

  • @coderabbitai pause to pause the reviews on a PR.
  • @coderabbitai resume to resume the paused reviews.
  • @coderabbitai review to trigger an incremental review. This is useful when automatic reviews are disabled for the repository.
  • @coderabbitai full review to do a full review from scratch and review all the files again.
  • @coderabbitai summary to regenerate the summary of the PR.
  • @coderabbitai generate docstrings to generate docstrings for this PR.
  • @coderabbitai generate sequence diagram to generate a sequence diagram of the changes in this PR.
  • @coderabbitai resolve resolve all the CodeRabbit review comments.
  • @coderabbitai configuration to show the current CodeRabbit configuration for the repository.
  • @coderabbitai help to get help.

Other keywords and placeholders

  • Add @coderabbitai ignore anywhere in the PR description to prevent this PR from being reviewed.
  • Add @coderabbitai summary to generate the high-level summary at a specific location in the PR description.
  • Add @coderabbitai anywhere in the PR title to generate the title automatically.

CodeRabbit Configuration File (.coderabbit.yaml)

  • You can programmatically configure CodeRabbit by adding a .coderabbit.yaml file to the root of your repository.
  • Please see the configuration documentation for more information.
  • If your editor has YAML language server enabled, you can add the path at the top of this file to enable auto-completion and validation: # yaml-language-server: $schema=https://coderabbit.ai/integrations/schema.v2.json

Documentation and Community

  • Visit our Documentation for detailed information on how to use CodeRabbit.
  • Join our Discord Community to get help, request features, and share feedback.
  • Follow us on X/Twitter for updates and announcements.

Copy link
Contributor

@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 (3)
ghost/core/core/server/services/lib/magic-link/MagicLink.js (1)

123-129: Fix ESLint spacing issue

There's inconsistent spacing around the default parameter assignment.

-function defaultGetText(url, type, email, locale=null) {
+function defaultGetText(url, type, email, locale = null) {
-function defaultGetHTML(url, type, email, locale=null) {
+function defaultGetHTML(url, type, email, locale = null) {
-function defaultGetSubject(type, locale=null) {
+function defaultGetSubject(type, locale = null) {

Also applies to: 140-146, 155-160

🧰 Tools
🪛 ESLint

[error] 123-123: 'locale' is assigned a value but never used.

(no-unused-vars)


[error] 123-123: Operator '=' must be spaced.

(space-infix-ops)

ghost/core/core/server/services/members/api.js (2)

78-98: Factor-out locale calculation & simplify switch for better readability

  1. sitewideLocaleeffectiveLocale is computed in three different methods; extracting a tiny helper (e.g. resolveLocale(requestedLocale)) will remove duplication and the risk of the branches diverging later.
  2. await i18n.withLocale(effectiveLocale, async (t) => { … }) creates an extra async layer that isn’t needed – nothing inside the callback is awaited. Removing async/await here shaves a micro-task and makes stack-traces cleaner.
  3. The explicit signin case followed by default: is redundant. A lookup map keeps the intent obvious and eliminates the “useless case clause” flagged by Biome.

Example refactor (trimmed for brevity):

-async getSubject(type, locale) {
-    const siteTitle = settingsCache.get('title');
-    const effectiveLocale = resolveLocale(locale);
-    return await i18n.withLocale(effectiveLocale, async (t) => {
-        switch (type) {
-
-        case 'signin':
-        default:
-            return …
-        }
-    });
+async getSubject(type, locale) {
+    const siteTitle = settingsCache.get('title');
+    const effectiveLocale = resolveLocale(locale);
+    return i18n.withLocale(effectiveLocale, (t) => {
+        const subjects = {
+            subscribe: `📫 ${t('Confirm your subscription to {siteTitle}', {siteTitle})}`,
+            signup: `🙌 ${t('Complete your sign up to {siteTitle}!', {siteTitle})}`,
+            'signup-paid': `🙌 ${t('Thank you for signing up to {siteTitle}!', {siteTitle})}`,
+            updateEmail: `📫 ${t('Confirm your email update for {siteTitle}!', {siteTitle})}`,
+            signin: `🔑 ${t('Secure sign in link for {siteTitle}', {siteTitle})}`
+        };
+        return subjects[type] ?? subjects.signin;
+    });
 }

Small, isolated helper functions will also keep unit-tests focused.

🧰 Tools
🪛 Biome (1.9.4)

[error] 93-94: Useless case clause.

because the default clause is present:

Unsafe fix: Remove the useless case.

(lint/complexity/noUselessSwitchCase)


193-217: Parsing the site domain: use the WHATWG URL API & optional-chaining

Lines 196-199 build a RegExp at runtime and access the match array manually:

const siteUrl = urlUtils.urlFor('home', true);
const domain = urlUtils.urlFor('home', true).match(new RegExp('^https?://([^/:?#]+)(?:[/:?#]|$)', 'i'));
const siteDomain = (domain && domain[1]);

Safer and shorter:

-const siteUrl = urlUtils.urlFor('home', true);
-const domain = urlUtils.urlFor('home', true).match(new RegExp('^https?://([^/:?#]+)(?:[/:?#]|$)', 'i'));
-const siteDomain = (domain && domain[1]);
+const siteUrl = urlUtils.urlFor('home', true);
+const siteDomain = new URL(siteUrl).hostname;

Benefits:
• Eliminates a dynamic RegExp (static-analysis hint)
• Avoids re-calling urlFor
• Handles edge-cases like non-standard ports automatically
• Readability ↑

Optional-chaining (urlUtils?.urlFor) could also be considered if you want to guard against unexpected undefined during early boot.

🧰 Tools
🪛 Biome (1.9.4)

[error] 199-200: Change to an optional chain.

Unsafe fix: Change to an optional chain.

(lint/complexity/useOptionalChain)


[error] 212-213: Useless case clause.

because the default clause is present:

Unsafe fix: Remove the useless case.

(lint/complexity/noUselessSwitchCase)


[error] 198-199: Use a regular expression literal instead of the RegExp constructor.

Regular expression literals avoid some escaping required in a string literal, and are easier to analyze statically.
Safe fix: Use a literal notation instead.

(lint/complexity/useRegexLiterals)

📜 Review details

Configuration used: CodeRabbit UI
Review profile: CHILL
Plan: Pro

📥 Commits

Reviewing files that changed from the base of the PR and between 7979d8f and 0138b37.

📒 Files selected for processing (9)
  • apps/portal/src/actions.js (3 hunks)
  • apps/portal/src/utils/api.js (2 hunks)
  • ghost/core/core/server/services/i18n.js (3 hunks)
  • ghost/core/core/server/services/lib/magic-link/MagicLink.js (5 hunks)
  • ghost/core/core/server/services/members/api.js (2 hunks)
  • ghost/core/core/server/services/members/members-api/controllers/RouterController.js (5 hunks)
  • ghost/core/core/server/services/members/members-api/members-api.js (2 hunks)
  • ghost/core/test/unit/api/canary/session.test.js (1 hunks)
  • ghost/core/test/unit/server/services/lib/magic-link/index.test.js (1 hunks)
🧰 Additional context used
🧬 Code Graph Analysis (2)
ghost/core/core/server/services/members/members-api/members-api.js (4)
ghost/core/core/server/services/i18n.js (2)
  • locale (42-42)
  • settingsCache (39-39)
ghost/core/core/server/services/members/api.js (1)
  • settingsCache (2-2)
ghost/core/core/server/services/members/members-api/controllers/RouterController.js (2)
  • tokenData (652-658)
  • tokenData (674-674)
ghost/core/core/server/services/members/members-api/controllers/MemberController.js (1)
  • tokenData (60-60)
ghost/core/core/server/services/i18n.js (2)
ghost/core/core/server/services/members/api.js (1)
  • i18n (21-21)
ghost/core/test/unit/api/canary/session.test.js (1)
  • i18n (5-5)
🪛 ESLint
ghost/core/test/unit/server/services/lib/magic-link/index.test.js

[error] 81-81: Expected indentation of 12 spaces but found 8.

(indent)


[error] 84-84: Expected indentation of 12 spaces but found 8.

(indent)

ghost/core/core/server/services/i18n.js

[error] 13-13: Direct calls to new Error() are not allowed. Please use @tryghost/errors.

(ghost/ghost-custom/no-native-error)


[error] 26-26: Direct calls to new Error() are not allowed. Please use @tryghost/errors.

(ghost/ghost-custom/no-native-error)

ghost/core/core/server/services/lib/magic-link/MagicLink.js

[error] 123-123: 'locale' is assigned a value but never used.

(no-unused-vars)


[error] 123-123: Operator '=' must be spaced.

(space-infix-ops)


[error] 140-140: 'locale' is assigned a value but never used.

(no-unused-vars)


[error] 140-140: Operator '=' must be spaced.

(space-infix-ops)


[error] 155-155: 'locale' is assigned a value but never used.

(no-unused-vars)


[error] 155-155: Operator '=' must be spaced.

(space-infix-ops)

🪛 Biome (1.9.4)
ghost/core/core/server/services/members/api.js

[error] 93-94: Useless case clause.

because the default clause is present:

Unsafe fix: Remove the useless case.

(lint/complexity/noUselessSwitchCase)


[error] 172-173: Useless case clause.

because the default clause is present:

Unsafe fix: Remove the useless case.

(lint/complexity/noUselessSwitchCase)


[error] 199-200: Change to an optional chain.

Unsafe fix: Change to an optional chain.

(lint/complexity/useOptionalChain)


[error] 212-213: Useless case clause.

because the default clause is present:

Unsafe fix: Remove the useless case.

(lint/complexity/noUselessSwitchCase)


[error] 198-199: Use a regular expression literal instead of the RegExp constructor.

Regular expression literals avoid some escaping required in a string literal, and are easier to analyze statically.
Safe fix: Use a literal notation instead.

(lint/complexity/useRegexLiterals)

⏰ Context from checks skipped due to timeout of 90000ms (8)
  • GitHub Check: Database tests (Node 22.13.1, mysql8)
  • GitHub Check: Database tests (Node 20.11.1, mysql8)
  • GitHub Check: Database tests (Node 20.11.1, sqlite3)
  • GitHub Check: Ghost-CLI tests
  • GitHub Check: Regression tests (Node 20.11.1, sqlite3)
  • GitHub Check: Regression tests (Node 20.11.1, mysql8)
  • GitHub Check: Unit tests (Node 20.11.1)
  • GitHub Check: Unit tests (Node 22.13.1)
🔇 Additional comments (15)
ghost/core/test/unit/api/canary/session.test.js (1)

5-6: Good addition of i18n initialization

Adding i18n initialization before tests ensures that the internationalization service is properly set up for locale-aware functionality being tested.

apps/portal/src/actions.js (3)

86-86: Consistently passing locale to magic link API

Good addition of locale to the signin magic link flow, which ensures users receive emails in their preferred language.


108-108: Consistently passing locale to magic link API

Good addition of locale to the signup magic link flow, maintaining consistency with the signin flow.


514-514: Consistently passing locale to magic link API

Good addition of locale to the oneClickSubscribe flow, completing the locale support across all magic link scenarios.

ghost/core/core/server/services/members/members-api/members-api.js (2)

210-210: Good function signature update for locale support

Adding the optional locale parameter with a null default value is a solid approach that maintains backward compatibility while enabling new functionality.


220-223: Well-implemented locale fallback logic

The fallback logic is correct - checking for the provided locale first, then falling back to the site's default locale from settings cache, and finally defaulting to 'en' if neither is available.

apps/portal/src/utils/api.js (1)

265-282: LGTM – Locale parameter properly added to sendMagicLink

The addition of an optional locale parameter to the sendMagicLink method and passing it in the request body (defaulting to null when not provided) successfully implements the locale-aware functionality required for magic links.

ghost/core/core/server/services/lib/magic-link/MagicLink.js (2)

59-59: Documentation properly updated for new parameter

The JSDoc update correctly documents the new optional locale parameter.


78-80: Correctly passing locale to email generation methods

These changes correctly propagate the locale parameter to the email content generation methods, allowing for localized email content.

ghost/core/core/server/services/members/members-api/controllers/RouterController.js (3)

561-562: Locale parameter correctly added to request destructuring

The locale parameter is properly extracted from the request body with a null default, which aligns with the client-side implementation.


629-661: Locale properly propagated through signup flow

The code correctly passes the locale parameter to the email sending service during the signup process.


663-676: Locale properly propagated through signin flow

The code correctly passes the locale parameter to the email sending service during the signin process.

ghost/core/core/server/services/i18n.js (2)

36-66: Well-designed initialization function

The init function is well-structured and properly handles both the initial locale setting and subsequent updates through events.


68-78: Good use of getters for accessing i18n functionality

The module exports a well-designed interface that makes the i18n functionality easily accessible while maintaining proper encapsulation.

ghost/core/core/server/services/members/api.js (1)

21-21: Import looks correct – thank you for wiring i18n in the service layer
No issues spotted with the import path; this matches the new services/i18n.js location and keeps the dependency surface small.

Copy link
Contributor

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

📜 Review details

Configuration used: CodeRabbit UI
Review profile: CHILL
Plan: Pro

📥 Commits

Reviewing files that changed from the base of the PR and between 0138b37 and 5625df3.

📒 Files selected for processing (5)
  • apps/portal/src/tests/SigninFlow.test.js (6 hunks)
  • apps/portal/src/tests/SignupFlow.test.js (6 hunks)
  • ghost/core/core/server/services/i18n.js (3 hunks)
  • ghost/core/core/server/services/lib/magic-link/MagicLink.js (5 hunks)
  • ghost/core/test/unit/server/services/lib/magic-link/index.test.js (1 hunks)
🚧 Files skipped from review as they are similar to previous changes (2)
  • ghost/core/core/server/services/lib/magic-link/MagicLink.js
  • ghost/core/core/server/services/i18n.js
🧰 Additional context used
🪛 ESLint
ghost/core/test/unit/server/services/lib/magic-link/index.test.js

[error] 72-72: Unexpected trailing comma.

(comma-dangle)

⏰ Context from checks skipped due to timeout of 90000ms (9)
  • GitHub Check: Ghost-CLI tests
  • GitHub Check: Database tests (Node 22.13.1, mysql8)
  • GitHub Check: Unit tests (Node 20.11.1)
  • GitHub Check: Database tests (Node 20.11.1, mysql8)
  • GitHub Check: Unit tests (Node 22.13.1)
  • GitHub Check: Regression tests (Node 20.11.1, sqlite3)
  • GitHub Check: Database tests (Node 20.11.1, sqlite3)
  • GitHub Check: Regression tests (Node 20.11.1, mysql8)
  • GitHub Check: Lint
🔇 Additional comments (4)
apps/portal/src/tests/SigninFlow.test.js (1)

157-158: LGTM: Locale parameter properly included in tests.

The addition of locale: expect.any(String) in the test expectations appropriately validates that the locale parameter is now being passed to the magic link endpoint, which aligns with the PR's objective of supporting localized magic links.

ghost/core/test/unit/server/services/lib/magic-link/index.test.js (2)

80-84: Good: Text and HTML generation tests now include locale parameter.

The test assertions now correctly verify that the locale parameter is passed to the email content generation functions. This ensures proper testing of the locale-aware email generation.


86-87: Looks good: Subject generation test includes locale parameter.

The test assertion now correctly verifies that the locale parameter is passed to the email subject generation function.

apps/portal/src/tests/SignupFlow.test.js (1)

238-239: LGTM: Locale parameter properly included in signup tests.

The addition of locale: expect.any(String) consistently across all test assertions correctly validates that the locale parameter is now being passed through the signup flow, which aligns with the PR's objective of supporting localized magic links.

Copy link
Contributor

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

🧹 Nitpick comments (4)
ghost/core/test/e2e-api/members/send-magic-link.test.js (4)

417-419: Fix padding in test suite declaration

Remove the blank line after the opening brace of the describe block to adhere to code style guidelines.

 describe('Magic links can be localized by passing a locale', function () {
-
     it('Sends magic link email in French when passedlocale is fr', async function () {
🧰 Tools
🪛 ESLint

[error] 417-419: Block must not be padded by blank lines.

(padded-blocks)


441-461: Maintain consistent string quote style

This test correctly verifies English localization, but use single quotes for strings to maintain consistency with the codebase style.

-            should(mail.text).containEql("Tap the link below to complete the signup process for Ghost");
-            should(mail.html).containEql("Tap the link below to complete the signup process for Ghost");
+            should(mail.text).containEql('Tap the link below to complete the signup process for Ghost');
+            should(mail.html).containEql('Tap the link below to complete the signup process for Ghost');
🧰 Tools
🪛 ESLint

[error] 459-459: Strings must use singlequote.

(quotes)


[error] 460-460: Strings must use singlequote.

(quotes)


512-536: Consider completing fallback test for unknown locales

The commented-out test for handling unknown locales is important for ensuring robust behavior. Consider completing this test once the fallback behavior is fixed.

I can help implement this test once the fallback behavior is properly determined (currently it's defaulting to English instead of the sitewide locale).


417-537: Add test for signin email type with locale

The current tests focus on the signup email type, but it would be beneficial to include a test for the signin email type with a specified locale to ensure complete coverage of the feature.

Consider adding a test like:

it('Sends signin magic link email with the specified locale', async function () {
    // Create a member first so signin works
    const email = '[email protected]';
    await membersService.api.members.create({email, name: 'Signin Test'});
    
    // Request a magic link with German locale
    await membersAgent.post('/api/send-magic-link')
        .body({
            email,
            emailType: 'signin',
            locale: 'de'
        })
        .expectEmptyBody()
        .expectStatus(201);

    // Check email is sent with German subject
    const mail = mockManager.assert.sentEmail({
        to: email,
        subject: /Melden Sie sich bei Ghost an/
    });

    // Verify German content in email
    should(mail.text).match(/Klicken Sie auf den Link unten, um sich bei/);
    should(mail.html).match(/Klicken Sie auf den Link unten, um sich bei/);
});
🧰 Tools
🪛 ESLint

[error] 417-419: Block must not be padded by blank lines.

(padded-blocks)


[error] 459-459: Strings must use singlequote.

(quotes)


[error] 460-460: Strings must use singlequote.

(quotes)

📜 Review details

Configuration used: CodeRabbit UI
Review profile: CHILL
Plan: Pro

📥 Commits

Reviewing files that changed from the base of the PR and between 5625df3 and 91c9aed.

📒 Files selected for processing (3)
  • apps/portal/src/tests/portal-links.test.js (1 hunks)
  • ghost/core/test/e2e-api/members/send-magic-link.test.js (1 hunks)
  • ghost/core/test/unit/server/services/lib/magic-link/index.test.js (1 hunks)
🚧 Files skipped from review as they are similar to previous changes (1)
  • ghost/core/test/unit/server/services/lib/magic-link/index.test.js
🧰 Additional context used
🧬 Code Graph Analysis (1)
ghost/core/test/e2e-api/members/send-magic-link.test.js (1)
ghost/core/test/e2e-api/admin/posts-legacy.test.js (1)
  • mockManager (13-13)
🪛 ESLint
ghost/core/test/e2e-api/members/send-magic-link.test.js

[error] 417-419: Block must not be padded by blank lines.

(padded-blocks)


[error] 459-459: Strings must use singlequote.

(quotes)


[error] 460-460: Strings must use singlequote.

(quotes)

⏰ Context from checks skipped due to timeout of 90000ms (8)
  • GitHub Check: Database tests (Node 20.11.1, mysql8)
  • GitHub Check: Database tests (Node 22.13.1, mysql8)
  • GitHub Check: Unit tests (Node 20.11.1)
  • GitHub Check: Database tests (Node 20.11.1, sqlite3)
  • GitHub Check: Regression tests (Node 20.11.1, mysql8)
  • GitHub Check: Unit tests (Node 22.13.1)
  • GitHub Check: Regression tests (Node 20.11.1, sqlite3)
  • GitHub Check: Ghost-CLI tests
🔇 Additional comments (4)
apps/portal/src/tests/portal-links.test.js (1)

235-236: LGTM: Properly testing locale parameter addition

This change correctly adds verification that the locale parameter is passed to the sendMagicLink API call, which aligns with the PR objective of supporting localized magic links.

ghost/core/test/e2e-api/members/send-magic-link.test.js (3)

419-439: LGTM: Good test coverage for French locale

This test properly verifies that when passing locale: 'fr' to the magic link endpoint, the email is sent with French content, checking both subject and body content.


463-485: LGTM: Good test for sitewide locale fallback

This test properly verifies that the magic link system falls back to the sitewide locale (Spanish in this case) when no explicit locale is provided in the request.


487-511: LGTM: Good test for locale override behavior

This test correctly verifies that a locale explicitly passed in the request overrides the sitewide locale setting, which is important for the feature to work properly.

Copy link
Contributor

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

♻️ Duplicate comments (1)
ghost/core/core/server/services/members/api.js (1)

121-213: Large text templates still duplicate 80 % of their content

Previous reviews already pointed out that having five near-identical blocks is hard to maintain.
Please consider extracting common boiler-plate and using a data-driven lookup instead of a switch.

🧰 Tools
🪛 Biome (1.9.4)

[error] 193-194: Useless case clause.

because the default clause is present:

Unsafe fix: Remove the useless case.

(lint/complexity/noUselessSwitchCase)

🧹 Nitpick comments (3)
ghost/core/core/server/services/members/api.js (3)

21-23: Avoid splitting i18n concerns across two packages

The file imports the default i18n instance from ../i18n and the SUPPORTED_LOCALES constant from @tryghost/i18n.
In most of Ghost’s code-base the singleton instance and the constants live in the same package; mixing them can lead to version skew or duplicated bundles when the two packages drift.

Consider importing both from the same source:

-const i18n = require('../i18n');
-const {SUPPORTED_LOCALES} = require('@tryghost/i18n');
+const {i18n, SUPPORTED_LOCALES} = require('../i18n'); // ← or `@tryghost/i18n` – choose one

This keeps your dependency graph simpler and reduces risk of accidentally loading two different sets of translations.


101-120: Switch repetition – remove redundant signin clause

case 'signin': is followed immediately by default: returning the exact same template string, so the explicit case is never reached.
Removing it cleans up the control flow and will silence the Biome “useless case clause” warning.

-                    case 'signin':
                     default:
                         return `🔑 ${t(\`Secure sign in link for {siteTitle}\`, {siteTitle, interpolation: {escapeValue: false}})}`;

(If you keep the named clause for readability, add a // falls through comment so linters know it’s intentional.)

🧰 Tools
🪛 Biome (1.9.4)

[error] 115-116: Useless case clause.

because the default clause is present:

Unsafe fix: Remove the useless case.

(lint/complexity/noUselessSwitchCase)


214-236: RegExp & optional-chaining clean-ups

  1. Creating a RegExp object on every call is unnecessary; a literal is faster and clearer.
  2. The domain array access can be simplified using optional chaining.
-const domain = urlUtils.urlFor('home', true).match(new RegExp('^https?://([^/:?#]+)(?:[/:?#]|$)', 'i'));
-const siteDomain = (domain && domain[1]);
+const domainMatch = siteUrl.match(/^https?:\/\/([^/:?#]+)(?:[/:?#]|$)/i);
+const siteDomain = domainMatch?.[1];

These are small, low-risk changes that improve readability and address the linter hints.

🧰 Tools
🪛 Biome (1.9.4)

[error] 219-220: Change to an optional chain.

Unsafe fix: Change to an optional chain.

(lint/complexity/useOptionalChain)


[error] 232-233: Useless case clause.

because the default clause is present:

Unsafe fix: Remove the useless case.

(lint/complexity/noUselessSwitchCase)


[error] 218-219: Use a regular expression literal instead of the RegExp constructor.

Regular expression literals avoid some escaping required in a string literal, and are easier to analyze statically.
Safe fix: Use a literal notation instead.

(lint/complexity/useRegexLiterals)

📜 Review details

Configuration used: CodeRabbit UI
Review profile: CHILL
Plan: Pro

📥 Commits

Reviewing files that changed from the base of the PR and between 91c9aed and 542cc5e.

📒 Files selected for processing (2)
  • ghost/core/core/server/services/members/api.js (3 hunks)
  • ghost/core/test/e2e-api/members/send-magic-link.test.js (1 hunks)
🚧 Files skipped from review as they are similar to previous changes (1)
  • ghost/core/test/e2e-api/members/send-magic-link.test.js
🧰 Additional context used
🧬 Code Graph Analysis (1)
ghost/core/core/server/services/members/api.js (1)
ghost/core/core/server/services/i18n.js (4)
  • i18n (42-42)
  • locale (47-47)
  • settingsCache (44-44)
  • t (36-36)
🪛 Biome (1.9.4)
ghost/core/core/server/services/members/api.js

[error] 115-116: Useless case clause.

because the default clause is present:

Unsafe fix: Remove the useless case.

(lint/complexity/noUselessSwitchCase)


[error] 193-194: Useless case clause.

because the default clause is present:

Unsafe fix: Remove the useless case.

(lint/complexity/noUselessSwitchCase)


[error] 219-220: Change to an optional chain.

Unsafe fix: Change to an optional chain.

(lint/complexity/useOptionalChain)


[error] 232-233: Useless case clause.

because the default clause is present:

Unsafe fix: Remove the useless case.

(lint/complexity/noUselessSwitchCase)


[error] 218-219: Use a regular expression literal instead of the RegExp constructor.

Regular expression literals avoid some escaping required in a string literal, and are easier to analyze statically.
Safe fix: Use a literal notation instead.

(lint/complexity/useRegexLiterals)

⏰ Context from checks skipped due to timeout of 90000ms (8)
  • GitHub Check: Ghost-CLI tests
  • GitHub Check: Regression tests (Node 20.11.1, mysql8)
  • GitHub Check: Regression tests (Node 20.11.1, sqlite3)
  • GitHub Check: Unit tests (Node 20.11.1)
  • GitHub Check: Unit tests (Node 22.13.1)
  • GitHub Check: Database tests (Node 20.11.1, sqlite3)
  • GitHub Check: Database tests (Node 20.11.1, mysql8)
  • GitHub Check: Database tests (Node 22.13.1, mysql8)

Copy link
Contributor

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

♻️ Duplicate comments (2)
ghost/core/core/server/services/members/api.js (2)

31-47: 🛠️ Refactor suggestion

Cache locale validation to improve performance

The validateLocale function is called multiple times for each email (in getSubject, getText, and getHTML), causing unnecessary async operations and potential I/O overhead. Consider caching validation results to avoid redundant locale loading.

+// Cache promises so concurrent calls share the same load
+const _localeLoadCache = new Map();

-async function validateLocale(locale) {
-    const sitewideLocale = settingsCache.get('locale');
+async function validateLocale(rawLocale) {
+    const sitewideLocale = (settingsCache.get('locale') || 'en').replace('_', '-').toLowerCase();
     
-    // If no locale provided, use sitewide locale or fall back to English
-    if (!locale || !SUPPORTED_LOCALES.includes(locale)) {
-        return sitewideLocale || 'en';
+    // Fast-path when no explicit locale is provided
+    if (!rawLocale) {
+        return sitewideLocale;
     }

+    // Canonicalise: `en_US` → `en-us`
+    const locale = rawLocale.replace('_', '-').toLowerCase();
+
+    if (!SUPPORTED_LOCALES.includes(locale)) {
+        return sitewideLocale;
+    }
 
-    try {
-        // Try to load the locale to validate it exists
-        await i18n.loadLocale(locale);
-        return locale;
-    } catch (e) {
-        // If locale loading fails, fall back to sitewide locale or English
-        return sitewideLocale || 'en';
+    // De-duplicate parallel calls
+    if (!_localeLoadCache.has(locale)) {
+        _localeLoadCache.set(locale, i18n.loadLocale(locale).catch(() => null));
+    }
+
+    const loaded = await _localeLoadCache.get(locale);
+    return loaded ? locale : sitewideLocale;
 }

117-209: 🛠️ Refactor suggestion

Reduce duplication in email templates

The five email templates duplicate 80% of their content. This makes maintenance difficult and increases the risk of inconsistencies.

Consider refactoring to:

  1. Extract common elements (greeting, footer, expiration notice) into shared helper functions
  2. Keep only the unique content for each email type
  3. Use a data-driven approach instead of a switch statement
async getText(url, type, email, locale) {
    const siteTitle = settingsCache.get('title');
    const effectiveLocale = await validateLocale(locale);
    
    return await i18n.withLocale(effectiveLocale, async (t) => {
+        // Common template parts
+        const greeting = t(`Hey there,`);
+        const expirationNotice = t('For your security, the link will expire in 24 hours time.');
+        const separator = '---';
+        const sentTo = t('Sent to {email}', {email});
+        
+        // Type-specific content
+        const typeContent = {
+            'subscribe': {
+                mainText: t('You\'re one tap away from subscribing to {siteTitle} — please confirm your email address with this link:', {siteTitle, interpolation: {escapeValue: false}}),
+                closingText: t('All the best!'),
+                disclaimerText: `${t('If you did not make this request, you can simply delete this message.')} ${t('You will not be subscribed.')}`
+            },
+            'signup': {
+                mainText: t('Tap the link below to complete the signup process for {siteTitle}, and be automatically signed in:', {siteTitle, interpolation: {escapeValue: false}}),
+                closingText: t('See you soon!'),
+                disclaimerText: `${t('If you did not make this request, you can simply delete this message.')} ${t('You will not be signed up, and no account will be created for you.')}`
+            },
+            // Add other types here
+        };
+        
+        // Default to signin if type not found
+        const content = typeContent[type] || typeContent['signin'];
+        
+        return trimLeadingWhitespace`
+            ${greeting}
+
+            ${content.mainText}
+
+            ${url}
+
+            ${expirationNotice}
+
+            ${content.closingText || ''}
+
+            ${separator}
+
+            ${sentTo}
+            ${content.disclaimerText}
+            `;
-        switch (type) {
-        case 'subscribe':
-            return trimLeadingWhitespace`
-                ${t(`Hey there,`)}
-
-                ${t('You\'re one tap away from subscribing to {siteTitle} — please confirm your email address with this link:', {siteTitle, interpolation: {escapeValue: false}})}
-
-            ${url}
-
-            ${t('For your security, the link will expire in 24 hours time.')}
-
-            ${t('All the best!')}
-
-                ---
-
-                ${t('Sent to {email}', {email})}
-                ${t('If you did not make this request, you can simply delete this message.')} ${t('You will not be subscribed.')}
-                `;
-        // Rest of cases...
-        }
    });
},
🧰 Tools
🪛 Biome (1.9.4)

[error] 189-190: Useless case clause.

because the default clause is present:

Unsafe fix: Remove the useless case.

(lint/complexity/noUselessSwitchCase)

🧹 Nitpick comments (2)
ghost/core/core/server/services/members/api.js (2)

97-116: Consider using a data-driven approach instead of switch statement

The switch statement for translating email subjects could be replaced with an object map for better maintainability. Also, fix the "Useless case clause" flagged by static analysis.

 async getSubject(type, locale) {
     const siteTitle = settingsCache.get('title');
     const effectiveLocale = await validateLocale(locale);
     
     return await i18n.withLocale(effectiveLocale, async (t) => {
-        switch (type) {
-        case 'subscribe':
-            return `📫 ${t(`Confirm your subscription to {siteTitle}`, {siteTitle, interpolation: {escapeValue: false}})}`;
-        case 'signup':
-            return `🙌 ${t(`Complete your sign up to {siteTitle}!`, {siteTitle, interpolation: {escapeValue: false}})}`;
-        case 'signup-paid':
-            return `🙌 ${t(`Thank you for signing up to {siteTitle}!`, {siteTitle, interpolation: {escapeValue: false}})}`;
-        case 'updateEmail':
-            return `📫 ${t(`Confirm your email update for {siteTitle}!`, {siteTitle, interpolation: {escapeValue: false}})}`;
-        case 'signin':
-        default:
-            return `🔑 ${t(`Secure sign in link for {siteTitle}`, {siteTitle, interpolation: {escapeValue: false}})}`;
-        }
+        const subjectTemplates = {
+            'subscribe': `📫 ${t(`Confirm your subscription to {siteTitle}`, {siteTitle, interpolation: {escapeValue: false}})}`,
+            'signup': `🙌 ${t(`Complete your sign up to {siteTitle}!`, {siteTitle, interpolation: {escapeValue: false}})}`,
+            'signup-paid': `🙌 ${t(`Thank you for signing up to {siteTitle}!`, {siteTitle, interpolation: {escapeValue: false}})}`,
+            'updateEmail': `📫 ${t(`Confirm your email update for {siteTitle}!`, {siteTitle, interpolation: {escapeValue: false}})}`,
+            'signin': `🔑 ${t(`Secure sign in link for {siteTitle}`, {siteTitle, interpolation: {escapeValue: false}})}`
+        };
+        
+        return subjectTemplates[type] || subjectTemplates['signin'];
     });
 },
🧰 Tools
🪛 Biome (1.9.4)

[error] 111-112: Useless case clause.

because the default clause is present:

Unsafe fix: Remove the useless case.

(lint/complexity/noUselessSwitchCase)


214-215: Use regex literal and optional chaining for better code

The static analysis tools flagged a few improvements that can be made here:

-const domain = urlUtils.urlFor('home', true).match(new RegExp('^https?://([^/:?#]+)(?:[/:?#]|$)', 'i'));
-const siteDomain = (domain && domain[1]);
+const domain = urlUtils.urlFor('home', true).match(/^https?:\/\/([^/:?#]+)(?:[/:?#]|$)/i);
+const siteDomain = domain?.[1];
🧰 Tools
🪛 Biome (1.9.4)

[error] 214-215: Use a regular expression literal instead of the RegExp constructor.

Regular expression literals avoid some escaping required in a string literal, and are easier to analyze statically.
Safe fix: Use a literal notation instead.

(lint/complexity/useRegexLiterals)

📜 Review details

Configuration used: CodeRabbit UI
Review profile: CHILL
Plan: Pro

📥 Commits

Reviewing files that changed from the base of the PR and between 542cc5e and ec91e7e.

📒 Files selected for processing (2)
  • ghost/core/core/server/services/members/api.js (3 hunks)
  • ghost/core/test/unit/server/services/lib/magic-link/index.test.js (1 hunks)
🚧 Files skipped from review as they are similar to previous changes (1)
  • ghost/core/test/unit/server/services/lib/magic-link/index.test.js
🧰 Additional context used
🪛 Biome (1.9.4)
ghost/core/core/server/services/members/api.js

[error] 111-112: Useless case clause.

because the default clause is present:

Unsafe fix: Remove the useless case.

(lint/complexity/noUselessSwitchCase)


[error] 189-190: Useless case clause.

because the default clause is present:

Unsafe fix: Remove the useless case.

(lint/complexity/noUselessSwitchCase)


[error] 215-216: Change to an optional chain.

Unsafe fix: Change to an optional chain.

(lint/complexity/useOptionalChain)


[error] 228-229: Useless case clause.

because the default clause is present:

Unsafe fix: Remove the useless case.

(lint/complexity/noUselessSwitchCase)


[error] 214-215: Use a regular expression literal instead of the RegExp constructor.

Regular expression literals avoid some escaping required in a string literal, and are easier to analyze statically.
Safe fix: Use a literal notation instead.

(lint/complexity/useRegexLiterals)

🔇 Additional comments (3)
ghost/core/core/server/services/members/api.js (3)

21-22: Import changes look good

The code now imports the full i18n module and SUPPORTED_LOCALES constant, which are essential for the new locale-aware functionality.


210-233: Email HTML generation is now properly localized

The HTML email generation now correctly passes the translation function to the email templates, enabling localized HTML emails.

Note: Consider refactoring to use a data-driven approach similar to the suggestion for getSubject to eliminate the "Useless case clause" warning.

🧰 Tools
🪛 Biome (1.9.4)

[error] 215-216: Change to an optional chain.

Unsafe fix: Change to an optional chain.

(lint/complexity/useOptionalChain)


[error] 228-229: Useless case clause.

because the default clause is present:

Unsafe fix: Remove the useless case.

(lint/complexity/noUselessSwitchCase)


[error] 214-215: Use a regular expression literal instead of the RegExp constructor.

Regular expression literals avoid some escaping required in a string literal, and are easier to analyze statically.
Safe fix: Use a literal notation instead.

(lint/complexity/useRegexLiterals)


31-215:

✅ Verification successful

Verify the impact of asynchronous email functions

This change converts synchronous email functions to asynchronous ones. Ensure all calling code has been updated to handle these promises correctly.


🏁 Script executed:

#!/bin/bash
# Search for direct calls to getSubject, getText, and getHTML that might not be awaiting the result

echo "Searching for potentially non-awaited calls to getSubject, getText, and getHTML..."
rg "getSubject\(|getText\(|getHTML\(" --type js | grep -v "await " | grep -v "async"

Length of output: 1759


No missing awaits for members API email functions

The search for non-awaited calls to getSubject, getText, and getHTML only surfaced references in other services (SettingsBREADService, NewslettersService, EmailRenderer) and unit tests—not calls on the membersApiInstance returned by createApiInstance. Since these async methods are invoked internally by the @tryghost/members-api library and there are no direct calls in our code to await them, no changes are required.

🧰 Tools
🪛 Biome (1.9.4)

[error] 111-112: Useless case clause.

because the default clause is present:

Unsafe fix: Remove the useless case.

(lint/complexity/noUselessSwitchCase)


[error] 189-190: Useless case clause.

because the default clause is present:

Unsafe fix: Remove the useless case.

(lint/complexity/noUselessSwitchCase)


[error] 214-215: Use a regular expression literal instead of the RegExp constructor.

Regular expression literals avoid some escaping required in a string literal, and are easier to analyze statically.
Safe fix: Use a literal notation instead.

(lint/complexity/useRegexLiterals)

Copy link
Contributor

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

♻️ Duplicate comments (2)
ghost/core/core/server/services/members/api.js (2)

127-218: 🛠️ Refactor suggestion

Large duplicate template blocks should be refactored

This locale-aware implementation works but contains significant code duplication that makes maintenance difficult.

Refactor the large switch statement with nearly identical templates to reduce duplication:

async getText(url, type, email, locale) {
    const siteTitle = settingsCache.get('title');
    const effectiveLocale = await validateLocale(locale);
    
    return await i18n.withLocale(effectiveLocale, async (t) => {
+       // Common email sections
+       const greeting = t(`Hey there,`);
+       const securityNotice = t('For your security, the link will expire in 24 hours time.');
+       const separator = '---';
+       const sentTo = t('Sent to {email}', {email});
+       
+       // Type-specific content
+       const typeContent = {
+           'subscribe': {
+               mainContent: t('You\'re one tap away from subscribing to {siteTitle} — please confirm your email address with this link:', {siteTitle, interpolation: {escapeValue: false}}),
+               farewell: t('All the best!'),
+               disclaimer: `${t('If you did not make this request, you can simply delete this message.')} ${t('You will not be subscribed.')}`
+           },
+           'signup': {
+               mainContent: t('Tap the link below to complete the signup process for {siteTitle}, and be automatically signed in:', {siteTitle, interpolation: {escapeValue: false}}),
+               farewell: t('See you soon!'),
+               disclaimer: `${t('If you did not make this request, you can simply delete this message.')} ${t('You will not be signed up, and no account will be created for you.')}`
+           },
+           'signup-paid': {
+               mainContent: t('Thank you for subscribing to {siteTitle}. Tap the link below to be automatically signed in:', {siteTitle, interpolation: {escapeValue: false}}),
+               farewell: t('See you soon!'),
+               disclaimer: t('Thank you for subscribing to {siteTitle}!', {siteTitle, interpolation: {escapeValue: false}})
+           },
+           'updateEmail': {
+               mainContent: t('Please confirm your email address with this link:'),
+               farewell: '',
+               disclaimer: `${t('If you did not make this request, you can simply delete this message.')} ${t('This email address will not be used.')}`
+           },
+           'signin': {
+               mainContent: t('Welcome back! Use this link to securely sign in to your {siteTitle} account:', {siteTitle, interpolation: {escapeValue: false}}),
+               farewell: t('See you soon!'),
+               disclaimer: t('If you did not make this request, you can safely ignore this email.')
+           }
+       };
+       
+       // Default to signin if type not found
+       const content = typeContent[type] || typeContent['signin'];
+       
+       // Assemble email from components
+       return trimLeadingWhitespace`
+           ${greeting}
+
+           ${content.mainContent}
+
+           ${url}
+
+           ${securityNotice}
+
+           ${content.farewell ? content.farewell + '\n\n' : ''}${separator}
+
+           ${sentTo}
+           ${content.disclaimer}
+           `;
-       switch (type) {
-       case 'subscribe':
-           return trimLeadingWhitespace`
-               ${t(`Hey there,`)}
-
-               ${t('You\'re one tap away from subscribing to {siteTitle} — please confirm your email address with this link:', {siteTitle, interpolation: {escapeValue: false}})}
-
-           ${url}
-
-           ${t('For your security, the link will expire in 24 hours time.')}
-
-           ${t('All the best!')}
-
-               ---
-
-               ${t('Sent to {email}', {email})}
-               ${t('If you did not make this request, you can simply delete this message.')} ${t('You will not be subscribed.')}
-               `;
-       case 'signup':
-           return trimLeadingWhitespace`
-               ${t(`Hey there,`)}
-
-               ${t('Tap the link below to complete the signup process for {siteTitle}, and be automatically signed in:', {siteTitle, interpolation: {escapeValue: false}})}
-
-               ${url}
-
-               ${t('For your security, the link will expire in 24 hours time.')}
-
-               ${t('See you soon!')}
-
-               ---
-
-               ${t('Sent to {email}', {email})}
-               ${t('If you did not make this request, you can simply delete this message.')} ${t('You will not be signed up, and no account will be created for you.')}
-               `;
-       case 'signup-paid':
-           return trimLeadingWhitespace`
-               ${t(`Hey there,`)}
-
-               ${t('Thank you for subscribing to {siteTitle}. Tap the link below to be automatically signed in:', {siteTitle, interpolation: {escapeValue: false}})}
-
-               ${url}
-
-               ${t('For your security, the link will expire in 24 hours time.')}
-
-               ${t('See you soon!')}
-
-               ---
-
-               ${t('Sent to {email}', {email})}
-               ${t('Thank you for subscribing to {siteTitle}!', {siteTitle, interpolation: {escapeValue: false}})}
-               `;
-       case 'updateEmail':
-           return trimLeadingWhitespace`
-               ${t(`Hey there,`)}
-
-               ${t('Please confirm your email address with this link:')}
-
-               ${url}
-
-               ${t('For your security, the link will expire in 24 hours time.')}
-
-               ---
-
-               ${t('Sent to {email}', {email})}
-               ${t('If you did not make this request, you can simply delete this message.')} ${t('This email address will not be used.')}
-               `;
-       case 'signin':
-       default:
-           return trimLeadingWhitespace`
-               ${t(`Hey there,`)}
-
-               ${t('Welcome back! Use this link to securely sign in to your {siteTitle} account:', {siteTitle, interpolation: {escapeValue: false}})}
-
-               ${url}
-
-               ${t('For your security, the link will expire in 24 hours time.')}
-
-               ${t('See you soon!')}
-
-               ---
-
-               ${t('Sent to {email}', {email})}
-               ${t('If you did not make this request, you can safely ignore this email.')}
-               `;
-       }
    });
}
🧰 Tools
🪛 Biome (1.9.4)

[error] 199-200: Useless case clause.

because the default clause is present:

Unsafe fix: Remove the useless case.

(lint/complexity/noUselessSwitchCase)


31-57: 🛠️ Refactor suggestion

Locale cache implementation could be more robust

The locale validation and caching logic is a good improvement, but it could be enhanced to handle edge cases better.

Consider implementing these improvements:

  1. Normalize locale strings (convert to lowercase, replace underscores with hyphens)
  2. Cache promise results to prevent duplicate concurrent loads of the same locale
// Cache for successfully loaded locales
-const _loadedLocalesCache = new Set();
+// Cache both successful loads and in-flight promises
+const _localeLoadCache = new Map();

async function validateLocale(rawLocale) {
    const sitewideLocale = settingsCache.get('locale');
    
+   // Normalize sitewideLocale for consistency
+   const normalizedSitewideLocale = (sitewideLocale || 'en').replace('_', '-').toLowerCase();
    
    // If no locale provided, use sitewide locale or fall back to English
-   if (!locale || !SUPPORTED_LOCALES.includes(locale)) {
-       return sitewideLocale || 'en';
+   if (!rawLocale) {
+       return normalizedSitewideLocale;
    }

+   // Normalize locale string (en_US → en-us)
+   const locale = rawLocale.replace('_', '-').toLowerCase();
+   
+   if (!SUPPORTED_LOCALES.includes(locale)) {
+       return normalizedSitewideLocale;
+   }

-   // Return cached locale if it's already been loaded successfully
-   if (_loadedLocalesCache.has(locale)) {
-       return locale;
-   }
+   // De-duplicate parallel calls for the same locale
+   if (!_localeLoadCache.has(locale)) {
+       _localeLoadCache.set(locale, i18n.loadLocale(locale)
+           .then(() => locale)
+           .catch(() => normalizedSitewideLocale));
+   }
    
-   try {
-       // Try to load the locale to validate it exists
-       await i18n.loadLocale(locale);
-       // Cache the successfully loaded locale
-       _loadedLocalesCache.add(locale);
-       return locale;
-   } catch (e) {
-       // If locale loading fails, fall back to sitewide locale or English
-       return sitewideLocale || 'en';
-   }
+   return _localeLoadCache.get(locale);
}
🧹 Nitpick comments (2)
ghost/core/core/server/services/members/api.js (2)

107-126: Good async locale-aware implementation but with a redundant case

The implementation correctly handles localized email subjects but contains a minor redundancy.

Remove the redundant 'signin' case that's identical to the default:

return await i18n.withLocale(effectiveLocale, async (t) => {
    switch (type) {
    case 'subscribe':
        return `📫 ${t(`Confirm your subscription to {siteTitle}`, {siteTitle, interpolation: {escapeValue: false}})}`;
    case 'signup':
        return `🙌 ${t(`Complete your sign up to {siteTitle}!`, {siteTitle, interpolation: {escapeValue: false}})}`;
    case 'signup-paid':
        return `🙌 ${t(`Thank you for signing up to {siteTitle}!`, {siteTitle, interpolation: {escapeValue: false}})}`;
    case 'updateEmail':
        return `📫 ${t(`Confirm your email update for {siteTitle}!`, {siteTitle, interpolation: {escapeValue: false}})}`;
-   case 'signin':
    default:
        return `🔑 ${t(`Secure sign in link for {siteTitle}`, {siteTitle, interpolation: {escapeValue: false}})}`;
    }
});
🧰 Tools
🪛 Biome (1.9.4)

[error] 121-122: Useless case clause.

because the default clause is present:

Unsafe fix: Remove the useless case.

(lint/complexity/noUselessSwitchCase)


220-243: Good HTML generation implementation with minor improvement opportunities

The HTML email generation code correctly implements locale awareness and passes the translation function to the email templates.

Consider these small improvements:

async getHTML(url, type, email, locale) {
    const siteTitle = settingsCache.get('title');
    const effectiveLocale = await validateLocale(locale);
    const siteUrl = urlUtils.urlFor('home', true);
-   const domain = urlUtils.urlFor('home', true).match(new RegExp('^https?://([^/:?#]+)(?:[/:?#]|$)', 'i'));
-   const siteDomain = (domain && domain[1]);
+   const domain = urlUtils.urlFor('home', true).match(/^https?:\/\/([^/:?#]+)(?:[/:?#]|$)/i);
+   const siteDomain = domain?.[1];
    const accentColor = settingsCache.get('accent_color');

    return await i18n.withLocale(effectiveLocale, async (t) => {
        switch (type) {
        case 'subscribe':
            return subscribeEmail({t, url, email, siteTitle, accentColor, siteDomain, siteUrl});
        case 'signup':
            return signupEmail({t, url, email, siteTitle, accentColor, siteDomain, siteUrl});
        case 'signup-paid':
            return signupPaidEmail({t, url, email, siteTitle, accentColor, siteDomain, siteUrl});
        case 'updateEmail':
            return updateEmail({t, url, email, siteTitle, accentColor, siteDomain, siteUrl});
-       case 'signin':
        default:
            return signinEmail({t, url, email, siteTitle, accentColor, siteDomain, siteUrl});
        }
    });
}
🧰 Tools
🪛 Biome (1.9.4)

[error] 225-226: Change to an optional chain.

Unsafe fix: Change to an optional chain.

(lint/complexity/useOptionalChain)


[error] 238-239: Useless case clause.

because the default clause is present:

Unsafe fix: Remove the useless case.

(lint/complexity/noUselessSwitchCase)


[error] 224-225: Use a regular expression literal instead of the RegExp constructor.

Regular expression literals avoid some escaping required in a string literal, and are easier to analyze statically.
Safe fix: Use a literal notation instead.

(lint/complexity/useRegexLiterals)

📜 Review details

Configuration used: CodeRabbit UI
Review profile: CHILL
Plan: Pro

📥 Commits

Reviewing files that changed from the base of the PR and between ec91e7e and 88159ef.

📒 Files selected for processing (1)
  • ghost/core/core/server/services/members/api.js (3 hunks)
🧰 Additional context used
🪛 Biome (1.9.4)
ghost/core/core/server/services/members/api.js

[error] 121-122: Useless case clause.

because the default clause is present:

Unsafe fix: Remove the useless case.

(lint/complexity/noUselessSwitchCase)


[error] 199-200: Useless case clause.

because the default clause is present:

Unsafe fix: Remove the useless case.

(lint/complexity/noUselessSwitchCase)


[error] 225-226: Change to an optional chain.

Unsafe fix: Change to an optional chain.

(lint/complexity/useOptionalChain)


[error] 238-239: Useless case clause.

because the default clause is present:

Unsafe fix: Remove the useless case.

(lint/complexity/noUselessSwitchCase)


[error] 224-225: Use a regular expression literal instead of the RegExp constructor.

Regular expression literals avoid some escaping required in a string literal, and are easier to analyze statically.
Safe fix: Use a literal notation instead.

(lint/complexity/useRegexLiterals)

⏰ Context from checks skipped due to timeout of 90000ms (9)
  • GitHub Check: Database tests (Node 20.11.1, mysql8)
  • GitHub Check: Unit tests (Node 22.13.1)
  • GitHub Check: Unit tests (Node 20.11.1)
  • GitHub Check: Regression tests (Node 20.11.1, mysql8)
  • GitHub Check: Database tests (Node 22.13.1, mysql8)
  • GitHub Check: Database tests (Node 20.11.1, sqlite3)
  • GitHub Check: Regression tests (Node 20.11.1, sqlite3)
  • GitHub Check: Ghost-CLI tests
  • GitHub Check: Lint
🔇 Additional comments (1)
ghost/core/core/server/services/members/api.js (1)

21-22: Appropriate i18n dependencies for localization support

Good addition of the necessary imports for internationalization support. This correctly brings in the i18n module and the list of supported locales needed for the locale validation logic.

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