Skip to content

Conversation

dahlia
Copy link
Member

@dahlia dahlia commented Aug 31, 2025

Summary

This PR implements a user invitation system that allows existing users to invite new users to join the platform via email invitations.

Features

  • GraphQL API: New invite mutation with comprehensive validation
  • Email invitations: Send personalized invitation emails with custom messages
  • Locale support: Recipients receive invitations in their preferred language
  • Invitation quotas: Track and enforce invitation limits per user
  • Web UI: New settings page for sending invitations with real-time validation

Changes

Backend

  • Added invite mutation to GraphQL schema with error handling
  • Implemented invitation validation logic (email uniqueness, quota checks)
  • Added email sending functionality with localized templates
  • Updated i18n models to support readonly arrays

Frontend (web-next)

  • Created LocaleSelect component for language selection
  • Added invite settings page at /settings/invite
  • Integrated with GraphQL mutation for sending invitations
  • Added invite tab to settings navigation

Technical Details

  • Uses existing signup token infrastructure for invitation links
  • Validates email addresses and checks for existing accounts
  • Respects user's invitation quota (currently 10 invitations)
  • Supports custom messages in invitations
  • Full internationalization support (en, ja, ko, zh-CN, zh-TW)

Testing

The invitation system has been implemented with:

  • Proper error handling for invalid emails
  • Quota enforcement
  • Email uniqueness validation
  • Localized email templates

Screenshots

The new invite settings page provides a clean interface for sending invitations with:

  • Email input with validation
  • Optional personalized message
  • Locale selection for the recipient
  • Clear feedback on remaining invitations

Summary by CodeRabbit

  • New Features

    • Invite friends by email from Settings → Invite; sends localized invitation emails and shows available locales.
  • UI

    • Added Invite tab and full invite page with form, validation, disabled state when out of invites, and success/error toasts.
    • New locale picker component for selecting invitation language.
  • Localization

    • Invitation email templates and UI strings added for en, ja, ko, zh-CN, zh-TW.
  • Style

    • Minor spacing adjustments in Preferences.

dahlia added 3 commits August 31, 2025 11:52
Implements invitation functionality to allow existing users to invite new users
via email. Includes GraphQL schema updates, resolver implementation, and
internationalization support across all supported locales.

- Add `invite` mutation to GraphQL schema
- Implement invitation validation and email sending logic
- Add invitation-related error types and result unions
- Update locale files with invitation-related translations
- Add invitation quota tracking for accounts
Implements a locale selection dropdown component using Solid and Relay.
The component displays available locales with native language names and
integrates with the existing i18n infrastructure.
Implements the invitation functionality in the settings area, allowing users
to send invitations to new users. Includes form validation, locale selection,
and integration with the GraphQL invite mutation.

- Add invite tab to settings navigation
- Create invite settings page with email and message form
- Add locale selection for invitation recipients
- Update i18n negotiateLocale to accept readonly arrays
- Display invitation quotas and validation errors
Copy link

coderabbitai bot commented Aug 31, 2025

Note

Other AI code review bot(s) detected

CodeRabbit has detected other AI code review bot(s) in this pull request and will avoid duplicating their findings in the review comments. This may lead to a less comprehensive review.

Walkthrough

Adds a GraphQL invitation feature (types, mutation, validation, token creation, email templating/sending with 48h expiry), locale discovery, locale-aware email templates and picker, frontend invite settings UI and route, i18n typing refinements, and small UI spacing/label fixes.

Changes

Cohort / File(s) Summary
GraphQL invite & wiring
graphql/invite.ts, graphql/schema.graphql, graphql/mod.ts
New invite types (Invitation, InviteValidationErrors, InviteResult), enums, EXPIRATION (48h), invite mutation with validation, atomic inviter quota decrement, signup token creation, locale-aware email templating and sending, error handling; module import wired.
Locale discovery
graphql/misc.ts
Adds availableLocales query that reads/caches locale JSON files and returns Intl.Locale[].
Locale templates (JSON)
graphql/locales/*.json
graphql/locales/en.json, .../ja.json, .../ko.json, .../zh-CN.json, .../zh-TW.json
Adds invite templates: emailSubject, emailContent, emailContentWithMessage with placeholders (inviter, inviterName, verifyUrl, expiration, message).
Frontend: locale picker
web-next/src/components/LocaleSelect.tsx
New Solid.js LocaleSelect component using availableLocales fragment, negotiateLocale, Intl.DisplayNames; exposes value/onChange.
Frontend: invite page & route
web-next/src/routes/(root)/[handle]/settings/invite.tsx, web-next/src/locales/*/messages.po
New invite settings route/component, preloaded query and invite mutation, form (email, locale, optional message, verifyUrl), validation mapping, toasts, access checks; many locale strings added for en-US, ja-JP, ko-KR, zh-CN, zh-TW.
Settings navigation
web-next/src/components/SettingsTabs.tsx
Adds "invite" to SettingsTab, adds Invite tab trigger, adjusts grid to 4 columns.
i18n typings/refactor
models/i18n.ts
Overloads and implementation updated to readonly array signatures and precomputes maximized locales; behavior preserved.
Minor UI tweak
web-next/src/routes/(root)/[handle]/settings/preferences.tsx
Fix label for attribute and adjust spacing (gap-2gap-1.5).

Sequence Diagram(s)

sequenceDiagram
  autonumber
  actor User
  participant Web as "Web UI"
  participant GQL as "GraphQL API"
  participant KV as "KV / DB"
  participant I18N as "Locale Templates (FS)"
  participant Mail as "Mailer"

  User->>Web: Open Invite page
  Web->>GQL: query availableLocales
  GQL->>I18N: read locales/*.json
  I18N-->>GQL: locales list
  GQL-->>Web: [Intl.Locale]

  User->>Web: submit invite(email, locale, message, verifyUrl)
  Web->>GQL: mutation invite(...)
  GQL->>KV: verify inviter auth & quota
  alt validation fails
    GQL-->>Web: InviteValidationErrors
  else
    GQL->>KV: atomic decrement leftInvitations
    GQL->>KV: create signup token (48h)
    GQL->>I18N: load & select template (negotiateLocale)
    GQL->>Mail: send email (expanded template)
    alt mail fails
      GQL->>KV: credit back leftInvitations
      GQL-->>Web: InviteValidationErrors (INVITER_EMAIL_SEND_FAILED)
    else
      GQL-->>Web: Invitation
    end
  end
Loading

Estimated code review effort

🎯 4 (Complex) | ⏱️ ~60 minutes

Possibly related PRs

Suggested labels

enhancement

Poem

I hop across the locale map,
I stitch a token, fold a wrap.
I send a nibble, soft and bright,
Invitations wing into the night.
Thump—new friends will soon arrive! 🐇✉️

✨ Finishing Touches
  • 📝 Generate Docstrings
🧪 Generate unit tests
  • Create PR with unit tests
  • Post copyable unit tests in a comment

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.
    • 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.
  • 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 the files in the src/scheduler package and generate a class diagram using mermaid and a README in the markdown format.

Support

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

CodeRabbit Commands (Invoked using PR/Issue comments)

Type @coderabbitai help to get the list of available commands.

Other keywords and placeholders

  • Add @coderabbitai ignore or @coderabbit 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

Status, Documentation and Community

  • Visit our Status Page to check the current availability of CodeRabbit.
  • 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

@gemini-code-assist gemini-code-assist bot left a comment

Choose a reason for hiding this comment

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

Summary of Changes

Hello @dahlia, I'm Gemini Code Assist1! I'm currently reviewing this pull request and will post my feedback shortly. In the meantime, here's a summary to help you and other reviewers quickly get up to speed!

This pull request introduces a new core feature that empowers existing users to expand the platform's community by inviting new members. It provides a complete end-to-end solution for user invitations, encompassing backend API logic, email delivery with internationalization, and a dedicated user interface for managing the invitation process. The primary goal is to facilitate organic growth and streamline the onboarding of new users through a controlled invitation mechanism.

Highlights

  • User Invitation System: Implemented a comprehensive user invitation system allowing existing users to invite new users via email.
  • GraphQL API Enhancements: Introduced a new invite GraphQL mutation with robust validation, including checks for email uniqueness and invitation quotas.
  • Internationalized Email Invitations: Enabled personalized invitation emails with support for multiple locales (English, Japanese, Korean, Simplified Chinese, Traditional Chinese), ensuring recipients receive invitations in their preferred language.
  • Web UI for Invitations: Added a new dedicated settings page at /settings/invite on the web UI, providing a user-friendly interface for sending invitations with real-time validation and feedback on remaining invitations.
  • Invitation Quota Management: Integrated functionality to track and enforce invitation limits per user, decrementing available invitations upon successful sends.
Using Gemini Code Assist

The full guide for Gemini Code Assist can be found on our documentation page, here are some quick tips.

Invoking Gemini

You can request assistance from Gemini at any point in your pull request via creating an issue comment (i.e. comment on the pull request page) using either /gemini <command> or @gemini-code-assist <command>. Below is a summary of the supported commands.

Feature Command Description
Code Review /gemini review Performs a code review for the current pull request in its current state.
Pull Request Summary /gemini summary Provides a summary of the current pull request in its current state.
Comment @gemini-code-assist Responds in comments when explicitly tagged, both in issue comments and review comments.
Help /gemini help Displays a list of available commands.

Customization

To customize Gemini Code Assist for GitHub experience, repository maintainers can create a configuration file and/or provide a custom code review style guide (such as PEP-8 for Python) by creating and adding files to a .gemini/ folder in the base of the repository. Detailed instructions can be found here.

Limitations & Feedback

Gemini Code Assist may make mistakes. Please leave feedback on any instances where its feedback is incorrect or counter productive. You can react with 👍 and 👎 on @gemini-code-assist comments. If you're interested in giving your feedback about your experience with Gemini Code Assist for Github and other Google products, sign up here.

You can also get AI-powered code generation, chat, as well as code reviews directly in the IDE at no cost with the Gemini Code Assist IDE Extension.

Footnotes

  1. Review the Privacy Notices, Generative AI Prohibited Use Policy, Terms of Service, and learn how to configure Gemini Code Assist in GitHub here. Gemini can make mistakes, so double check it and use code with caution.

Copy link

@gemini-code-assist gemini-code-assist bot left a comment

Choose a reason for hiding this comment

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

Code Review

This pull request introduces a comprehensive user invitation system, including a new GraphQL mutation, email notifications with localization, and a user-friendly web interface. The implementation is well-structured and covers various aspects from backend logic to frontend integration. I've identified a few areas for improvement, mainly concerning error handling and performance, to enhance the robustness and efficiency of the new system.

@dahlia dahlia linked an issue Aug 31, 2025 that may be closed by this pull request
Copy link

@coderabbitai coderabbitai bot left a comment

Choose a reason for hiding this comment

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

Actionable comments posted: 5

🧹 Nitpick comments (14)
graphql/locales/ko.json (2)

9-9: Fix minor Korean spelling: “메세지” → “메시지”.

Standard spelling improves UX.

Apply this diff:

-    "emailContentWithMessage": "{{inviter}} 님이 Hackers' Pub에 초대합니다! 다음은 {{inviterName}} 님의 메세지입니다:\n\n{{message}}\n\n초대장을 받으려면 아래 링크를 클릭하세요:\n\n{{verifyUrl}}\n\n이 링크는 {{expiration}} 후에 만료됩니다.\n"
+    "emailContentWithMessage": "{{inviter}} 님이 Hackers' Pub에 초대합니다! 다음은 {{inviterName}} 님의 메시지입니다:\n\n{{message}}\n\n초대장을 받으려면 아래 링크를 클릭하세요:\n\n{{verifyUrl}}\n\n이 링크는 {{expiration}} 후에 만료됩니다.\n"

8-8: Optional phrasing tweak: “초대장을 받으려면” → “초대를 수락하려면”.

Closer to the English intent (“accept the invitation”).

Apply this diff:

-    "emailContent": "{{inviter}} 님이 Hackers' Pub에 초대합니다!\n\nHackers' Pub은 소프트웨어 프로그래머들이 지식과 경험을 서로 나누는 곳입니다. 또한 ActivityPub을 지원하는 소셜 네트워크이기도 하며, 연합우주(fediverse)에서 즐겨찾는 소프트웨어 프로그래머들을 팔로하고 최신 콘텐츠를 받아 볼 수 있습니다.\n\n초대장을 받으려면 아래 링크를 클릭하세요:\n\n{{verifyUrl}}\n\n이 링크는 {{expiration}} 후에 만료됩니다.\n",
+    "emailContent": "{{inviter}} 님이 Hackers' Pub에 초대합니다!\n\nHackers' Pub은 소프트웨어 프로그래머들이 지식과 경험을 서로 나누는 곳입니다. 또한 ActivityPub을 지원하는 소셜 네트워크이기도 하며, 연합우주(fediverse)에서 즐겨찾는 소프트웨어 프로그래머들을 팔로하고 최신 콘텐츠를 받아 볼 수 있습니다.\n\n초대를 수락하려면 아래 링크를 클릭하세요:\n\n{{verifyUrl}}\n\n이 링크는 {{expiration}} 후에 만료됩니다.\n",
models/i18n.ts (2)

189-206: Precompute maximize() for available locales to avoid repeated work.

maximize() is called inside three loops; precomputing reduces allocations and CPU in hot paths.

Apply this diff:

-  const availables = availableLocales.map((l) =>
-    typeof l === "string" ? new Intl.Locale(l) : l
-  );
+  const availables = availableLocales.map((l) =>
+    typeof l === "string" ? new Intl.Locale(l) : l
+  );
+  const pre = availables.map((raw) => ({ raw, max: raw.maximize() }));
@@
-    // First try exact match
-    for (const available of availables) {
-      const availableMaximized = available.maximize();
-      if (wantedMaximized.baseName === availableMaximized.baseName) {
-        return available;
-      }
-    }
+    // First try exact match
+    for (const a of pre) {
+      if (wantedMaximized.baseName === a.max.baseName) {
+        return a.raw;
+      }
+    }
@@
-    for (const available of availables) {
-      const availableMaximized = available.maximize();
-      if (
-        wantedMaximized.language === availableMaximized.language &&
-        wantedMaximized.script === availableMaximized.script
-      ) {
-        return available;
-      }
-    }
+    for (const a of pre) {
+      if (
+        wantedMaximized.language === a.max.language &&
+        wantedMaximized.script === a.max.script
+      ) {
+        return a.raw;
+      }
+    }
@@
-    for (const available of availables) {
-      const availableMaximized = available.maximize();
-      if (wantedMaximized.language === availableMaximized.language) {
-        return available;
-      }
-    }
+    for (const a of pre) {
+      if (wantedMaximized.language === a.max.language) {
+        return a.raw;
+      }
+    }

189-191: Minor naming nit: prefer “availableLocalesNormalized” over “availables”.

Improves readability.

Apply this diff:

-  const availables = availableLocales.map((l) =>
+  const availableLocalesNormalized = availableLocales.map((l) =>
     typeof l === "string" ? new Intl.Locale(l) : l
   );
-  const pre = availables.map((raw) => ({ raw, max: raw.maximize() }));
+  const pre = availableLocalesNormalized.map((raw) => ({ raw, max: raw.maximize() }));
web-next/src/routes/(root)/[handle]/settings/preferences.tsx (1)

199-206: Label “for” likely mismatches the checkbox input id

Checkbox id is "prefer-ai-summary" but Label targets "prefer-ai-summary-input". This breaks label–control association (a11y and click target). If Checkbox forwards id to the underlying input, align them:

- <Label for="prefer-ai-summary-input">
+ <Label for="prefer-ai-summary">
web-next/src/components/SettingsTabs.tsx (1)

9-9: New Invite tab wired correctly; consider avoiding hard-coded grid column count

The tab, href, and union type update look good. To make the layout resilient if tabs change, avoid grid-cols-4:

- <TabsList class="grid max-w-prose mx-auto grid-cols-4">
+ <TabsList class="grid max-w-prose mx-auto grid-flow-col auto-cols-fr">

Also applies to: 31-31, 46-52

graphql/misc.ts (1)

7-24: Cache availableLocales to avoid filesystem work on every query

This list is static at runtime; memoize it to reduce I/O.

+let cachedLocales: Intl.Locale[] | null = null;
 builder.queryField("availableLocales", (t) =>
   t.field({
     type: ["Locale"],
     async resolve(_root, _args, _ctx) {
-      const availableLocales: Intl.Locale[] = [];
+      if (cachedLocales) return cachedLocales;
+      const availableLocales: Intl.Locale[] = [];
       const files = expandGlob(join(LOCALES_DIR, "*.json"), {
         includeDirs: false,
       });
       for await (const file of files) {
         if (!file.isFile) continue;
         const match = file.name.match(/^(.+)\.json$/);
         if (match == null) continue;
         const localeName = match[1];
-        availableLocales.push(new Intl.Locale(localeName));
+        try {
+          availableLocales.push(new Intl.Locale(localeName));
+        } catch {
+          // ignore invalid tags
+        }
       }
-      return availableLocales;
+      cachedLocales = availableLocales;
+      return availableLocales;
     },
   }));
web-next/src/routes/(root)/[handle]/settings/invite.tsx (2)

231-233: UX copy nits and validation clarity.

  • “Invite your friends… You can invite up to # [people/person]” reads like a quota cap, but you’re displaying remaining invitations. Consider “You can invite # more [people/person].”
  • When invalid, you flip to “invalid” state but still render the generic description via the first Match when emailError() == null. Looks good, just ensure TextFieldDescription is hidden on error as you already do.

Also applies to: 247-252, 264-295, 322-332


146-147: Use a stable verifyUrl template origin for SSR/preload contexts.

window.location.origin will throw during SSR or non-DOM preloads. Prefer deriving origin from config or request context on the server and passing it in, or guard for client-only usage.

-        verifyUrl: `${window.location.origin}/sign/up/{token}?code={code}`,
+        verifyUrl: `${location.origin ?? globalThis.location?.origin ?? ""}/sign/up/{token}?code={code}`,

Or inject the base URL from server config at build/runtime.

web-next/src/components/LocaleSelect.tsx (2)

22-31: Memoize available locales to avoid double reactive reads.

You read availableLocales()?.availableLocales twice. Cache it once for cleaner reactivity.

 export function LocaleSelect(props: LocaleSelectProps) {
   const { i18n } = useLingui();
   const availableLocales = createFragment(
@@
   );
+  const list = () => availableLocales()?.availableLocales ?? [];
   return (
     <Select
       value={toLocaleInfo(
         negotiateLocale(
           props.value,
-          availableLocales()?.availableLocales ?? [],
+          list(),
         )?.baseName ?? "en",
         i18n.locale,
       )}
       onChange={(o) => props.onChange(o?.code ?? "en")}
       options={mapLocaleInfo(
-        availableLocales()?.availableLocales ?? [],
+        list(),
         i18n.locale,
       )}

Also applies to: 33-46


105-118: Guard DisplayNames fallbacks for uncommon locales.

Intl.DisplayNames may return undefined; you already fall back to l. Consider trimming region/scripts for nicer names if displayNames.of(l) is undefined.

graphql/invite.ts (2)

236-265: Cache locale templates to avoid disk I/O on every invite.

getEmailTemplate scans and reads files on each call. Cache availableLocales and parsed JSON per locale in a module-level Map.


220-227: Consider crediting back an invitation when email send fails.

You log failures but still consume the invite. Depending on product policy, you may want to roll back the decrement or surface an error result.

graphql/schema.graphql (1)

494-497: Fix typo in docstrings (“variabvles” → “variables”).

Minor but user-facing in schema docs.

-    The RFC 6570-compliant URI Template for the verification link.  Available variabvles: `{token}` and `{code}`.
+    The RFC 6570-compliant URI Template for the verification link.  Available variables: `{token}` and `{code}`.

Also applies to: 513-516

📜 Review details

Configuration used: CodeRabbit UI

Review profile: CHILL

Plan: Pro

💡 Knowledge Base configuration:

  • MCP integration is disabled by default for public repositories
  • Jira integration is disabled by default for public repositories
  • Linear integration is disabled by default for public repositories

You can enable these sources in your CodeRabbit configuration.

📥 Commits

Reviewing files that changed from the base of the PR and between 95553a2 and 5eec31c.

⛔ Files ignored due to path filters (1)
  • deno.lock is excluded by !**/*.lock
📒 Files selected for processing (14)
  • graphql/invite.ts (1 hunks)
  • graphql/locales/en.json (1 hunks)
  • graphql/locales/ja.json (1 hunks)
  • graphql/locales/ko.json (1 hunks)
  • graphql/locales/zh-CN.json (1 hunks)
  • graphql/locales/zh-TW.json (1 hunks)
  • graphql/misc.ts (1 hunks)
  • graphql/mod.ts (1 hunks)
  • graphql/schema.graphql (3 hunks)
  • models/i18n.ts (2 hunks)
  • web-next/src/components/LocaleSelect.tsx (1 hunks)
  • web-next/src/components/SettingsTabs.tsx (3 hunks)
  • web-next/src/routes/(root)/[handle]/settings/invite.tsx (1 hunks)
  • web-next/src/routes/(root)/[handle]/settings/preferences.tsx (2 hunks)
🧰 Additional context used
🧬 Code graph analysis (4)
graphql/misc.ts (1)
models/i18n.ts (1)
  • Locale (30-30)
web-next/src/routes/(root)/[handle]/settings/invite.tsx (6)
web-next/src/routes/(root)/[handle]/settings/preferences.tsx (1)
  • route (36-43)
web-next/src/components/Title.tsx (1)
  • Title (7-14)
web-next/src/components/ProfilePageBreadcrumb.tsx (1)
  • ProfilePageBreadcrumb (16-49)
web-next/src/components/SettingsTabs.tsx (1)
  • SettingsTabs (16-65)
web-next/src/components/Trans.tsx (1)
  • Trans (22-42)
web-next/src/components/LocaleSelect.tsx (1)
  • LocaleSelect (22-89)
web-next/src/components/LocaleSelect.tsx (1)
models/i18n.ts (1)
  • negotiateLocale (173-225)
graphql/invite.ts (6)
models/uuid.ts (1)
  • Uuid (4-4)
models/i18n.ts (2)
  • Locale (30-30)
  • negotiateLocale (173-225)
models/account.ts (1)
  • normalizeEmail (522-536)
models/schema.ts (1)
  • accountTable (41-96)
models/signup.ts (2)
  • createSignupToken (36-64)
  • SignupToken (23-29)
graphql/email.ts (1)
  • EMAIL_FROM (11-11)
🪛 Biome (2.1.2)
web-next/src/routes/(root)/[handle]/settings/invite.tsx

[error] 54-54: Shouldn't redeclare 'invitePageQuery'. Consider to delete it or rename it.

'invitePageQuery' is defined here:

(lint/suspicious/noRedeclare)


[error] 82-82: Shouldn't redeclare 'inviteMutation'. Consider to delete it or rename it.

'inviteMutation' is defined here:

(lint/suspicious/noRedeclare)

🔇 Additional comments (12)
graphql/locales/zh-TW.json (1)

6-9: Invite templates look good and placeholders are consistent.

Subject/body read naturally, placeholders (inviter, inviterName, verifyUrl, expiration, message) match the expected set.

graphql/locales/ja.json (1)

6-9: Japanese invite strings read well; placeholders are correct.

No functional or linguistic blockers spotted.

graphql/locales/en.json (1)

6-9: English invite copy is clear; placeholders align across locales.

Good consistency with other languages and expected variables.

models/i18n.ts (2)

156-160: Accepting readonly arrays is a solid API improvement.

Safer contracts with no behavior change. 👍


168-176: Unify overloads on readonly arrays—good type hygiene.

Signatures are consistent and non-mutating across overloads.

web-next/src/routes/(root)/[handle]/settings/preferences.tsx (1)

214-224: Spacing tweak LGTM

Switching to gap-1.5 tightens density nicely without layout risk.

Also applies to: 225-236

graphql/mod.ts (1)

7-7: Registering invite and misc modules — looks good

Side-effect imports ensure the new schema pieces are included during builder init. No ordering concerns spotted.

Also applies to: 9-9

graphql/locales/zh-CN.json (1)

6-10: Invite templates added — placeholder parity verified across locales
All locale files now define the same placeholders (expiration, inviter, inviterName, message, verifyUrl).

graphql/schema.graphql (4)

387-394: Invitation type addition looks good.

Fields are minimal and match resolver output.


405-406: Union definition LGTM.

InviteResult union correctly matches resolver’s discriminants.


477-487: Docstring OK; ensure parity with backend template variables.

The available variables list matches resolver checks for {token} and {code}.


788-789: Query.availableLocales addition matches frontend usage.

Works with LocaleSelect fragment.

Fill in missing translations for invitation functionality across all supported
locales (ja-JP, ko-KR, zh-CN, zh-TW). Translations include invitation form
labels, error messages, validation feedback, and UI elements.

All translations follow established terminology from glossary files and
maintain consistency with existing locale conventions.

Co-Authored-By: Claude <[email protected]>
Copy link

@coderabbitai coderabbitai bot left a comment

Choose a reason for hiding this comment

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

Actionable comments posted: 2

Caution

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

⚠️ Outside diff range comments (1)
web-next/src/locales/zh-TW/messages.po (1)

627-629: Typo: duplicated word “通行通行金鑰”

User-facing typo; fix to single “通行金鑰”.

-msgstr "已註冊的通行通行金鑰"
+msgstr "已註冊的通行金鑰"
🧹 Nitpick comments (16)
web-next/src/locales/ko-KR/messages.po (3)

218-221: Use “선호하는” to better match “prefers”

Minor tone tweak to mirror the English nuance.

-msgstr "초대장을 받을 친구가 사용하는 언어를 선택하세요. 이 언어는 초대장에만 사용됩니다."
+msgstr "초대장을 받을 친구가 선호하는 언어를 선택하세요. 이 언어는 초대장에만 사용됩니다."

739-742: Fix spacing/comma around “뿐만 아니라”

Korean grammar prefers 붙여쓰기 and no comma here.

-msgstr "이메일 주소는 초대장을 받을 때 뿐만 아니라, 계정에 로그인할 때도 사용됩니다."
+msgstr "이메일 주소는 초대장을 받을 때뿐만 아니라 계정에 로그인할 때도 사용됩니다."

14-14: Add explicit Plural-Forms header for better PO tooling compatibility

Some tools warn when this is empty; Korean should declare a single plural form.

-Plural-Forms: 
+Plural-Forms: nplurals=1; plural=0;
web-next/src/locales/zh-TW/messages.po (11)

26-30: Clarify remaining-invites and use natural counter (“位”)

Aligns better with invitationsLeft and reads more native.

-msgstr "{0, plural, other {邀請你的朋友加入 Hackers' Pub。你可以邀請最多 # 個人。}}"
+msgstr "{0, plural, other {邀請你的朋友加入 Hackers' Pub。你還可以邀請 # 位朋友。}}"

173-176: More natural phrasing for “unexpected error”

Minor tone improvement for zh-TW.

-msgstr "發生了意外錯誤。請稍後再試。"
+msgstr "發生未預期的錯誤。請稍後再試。"

218-221: Clarify scope to the invitation email and use “偏好”

Improves clarity and tone.

-msgstr "選擇你的朋友使用的語言。這個語言只會用於邀請。"
+msgstr "選擇你的朋友偏好的語言。此語言僅用於邀請信。"

391-394: Consider “邀請信語言” for precision

Makes it obvious it’s for the email.

-msgstr "邀請語言"
+msgstr "邀請信語言"

395-398: Prefer TW-local “寄出” over “發送”

Sounds more native in zh-TW UI.

-msgstr "邀請已發送"
+msgstr "邀請已寄出"

653-656: Button copy: “送出” is more idiomatic than “發送”

Common in zh-TW interfaces.

-msgstr "發送"
+msgstr "送出"

657-660: Progress copy: match button with “送出中…”

Keeps wording consistent.

-msgstr "發送中…"
+msgstr "送出中…"

739-742: Tighten the subject and flow

Add “此” and parallel structure for clarity.

-msgstr "電子郵件地址不僅用於接收邀請,還用於登入帳戶。"
+msgstr "此電子郵件地址不僅用於接收邀請,也將用於登入帳戶。"

747-750: Align success toast with “寄出”

Keeps terminology consistent with the send action.

-msgstr "邀請已成功發送。"
+msgstr "邀請已成功寄出。"

886-889: Unify with earlier term “邀請信”

Keep the same noun throughout the invite flow.

-msgstr "你的朋友將在邀請郵件中看到此訊息。"
+msgstr "你的朋友將在邀請信中看到此訊息。"

610-613: Unify “Code of conduct” term

Elsewhere it’s “行為準則”; keep it consistent.

-msgstr "閱讀完整的行為守則"
+msgstr "閱讀完整的行為準則"
web-next/src/locales/ja-JP/messages.po (2)

26-30: Align copy with “remaining invites” count (uses invitationsLeft).

The placeholder comment says invitationsLeft; “最大#人” reads as a quota cap, not remaining. Suggest “あと#人招待できます。”

-msgstr "{0, plural, one {友達をHackers' Pubに招待しましょう。最大#人まで招待できます。} other {友達をHackers' Pubに招待しましょう。最大#人まで招待できます。}}"
+msgstr "{0, plural, one {友達をHackers' Pubに招待しましょう。あと#人招待できます。} other {友達をHackers' Pubに招待しましょう。あと#人招待できます。}}"

218-221: Use “招待メール” consistently (avoid “招待状”) and tighten phrasing.

The flow explicitly sends email invitations; “招待メール” is clearer and consistent with other keys.

-msgstr "招待する友達の使用言語を選択してください。この言語は招待状にのみ使用されます。"
+msgstr "友達が希望する言語を選択してください。この言語は招待メールにのみ使用されます。"

-msgstr "招待状の送信に失敗しました"
+msgstr "招待メールの送信に失敗しました"

-msgstr "招待状の言語"
+msgstr "招待メールの言語"

-msgstr "招待状を送信しました"
+msgstr "招待メールを送信しました"

-msgstr "招待状が残っていません"
+msgstr "招待枠は残っていません"

-msgstr "メールアドレスは招待状を受け取るだけでなく、アカウントへのログインにも使用されます。"
+msgstr "メールアドレスは招待メールの受信だけでなく、アカウントへのログインにも使用されます。"

-msgstr "招待状が正常に送信されました。"
+msgstr "招待メールが送信されました。"

-msgstr "招待状が残っていません。追加されるまでお待ちください。"
+msgstr "招待枠はありません。追加されるまでお待ちください。"

-msgstr "友達は招待メールでこのメッセージを見ることができます。"
+msgstr "友達は招待メールでこのメッセージを確認できます。"

Also applies to: 333-337, 391-394, 395-398, 497-500, 739-742, 747-750, 857-861, 886-889

📜 Review details

Configuration used: CodeRabbit UI

Review profile: CHILL

Plan: Pro

💡 Knowledge Base configuration:

  • MCP integration is disabled by default for public repositories
  • Jira integration is disabled by default for public repositories
  • Linear integration is disabled by default for public repositories

You can enable these sources in your CodeRabbit configuration.

📥 Commits

Reviewing files that changed from the base of the PR and between 5eec31c and f3f33c8.

📒 Files selected for processing (5)
  • web-next/src/locales/en-US/messages.po (14 hunks)
  • web-next/src/locales/ja-JP/messages.po (14 hunks)
  • web-next/src/locales/ko-KR/messages.po (14 hunks)
  • web-next/src/locales/zh-CN/messages.po (14 hunks)
  • web-next/src/locales/zh-TW/messages.po (14 hunks)
✅ Files skipped from review due to trivial changes (2)
  • web-next/src/locales/en-US/messages.po
  • web-next/src/locales/zh-CN/messages.po
⏰ Context from checks skipped due to timeout of 90000ms. You can increase the timeout in your CodeRabbit configuration to a maximum of 15 minutes (900000ms). (2)
  • GitHub Check: image (ubuntu-latest)
  • GitHub Check: image (ubuntu-24.04-arm)
🔇 Additional comments (27)
web-next/src/locales/ko-KR/messages.po (16)

26-30: Plural ICU for ko handled correctly — OK

Single-form translation with “other {…}” is appropriate for Korean; placeholders preserved.


73-77: Placeholder preserved and natural phrasing — OK

Reads idiomatically: “{0} 님은 이미 … 회원입니다.”


173-176: Generic error copy is clean and polite — OK


266-270: Reference added for invite page — OK

String and translation remain correct.


287-290: Label translation is concise and correct — OK


333-337: Failure copy is clear — OK


391-404: Invite-related labels/messages are consistent — OK

“초대장 언어”, “초대장이 발송되었습니다”, “초대” all consistent with other invite strings.


565-568: Unrelated reference shift (SettingsTabs) — no action


573-576: Form error guidance reads naturally — OK


653-660: CTA and progress states look good — OK


661-667: “Settings” reference addition — OK


735-738: Validation message is accurate — OK


747-750: Success message is clear — OK


849-852: Optional field hint is clear — OK


857-861: Quota-exhausted copy is consistent with other strings — OK


886-889: Explanatory helper text reads well — OK

web-next/src/locales/zh-TW/messages.po (9)

73-77: LGTM — accurate and idiomatic

Translation reads well and preserves meaning.


266-270: LGTM — “Email address” is correct

Term choice is standard in zh-TW.


287-290: LGTM — “Extra message” OK

Clear and idiomatic.


333-337: LGTM — failure toast

Consistent with other error messages.


399-404: LGTM — tab label

Concise and correct.


497-500: LGTM — quota message

Term “邀請名額” is clear.


573-576: LGTM — validation prompt

Tone and wording are good.


735-738: LGTM — invalid email

Clear and consistent with other validations.


849-852: LGTM — helper text

Natural and succinct.

web-next/src/locales/ja-JP/messages.po (2)

1-15: Set correct Plural-Forms header for Japanese.

Prevents gettext tooling warnings and keeps PO metadata complete.

-"Plural-Forms: \n"
+"Plural-Forms: nplurals=1; plural=0;\n"

653-660: LGTM for these additions.

Natural translations and correct tone/placeholders.

Also applies to: 399-404, 287-290, 849-852, 173-176, 266-270, 735-738

dahlia and others added 5 commits August 31, 2025 13:06
- Add caching for availableLocales query to avoid filesystem operations on every request
- Add error handling for invalid locale tags in misc.ts
- Implement email template caching to reduce disk I/O overhead
- Preload all locale templates at startup for better performance

Co-Authored-By: Claude <[email protected]>
- Fix Korean spelling error in email template: "메세지" → "메시지"
- Fix Traditional Chinese typo: "通行通行金鑰" → "通行金鑰"
- Optimize negotiateLocale performance by precomputing maximize() results
- Fix accessibility issue with label for attribute mismatch

Co-Authored-By: Claude <[email protected]>
- Replace window.location.origin with globalThis.location?.origin fallback
- Prevents SSR errors when window is not available
- Provides fallback URL for server-side rendering contexts

Co-Authored-By: Claude <[email protected]>
- Add INVITER_EMAIL_SEND_FAILED to InviteInviterError enum
- Auto-generated from graphql/invite.ts enum changes
- Supports proper error handling for email send failures

Co-Authored-By: Claude <[email protected]>
- Add invitation quota rollback when email sending fails
- Implement INVITER_EMAIL_SEND_FAILED error type for proper user feedback
- Add email template caching to reduce disk I/O overhead
- Add error handling for invalid locale tags in availableLocales query

Co-Authored-By: Claude <[email protected]>
Copy link

@coderabbitai coderabbitai bot left a comment

Choose a reason for hiding this comment

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

Actionable comments posted: 0

Caution

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

⚠️ Outside diff range comments (1)
web-next/src/locales/ja-JP/messages.po (1)

791-794: Fix typos: ユーサー → ユーザー (twice).

User-facing string contains a misspelling.

Apply:

- msgstr "他のユーサーが使用しているユーサー名です。"
+ msgstr "他のユーザーが使用しているユーザー名です。"
♻️ Duplicate comments (3)
web-next/src/routes/(root)/[handle]/settings/invite.tsx (2)

145-145: Avoid double trim; use fallback to null.

Slightly simpler and avoids duplicate work.

-        message: message().trim() === "" ? null : message().trim(),
+        message: message().trim() || null,

54-71: Fix Biome noRedeclare by renaming GraphQL document constants.

Identifiers collide with imported types (invitePageQuery, inviteMutation), blocking CI.

Apply:

-const invitePageQuery = graphql`
+const InvitePageQueryNode = graphql`
   query invitePageQuery($username: String!) {
@@
-const loadInvitePageQuery = query(
+const loadInvitePageQuery = query(
   (handle: string) =>
     loadQuery<invitePageQuery>(
       useRelayEnvironment()(),
-      invitePageQuery,
+      InvitePageQueryNode,
       { username: handle.replace(/^@/, "") },
     ),
   "loadInvitePageQuery",
 );
@@
-const inviteMutation = graphql`
+const InviteMutationNode = graphql`
   mutation inviteMutation(
@@
   }
 `;
@@
   const data = createPreloadedQuery<invitePageQuery>(
-    invitePageQuery,
+    InvitePageQueryNode,
     () => loadInvitePageQuery(params.handle),
   );
@@
-  const [send] = createMutation<inviteMutation>(inviteMutation);
+  const [send] = createMutation<inviteMutation>(InviteMutationNode);

Also applies to: 72-80, 82-114, 120-123, 136-137

graphql/invite.ts (1)

194-196: Gate on verifyUrl errors; remove duplicated email check.

Currently errors.email is checked twice and errors.verifyUrl is ignored, allowing bad templates to proceed.

Apply:

-      if (
-        errors.inviter != null || errors.email != null ||
-        errors.email != null || ctx.account == null || email == null
-      ) {
+      if (
+        errors.inviter != null ||
+        errors.email != null ||
+        errors.verifyUrl != null ||
+        ctx.account == null ||
+        email == null
+      ) {
         return errors;
       }
🧹 Nitpick comments (4)
web-next/src/routes/(root)/[handle]/settings/invite.tsx (1)

259-265: Add email autofill hints.

Improves UX and mobile keyboards.

-                        <TextFieldInput
+                        <TextFieldInput
                           type="email"
                           required
                           id="email"
+                          autocomplete="email"
+                          inputmode="email"
                           placeholder="[email protected]"
graphql/invite.ts (3)

216-223: Normalize locale to Intl.Locale before use and return.

Ensures type-safety for getEmailMessage/getEmailTemplate and the Invitation.locale field.

-      const message = await getEmailMessage({
-        locale: args.locale,
+      const normalizedLocale = typeof args.locale === "string"
+        ? new Intl.Locale(args.locale)
+        : (args.locale as Intl.Locale);
+      const message = await getEmailMessage({
+        locale: normalizedLocale,
         inviter: ctx.account,
         verifyUrlTemplate: args.verifyUrl,
         to: email,
         token,
         message: args.message ?? undefined,
       });
@@
       return {
         inviterId: ctx.account.id,
         email,
-        locale: args.locale,
+        locale: normalizedLocale,
         message: args.message ?? undefined,
       };

Also applies to: 240-245


337-340: Provide a fallback when Temporal.Duration.toLocaleString is unsupported.

Deno/Node runtimes may not support DurationFormat; add a robust fallback.

-  const expiration = EXPIRATION.toLocaleString(locale.baseName, {
-    // @ts-ignore: DurationFormatOptions, not DateTimeFormatOptions
-    style: "long",
-  });
+  const expiration = (() => {
+    const anyDuration = EXPIRATION as unknown as { toLocaleString?: (loc: string, opts?: unknown) => string };
+    if (typeof anyDuration.toLocaleString === "function") {
+      // @ts-ignore DurationFormatOptions where supported
+      return anyDuration.toLocaleString(locale.baseName, { style: "long" });
+    }
+    const hours = Math.round(EXPIRATION.total("hour"));
+    return `${hours} ${hours === 1 ? "hour" : "hours"}`;
+  })();

251-299: Consider extracting locale-template caching to a shared utility.

loadEmailTemplates duplicates concerns with the locales discovery query; a shared module avoids drift.

I can draft a small graphql/locales/cache.ts and update both call-sites. Want a patch?

Also applies to: 300-321

📜 Review details

Configuration used: CodeRabbit UI

Review profile: CHILL

Plan: Pro

💡 Knowledge Base configuration:

  • MCP integration is disabled by default for public repositories
  • Jira integration is disabled by default for public repositories
  • Linear integration is disabled by default for public repositories

You can enable these sources in your CodeRabbit configuration.

📥 Commits

Reviewing files that changed from the base of the PR and between 133ffb6 and aff5ea6.

📒 Files selected for processing (12)
  • graphql/invite.ts (1 hunks)
  • graphql/locales/ko.json (1 hunks)
  • graphql/misc.ts (1 hunks)
  • graphql/schema.graphql (3 hunks)
  • models/i18n.ts (3 hunks)
  • web-next/src/locales/en-US/messages.po (14 hunks)
  • web-next/src/locales/ja-JP/messages.po (14 hunks)
  • web-next/src/locales/ko-KR/messages.po (14 hunks)
  • web-next/src/locales/zh-CN/messages.po (14 hunks)
  • web-next/src/locales/zh-TW/messages.po (15 hunks)
  • web-next/src/routes/(root)/[handle]/settings/invite.tsx (1 hunks)
  • web-next/src/routes/(root)/[handle]/settings/preferences.tsx (3 hunks)
🚧 Files skipped from review as they are similar to previous changes (8)
  • models/i18n.ts
  • web-next/src/locales/zh-CN/messages.po
  • graphql/misc.ts
  • web-next/src/locales/zh-TW/messages.po
  • web-next/src/locales/en-US/messages.po
  • web-next/src/routes/(root)/[handle]/settings/preferences.tsx
  • graphql/locales/ko.json
  • graphql/schema.graphql
🧰 Additional context used
🧬 Code graph analysis (2)
web-next/src/routes/(root)/[handle]/settings/invite.tsx (6)
web-next/src/routes/(root)/[handle]/settings/preferences.tsx (1)
  • route (36-43)
web-next/src/components/Title.tsx (1)
  • Title (7-14)
web-next/src/components/ProfilePageBreadcrumb.tsx (1)
  • ProfilePageBreadcrumb (16-49)
web-next/src/components/SettingsTabs.tsx (1)
  • SettingsTabs (16-65)
web-next/src/components/Trans.tsx (1)
  • Trans (22-42)
web-next/src/components/LocaleSelect.tsx (1)
  • LocaleSelect (22-89)
graphql/invite.ts (6)
models/uuid.ts (1)
  • Uuid (4-4)
models/i18n.ts (2)
  • Locale (30-30)
  • negotiateLocale (173-226)
models/account.ts (1)
  • normalizeEmail (522-536)
models/schema.ts (1)
  • accountTable (41-96)
models/signup.ts (2)
  • createSignupToken (36-64)
  • SignupToken (23-29)
graphql/email.ts (1)
  • EMAIL_FROM (11-11)
🪛 Biome (2.1.2)
web-next/src/routes/(root)/[handle]/settings/invite.tsx

[error] 54-54: Shouldn't redeclare 'invitePageQuery'. Consider to delete it or rename it.

'invitePageQuery' is defined here:

(lint/suspicious/noRedeclare)


[error] 82-82: Shouldn't redeclare 'inviteMutation'. Consider to delete it or rename it.

'inviteMutation' is defined here:

(lint/suspicious/noRedeclare)

⏰ Context from checks skipped due to timeout of 90000ms. You can increase the timeout in your CodeRabbit configuration to a maximum of 15 minutes (900000ms). (2)
  • GitHub Check: image (ubuntu-24.04-arm)
  • GitHub Check: image (ubuntu-latest)
🔇 Additional comments (3)
web-next/src/locales/ko-KR/messages.po (1)

26-30: Korean invitation flow translations look good.

Pluralization, error/success states, field labels, and helper texts read naturally and align with the UI and mutation behaviors.

Also applies to: 73-77, 173-176, 218-221, 266-270, 287-290, 333-337, 391-394, 395-398, 399-404, 497-500, 573-576, 653-656, 657-660, 735-742, 747-750, 849-852, 857-861, 886-889

web-next/src/locales/ja-JP/messages.po (1)

26-30: Japanese invitation flow translations LGTM.

Copy matches product terminology and pluralization is handled appropriately for ja-JP.

Also applies to: 73-77, 173-176, 218-221, 266-270, 287-290, 333-337, 391-394, 395-398, 399-404, 497-500, 573-576, 653-656, 657-660, 735-742, 747-750, 849-852, 857-861, 886-889

graphql/invite.ts (1)

224-239: Good: credit back invitation on email send failure.

Prevents silent quota loss and surfaces a clear validation error to the client.

@dahlia dahlia merged commit eaaacd1 into hackers-pub:main Aug 31, 2025
5 checks passed
@dahlia dahlia deleted the web-next/invite branch August 31, 2025 04:23
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.

web-next: Invitations in the settings
1 participant