From 308f47f0692f414ebf5d40394edd253b3b427f01 Mon Sep 17 00:00:00 2001 From: Hong Minhee Date: Sun, 31 Aug 2025 11:52:17 +0900 Subject: [PATCH 1/9] Add invite system with GraphQL mutations 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 --- graphql/invite.ts | 312 +++++++++++++++++++++++++++++++++++++ graphql/locales/en.json | 5 + graphql/locales/ja.json | 5 + graphql/locales/ko.json | 5 + graphql/locales/zh-CN.json | 5 + graphql/locales/zh-TW.json | 5 + graphql/misc.ts | 24 +++ graphql/mod.ts | 2 + graphql/schema.graphql | 44 ++++++ 9 files changed, 407 insertions(+) create mode 100644 graphql/invite.ts create mode 100644 graphql/misc.ts diff --git a/graphql/invite.ts b/graphql/invite.ts new file mode 100644 index 00000000..8945c3cb --- /dev/null +++ b/graphql/invite.ts @@ -0,0 +1,312 @@ +import { normalizeEmail } from "@hackerspub/models/account"; +import { negotiateLocale } from "@hackerspub/models/i18n"; +import { + type Account as AccountTable, + accountTable, + type Actor, +} from "@hackerspub/models/schema"; +import { createSignupToken, type SignupToken } from "@hackerspub/models/signup"; +import type { Uuid } from "@hackerspub/models/uuid"; +import { getLogger } from "@logtape/logtape"; +import { expandGlob } from "@std/fs"; +import { join } from "@std/path"; +import { createMessage, type Message } from "@upyo/core"; +import { and, eq, gt, sql } from "drizzle-orm"; +import { parseTemplate } from "url-template"; +import { Account } from "./account.ts"; +import { builder } from "./builder.ts"; +import { EMAIL_FROM } from "./email.ts"; + +const logger = getLogger(["hackerspub", "graphql", "invite"]); + +interface Invitation { + inviterId: Uuid; + email: string; + locale: Intl.Locale; + message?: string; +} + +const InvitationRef = builder.objectRef("Invitation"); + +InvitationRef.implement({ + description: "An invitation that has been created.", + fields: (t) => ({ + inviter: t.field({ + type: Account, + async resolve(invitation, _, ctx) { + const account = await ctx.db.query.accountTable.findFirst({ + where: { id: invitation.inviterId }, + with: { actor: true }, + }); + if (account == null) { + throw new Error( + `Account with ID ${invitation.inviterId} not found.`, + ); + } + return account; + }, + }), + email: t.field({ + type: "Email", + resolve(invitation) { + return invitation.email; + }, + }), + locale: t.field({ + type: "Locale", + resolve(invitation) { + return invitation.locale; + }, + }), + message: t.field({ + type: "Markdown", + nullable: true, + resolve(invitation) { + return invitation.message ?? null; + }, + }), + }), +}); + +const InviteInviterError = builder.enumType("InviteInviterError", { + values: ["INVITER_NOT_AUTHENTICATED", "INVITER_NO_INVITATIONS_LEFT"] as const, +}); + +const InviteEmailError = builder.enumType("InviteEmailError", { + values: ["EMAIL_INVALID", "EMAIL_ALREADY_TAKEN"] as const, +}); + +const InviteVerifyUrlError = builder.enumType("InviteVerifyUrlError", { + values: ["VERIFY_URL_NO_TOKEN", "VERIFY_URL_NO_CODE"] as const, +}); + +interface InviteValidationErrors { + inviter?: typeof InviteInviterError.$inferType; + email?: typeof InviteEmailError.$inferType; + verifyUrl?: typeof InviteVerifyUrlError.$inferType; + emailOwnerId?: Uuid; +} + +const InviteValidationErrorsRef = builder.objectRef( + "InviteValidationErrors", +); + +InviteValidationErrorsRef.implement({ + description: "Validation errors that occurred during the invitation process.", + fields: (t) => ({ + inviter: t.field({ + type: InviteInviterError, + nullable: true, + resolve: (errors) => errors.inviter ?? null, + }), + email: t.field({ + type: InviteEmailError, + nullable: true, + resolve: (errors) => errors.email ?? null, + }), + verifyUrl: t.field({ + type: InviteVerifyUrlError, + nullable: true, + resolve: (errors) => errors.verifyUrl ?? null, + }), + emailOwner: t.field({ + type: Account, + nullable: true, + resolve(errors, _, ctx) { + if (errors.emailOwnerId == null) return null; + return ctx.db.query.accountTable.findFirst({ + where: { id: errors.emailOwnerId }, + }); + }, + }), + }), +}); + +const InviteResultRef = builder.unionType("InviteResult", { + types: [InvitationRef, InviteValidationErrorsRef], + resolveType(obj) { + if ("inviterId" in obj) return InvitationRef; + return InviteValidationErrorsRef; + }, +}); + +export const EXPIRATION = Temporal.Duration.from({ hours: 48 }); + +builder.mutationField("invite", (t) => + t.field({ + type: InviteResultRef, + args: { + email: t.arg({ type: "Email", required: true }), + locale: t.arg({ type: "Locale", required: true }), + message: t.arg({ type: "Markdown" }), + verifyUrl: t.arg({ + type: "URITemplate", + required: true, + description: + "The RFC 6570-compliant URI Template for the verification link. Available variables: `{token}` and `{code}`.", + }), + }, + async resolve(_root, args, ctx) { + const errors = {} as InviteValidationErrors; + if (ctx.account == null) errors.inviter = "INVITER_NOT_AUTHENTICATED"; + else if (ctx.account.leftInvitations < 1) { + errors.inviter = "INVITER_NO_INVITATIONS_LEFT"; + } + let email: string | undefined; + try { + email = normalizeEmail(args.email); + } catch { + errors.email = "EMAIL_INVALID"; + } + if (email != null) { + const existingEmail = await ctx.db.query.accountEmailTable.findFirst({ + where: { email }, + }); + if (existingEmail != null) { + errors.email = "EMAIL_ALREADY_TAKEN"; + errors.emailOwnerId = existingEmail.accountId; + } + } + const verifyUrlTemplate = parseTemplate(args.verifyUrl); + const a = verifyUrlTemplate.expand({ + token: "00000000-0000-0000-0000-000000000000", + code: "AAAAAA", + }); + const b = verifyUrlTemplate.expand({ + token: "ffffffff-ffff-ffff-ffff-ffffffffffff", + code: "AAAAAA", + }); + if (a === b) { + errors.verifyUrl = "VERIFY_URL_NO_TOKEN"; + } + const c = verifyUrlTemplate.expand({ + token: "00000000-0000-0000-0000-000000000000", + code: "BBBBBB", + }); + if (a === c) { + errors.verifyUrl = "VERIFY_URL_NO_CODE"; + } + if ( + errors.inviter != null || errors.email != null || + errors.email != null || ctx.account == null || email == null + ) { + return errors; + } + const updated = await ctx.db.update(accountTable).set({ + leftInvitations: sql`${accountTable.leftInvitations} - 1`, + }).where( + and( + eq(accountTable.id, ctx.account.id), + gt(accountTable.leftInvitations, 0), + ), + ).returning(); + if (updated.length < 1) { + return { + inviter: "INVITER_NO_INVITATIONS_LEFT", + } satisfies InviteValidationErrors; + } + const token = await createSignupToken(ctx.kv, email, { + inviterId: ctx.account.id, + expiration: EXPIRATION, + }); + const message = await getEmailMessage({ + locale: args.locale, + inviter: ctx.account, + verifyUrlTemplate: args.verifyUrl, + to: email, + token, + message: args.message ?? undefined, + }); + const receipt = await ctx.email.send(message); + if (!receipt.successful) { + logger.error( + "Failed to send invitation email: {errors}", + { errors: receipt.errorMessages }, + ); + } + return { + inviterId: ctx.account.id, + email, + locale: args.locale, + message: args.message ?? undefined, + }; + }, + })); + +const LOCALES_DIR = join(import.meta.dirname!, "locales"); + +async function getEmailTemplate( + locale: Intl.Locale, + message: boolean, +): Promise<{ subject: string; content: string }> { + const availableLocales: Record = {}; + 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[localeName] = file.path; + } + const selectedLocale = + negotiateLocale(locale, Object.keys(availableLocales)) ?? + new Intl.Locale("en"); + const path = availableLocales[selectedLocale.baseName]; + const json = await Deno.readTextFile(path); + const data = JSON.parse(json); + return { + subject: data.invite.emailSubject, + content: message + ? data.invite.emailContentWithMessage + : data.invite.emailContent, + }; +} + +async function getEmailMessage( + { locale, inviter, to, verifyUrlTemplate, token, message }: { + locale: Intl.Locale; + inviter: AccountTable & { actor: Actor }; + to: string; + verifyUrlTemplate: string; + token: SignupToken; + message?: string; + }, +): Promise { + const verifyUrl = parseTemplate(verifyUrlTemplate).expand({ + token: token.token, + code: token.code, + }); + const expiration = EXPIRATION.toLocaleString(locale.baseName, { + // @ts-ignore: DurationFormatOptions, not DateTimeFormatOptions + style: "long", + }); + const template = await getEmailTemplate(locale, message != null); + function substitute(template: string): string { + return template.replaceAll( + /\{\{(verifyUrl|code|expiration|inviter|inviterName|message)\}\}/g, + (m) => { + return m === "{{verifyUrl}}" + ? verifyUrl + : m === "{{code}}" + ? token.code + : m === "{{expiration}}" + ? expiration + : m === "{{inviter}}" + ? `${inviter.name} (${inviter.actor.handle})` + : m === "{{inviterName}}" + ? inviter.name + : (message ?? ""); + }, + ); + } + return createMessage({ + from: EMAIL_FROM, + to, + subject: substitute(template.subject), + content: { + text: substitute(template.content), + }, + }); +} diff --git a/graphql/locales/en.json b/graphql/locales/en.json index bb7ee11d..24de581a 100644 --- a/graphql/locales/en.json +++ b/graphql/locales/en.json @@ -2,5 +2,10 @@ "login": { "emailSubject": "Sign in to Hackers' Pub", "emailContent": "Welcome back to Hackers' Pub! To sign in, click the following link:\n\n{{verifyUrl}}\n\nor submit this code through the sign-in page:\n\n{{code}}\n\nThis link and code will expire in {{expiration}}.\n\nIf you didn't request this email, you can safely ignore it.\n" + }, + "invite": { + "emailSubject": "{{inviterName}} invites you to Hackers' Pub!", + "emailContent": "{{inviter}} invites you to Hackers' Pub!\n\nHackers' Pub is a place for software engineers to share their knowledge and experience with each other. It's also an ActivityPub-enabled social network, so you can follow your favorite hackers in the fediverse and get their latest posts in your feed.\n\nTo accept the invitation, click the following link:\n\n{{verifyUrl}}\n\nThis link will expire in {{expiration}}.\n", + "emailContentWithMessage": "{{inviter}} invites you to Hackers' Pub! Here's a message from {{inviterName}}:\n\n{{message}}\n\nTo accept the invitation, click the following link:\n\n{{verifyUrl}}\n\nThis link will expire in {{expiration}}.\n" } } diff --git a/graphql/locales/ja.json b/graphql/locales/ja.json index 12e858a3..2c233f41 100644 --- a/graphql/locales/ja.json +++ b/graphql/locales/ja.json @@ -2,5 +2,10 @@ "login": { "emailSubject": "Hackers' Pubへのログイン", "emailContent": "Hackers' Pubへお帰りなさい!ログインするには以下のリンクをクリックしてください:\n\n{{verifyUrl}}\n\n他には下のコードをページに入力して下さい:\n\n{{code}}\n\nこのリンクとコードは{{expiration}}後に有効期限が切れます。\n\nこのメールに心当たりがない場合は、無視していただいて構いません。\n" + }, + "invite": { + "emailSubject": "{{inviterName}}さんがHackers' Pubに招待しています!", + "emailContent": "{{inviter}}さんがHackers' Pubに招待しています!\n\nHackers' Pubは、ソフトウェアエンジニアが知識と経験を共有する場所です。また、ActivityPubに対応したソーシャルネットワークでもあり、フェディバース(fediverse)で気に入ったハッカーをフォローして、最新のコンテンツをフィードで受け取ることができます。\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" } } diff --git a/graphql/locales/ko.json b/graphql/locales/ko.json index 4b6f84fb..d0deef93 100644 --- a/graphql/locales/ko.json +++ b/graphql/locales/ko.json @@ -2,5 +2,10 @@ "login": { "emailSubject": "Hackers' Pub 로그인", "emailContent": "Hackers' Pub에 다시 오신 것을 환영합니다! 로그인하려면 다음 링크를 클릭하세요:\n\n{{verifyUrl}}\n\n혹은 이 코드를 입력해주세요:\n\n{{code}}\n\n이 링크와 코드는 {{expiration}} 후에 만료됩니다.\n\n이 이메일을 요청하지 않았다면 무시해도 됩니다.\n" + }, + "invite": { + "emailSubject": "{{inviterName}} 님이 Hackers' Pub에 초대합니다!", + "emailContent": "{{inviter}} 님이 Hackers' Pub에 초대합니다!\n\nHackers' Pub은 소프트웨어 프로그래머들이 지식과 경험을 서로 나누는 곳입니다. 또한 ActivityPub을 지원하는 소셜 네트워크이기도 하며, 연합우주(fediverse)에서 즐겨찾는 소프트웨어 프로그래머들을 팔로하고 최신 콘텐츠를 받아 볼 수 있습니다.\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" } } diff --git a/graphql/locales/zh-CN.json b/graphql/locales/zh-CN.json index 5e2ea483..46c406fd 100644 --- a/graphql/locales/zh-CN.json +++ b/graphql/locales/zh-CN.json @@ -2,5 +2,10 @@ "login": { "emailSubject": "登录 Hackers' Pub", "emailContent": "欢迎回到 Hackers' Pub!要登录,点击以下链接:\n\n{{verifyUrl}}\n\n或通过登录页面提交此密码:\n\n{{code}}\n\n该链接和密码将于 {{expiration}} 失效。\n\n如果你并没申请这个电子邮件,可以安全地忽略它。\n" + }, + "invite": { + "emailSubject": "{{inviterName}} 邀请你加入 Hackers' Pub!", + "emailContent": "{{inviter}} 邀请你加入 Hackers' Pub!\n\nHackers' Pub 是个为软件工程师共同分享知识和经验的地方。它也是启用了 ActivityPub 的社交网络,这样你可以在联邦宇宙里关注你喜爱的黑客,并获取最新的帖子。\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" } } diff --git a/graphql/locales/zh-TW.json b/graphql/locales/zh-TW.json index 14576f4c..8073679f 100644 --- a/graphql/locales/zh-TW.json +++ b/graphql/locales/zh-TW.json @@ -2,5 +2,10 @@ "login": { "emailSubject": "登入 Hackers' Pub", "emailContent": "歡迎回到 Hackers' Pub!要登入,點擊以下連結:\n\n{{verifyUrl}}\n\n或透過登入頁面提交此密碼:\n\n{{code}}\n\n該連結和密碼將於 {{expiration}} 失效。\n\n如果你並沒申請這個電子郵件,可以安全地忽略它。\n" + }, + "invite": { + "emailSubject": "{{inviterName}} 邀請你加入 Hackers' Pub!", + "emailContent": "{{inviter}} 邀請你加入 Hackers' Pub!\n\nHackers' Pub 是個為軟體工程師共同分享知識和經驗的地方。它也是啟用了 ActivityPub 的社交網路,這樣你可以在聯邦宇宙裡關注你喜愛的駭客,並獲取最新的貼文。\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" } } diff --git a/graphql/misc.ts b/graphql/misc.ts new file mode 100644 index 00000000..5d748c6e --- /dev/null +++ b/graphql/misc.ts @@ -0,0 +1,24 @@ +import { expandGlob } from "@std/fs"; +import { join } from "@std/path"; +import { builder } from "./builder.ts"; + +const LOCALES_DIR = join(import.meta.dirname!, "locales"); + +builder.queryField("availableLocales", (t) => + t.field({ + type: ["Locale"], + async resolve(_root, _args, _ctx) { + 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)); + } + return availableLocales; + }, + })); diff --git a/graphql/mod.ts b/graphql/mod.ts index 9d815348..809597d6 100644 --- a/graphql/mod.ts +++ b/graphql/mod.ts @@ -4,7 +4,9 @@ import "./account.ts"; import "./actor.ts"; import { builder } from "./builder.ts"; import "./doc.ts"; +import "./invite.ts"; import "./login.ts"; +import "./misc.ts"; import "./notification.ts"; import "./passkey.ts"; import "./poll.ts"; diff --git a/graphql/schema.graphql b/graphql/schema.graphql index c8c72105..bdc02920 100644 --- a/graphql/schema.graphql +++ b/graphql/schema.graphql @@ -384,6 +384,39 @@ type InvalidInputError { inputPath: String! } +"""An invitation that has been created.""" +type Invitation { + email: Email! + inviter: Account! + locale: Locale! + message: Markdown +} + +enum InviteEmailError { + EMAIL_ALREADY_TAKEN + EMAIL_INVALID +} + +enum InviteInviterError { + INVITER_NOT_AUTHENTICATED + INVITER_NO_INVITATIONS_LEFT +} + +union InviteResult = Invitation | InviteValidationErrors + +"""Validation errors that occurred during the invitation process.""" +type InviteValidationErrors { + email: InviteEmailError + emailOwner: Account + inviter: InviteInviterError + verifyUrl: InviteVerifyUrlError +} + +enum InviteVerifyUrlError { + VERIFY_URL_NO_CODE + VERIFY_URL_NO_TOKEN +} + """ The `JSON` scalar type represents JSON values as specified by [ECMA-404](http://www.ecma-international.org/publications/files/ECMA-ST/ECMA-404.pdf). """ @@ -441,6 +474,16 @@ type Mutation { sessionId: UUID! ): JSON! getPasskeyRegistrationOptions(accountId: ID!): JSON! + invite( + email: Email! + locale: Locale! + message: Markdown + + """ + The RFC 6570-compliant URI Template for the verification link. Available variables: `{token}` and `{code}`. + """ + verifyUrl: URITemplate! + ): InviteResult! loginByEmail( """The email of the account to sign in.""" email: String! @@ -742,6 +785,7 @@ type Query { handle: String! ): Actor actorByUuid(uuid: UUID!): Actor + availableLocales: [Locale!]! codeOfConduct( """The locale for the Code of Conduct.""" locale: Locale! From d1fc99ddfbd6a52e67225845ffd9c22f9c192568 Mon Sep 17 00:00:00 2001 From: Hong Minhee Date: Sun, 31 Aug 2025 11:53:25 +0900 Subject: [PATCH 2/9] web-next: `LocaleSelect` component 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. --- web-next/src/components/LocaleSelect.tsx | 118 +++++++++++++++++++++++ 1 file changed, 118 insertions(+) create mode 100644 web-next/src/components/LocaleSelect.tsx diff --git a/web-next/src/components/LocaleSelect.tsx b/web-next/src/components/LocaleSelect.tsx new file mode 100644 index 00000000..681237e3 --- /dev/null +++ b/web-next/src/components/LocaleSelect.tsx @@ -0,0 +1,118 @@ +import { negotiateLocale } from "@hackerspub/models/i18n"; +import { graphql } from "relay-runtime"; +import { Show } from "solid-js"; +import { createFragment } from "solid-relay"; +import { + Select, + SelectContent, + SelectItem, + SelectTrigger, + SelectValue, +} from "~/components/ui/select.tsx"; +import { useLingui } from "~/lib/i18n/macro.d.ts"; +import { LocaleSelect_availableLocales$key } from "./__generated__/LocaleSelect_availableLocales.graphql.ts"; + +export interface LocaleSelectProps { + readonly $availableLocales: LocaleSelect_availableLocales$key; + readonly value: string; + onChange(value: string): void; + readonly class?: string; +} + +export function LocaleSelect(props: LocaleSelectProps) { + const { i18n } = useLingui(); + const availableLocales = createFragment( + graphql` + fragment LocaleSelect_availableLocales on Query { + availableLocales + } + `, + () => props.$availableLocales, + ); + return ( + + ); +} + +interface LocaleInfo { + readonly code: string; + readonly name: string; + readonly nativeName: string; +} + +function toLocaleInfo(locale: string, currentLocale: string): LocaleInfo { + return mapLocaleInfo([locale], currentLocale)[0]; +} + +function mapLocaleInfo( + locales: readonly string[], + currentLocale: string, +): LocaleInfo[] { + const displayNames = new Intl.DisplayNames(currentLocale, { + type: "language", + }); + const list = locales.map((l) => { + const nativeNames = new Intl.DisplayNames(l, { type: "language" }); + return ({ + code: l, + name: displayNames.of(l) ?? l, + nativeName: nativeNames.of(l) ?? l, + }); + }); + list.sort((a, b) => a.name.localeCompare(b.name)); + return list; +} From 5eec31c2eb1442091e219427f4efd66049143c1a Mon Sep 17 00:00:00 2001 From: Hong Minhee Date: Sun, 31 Aug 2025 11:56:47 +0900 Subject: [PATCH 3/9] web-next: Add invite settings page 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 --- deno.lock | 1 + models/i18n.ts | 10 +- web-next/src/components/SettingsTabs.tsx | 11 +- .../(root)/[handle]/settings/invite.tsx | 350 ++++++++++++++++++ .../(root)/[handle]/settings/preferences.tsx | 4 +- 5 files changed, 367 insertions(+), 9 deletions(-) create mode 100644 web-next/src/routes/(root)/[handle]/settings/invite.tsx diff --git a/deno.lock b/deno.lock index 289857ee..128f58a5 100644 --- a/deno.lock +++ b/deno.lock @@ -212,6 +212,7 @@ "npm:unist-util-visit@5": "5.0.0", "npm:uri-template-router@^0.0.17": "0.0.17", "npm:url-template@^3.1.1": "3.1.1", + "npm:vinxi@0.5.8": "0.5.8_@babel+core@7.28.3_ioredis@5.7.0_drizzle-orm@1.0.0-beta.1-5e64efc__@opentelemetry+api@1.9.0__postgres@3.4.7_@opentelemetry+api@1.9.0_postgres@3.4.7_@types+node@24.2.0", "npm:vinxi@~0.5.8": "0.5.8_@babel+core@7.28.3_ioredis@5.7.0_drizzle-orm@1.0.0-beta.1-5e64efc__@opentelemetry+api@1.9.0__postgres@3.4.7_@opentelemetry+api@1.9.0_postgres@3.4.7_@types+node@24.2.0", "npm:vite-plugin-cjs-interop@^2.2.0": "2.2.0_vite@6.3.5__picomatch@4.0.3_@types+node@24.2.0", "npm:vite-plugin-relay-lite@0.11": "0.11.0_graphql@16.11.0-canary.pr.4364.2b4ffe237616247a733274dfdcb404c3d55d9f02_vite@6.3.5__picomatch@4.0.3_@types+node@24.2.0", diff --git a/models/i18n.ts b/models/i18n.ts index 28f9395c..6b17da00 100644 --- a/models/i18n.ts +++ b/models/i18n.ts @@ -155,7 +155,7 @@ export function findNearestLocale( */ export function negotiateLocale( wantedLocale: Intl.Locale | string, - availableLocales: (Intl.Locale | string)[], + availableLocales: readonly (Intl.Locale | string)[], ): Intl.Locale | undefined; /** @@ -166,13 +166,13 @@ export function negotiateLocale( * @returns The best matching locale, or undefined if no match is found. */ export function negotiateLocale( - wantedLocales: (Intl.Locale | string)[], - availableLocales: (Intl.Locale | string)[], + wantedLocales: readonly (Intl.Locale | string)[], + availableLocales: readonly (Intl.Locale | string)[], ): Intl.Locale | undefined; export function negotiateLocale( - wantedLocales: Intl.Locale | string | (Intl.Locale | string)[], - availableLocales: (Intl.Locale | string)[], + wantedLocales: Intl.Locale | string | readonly (Intl.Locale | string)[], + availableLocales: readonly (Intl.Locale | string)[], ): Intl.Locale | undefined { if (availableLocales.length === 0) { return undefined; diff --git a/web-next/src/components/SettingsTabs.tsx b/web-next/src/components/SettingsTabs.tsx index cdde5537..c7d92c18 100644 --- a/web-next/src/components/SettingsTabs.tsx +++ b/web-next/src/components/SettingsTabs.tsx @@ -6,7 +6,7 @@ import { Tabs, TabsList, TabsTrigger } from "~/components/ui/tabs.tsx"; import { useLingui } from "~/lib/i18n/macro.d.ts"; import type { SettingsTabs_account$key } from "./__generated__/SettingsTabs_account.graphql.ts"; -export type SettingsTab = "profile" | "preferences" | "passkeys"; +export type SettingsTab = "profile" | "preferences" | "invite" | "passkeys"; export interface SettingsTabsProps { selected: SettingsTab; @@ -28,7 +28,7 @@ export function SettingsTabs(props: SettingsTabsProps) { {(account) => ( - + {t`Preferences`} + + {t`Invite`} + + loadQuery( + useRelayEnvironment()(), + invitePageQuery, + { username: handle.replace(/^@/, "") }, + ), + "loadInvitePageQuery", +); + +const inviteMutation = graphql` + mutation inviteMutation( + $email: Email!, + $locale: Locale!, + $message: Markdown, + $verifyUrl: URITemplate! + ) { + invite( + email: $email, + locale: $locale, + message: $message, + verifyUrl: $verifyUrl, + ) { + __typename + ... on Invitation { + inviter { + id + invitationsLeft + } + } + ... on InviteValidationErrors { + account: inviter + email + verifyUrl + emailOwner { + name + handle + username + } + } + } + } +`; + +export default function InvitePage() { + const params = useParams(); + const location = useLocation(); + const { t, i18n } = useLingui(); + const data = createPreloadedQuery( + invitePageQuery, + () => loadInvitePageQuery(params.handle), + ); + const [inviterError, setInviterError] = createSignal< + InviteInviterError | undefined + >(); + const [email, setEmail] = createSignal(""); + const [emailError, setEmailError] = createSignal< + InviteEmailError | undefined + >(); + const [emailOwner, setEmailOwner] = createSignal< + { name: string; handle: string; username: string } | undefined + >(); + const [invitationLanguage, setInvitationLanguage] = createSignal(i18n.locale); + const [message, setMessage] = createSignal(""); + const [send] = createMutation(inviteMutation); + const [sending, setSending] = createSignal(false); + function onSubmit(event: SubmitEvent) { + event.preventDefault(); + setSending(true); + send({ + variables: { + email: email(), + locale: invitationLanguage(), + message: message().trim() === "" ? null : message().trim(), + verifyUrl: `${window.location.origin}/sign/up/{token}?code={code}`, + }, + onCompleted({ invite }) { + setSending(false); + if (invite.__typename === "InviteValidationErrors") { + setInviterError(invite.account ?? undefined); + setEmailError(invite.email ?? undefined); + setEmailOwner(invite.emailOwner ?? undefined); + showToast({ + variant: "error", + title: t`Failed to send invitation`, + description: t`Please correct the errors and try again.`, + }); + } else { + setInviterError(undefined); + setEmailError(undefined); + setEmailOwner(undefined); + setEmail(""); + setMessage(""); + showToast({ + title: t`Invitation sent`, + description: t`The invitation has been sent successfully.`, + }); + } + }, + onError(error) { + console.error(error); + setSending(false); + showToast({ + variant: "error", + title: t`Failed to send invitation`, + description: + t`An unexpected error occurred. Please try again later.` + + (import.meta.env.DEV ? `\n\n${error.message}` : ""), + }); + }, + }); + } + + return ( + + {(data) => ( + <> + + } + > + {(viewer) => ( + + {(account) => ( + + + + )} + + )} + + + {(account) => ( + <> + {t`Invite`} + + + + + {t`Settings`} + + + + + + {t`Invite`} + + + +
+
+ +
+

+ 0} + fallback={t`You have no invitations left. Please wait until you receive more.`} + > + {i18n._(msg`${ + plural(account().invitationsLeft, { + one: + "Invite your friends to Hackers' Pub. You can invite up to # person.", + other: + "Invite your friends to Hackers' Pub. You can invite up to # people.", + }) + }`)} + +

+ + + {t`Email address`} + + setEmail(e.currentTarget.value)} + /> + + + + {t`The email address is not only used for receiving the invitation, but also for signing in to the account.`} + + + + + {t`The email address is invalid.`} + + + + + ( + + {emailOwner()?.name}{" "} + + ({emailOwner()?.handle}) + + + ), + }} + /> + + + + +
+ + +

+ {t`Choose the language your friend prefers. This language will only be used for the invitation.`} +

+
+ + + {t`Extra message`} + + setMessage(e.currentTarget.value)} + placeholder={t`You can leave this field empty.`} + /> + + {t`Your friend will see this message in the invitation email.`} + + + + +

+ {t`You have no invitations left. Please wait until you receive more.`} +

+
+
+
+
+ + )} +
+ + )} +
+ ); +} diff --git a/web-next/src/routes/(root)/[handle]/settings/preferences.tsx b/web-next/src/routes/(root)/[handle]/settings/preferences.tsx index e3aa236f..03700ac4 100644 --- a/web-next/src/routes/(root)/[handle]/settings/preferences.tsx +++ b/web-next/src/routes/(root)/[handle]/settings/preferences.tsx @@ -211,7 +211,7 @@ export default function PreferencesPage() {
-
+
-
+
Date: Sun, 31 Aug 2025 12:50:19 +0900 Subject: [PATCH 4/9] web-next: Complete invitation system translations 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 --- web-next/src/locales/en-US/messages.po | 86 +++++++++++++++++++++++++- web-next/src/locales/ja-JP/messages.po | 86 +++++++++++++++++++++++++- web-next/src/locales/ko-KR/messages.po | 86 +++++++++++++++++++++++++- web-next/src/locales/zh-CN/messages.po | 86 +++++++++++++++++++++++++- web-next/src/locales/zh-TW/messages.po | 86 +++++++++++++++++++++++++- 5 files changed, 425 insertions(+), 5 deletions(-) diff --git a/web-next/src/locales/en-US/messages.po b/web-next/src/locales/en-US/messages.po index 118d4e03..9e831b2d 100644 --- a/web-next/src/locales/en-US/messages.po +++ b/web-next/src/locales/en-US/messages.po @@ -23,6 +23,11 @@ msgstr "{0, plural, one {# follower} other {# followers}}" msgid "{0, plural, one {# following} other {# following}}" msgstr "{0, plural, one {# following} other {# following}}" +#. placeholder {0}: account().invitationsLeft +#: src/routes/(root)/[handle]/settings/invite.tsx:237 +msgid "{0, plural, one {Invite your friends to Hackers' Pub. You can invite up to # person.} other {Invite your friends to Hackers' Pub. You can invite up to # people.}}" +msgstr "{0, plural, one {Invite your friends to Hackers' Pub. You can invite up to # person.} other {Invite your friends to Hackers' Pub. You can invite up to # people.}}" + #. placeholder {0}: "ACTOR" #. placeholder {1}: "COUNT" #: src/components/notification/FollowNotificationCard.tsx:29 @@ -65,6 +70,11 @@ msgstr "{0} and {1} others shared your post" msgid "{0} followed you" msgstr "{0} followed you" +#. placeholder {0}: "USER" +#: src/routes/(root)/[handle]/settings/invite.tsx:281 +msgid "{0} is already a member of Hackers' Pub." +msgstr "{0} is already a member of Hackers' Pub." + #. placeholder {0}: "ACTOR" #: src/components/notification/MentionNotificationCard.tsx:35 msgid "{0} mentioned you" @@ -160,6 +170,10 @@ msgstr "An error occurred while saving your preferences. Please try again, or co msgid "An error occurred while saving your settings. Please try again, or contact support if the problem persists." msgstr "An error occurred while saving your settings. Please try again, or contact support if the problem persists." +#: src/routes/(root)/[handle]/settings/invite.tsx:178 +msgid "An unexpected error occurred. Please try again later." +msgstr "An unexpected error occurred. Please try again later." + #. placeholder {0}: passkeyToRevoke()?.name #: src/routes/(root)/[handle]/settings/passkeys.tsx:539 msgid "Are you sure you want to revoke passkey {0}? You won't be able to use it to sign in to your account anymore." @@ -201,6 +215,10 @@ msgstr "Bio is too long. Maximum length is 512 characters." msgid "Cancel" msgstr "Cancel" +#: src/routes/(root)/[handle]/settings/invite.tsx:305 +msgid "Choose the language your friend prefers. This language will only be used for the invitation." +msgstr "Choose the language your friend prefers. This language will only be used for the invitation." + #: src/components/AppSidebar.tsx:321 #: src/routes/(root)/coc.tsx:53 #: src/routes/(root)/coc.tsx:58 @@ -245,6 +263,7 @@ msgstr "Do you need an account? Hackers' Pub is invite-only—please ask a frien msgid "Drag to select the area you want to keep, then click “Crop” to update your avatar." msgstr "Drag to select the area you want to keep, then click “Crop” to update your avatar." +#: src/routes/(root)/[handle]/settings/invite.tsx:254 #: src/routes/(root)/sign/up/[token].tsx:337 msgid "Email address" msgstr "Email address" @@ -265,6 +284,10 @@ msgstr "Error" msgid "ex) My key" msgstr "ex) My key" +#: src/routes/(root)/[handle]/settings/invite.tsx:310 +msgid "Extra message" +msgstr "Extra message" + #: src/components/ActorArticleList.tsx:75 msgid "Failed to load more articles; click to retry" msgstr "Failed to load more articles; click to retry" @@ -307,6 +330,11 @@ msgstr "Failed to save preferences" msgid "Failed to save settings" msgstr "Failed to save settings" +#: src/routes/(root)/[handle]/settings/invite.tsx:156 +#: src/routes/(root)/[handle]/settings/invite.tsx:176 +msgid "Failed to send invitation" +msgstr "Failed to send invitation" + #. placeholder {0}: error.message #: src/components/AppSidebar.tsx:81 msgid "Failed to sign out: {0}" @@ -360,6 +388,20 @@ msgstr "I have read and agree to the Code of conduct." msgid "If enabled, the AI will generate a summary of the article for you. Otherwise, the first few lines of the article will be used as the summary." msgstr "If enabled, the AI will generate a summary of the article for you. Otherwise, the first few lines of the article will be used as the summary." +#: src/routes/(root)/[handle]/settings/invite.tsx:298 +msgid "Invitation language" +msgstr "Invitation language" + +#: src/routes/(root)/[handle]/settings/invite.tsx:166 +msgid "Invitation sent" +msgstr "Invitation sent" + +#: src/components/SettingsTabs.tsx:51 +#: src/routes/(root)/[handle]/settings/invite.tsx:210 +#: src/routes/(root)/[handle]/settings/invite.tsx:221 +msgid "Invite" +msgstr "Invite" + #: src/routes/(root)/[handle]/settings/index.tsx:460 msgid "John Doe" msgstr "John Doe" @@ -452,6 +494,10 @@ msgstr "Never used" msgid "No followers found" msgstr "No followers found" +#: src/routes/(root)/[handle]/settings/invite.tsx:328 +msgid "No invitations left" +msgstr "No invitations left" + #: src/components/ActorArticleList.tsx:85 msgid "No notes articles" msgstr "No notes articles" @@ -516,7 +562,7 @@ msgstr "Passkey revoked" msgid "passkeys" msgstr "passkeys" -#: src/components/SettingsTabs.tsx:51 +#: src/components/SettingsTabs.tsx:58 msgid "Passkeys" msgstr "Passkeys" @@ -524,6 +570,10 @@ msgstr "Passkeys" msgid "Please choose an image file smaller than 5 MiB." msgstr "Please choose an image file smaller than 5 MiB." +#: src/routes/(root)/[handle]/settings/invite.tsx:157 +msgid "Please correct the errors and try again." +msgstr "Please correct the errors and try again." + #: src/components/ProfileTabs.tsx:38 msgid "Posts" msgstr "Posts" @@ -600,8 +650,17 @@ msgstr "Save" msgid "Saving…" msgstr "Saving…" +#: src/routes/(root)/[handle]/settings/invite.tsx:331 +msgid "Send" +msgstr "Send" + +#: src/routes/(root)/[handle]/settings/invite.tsx:330 +msgid "Sending…" +msgstr "Sending…" + #: src/components/AppSidebar.tsx:288 #: src/routes/(root)/[handle]/settings/index.tsx:152 +#: src/routes/(root)/[handle]/settings/invite.tsx:215 #: src/routes/(root)/[handle]/settings/passkeys.tsx:379 #: src/routes/(root)/[handle]/settings/preferences.tsx:181 msgid "Settings" @@ -673,10 +732,22 @@ msgstr "The default privacy setting for your notes." msgid "The default privacy setting for your shares." msgstr "The default privacy setting for your shares." +#: src/routes/(root)/[handle]/settings/invite.tsx:272 +msgid "The email address is invalid." +msgstr "The email address is invalid." + +#: src/routes/(root)/[handle]/settings/invite.tsx:267 +msgid "The email address is not only used for receiving the invitation, but also for signing in to the account." +msgstr "The email address is not only used for receiving the invitation, but also for signing in to the account." + #: src/routes/(root)/[handle]/settings/passkeys.tsx:429 msgid "The following passkeys are registered to your account. You can use them to sign in to your account." msgstr "The following passkeys are registered to your account. You can use them to sign in to your account." +#: src/routes/(root)/[handle]/settings/invite.tsx:167 +msgid "The invitation has been sent successfully." +msgstr "The invitation has been sent successfully." + #: src/routes/(root)/[handle]/settings/passkeys.tsx:326 msgid "The passkey has been successfully revoked." msgstr "The passkey has been successfully revoked." @@ -775,10 +846,19 @@ msgstr "You can change it only once, and the old username will become available msgid "You can leave this empty to remove the link." msgstr "You can leave this empty to remove the link." +#: src/routes/(root)/[handle]/settings/invite.tsx:316 +msgid "You can leave this field empty." +msgstr "You can leave this field empty." + #: src/routes/(root)/[handle]/settings/passkeys.tsx:441 msgid "You don't have any passkeys registered yet." msgstr "You don't have any passkeys registered yet." +#: src/routes/(root)/[handle]/settings/invite.tsx:235 +#: src/routes/(root)/[handle]/settings/invite.tsx:337 +msgid "You have no invitations left. Please wait until you receive more." +msgstr "You have no invitations left. Please wait until you receive more." + #: src/routes/(root)/sign/up/[token].tsx:433 msgid "You were invited by" msgstr "You were invited by" @@ -803,6 +883,10 @@ msgstr "Your bio will be displayed on your profile. You can use Markdown to form msgid "Your email address will be used to sign in to your account." msgstr "Your email address will be used to sign in to your account." +#: src/routes/(root)/[handle]/settings/invite.tsx:319 +msgid "Your friend will see this message in the invitation email." +msgstr "Your friend will see this message in the invitation email." + #: src/routes/(root)/[handle]/settings/index.tsx:464 #: src/routes/(root)/sign/up/[token].tsx:400 msgid "Your name will be displayed on your profile and in your posts." diff --git a/web-next/src/locales/ja-JP/messages.po b/web-next/src/locales/ja-JP/messages.po index 9bcb9c35..b0e3d1fe 100644 --- a/web-next/src/locales/ja-JP/messages.po +++ b/web-next/src/locales/ja-JP/messages.po @@ -23,6 +23,11 @@ msgstr "{0, plural, other {#フォロワー}}" msgid "{0, plural, one {# following} other {# following}}" msgstr "{0, plural, other {#フォロー}}" +#. placeholder {0}: account().invitationsLeft +#: src/routes/(root)/[handle]/settings/invite.tsx:237 +msgid "{0, plural, one {Invite your friends to Hackers' Pub. You can invite up to # person.} other {Invite your friends to Hackers' Pub. You can invite up to # people.}}" +msgstr "{0, plural, one {友達をHackers' Pubに招待しましょう。最大#人まで招待できます。} other {友達をHackers' Pubに招待しましょう。最大#人まで招待できます。}}" + #. placeholder {0}: "ACTOR" #. placeholder {1}: "COUNT" #: src/components/notification/FollowNotificationCard.tsx:29 @@ -65,6 +70,11 @@ msgstr "{0}さん外{1}人があなたのコンテンツを共有しました" msgid "{0} followed you" msgstr "{0}さんがあなたをフォローしました" +#. placeholder {0}: "USER" +#: src/routes/(root)/[handle]/settings/invite.tsx:281 +msgid "{0} is already a member of Hackers' Pub." +msgstr "{0}さんは既にHackers' Pubのメンバーです。" + #. placeholder {0}: "ACTOR" #: src/components/notification/MentionNotificationCard.tsx:35 msgid "{0} mentioned you" @@ -160,6 +170,10 @@ msgstr "環境設定の保存中にエラーが発生しました。再度お試 msgid "An error occurred while saving your settings. Please try again, or contact support if the problem persists." msgstr "設定の保存中にエラーが発生しました。再度お試しいただくか、問題が解決しない場合はサポートにお問い合わせください。" +#: src/routes/(root)/[handle]/settings/invite.tsx:178 +msgid "An unexpected error occurred. Please try again later." +msgstr "予期しないエラーが発生しました。後でもう一度お試しください。" + #. placeholder {0}: passkeyToRevoke()?.name #: src/routes/(root)/[handle]/settings/passkeys.tsx:539 msgid "Are you sure you want to revoke passkey {0}? You won't be able to use it to sign in to your account anymore." @@ -201,6 +215,10 @@ msgstr "自己紹介が長すぎます。最大512文字です。" msgid "Cancel" msgstr "キャンセル" +#: src/routes/(root)/[handle]/settings/invite.tsx:305 +msgid "Choose the language your friend prefers. This language will only be used for the invitation." +msgstr "招待する友達の使用言語を選択してください。この言語は招待状にのみ使用されます。" + #: src/components/AppSidebar.tsx:321 #: src/routes/(root)/coc.tsx:53 #: src/routes/(root)/coc.tsx:58 @@ -245,6 +263,7 @@ msgstr "アカウントが必要ですか?Hackers' Pubは招待制ですので msgid "Drag to select the area you want to keep, then click “Crop” to update your avatar." msgstr "保持したい領域をドラッグして選択し、「切り抜き」をクリックしてアイコンを更新してください。" +#: src/routes/(root)/[handle]/settings/invite.tsx:254 #: src/routes/(root)/sign/up/[token].tsx:337 msgid "Email address" msgstr "メールアドレス" @@ -265,6 +284,10 @@ msgstr "エラー" msgid "ex) My key" msgstr "例) 私のキー" +#: src/routes/(root)/[handle]/settings/invite.tsx:310 +msgid "Extra message" +msgstr "追加メッセージ" + #: src/components/ActorArticleList.tsx:75 msgid "Failed to load more articles; click to retry" msgstr "記事の読み込みに失敗しました。クリックして再試行してください" @@ -307,6 +330,11 @@ msgstr "環境設定の保存に失敗しました" msgid "Failed to save settings" msgstr "設定の保存に失敗しました" +#: src/routes/(root)/[handle]/settings/invite.tsx:156 +#: src/routes/(root)/[handle]/settings/invite.tsx:176 +msgid "Failed to send invitation" +msgstr "招待状の送信に失敗しました" + #. placeholder {0}: error.message #: src/components/AppSidebar.tsx:81 msgid "Failed to sign out: {0}" @@ -360,6 +388,20 @@ msgstr "行動規範に同意します。" msgid "If enabled, the AI will generate a summary of the article for you. Otherwise, the first few lines of the article will be used as the summary." msgstr "有効にすると、AIが記事の要約を生成します。無効の場合は、記事の最初の数行が要約として使用されます。" +#: src/routes/(root)/[handle]/settings/invite.tsx:298 +msgid "Invitation language" +msgstr "招待状の言語" + +#: src/routes/(root)/[handle]/settings/invite.tsx:166 +msgid "Invitation sent" +msgstr "招待状を送信しました" + +#: src/components/SettingsTabs.tsx:51 +#: src/routes/(root)/[handle]/settings/invite.tsx:210 +#: src/routes/(root)/[handle]/settings/invite.tsx:221 +msgid "Invite" +msgstr "招待" + #: src/routes/(root)/[handle]/settings/index.tsx:460 msgid "John Doe" msgstr "田中太郎" @@ -452,6 +494,10 @@ msgstr "使用履歴なし" msgid "No followers found" msgstr "フォロワーはいません" +#: src/routes/(root)/[handle]/settings/invite.tsx:328 +msgid "No invitations left" +msgstr "招待状が残っていません" + #: src/components/ActorArticleList.tsx:85 msgid "No notes articles" msgstr "記事はありません" @@ -516,7 +562,7 @@ msgstr "パスキーを取り消しました" msgid "passkeys" msgstr "パスキー" -#: src/components/SettingsTabs.tsx:51 +#: src/components/SettingsTabs.tsx:58 msgid "Passkeys" msgstr "パスキー" @@ -524,6 +570,10 @@ msgstr "パスキー" msgid "Please choose an image file smaller than 5 MiB." msgstr "5 MiB未満の画像ファイルを選択してください。" +#: src/routes/(root)/[handle]/settings/invite.tsx:157 +msgid "Please correct the errors and try again." +msgstr "エラーを修正して、もう一度お試しください。" + #: src/components/ProfileTabs.tsx:38 msgid "Posts" msgstr "コンテンツ" @@ -600,8 +650,17 @@ msgstr "保存" msgid "Saving…" msgstr "保存中…" +#: src/routes/(root)/[handle]/settings/invite.tsx:331 +msgid "Send" +msgstr "送信" + +#: src/routes/(root)/[handle]/settings/invite.tsx:330 +msgid "Sending…" +msgstr "送信中…" + #: src/components/AppSidebar.tsx:288 #: src/routes/(root)/[handle]/settings/index.tsx:152 +#: src/routes/(root)/[handle]/settings/invite.tsx:215 #: src/routes/(root)/[handle]/settings/passkeys.tsx:379 #: src/routes/(root)/[handle]/settings/preferences.tsx:181 msgid "Settings" @@ -673,10 +732,22 @@ msgstr "投稿のデフォルト公開設定です。" msgid "The default privacy setting for your shares." msgstr "共有のデフォルト公開設定です。" +#: src/routes/(root)/[handle]/settings/invite.tsx:272 +msgid "The email address is invalid." +msgstr "メールアドレスが無効です。" + +#: src/routes/(root)/[handle]/settings/invite.tsx:267 +msgid "The email address is not only used for receiving the invitation, but also for signing in to the account." +msgstr "メールアドレスは招待状を受け取るだけでなく、アカウントへのログインにも使用されます。" + #: src/routes/(root)/[handle]/settings/passkeys.tsx:429 msgid "The following passkeys are registered to your account. You can use them to sign in to your account." msgstr "以下のパスキーがあなたのアカウントに登録されています。これらを使用してアカウントにログインできます。" +#: src/routes/(root)/[handle]/settings/invite.tsx:167 +msgid "The invitation has been sent successfully." +msgstr "招待状が正常に送信されました。" + #: src/routes/(root)/[handle]/settings/passkeys.tsx:326 msgid "The passkey has been successfully revoked." msgstr "パスキーを正常に取り消しました。" @@ -775,10 +846,19 @@ msgstr "変更は1回のみ可能で、変更前のユーザー名は他のユ msgid "You can leave this empty to remove the link." msgstr "リンクを削除する場合は空にしてください。" +#: src/routes/(root)/[handle]/settings/invite.tsx:316 +msgid "You can leave this field empty." +msgstr "このフィールドは空欄でも構いません。" + #: src/routes/(root)/[handle]/settings/passkeys.tsx:441 msgid "You don't have any passkeys registered yet." msgstr "まだパスキーが登録されていません。" +#: src/routes/(root)/[handle]/settings/invite.tsx:235 +#: src/routes/(root)/[handle]/settings/invite.tsx:337 +msgid "You have no invitations left. Please wait until you receive more." +msgstr "招待状が残っていません。追加されるまでお待ちください。" + #: src/routes/(root)/sign/up/[token].tsx:433 msgid "You were invited by" msgstr "あなたを招待した方" @@ -803,6 +883,10 @@ msgstr "自己紹介はプロフィールに表示されます。Markdownで書 msgid "Your email address will be used to sign in to your account." msgstr "メールアドレスはアカウントへのログインに使用されます。" +#: src/routes/(root)/[handle]/settings/invite.tsx:319 +msgid "Your friend will see this message in the invitation email." +msgstr "友達は招待メールでこのメッセージを見ることができます。" + #: src/routes/(root)/[handle]/settings/index.tsx:464 #: src/routes/(root)/sign/up/[token].tsx:400 msgid "Your name will be displayed on your profile and in your posts." diff --git a/web-next/src/locales/ko-KR/messages.po b/web-next/src/locales/ko-KR/messages.po index 7109346c..356b3ad4 100644 --- a/web-next/src/locales/ko-KR/messages.po +++ b/web-next/src/locales/ko-KR/messages.po @@ -23,6 +23,11 @@ msgstr "{0, plural, other {# 팔로워}}" msgid "{0, plural, one {# following} other {# following}}" msgstr "{0, plural, other {# 팔로잉}}" +#. placeholder {0}: account().invitationsLeft +#: src/routes/(root)/[handle]/settings/invite.tsx:237 +msgid "{0, plural, one {Invite your friends to Hackers' Pub. You can invite up to # person.} other {Invite your friends to Hackers' Pub. You can invite up to # people.}}" +msgstr "{0, plural, other {Hackers' Pub에 친구를 초대하세요. 최대 #명까지 초대할 수 있습니다.}}" + #. placeholder {0}: "ACTOR" #. placeholder {1}: "COUNT" #: src/components/notification/FollowNotificationCard.tsx:29 @@ -65,6 +70,11 @@ msgstr "{0} 님 외 {1}명이 회원님의 콘텐츠를 공유했습니다" msgid "{0} followed you" msgstr "{0} 님이 팔로했습니다" +#. placeholder {0}: "USER" +#: src/routes/(root)/[handle]/settings/invite.tsx:281 +msgid "{0} is already a member of Hackers' Pub." +msgstr "{0} 님은 이미 Hackers' Pub의 회원입니다." + #. placeholder {0}: "ACTOR" #: src/components/notification/MentionNotificationCard.tsx:35 msgid "{0} mentioned you" @@ -160,6 +170,10 @@ msgstr "환경 설정 저장 중 오류가 발생했습니다. 다시 시도하 msgid "An error occurred while saving your settings. Please try again, or contact support if the problem persists." msgstr "설정 저장 중 오류가 발생했습니다. 다시 시도하거나 문제가 지속되면 지원팀에 문의하세요." +#: src/routes/(root)/[handle]/settings/invite.tsx:178 +msgid "An unexpected error occurred. Please try again later." +msgstr "예상치 못한 오류가 발생했습니다. 나중에 다시 시도해주세요." + #. placeholder {0}: passkeyToRevoke()?.name #: src/routes/(root)/[handle]/settings/passkeys.tsx:539 msgid "Are you sure you want to revoke passkey {0}? You won't be able to use it to sign in to your account anymore." @@ -201,6 +215,10 @@ msgstr "약력이 너무 깁니다. 최대 길이는 512자입니다." msgid "Cancel" msgstr "취소" +#: src/routes/(root)/[handle]/settings/invite.tsx:305 +msgid "Choose the language your friend prefers. This language will only be used for the invitation." +msgstr "초대장을 받을 친구가 사용하는 언어를 선택하세요. 이 언어는 초대장에만 사용됩니다." + #: src/components/AppSidebar.tsx:321 #: src/routes/(root)/coc.tsx:53 #: src/routes/(root)/coc.tsx:58 @@ -245,6 +263,7 @@ msgstr "계정이 필요하신가요? Hackers' Pub은 초대 전용입니다. msgid "Drag to select the area you want to keep, then click “Crop” to update your avatar." msgstr "유지하려는 영역을 드래그하여 선택한 다음 “자르기”를 클릭하여 프로필 사진을 업데이트하세요." +#: src/routes/(root)/[handle]/settings/invite.tsx:254 #: src/routes/(root)/sign/up/[token].tsx:337 msgid "Email address" msgstr "이메일 주소" @@ -265,6 +284,10 @@ msgstr "오류" msgid "ex) My key" msgstr "예) 나의 키" +#: src/routes/(root)/[handle]/settings/invite.tsx:310 +msgid "Extra message" +msgstr "추가 메시지" + #: src/components/ActorArticleList.tsx:75 msgid "Failed to load more articles; click to retry" msgstr "게시글 불러오기 실패. 클릭하여 재시도하세요" @@ -307,6 +330,11 @@ msgstr "환경 설정 저장 실패" msgid "Failed to save settings" msgstr "설정 저장 실패" +#: src/routes/(root)/[handle]/settings/invite.tsx:156 +#: src/routes/(root)/[handle]/settings/invite.tsx:176 +msgid "Failed to send invitation" +msgstr "초대장 발송에 실패했습니다" + #. placeholder {0}: error.message #: src/components/AppSidebar.tsx:81 msgid "Failed to sign out: {0}" @@ -360,6 +388,20 @@ msgstr "Hackers' Pub의 행동 강령을 읽고 동의합니다." msgid "If enabled, the AI will generate a summary of the article for you. Otherwise, the first few lines of the article will be used as the summary." msgstr "활성화하면 AI가 글의 요약을 생성합니다. 비활성화 시 글의 처음 몇 줄이 요약으로 사용됩니다." +#: src/routes/(root)/[handle]/settings/invite.tsx:298 +msgid "Invitation language" +msgstr "초대장 언어" + +#: src/routes/(root)/[handle]/settings/invite.tsx:166 +msgid "Invitation sent" +msgstr "초대장이 발송되었습니다" + +#: src/components/SettingsTabs.tsx:51 +#: src/routes/(root)/[handle]/settings/invite.tsx:210 +#: src/routes/(root)/[handle]/settings/invite.tsx:221 +msgid "Invite" +msgstr "초대" + #: src/routes/(root)/[handle]/settings/index.tsx:460 msgid "John Doe" msgstr "홍길동" @@ -452,6 +494,10 @@ msgstr "사용된 적 없음" msgid "No followers found" msgstr "팔로워가 없습니다" +#: src/routes/(root)/[handle]/settings/invite.tsx:328 +msgid "No invitations left" +msgstr "남은 초대장이 없습니다" + #: src/components/ActorArticleList.tsx:85 msgid "No notes articles" msgstr "게시글이 없습니다" @@ -516,7 +562,7 @@ msgstr "패스키 취소됨" msgid "passkeys" msgstr "패스키" -#: src/components/SettingsTabs.tsx:51 +#: src/components/SettingsTabs.tsx:58 msgid "Passkeys" msgstr "패스키" @@ -524,6 +570,10 @@ msgstr "패스키" msgid "Please choose an image file smaller than 5 MiB." msgstr "5 MiB 미만의 이미지 파일을 선택해주세요." +#: src/routes/(root)/[handle]/settings/invite.tsx:157 +msgid "Please correct the errors and try again." +msgstr "오류를 수정하고 다시 시도해주세요." + #: src/components/ProfileTabs.tsx:38 msgid "Posts" msgstr "콘텐츠" @@ -600,8 +650,17 @@ msgstr "저장" msgid "Saving…" msgstr "저장 중…" +#: src/routes/(root)/[handle]/settings/invite.tsx:331 +msgid "Send" +msgstr "보내기" + +#: src/routes/(root)/[handle]/settings/invite.tsx:330 +msgid "Sending…" +msgstr "보내는 중…" + #: src/components/AppSidebar.tsx:288 #: src/routes/(root)/[handle]/settings/index.tsx:152 +#: src/routes/(root)/[handle]/settings/invite.tsx:215 #: src/routes/(root)/[handle]/settings/passkeys.tsx:379 #: src/routes/(root)/[handle]/settings/preferences.tsx:181 msgid "Settings" @@ -673,10 +732,22 @@ msgstr "단문의 기본 공개 설정입니다." msgid "The default privacy setting for your shares." msgstr "공유의 기본 공개 설정입니다." +#: src/routes/(root)/[handle]/settings/invite.tsx:272 +msgid "The email address is invalid." +msgstr "이메일 주소가 유효하지 않습니다." + +#: src/routes/(root)/[handle]/settings/invite.tsx:267 +msgid "The email address is not only used for receiving the invitation, but also for signing in to the account." +msgstr "이메일 주소는 초대장을 받을 때 뿐만 아니라, 계정에 로그인할 때도 사용됩니다." + #: src/routes/(root)/[handle]/settings/passkeys.tsx:429 msgid "The following passkeys are registered to your account. You can use them to sign in to your account." msgstr "다음 패스키들이 계정에 등록되어 있습니다. 이들을 사용하여 계정에 로그인할 수 있습니다." +#: src/routes/(root)/[handle]/settings/invite.tsx:167 +msgid "The invitation has been sent successfully." +msgstr "초대장이 성공적으로 발송되었습니다." + #: src/routes/(root)/[handle]/settings/passkeys.tsx:326 msgid "The passkey has been successfully revoked." msgstr "성공적으로 패스키를 취소했습니다." @@ -775,10 +846,19 @@ msgstr "아이디는 단 한 번만 변경할 수 있으며, 변경하기 전 msgid "You can leave this empty to remove the link." msgstr "링크를 삭제하려면 이곳을 비워두세요." +#: src/routes/(root)/[handle]/settings/invite.tsx:316 +msgid "You can leave this field empty." +msgstr "이 필드는 비워둘 수 있습니다." + #: src/routes/(root)/[handle]/settings/passkeys.tsx:441 msgid "You don't have any passkeys registered yet." msgstr "등록된 패스키가 아직 없습니다." +#: src/routes/(root)/[handle]/settings/invite.tsx:235 +#: src/routes/(root)/[handle]/settings/invite.tsx:337 +msgid "You have no invitations left. Please wait until you receive more." +msgstr "남은 초대장이 없습니다. 추가로 받을 때까지 기다려주세요." + #: src/routes/(root)/sign/up/[token].tsx:433 msgid "You were invited by" msgstr "당신을 초대한 분" @@ -803,6 +883,10 @@ msgstr "약력은 프로필에 표시됩니다. Markdown을 사용할 수 있습 msgid "Your email address will be used to sign in to your account." msgstr "이메일 주소는 계정에 로그인할 때 사용됩니다." +#: src/routes/(root)/[handle]/settings/invite.tsx:319 +msgid "Your friend will see this message in the invitation email." +msgstr "초대장을 받는 친구가 볼 수 있는 메시지입니다." + #: src/routes/(root)/[handle]/settings/index.tsx:464 #: src/routes/(root)/sign/up/[token].tsx:400 msgid "Your name will be displayed on your profile and in your posts." diff --git a/web-next/src/locales/zh-CN/messages.po b/web-next/src/locales/zh-CN/messages.po index 76adc09c..211ce708 100644 --- a/web-next/src/locales/zh-CN/messages.po +++ b/web-next/src/locales/zh-CN/messages.po @@ -23,6 +23,11 @@ msgstr "{0, plural, other {被 # 人关注}}" msgid "{0, plural, one {# following} other {# following}}" msgstr "{0, plural, other {关注 # 人}}" +#. placeholder {0}: account().invitationsLeft +#: src/routes/(root)/[handle]/settings/invite.tsx:237 +msgid "{0, plural, one {Invite your friends to Hackers' Pub. You can invite up to # person.} other {Invite your friends to Hackers' Pub. You can invite up to # people.}}" +msgstr "{0, plural, other {邀请你的朋友加入 Hackers' Pub。你可以邀请最多 # 个人。}}" + #. placeholder {0}: "ACTOR" #. placeholder {1}: "COUNT" #: src/components/notification/FollowNotificationCard.tsx:29 @@ -65,6 +70,11 @@ msgstr "{0} 和其他 {1} 人转发了你的内容" msgid "{0} followed you" msgstr "{0} 关注了你" +#. placeholder {0}: "USER" +#: src/routes/(root)/[handle]/settings/invite.tsx:281 +msgid "{0} is already a member of Hackers' Pub." +msgstr "{0} 已经是 Hackers' Pub 的成员。" + #. placeholder {0}: "ACTOR" #: src/components/notification/MentionNotificationCard.tsx:35 msgid "{0} mentioned you" @@ -160,6 +170,10 @@ msgstr "保存设置时出现错误。请重试,如果问题持续存在,请 msgid "An error occurred while saving your settings. Please try again, or contact support if the problem persists." msgstr "保存设置时发生错误。请重试,如果问题仍然存在,请联系客服。" +#: src/routes/(root)/[handle]/settings/invite.tsx:178 +msgid "An unexpected error occurred. Please try again later." +msgstr "发生了意外错误。请稍后再试。" + #. placeholder {0}: passkeyToRevoke()?.name #: src/routes/(root)/[handle]/settings/passkeys.tsx:539 msgid "Are you sure you want to revoke passkey {0}? You won't be able to use it to sign in to your account anymore." @@ -201,6 +215,10 @@ msgstr "个人简介太长。不能长于 512 字符。" msgid "Cancel" msgstr "取消" +#: src/routes/(root)/[handle]/settings/invite.tsx:305 +msgid "Choose the language your friend prefers. This language will only be used for the invitation." +msgstr "选择你的朋友使用的语言。这个语言只会用于邀请。" + #: src/components/AppSidebar.tsx:321 #: src/routes/(root)/coc.tsx:53 #: src/routes/(root)/coc.tsx:58 @@ -245,6 +263,7 @@ msgstr "需要创建账户吗?Hackers' Pub 仅限邀请,请联系朋友邀 msgid "Drag to select the area you want to keep, then click “Crop” to update your avatar." msgstr "拖动选择要保留的区域,然后点击「裁剪」来更新你的头像。" +#: src/routes/(root)/[handle]/settings/invite.tsx:254 #: src/routes/(root)/sign/up/[token].tsx:337 msgid "Email address" msgstr "电子邮件地址" @@ -265,6 +284,10 @@ msgstr "错误" msgid "ex) My key" msgstr "例如:我的通行密钥" +#: src/routes/(root)/[handle]/settings/invite.tsx:310 +msgid "Extra message" +msgstr "额外消息" + #: src/components/ActorArticleList.tsx:75 msgid "Failed to load more articles; click to retry" msgstr "加载更多文章失败,点击重试" @@ -307,6 +330,11 @@ msgstr "保存设置失败" msgid "Failed to save settings" msgstr "保存设置失败" +#: src/routes/(root)/[handle]/settings/invite.tsx:156 +#: src/routes/(root)/[handle]/settings/invite.tsx:176 +msgid "Failed to send invitation" +msgstr "发送邀请失败" + #. placeholder {0}: error.message #: src/components/AppSidebar.tsx:81 msgid "Failed to sign out: {0}" @@ -360,6 +388,20 @@ msgstr "我同意 Hackers' Pub 的行为准则。" msgid "If enabled, the AI will generate a summary of the article for you. Otherwise, the first few lines of the article will be used as the summary." msgstr "启用后,AI 将为您生成文章摘要。否则,将使用文章的前几行作为摘要。" +#: src/routes/(root)/[handle]/settings/invite.tsx:298 +msgid "Invitation language" +msgstr "邀请语言" + +#: src/routes/(root)/[handle]/settings/invite.tsx:166 +msgid "Invitation sent" +msgstr "邀请已发送" + +#: src/components/SettingsTabs.tsx:51 +#: src/routes/(root)/[handle]/settings/invite.tsx:210 +#: src/routes/(root)/[handle]/settings/invite.tsx:221 +msgid "Invite" +msgstr "邀请" + #: src/routes/(root)/[handle]/settings/index.tsx:460 msgid "John Doe" msgstr "张三" @@ -452,6 +494,10 @@ msgstr "从未使用过" msgid "No followers found" msgstr "未找到粉丝" +#: src/routes/(root)/[handle]/settings/invite.tsx:328 +msgid "No invitations left" +msgstr "没有剩余邀请名额" + #: src/components/ActorArticleList.tsx:85 msgid "No notes articles" msgstr "未找到文章" @@ -516,7 +562,7 @@ msgstr "通行密钥已撤销" msgid "passkeys" msgstr "通行密钥" -#: src/components/SettingsTabs.tsx:51 +#: src/components/SettingsTabs.tsx:58 msgid "Passkeys" msgstr "通行密钥" @@ -524,6 +570,10 @@ msgstr "通行密钥" msgid "Please choose an image file smaller than 5 MiB." msgstr "请选择小于 5 MiB 的图片文件。" +#: src/routes/(root)/[handle]/settings/invite.tsx:157 +msgid "Please correct the errors and try again." +msgstr "请修正错误并重试。" + #: src/components/ProfileTabs.tsx:38 msgid "Posts" msgstr "内容" @@ -600,8 +650,17 @@ msgstr "保存" msgid "Saving…" msgstr "保存中…" +#: src/routes/(root)/[handle]/settings/invite.tsx:331 +msgid "Send" +msgstr "发送" + +#: src/routes/(root)/[handle]/settings/invite.tsx:330 +msgid "Sending…" +msgstr "发送中…" + #: src/components/AppSidebar.tsx:288 #: src/routes/(root)/[handle]/settings/index.tsx:152 +#: src/routes/(root)/[handle]/settings/invite.tsx:215 #: src/routes/(root)/[handle]/settings/passkeys.tsx:379 #: src/routes/(root)/[handle]/settings/preferences.tsx:181 msgid "Settings" @@ -673,10 +732,22 @@ msgstr "您帖子的默认隐私设置。" msgid "The default privacy setting for your shares." msgstr "您转帖的默认隐私设置。" +#: src/routes/(root)/[handle]/settings/invite.tsx:272 +msgid "The email address is invalid." +msgstr "电子邮件地址无效。" + +#: src/routes/(root)/[handle]/settings/invite.tsx:267 +msgid "The email address is not only used for receiving the invitation, but also for signing in to the account." +msgstr "电子邮件地址不仅用于接收邀请,还用于登录账户。" + #: src/routes/(root)/[handle]/settings/passkeys.tsx:429 msgid "The following passkeys are registered to your account. You can use them to sign in to your account." msgstr "以下通行密钥已注册到你的账户。你可以使用它们登录你的账户。" +#: src/routes/(root)/[handle]/settings/invite.tsx:167 +msgid "The invitation has been sent successfully." +msgstr "邀请已成功发送。" + #: src/routes/(root)/[handle]/settings/passkeys.tsx:326 msgid "The passkey has been successfully revoked." msgstr "通行密钥已成功撤销。" @@ -775,10 +846,19 @@ msgstr "你只能更改一次用户名,而旧的用户名会公开为别人使 msgid "You can leave this empty to remove the link." msgstr "您可以将此处留空以删除链接。" +#: src/routes/(root)/[handle]/settings/invite.tsx:316 +msgid "You can leave this field empty." +msgstr "你可以留空此字段。" + #: src/routes/(root)/[handle]/settings/passkeys.tsx:441 msgid "You don't have any passkeys registered yet." msgstr "您尚未注册任何通行密钥。" +#: src/routes/(root)/[handle]/settings/invite.tsx:235 +#: src/routes/(root)/[handle]/settings/invite.tsx:337 +msgid "You have no invitations left. Please wait until you receive more." +msgstr "你没有剩余邀请名额。请稍后再试。" + #: src/routes/(root)/sign/up/[token].tsx:433 msgid "You were invited by" msgstr "您被以下用户邀请" @@ -803,6 +883,10 @@ msgstr "你的个人简介将在你的个人资料页面显示。你可以用 Ma msgid "Your email address will be used to sign in to your account." msgstr "你的电子邮件地址将用于登录。" +#: src/routes/(root)/[handle]/settings/invite.tsx:319 +msgid "Your friend will see this message in the invitation email." +msgstr "你的朋友将在邀请邮件中看到此信息。" + #: src/routes/(root)/[handle]/settings/index.tsx:464 #: src/routes/(root)/sign/up/[token].tsx:400 msgid "Your name will be displayed on your profile and in your posts." diff --git a/web-next/src/locales/zh-TW/messages.po b/web-next/src/locales/zh-TW/messages.po index 7f8b8e6f..b8aefa53 100644 --- a/web-next/src/locales/zh-TW/messages.po +++ b/web-next/src/locales/zh-TW/messages.po @@ -23,6 +23,11 @@ msgstr "{0, plural, other {被 # 人關注}}" msgid "{0, plural, one {# following} other {# following}}" msgstr "{0, plural, other {關注 # 人}}" +#. placeholder {0}: account().invitationsLeft +#: src/routes/(root)/[handle]/settings/invite.tsx:237 +msgid "{0, plural, one {Invite your friends to Hackers' Pub. You can invite up to # person.} other {Invite your friends to Hackers' Pub. You can invite up to # people.}}" +msgstr "{0, plural, other {邀請你的朋友加入 Hackers' Pub。你可以邀請最多 # 個人。}}" + #. placeholder {0}: "ACTOR" #. placeholder {1}: "COUNT" #: src/components/notification/FollowNotificationCard.tsx:29 @@ -65,6 +70,11 @@ msgstr "{0} 和其他 {1} 人轉貼了你的內容" msgid "{0} followed you" msgstr "{0} 關注了你" +#. placeholder {0}: "USER" +#: src/routes/(root)/[handle]/settings/invite.tsx:281 +msgid "{0} is already a member of Hackers' Pub." +msgstr "{0} 已經是 Hackers' Pub 的成員。" + #. placeholder {0}: "ACTOR" #: src/components/notification/MentionNotificationCard.tsx:35 msgid "{0} mentioned you" @@ -160,6 +170,10 @@ msgstr "儲存設定時發生錯誤。請重試,如果問題持續存在,請 msgid "An error occurred while saving your settings. Please try again, or contact support if the problem persists." msgstr "儲存設定時發生錯誤。請重試,如果問題仍然存在,請聯繫客服。" +#: src/routes/(root)/[handle]/settings/invite.tsx:178 +msgid "An unexpected error occurred. Please try again later." +msgstr "發生了意外錯誤。請稍後再試。" + #. placeholder {0}: passkeyToRevoke()?.name #: src/routes/(root)/[handle]/settings/passkeys.tsx:539 msgid "Are you sure you want to revoke passkey {0}? You won't be able to use it to sign in to your account anymore." @@ -201,6 +215,10 @@ msgstr "個人簡介太長。不能長於 512 字元。" msgid "Cancel" msgstr "取消" +#: src/routes/(root)/[handle]/settings/invite.tsx:305 +msgid "Choose the language your friend prefers. This language will only be used for the invitation." +msgstr "選擇你的朋友使用的語言。這個語言只會用於邀請。" + #: src/components/AppSidebar.tsx:321 #: src/routes/(root)/coc.tsx:53 #: src/routes/(root)/coc.tsx:58 @@ -245,6 +263,7 @@ msgstr "需要建立帳戶嗎?Hackers' Pub 僅限邀請,請聯繫朋友邀 msgid "Drag to select the area you want to keep, then click “Crop” to update your avatar." msgstr "拖動選擇要保留的區域,然後點擊「裁剪」來更新你的頭像。" +#: src/routes/(root)/[handle]/settings/invite.tsx:254 #: src/routes/(root)/sign/up/[token].tsx:337 msgid "Email address" msgstr "電子郵件地址" @@ -265,6 +284,10 @@ msgstr "錯誤" msgid "ex) My key" msgstr "例如:我的通行金鑰" +#: src/routes/(root)/[handle]/settings/invite.tsx:310 +msgid "Extra message" +msgstr "額外訊息" + #: src/components/ActorArticleList.tsx:75 msgid "Failed to load more articles; click to retry" msgstr "載入更多文章失敗,點擊重試" @@ -307,6 +330,11 @@ msgstr "儲存設定失敗" msgid "Failed to save settings" msgstr "儲存設定失敗" +#: src/routes/(root)/[handle]/settings/invite.tsx:156 +#: src/routes/(root)/[handle]/settings/invite.tsx:176 +msgid "Failed to send invitation" +msgstr "發送邀請失敗" + #. placeholder {0}: error.message #: src/components/AppSidebar.tsx:81 msgid "Failed to sign out: {0}" @@ -360,6 +388,20 @@ msgstr "我同意 Hackers' Pub 的行為準則。" msgid "If enabled, the AI will generate a summary of the article for you. Otherwise, the first few lines of the article will be used as the summary." msgstr "啟用後,AI 將為您生成文章摘要。否則,將使用文章的前幾行作為摘要。" +#: src/routes/(root)/[handle]/settings/invite.tsx:298 +msgid "Invitation language" +msgstr "邀請語言" + +#: src/routes/(root)/[handle]/settings/invite.tsx:166 +msgid "Invitation sent" +msgstr "邀請已發送" + +#: src/components/SettingsTabs.tsx:51 +#: src/routes/(root)/[handle]/settings/invite.tsx:210 +#: src/routes/(root)/[handle]/settings/invite.tsx:221 +msgid "Invite" +msgstr "邀請" + #: src/routes/(root)/[handle]/settings/index.tsx:460 msgid "John Doe" msgstr "張三" @@ -452,6 +494,10 @@ msgstr "從未使用過" msgid "No followers found" msgstr "未找到粉絲" +#: src/routes/(root)/[handle]/settings/invite.tsx:328 +msgid "No invitations left" +msgstr "沒有剩餘邀請名額" + #: src/components/ActorArticleList.tsx:85 msgid "No notes articles" msgstr "未找到文章" @@ -516,7 +562,7 @@ msgstr "通行金鑰已撤銷" msgid "passkeys" msgstr "通行金鑰" -#: src/components/SettingsTabs.tsx:51 +#: src/components/SettingsTabs.tsx:58 msgid "Passkeys" msgstr "通行金鑰" @@ -524,6 +570,10 @@ msgstr "通行金鑰" msgid "Please choose an image file smaller than 5 MiB." msgstr "請選擇小於 5 MiB 的圖片檔案。" +#: src/routes/(root)/[handle]/settings/invite.tsx:157 +msgid "Please correct the errors and try again." +msgstr "請修正錯誤並重試。" + #: src/components/ProfileTabs.tsx:38 msgid "Posts" msgstr "內容" @@ -600,8 +650,17 @@ msgstr "儲存" msgid "Saving…" msgstr "儲存中…" +#: src/routes/(root)/[handle]/settings/invite.tsx:331 +msgid "Send" +msgstr "發送" + +#: src/routes/(root)/[handle]/settings/invite.tsx:330 +msgid "Sending…" +msgstr "發送中…" + #: src/components/AppSidebar.tsx:288 #: src/routes/(root)/[handle]/settings/index.tsx:152 +#: src/routes/(root)/[handle]/settings/invite.tsx:215 #: src/routes/(root)/[handle]/settings/passkeys.tsx:379 #: src/routes/(root)/[handle]/settings/preferences.tsx:181 msgid "Settings" @@ -673,10 +732,22 @@ msgstr "您貼文的預設隱私設定。" msgid "The default privacy setting for your shares." msgstr "您轉貼的預設隱私設定。" +#: src/routes/(root)/[handle]/settings/invite.tsx:272 +msgid "The email address is invalid." +msgstr "電子郵件地址無效。" + +#: src/routes/(root)/[handle]/settings/invite.tsx:267 +msgid "The email address is not only used for receiving the invitation, but also for signing in to the account." +msgstr "電子郵件地址不僅用於接收邀請,還用於登入帳戶。" + #: src/routes/(root)/[handle]/settings/passkeys.tsx:429 msgid "The following passkeys are registered to your account. You can use them to sign in to your account." msgstr "以下通行通行金鑰已註冊到你的帳戶。你可以使用它們登入你的帳戶。" +#: src/routes/(root)/[handle]/settings/invite.tsx:167 +msgid "The invitation has been sent successfully." +msgstr "邀請已成功發送。" + #: src/routes/(root)/[handle]/settings/passkeys.tsx:326 msgid "The passkey has been successfully revoked." msgstr "通行金鑰已成功撤銷。" @@ -775,10 +846,19 @@ msgstr "你只能更改一次使用者名稱,而舊的使用者名稱會公開 msgid "You can leave this empty to remove the link." msgstr "您可以將此處留空以刪除連結。" +#: src/routes/(root)/[handle]/settings/invite.tsx:316 +msgid "You can leave this field empty." +msgstr "你可以留空此欄位。" + #: src/routes/(root)/[handle]/settings/passkeys.tsx:441 msgid "You don't have any passkeys registered yet." msgstr "您尚未註冊任何通行金鑰。" +#: src/routes/(root)/[handle]/settings/invite.tsx:235 +#: src/routes/(root)/[handle]/settings/invite.tsx:337 +msgid "You have no invitations left. Please wait until you receive more." +msgstr "你沒有剩餘邀請名額。請稍後再試。" + #: src/routes/(root)/sign/up/[token].tsx:433 msgid "You were invited by" msgstr "您的邀請人" @@ -803,6 +883,10 @@ msgstr "你的個人簡介將在你的個人資料頁面顯示。你可以用 Ma msgid "Your email address will be used to sign in to your account." msgstr "你的電子郵件地址將用於登入。" +#: src/routes/(root)/[handle]/settings/invite.tsx:319 +msgid "Your friend will see this message in the invitation email." +msgstr "你的朋友將在邀請郵件中看到此訊息。" + #: src/routes/(root)/[handle]/settings/index.tsx:464 #: src/routes/(root)/sign/up/[token].tsx:400 msgid "Your name will be displayed on your profile and in your posts." From 43e43c844a68d6110ac948b7b9fa65a311350b7c Mon Sep 17 00:00:00 2001 From: Hong Minhee Date: Sun, 31 Aug 2025 13:06:10 +0900 Subject: [PATCH 5/9] Optimize invitation system performance with caching - 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 --- graphql/invite.ts | 65 +++++++++++++++++++++++++++++++++++++---------- graphql/misc.ts | 12 ++++++++- 2 files changed, 63 insertions(+), 14 deletions(-) diff --git a/graphql/invite.ts b/graphql/invite.ts index 8945c3cb..2eeacca0 100644 --- a/graphql/invite.ts +++ b/graphql/invite.ts @@ -69,7 +69,7 @@ InvitationRef.implement({ }); const InviteInviterError = builder.enumType("InviteInviterError", { - values: ["INVITER_NOT_AUTHENTICATED", "INVITER_NO_INVITATIONS_LEFT"] as const, + values: ["INVITER_NOT_AUTHENTICATED", "INVITER_NO_INVITATIONS_LEFT", "INVITER_EMAIL_SEND_FAILED"] as const, }); const InviteEmailError = builder.enumType("InviteEmailError", { @@ -223,6 +223,15 @@ builder.mutationField("invite", (t) => "Failed to send invitation email: {errors}", { errors: receipt.errorMessages }, ); + // Credit back the invitation on email send failure + await ctx.db.update(accountTable).set({ + leftInvitations: sql`${accountTable.leftInvitations} + 1`, + }).where(eq(accountTable.id, ctx.account.id)); + + // Return validation error to inform the user + return { + inviter: "INVITER_EMAIL_SEND_FAILED", + } satisfies InviteValidationErrors; } return { inviterId: ctx.account.id, @@ -235,32 +244,62 @@ builder.mutationField("invite", (t) => const LOCALES_DIR = join(import.meta.dirname!, "locales"); -async function getEmailTemplate( - locale: Intl.Locale, - message: boolean, -): Promise<{ subject: string; content: string }> { +// Cache for email templates +let cachedTemplates: Map | null = null; +let cachedAvailableLocales: Record | null = null; + +async function loadEmailTemplates(): Promise { + if (cachedTemplates && cachedAvailableLocales) return; + const availableLocales: Record = {}; + const templates = new Map(); + 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[localeName] = file.path; + + try { + const json = await Deno.readTextFile(file.path); + const data = JSON.parse(json); + templates.set(localeName, { + subject: data.invite.emailSubject, + emailContent: data.invite.emailContent, + emailContentWithMessage: data.invite.emailContentWithMessage, + }); + } catch (error) { + console.warn(`Failed to load email template for locale ${localeName}:`, error); + } } + + cachedTemplates = templates; + cachedAvailableLocales = availableLocales; +} + +async function getEmailTemplate( + locale: Intl.Locale, + message: boolean, +): Promise<{ subject: string; content: string }> { + await loadEmailTemplates(); + const selectedLocale = - negotiateLocale(locale, Object.keys(availableLocales)) ?? + negotiateLocale(locale, Object.keys(cachedAvailableLocales!)) ?? new Intl.Locale("en"); - const path = availableLocales[selectedLocale.baseName]; - const json = await Deno.readTextFile(path); - const data = JSON.parse(json); + + const template = cachedTemplates!.get(selectedLocale.baseName); + if (!template) { + throw new Error(`No email template found for locale ${selectedLocale.baseName}`); + } + return { - subject: data.invite.emailSubject, - content: message - ? data.invite.emailContentWithMessage - : data.invite.emailContent, + subject: template.subject, + content: message ? template.emailContentWithMessage : template.emailContent, }; } diff --git a/graphql/misc.ts b/graphql/misc.ts index 5d748c6e..fb6d8bf0 100644 --- a/graphql/misc.ts +++ b/graphql/misc.ts @@ -4,10 +4,14 @@ import { builder } from "./builder.ts"; const LOCALES_DIR = join(import.meta.dirname!, "locales"); +let cachedLocales: Intl.Locale[] | null = null; + builder.queryField("availableLocales", (t) => t.field({ type: ["Locale"], async resolve(_root, _args, _ctx) { + if (cachedLocales) return cachedLocales; + const availableLocales: Intl.Locale[] = []; const files = expandGlob(join(LOCALES_DIR, "*.json"), { includeDirs: false, @@ -17,8 +21,14 @@ builder.queryField("availableLocales", (t) => 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 locale tags + } } + + cachedLocales = availableLocales; return availableLocales; }, })); From c9a693ba8c3f52dd808dccb7cbd555a503836663 Mon Sep 17 00:00:00 2001 From: Hong Minhee Date: Sun, 31 Aug 2025 13:07:13 +0900 Subject: [PATCH 6/9] Fix typos and bugs from code review feedback MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - 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 --- graphql/locales/ko.json | 2 +- models/i18n.ts | 29 ++++++----- web-next/src/locales/en-US/messages.po | 48 ++++++++--------- web-next/src/locales/ja-JP/messages.po | 48 ++++++++--------- web-next/src/locales/ko-KR/messages.po | 48 ++++++++--------- web-next/src/locales/zh-CN/messages.po | 48 ++++++++--------- web-next/src/locales/zh-TW/messages.po | 52 +++++++++---------- .../(root)/[handle]/settings/preferences.tsx | 2 +- 8 files changed, 139 insertions(+), 138 deletions(-) diff --git a/graphql/locales/ko.json b/graphql/locales/ko.json index d0deef93..163e8ac7 100644 --- a/graphql/locales/ko.json +++ b/graphql/locales/ko.json @@ -6,6 +6,6 @@ "invite": { "emailSubject": "{{inviterName}} 님이 Hackers' Pub에 초대합니다!", "emailContent": "{{inviter}} 님이 Hackers' Pub에 초대합니다!\n\nHackers' Pub은 소프트웨어 프로그래머들이 지식과 경험을 서로 나누는 곳입니다. 또한 ActivityPub을 지원하는 소셜 네트워크이기도 하며, 연합우주(fediverse)에서 즐겨찾는 소프트웨어 프로그래머들을 팔로하고 최신 콘텐츠를 받아 볼 수 있습니다.\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" + "emailContentWithMessage": "{{inviter}} 님이 Hackers' Pub에 초대합니다! 다음은 {{inviterName}} 님의 메시지입니다:\n\n{{message}}\n\n초대장을 받으려면 아래 링크를 클릭하세요:\n\n{{verifyUrl}}\n\n이 링크는 {{expiration}} 후에 만료됩니다.\n" } } diff --git a/models/i18n.ts b/models/i18n.ts index 6b17da00..d368e7d8 100644 --- a/models/i18n.ts +++ b/models/i18n.ts @@ -186,37 +186,38 @@ export function negotiateLocale( : wantedLocales, ]; - const availables = availableLocales.map((l) => + const availableLocalesNormalized = availableLocales.map((l) => typeof l === "string" ? new Intl.Locale(l) : l ); + const availablesWithMaximized = availableLocalesNormalized.map((raw) => ({ + raw, + max: raw.maximize(), + })); for (const wanted of wantedArray) { const wantedMaximized = wanted.maximize(); // First try exact match - for (const available of availables) { - const availableMaximized = available.maximize(); - if (wantedMaximized.baseName === availableMaximized.baseName) { - return available; + for (const a of availablesWithMaximized) { + if (wantedMaximized.baseName === a.max.baseName) { + return a.raw; } } // Then try language + script match (e.g., zh-Hant) - for (const available of availables) { - const availableMaximized = available.maximize(); + for (const a of availablesWithMaximized) { if ( - wantedMaximized.language === availableMaximized.language && - wantedMaximized.script === availableMaximized.script + wantedMaximized.language === a.max.language && + wantedMaximized.script === a.max.script ) { - return available; + return a.raw; } } // Finally try language-only match - for (const available of availables) { - const availableMaximized = available.maximize(); - if (wantedMaximized.language === availableMaximized.language) { - return available; + for (const a of availablesWithMaximized) { + if (wantedMaximized.language === a.max.language) { + return a.raw; } } } diff --git a/web-next/src/locales/en-US/messages.po b/web-next/src/locales/en-US/messages.po index 9e831b2d..250ec031 100644 --- a/web-next/src/locales/en-US/messages.po +++ b/web-next/src/locales/en-US/messages.po @@ -24,7 +24,7 @@ msgid "{0, plural, one {# following} other {# following}}" msgstr "{0, plural, one {# following} other {# following}}" #. placeholder {0}: account().invitationsLeft -#: src/routes/(root)/[handle]/settings/invite.tsx:237 +#: src/routes/(root)/[handle]/settings/invite.tsx:239 msgid "{0, plural, one {Invite your friends to Hackers' Pub. You can invite up to # person.} other {Invite your friends to Hackers' Pub. You can invite up to # people.}}" msgstr "{0, plural, one {Invite your friends to Hackers' Pub. You can invite up to # person.} other {Invite your friends to Hackers' Pub. You can invite up to # people.}}" @@ -71,7 +71,7 @@ msgid "{0} followed you" msgstr "{0} followed you" #. placeholder {0}: "USER" -#: src/routes/(root)/[handle]/settings/invite.tsx:281 +#: src/routes/(root)/[handle]/settings/invite.tsx:283 msgid "{0} is already a member of Hackers' Pub." msgstr "{0} is already a member of Hackers' Pub." @@ -170,7 +170,7 @@ msgstr "An error occurred while saving your preferences. Please try again, or co msgid "An error occurred while saving your settings. Please try again, or contact support if the problem persists." msgstr "An error occurred while saving your settings. Please try again, or contact support if the problem persists." -#: src/routes/(root)/[handle]/settings/invite.tsx:178 +#: src/routes/(root)/[handle]/settings/invite.tsx:180 msgid "An unexpected error occurred. Please try again later." msgstr "An unexpected error occurred. Please try again later." @@ -215,7 +215,7 @@ msgstr "Bio is too long. Maximum length is 512 characters." msgid "Cancel" msgstr "Cancel" -#: src/routes/(root)/[handle]/settings/invite.tsx:305 +#: src/routes/(root)/[handle]/settings/invite.tsx:307 msgid "Choose the language your friend prefers. This language will only be used for the invitation." msgstr "Choose the language your friend prefers. This language will only be used for the invitation." @@ -263,7 +263,7 @@ msgstr "Do you need an account? Hackers' Pub is invite-only—please ask a frien msgid "Drag to select the area you want to keep, then click “Crop” to update your avatar." msgstr "Drag to select the area you want to keep, then click “Crop” to update your avatar." -#: src/routes/(root)/[handle]/settings/invite.tsx:254 +#: src/routes/(root)/[handle]/settings/invite.tsx:256 #: src/routes/(root)/sign/up/[token].tsx:337 msgid "Email address" msgstr "Email address" @@ -284,7 +284,7 @@ msgstr "Error" msgid "ex) My key" msgstr "ex) My key" -#: src/routes/(root)/[handle]/settings/invite.tsx:310 +#: src/routes/(root)/[handle]/settings/invite.tsx:312 msgid "Extra message" msgstr "Extra message" @@ -330,8 +330,8 @@ msgstr "Failed to save preferences" msgid "Failed to save settings" msgstr "Failed to save settings" -#: src/routes/(root)/[handle]/settings/invite.tsx:156 -#: src/routes/(root)/[handle]/settings/invite.tsx:176 +#: src/routes/(root)/[handle]/settings/invite.tsx:158 +#: src/routes/(root)/[handle]/settings/invite.tsx:178 msgid "Failed to send invitation" msgstr "Failed to send invitation" @@ -388,17 +388,17 @@ msgstr "I have read and agree to the Code of conduct." msgid "If enabled, the AI will generate a summary of the article for you. Otherwise, the first few lines of the article will be used as the summary." msgstr "If enabled, the AI will generate a summary of the article for you. Otherwise, the first few lines of the article will be used as the summary." -#: src/routes/(root)/[handle]/settings/invite.tsx:298 +#: src/routes/(root)/[handle]/settings/invite.tsx:300 msgid "Invitation language" msgstr "Invitation language" -#: src/routes/(root)/[handle]/settings/invite.tsx:166 +#: src/routes/(root)/[handle]/settings/invite.tsx:168 msgid "Invitation sent" msgstr "Invitation sent" #: src/components/SettingsTabs.tsx:51 -#: src/routes/(root)/[handle]/settings/invite.tsx:210 -#: src/routes/(root)/[handle]/settings/invite.tsx:221 +#: src/routes/(root)/[handle]/settings/invite.tsx:212 +#: src/routes/(root)/[handle]/settings/invite.tsx:223 msgid "Invite" msgstr "Invite" @@ -494,7 +494,7 @@ msgstr "Never used" msgid "No followers found" msgstr "No followers found" -#: src/routes/(root)/[handle]/settings/invite.tsx:328 +#: src/routes/(root)/[handle]/settings/invite.tsx:330 msgid "No invitations left" msgstr "No invitations left" @@ -570,7 +570,7 @@ msgstr "Passkeys" msgid "Please choose an image file smaller than 5 MiB." msgstr "Please choose an image file smaller than 5 MiB." -#: src/routes/(root)/[handle]/settings/invite.tsx:157 +#: src/routes/(root)/[handle]/settings/invite.tsx:159 msgid "Please correct the errors and try again." msgstr "Please correct the errors and try again." @@ -650,17 +650,17 @@ msgstr "Save" msgid "Saving…" msgstr "Saving…" -#: src/routes/(root)/[handle]/settings/invite.tsx:331 +#: src/routes/(root)/[handle]/settings/invite.tsx:333 msgid "Send" msgstr "Send" -#: src/routes/(root)/[handle]/settings/invite.tsx:330 +#: src/routes/(root)/[handle]/settings/invite.tsx:332 msgid "Sending…" msgstr "Sending…" #: src/components/AppSidebar.tsx:288 #: src/routes/(root)/[handle]/settings/index.tsx:152 -#: src/routes/(root)/[handle]/settings/invite.tsx:215 +#: src/routes/(root)/[handle]/settings/invite.tsx:217 #: src/routes/(root)/[handle]/settings/passkeys.tsx:379 #: src/routes/(root)/[handle]/settings/preferences.tsx:181 msgid "Settings" @@ -732,11 +732,11 @@ msgstr "The default privacy setting for your notes." msgid "The default privacy setting for your shares." msgstr "The default privacy setting for your shares." -#: src/routes/(root)/[handle]/settings/invite.tsx:272 +#: src/routes/(root)/[handle]/settings/invite.tsx:274 msgid "The email address is invalid." msgstr "The email address is invalid." -#: src/routes/(root)/[handle]/settings/invite.tsx:267 +#: src/routes/(root)/[handle]/settings/invite.tsx:269 msgid "The email address is not only used for receiving the invitation, but also for signing in to the account." msgstr "The email address is not only used for receiving the invitation, but also for signing in to the account." @@ -744,7 +744,7 @@ msgstr "The email address is not only used for receiving the invitation, but als msgid "The following passkeys are registered to your account. You can use them to sign in to your account." msgstr "The following passkeys are registered to your account. You can use them to sign in to your account." -#: src/routes/(root)/[handle]/settings/invite.tsx:167 +#: src/routes/(root)/[handle]/settings/invite.tsx:169 msgid "The invitation has been sent successfully." msgstr "The invitation has been sent successfully." @@ -846,7 +846,7 @@ msgstr "You can change it only once, and the old username will become available msgid "You can leave this empty to remove the link." msgstr "You can leave this empty to remove the link." -#: src/routes/(root)/[handle]/settings/invite.tsx:316 +#: src/routes/(root)/[handle]/settings/invite.tsx:318 msgid "You can leave this field empty." msgstr "You can leave this field empty." @@ -854,8 +854,8 @@ msgstr "You can leave this field empty." msgid "You don't have any passkeys registered yet." msgstr "You don't have any passkeys registered yet." -#: src/routes/(root)/[handle]/settings/invite.tsx:235 -#: src/routes/(root)/[handle]/settings/invite.tsx:337 +#: src/routes/(root)/[handle]/settings/invite.tsx:237 +#: src/routes/(root)/[handle]/settings/invite.tsx:339 msgid "You have no invitations left. Please wait until you receive more." msgstr "You have no invitations left. Please wait until you receive more." @@ -883,7 +883,7 @@ msgstr "Your bio will be displayed on your profile. You can use Markdown to form msgid "Your email address will be used to sign in to your account." msgstr "Your email address will be used to sign in to your account." -#: src/routes/(root)/[handle]/settings/invite.tsx:319 +#: src/routes/(root)/[handle]/settings/invite.tsx:321 msgid "Your friend will see this message in the invitation email." msgstr "Your friend will see this message in the invitation email." diff --git a/web-next/src/locales/ja-JP/messages.po b/web-next/src/locales/ja-JP/messages.po index b0e3d1fe..3018eaf0 100644 --- a/web-next/src/locales/ja-JP/messages.po +++ b/web-next/src/locales/ja-JP/messages.po @@ -24,7 +24,7 @@ msgid "{0, plural, one {# following} other {# following}}" msgstr "{0, plural, other {#フォロー}}" #. placeholder {0}: account().invitationsLeft -#: src/routes/(root)/[handle]/settings/invite.tsx:237 +#: src/routes/(root)/[handle]/settings/invite.tsx:239 msgid "{0, plural, one {Invite your friends to Hackers' Pub. You can invite up to # person.} other {Invite your friends to Hackers' Pub. You can invite up to # people.}}" msgstr "{0, plural, one {友達をHackers' Pubに招待しましょう。最大#人まで招待できます。} other {友達をHackers' Pubに招待しましょう。最大#人まで招待できます。}}" @@ -71,7 +71,7 @@ msgid "{0} followed you" msgstr "{0}さんがあなたをフォローしました" #. placeholder {0}: "USER" -#: src/routes/(root)/[handle]/settings/invite.tsx:281 +#: src/routes/(root)/[handle]/settings/invite.tsx:283 msgid "{0} is already a member of Hackers' Pub." msgstr "{0}さんは既にHackers' Pubのメンバーです。" @@ -170,7 +170,7 @@ msgstr "環境設定の保存中にエラーが発生しました。再度お試 msgid "An error occurred while saving your settings. Please try again, or contact support if the problem persists." msgstr "設定の保存中にエラーが発生しました。再度お試しいただくか、問題が解決しない場合はサポートにお問い合わせください。" -#: src/routes/(root)/[handle]/settings/invite.tsx:178 +#: src/routes/(root)/[handle]/settings/invite.tsx:180 msgid "An unexpected error occurred. Please try again later." msgstr "予期しないエラーが発生しました。後でもう一度お試しください。" @@ -215,7 +215,7 @@ msgstr "自己紹介が長すぎます。最大512文字です。" msgid "Cancel" msgstr "キャンセル" -#: src/routes/(root)/[handle]/settings/invite.tsx:305 +#: src/routes/(root)/[handle]/settings/invite.tsx:307 msgid "Choose the language your friend prefers. This language will only be used for the invitation." msgstr "招待する友達の使用言語を選択してください。この言語は招待状にのみ使用されます。" @@ -263,7 +263,7 @@ msgstr "アカウントが必要ですか?Hackers' Pubは招待制ですので msgid "Drag to select the area you want to keep, then click “Crop” to update your avatar." msgstr "保持したい領域をドラッグして選択し、「切り抜き」をクリックしてアイコンを更新してください。" -#: src/routes/(root)/[handle]/settings/invite.tsx:254 +#: src/routes/(root)/[handle]/settings/invite.tsx:256 #: src/routes/(root)/sign/up/[token].tsx:337 msgid "Email address" msgstr "メールアドレス" @@ -284,7 +284,7 @@ msgstr "エラー" msgid "ex) My key" msgstr "例) 私のキー" -#: src/routes/(root)/[handle]/settings/invite.tsx:310 +#: src/routes/(root)/[handle]/settings/invite.tsx:312 msgid "Extra message" msgstr "追加メッセージ" @@ -330,8 +330,8 @@ msgstr "環境設定の保存に失敗しました" msgid "Failed to save settings" msgstr "設定の保存に失敗しました" -#: src/routes/(root)/[handle]/settings/invite.tsx:156 -#: src/routes/(root)/[handle]/settings/invite.tsx:176 +#: src/routes/(root)/[handle]/settings/invite.tsx:158 +#: src/routes/(root)/[handle]/settings/invite.tsx:178 msgid "Failed to send invitation" msgstr "招待状の送信に失敗しました" @@ -388,17 +388,17 @@ msgstr "行動規範に同意します。" msgid "If enabled, the AI will generate a summary of the article for you. Otherwise, the first few lines of the article will be used as the summary." msgstr "有効にすると、AIが記事の要約を生成します。無効の場合は、記事の最初の数行が要約として使用されます。" -#: src/routes/(root)/[handle]/settings/invite.tsx:298 +#: src/routes/(root)/[handle]/settings/invite.tsx:300 msgid "Invitation language" msgstr "招待状の言語" -#: src/routes/(root)/[handle]/settings/invite.tsx:166 +#: src/routes/(root)/[handle]/settings/invite.tsx:168 msgid "Invitation sent" msgstr "招待状を送信しました" #: src/components/SettingsTabs.tsx:51 -#: src/routes/(root)/[handle]/settings/invite.tsx:210 -#: src/routes/(root)/[handle]/settings/invite.tsx:221 +#: src/routes/(root)/[handle]/settings/invite.tsx:212 +#: src/routes/(root)/[handle]/settings/invite.tsx:223 msgid "Invite" msgstr "招待" @@ -494,7 +494,7 @@ msgstr "使用履歴なし" msgid "No followers found" msgstr "フォロワーはいません" -#: src/routes/(root)/[handle]/settings/invite.tsx:328 +#: src/routes/(root)/[handle]/settings/invite.tsx:330 msgid "No invitations left" msgstr "招待状が残っていません" @@ -570,7 +570,7 @@ msgstr "パスキー" msgid "Please choose an image file smaller than 5 MiB." msgstr "5 MiB未満の画像ファイルを選択してください。" -#: src/routes/(root)/[handle]/settings/invite.tsx:157 +#: src/routes/(root)/[handle]/settings/invite.tsx:159 msgid "Please correct the errors and try again." msgstr "エラーを修正して、もう一度お試しください。" @@ -650,17 +650,17 @@ msgstr "保存" msgid "Saving…" msgstr "保存中…" -#: src/routes/(root)/[handle]/settings/invite.tsx:331 +#: src/routes/(root)/[handle]/settings/invite.tsx:333 msgid "Send" msgstr "送信" -#: src/routes/(root)/[handle]/settings/invite.tsx:330 +#: src/routes/(root)/[handle]/settings/invite.tsx:332 msgid "Sending…" msgstr "送信中…" #: src/components/AppSidebar.tsx:288 #: src/routes/(root)/[handle]/settings/index.tsx:152 -#: src/routes/(root)/[handle]/settings/invite.tsx:215 +#: src/routes/(root)/[handle]/settings/invite.tsx:217 #: src/routes/(root)/[handle]/settings/passkeys.tsx:379 #: src/routes/(root)/[handle]/settings/preferences.tsx:181 msgid "Settings" @@ -732,11 +732,11 @@ msgstr "投稿のデフォルト公開設定です。" msgid "The default privacy setting for your shares." msgstr "共有のデフォルト公開設定です。" -#: src/routes/(root)/[handle]/settings/invite.tsx:272 +#: src/routes/(root)/[handle]/settings/invite.tsx:274 msgid "The email address is invalid." msgstr "メールアドレスが無効です。" -#: src/routes/(root)/[handle]/settings/invite.tsx:267 +#: src/routes/(root)/[handle]/settings/invite.tsx:269 msgid "The email address is not only used for receiving the invitation, but also for signing in to the account." msgstr "メールアドレスは招待状を受け取るだけでなく、アカウントへのログインにも使用されます。" @@ -744,7 +744,7 @@ msgstr "メールアドレスは招待状を受け取るだけでなく、アカ msgid "The following passkeys are registered to your account. You can use them to sign in to your account." msgstr "以下のパスキーがあなたのアカウントに登録されています。これらを使用してアカウントにログインできます。" -#: src/routes/(root)/[handle]/settings/invite.tsx:167 +#: src/routes/(root)/[handle]/settings/invite.tsx:169 msgid "The invitation has been sent successfully." msgstr "招待状が正常に送信されました。" @@ -846,7 +846,7 @@ msgstr "変更は1回のみ可能で、変更前のユーザー名は他のユ msgid "You can leave this empty to remove the link." msgstr "リンクを削除する場合は空にしてください。" -#: src/routes/(root)/[handle]/settings/invite.tsx:316 +#: src/routes/(root)/[handle]/settings/invite.tsx:318 msgid "You can leave this field empty." msgstr "このフィールドは空欄でも構いません。" @@ -854,8 +854,8 @@ msgstr "このフィールドは空欄でも構いません。" msgid "You don't have any passkeys registered yet." msgstr "まだパスキーが登録されていません。" -#: src/routes/(root)/[handle]/settings/invite.tsx:235 -#: src/routes/(root)/[handle]/settings/invite.tsx:337 +#: src/routes/(root)/[handle]/settings/invite.tsx:237 +#: src/routes/(root)/[handle]/settings/invite.tsx:339 msgid "You have no invitations left. Please wait until you receive more." msgstr "招待状が残っていません。追加されるまでお待ちください。" @@ -883,7 +883,7 @@ msgstr "自己紹介はプロフィールに表示されます。Markdownで書 msgid "Your email address will be used to sign in to your account." msgstr "メールアドレスはアカウントへのログインに使用されます。" -#: src/routes/(root)/[handle]/settings/invite.tsx:319 +#: src/routes/(root)/[handle]/settings/invite.tsx:321 msgid "Your friend will see this message in the invitation email." msgstr "友達は招待メールでこのメッセージを見ることができます。" diff --git a/web-next/src/locales/ko-KR/messages.po b/web-next/src/locales/ko-KR/messages.po index 356b3ad4..96e35f51 100644 --- a/web-next/src/locales/ko-KR/messages.po +++ b/web-next/src/locales/ko-KR/messages.po @@ -24,7 +24,7 @@ msgid "{0, plural, one {# following} other {# following}}" msgstr "{0, plural, other {# 팔로잉}}" #. placeholder {0}: account().invitationsLeft -#: src/routes/(root)/[handle]/settings/invite.tsx:237 +#: src/routes/(root)/[handle]/settings/invite.tsx:239 msgid "{0, plural, one {Invite your friends to Hackers' Pub. You can invite up to # person.} other {Invite your friends to Hackers' Pub. You can invite up to # people.}}" msgstr "{0, plural, other {Hackers' Pub에 친구를 초대하세요. 최대 #명까지 초대할 수 있습니다.}}" @@ -71,7 +71,7 @@ msgid "{0} followed you" msgstr "{0} 님이 팔로했습니다" #. placeholder {0}: "USER" -#: src/routes/(root)/[handle]/settings/invite.tsx:281 +#: src/routes/(root)/[handle]/settings/invite.tsx:283 msgid "{0} is already a member of Hackers' Pub." msgstr "{0} 님은 이미 Hackers' Pub의 회원입니다." @@ -170,7 +170,7 @@ msgstr "환경 설정 저장 중 오류가 발생했습니다. 다시 시도하 msgid "An error occurred while saving your settings. Please try again, or contact support if the problem persists." msgstr "설정 저장 중 오류가 발생했습니다. 다시 시도하거나 문제가 지속되면 지원팀에 문의하세요." -#: src/routes/(root)/[handle]/settings/invite.tsx:178 +#: src/routes/(root)/[handle]/settings/invite.tsx:180 msgid "An unexpected error occurred. Please try again later." msgstr "예상치 못한 오류가 발생했습니다. 나중에 다시 시도해주세요." @@ -215,7 +215,7 @@ msgstr "약력이 너무 깁니다. 최대 길이는 512자입니다." msgid "Cancel" msgstr "취소" -#: src/routes/(root)/[handle]/settings/invite.tsx:305 +#: src/routes/(root)/[handle]/settings/invite.tsx:307 msgid "Choose the language your friend prefers. This language will only be used for the invitation." msgstr "초대장을 받을 친구가 사용하는 언어를 선택하세요. 이 언어는 초대장에만 사용됩니다." @@ -263,7 +263,7 @@ msgstr "계정이 필요하신가요? Hackers' Pub은 초대 전용입니다. msgid "Drag to select the area you want to keep, then click “Crop” to update your avatar." msgstr "유지하려는 영역을 드래그하여 선택한 다음 “자르기”를 클릭하여 프로필 사진을 업데이트하세요." -#: src/routes/(root)/[handle]/settings/invite.tsx:254 +#: src/routes/(root)/[handle]/settings/invite.tsx:256 #: src/routes/(root)/sign/up/[token].tsx:337 msgid "Email address" msgstr "이메일 주소" @@ -284,7 +284,7 @@ msgstr "오류" msgid "ex) My key" msgstr "예) 나의 키" -#: src/routes/(root)/[handle]/settings/invite.tsx:310 +#: src/routes/(root)/[handle]/settings/invite.tsx:312 msgid "Extra message" msgstr "추가 메시지" @@ -330,8 +330,8 @@ msgstr "환경 설정 저장 실패" msgid "Failed to save settings" msgstr "설정 저장 실패" -#: src/routes/(root)/[handle]/settings/invite.tsx:156 -#: src/routes/(root)/[handle]/settings/invite.tsx:176 +#: src/routes/(root)/[handle]/settings/invite.tsx:158 +#: src/routes/(root)/[handle]/settings/invite.tsx:178 msgid "Failed to send invitation" msgstr "초대장 발송에 실패했습니다" @@ -388,17 +388,17 @@ msgstr "Hackers' Pub의 행동 강령을 읽고 동의합니다." msgid "If enabled, the AI will generate a summary of the article for you. Otherwise, the first few lines of the article will be used as the summary." msgstr "활성화하면 AI가 글의 요약을 생성합니다. 비활성화 시 글의 처음 몇 줄이 요약으로 사용됩니다." -#: src/routes/(root)/[handle]/settings/invite.tsx:298 +#: src/routes/(root)/[handle]/settings/invite.tsx:300 msgid "Invitation language" msgstr "초대장 언어" -#: src/routes/(root)/[handle]/settings/invite.tsx:166 +#: src/routes/(root)/[handle]/settings/invite.tsx:168 msgid "Invitation sent" msgstr "초대장이 발송되었습니다" #: src/components/SettingsTabs.tsx:51 -#: src/routes/(root)/[handle]/settings/invite.tsx:210 -#: src/routes/(root)/[handle]/settings/invite.tsx:221 +#: src/routes/(root)/[handle]/settings/invite.tsx:212 +#: src/routes/(root)/[handle]/settings/invite.tsx:223 msgid "Invite" msgstr "초대" @@ -494,7 +494,7 @@ msgstr "사용된 적 없음" msgid "No followers found" msgstr "팔로워가 없습니다" -#: src/routes/(root)/[handle]/settings/invite.tsx:328 +#: src/routes/(root)/[handle]/settings/invite.tsx:330 msgid "No invitations left" msgstr "남은 초대장이 없습니다" @@ -570,7 +570,7 @@ msgstr "패스키" msgid "Please choose an image file smaller than 5 MiB." msgstr "5 MiB 미만의 이미지 파일을 선택해주세요." -#: src/routes/(root)/[handle]/settings/invite.tsx:157 +#: src/routes/(root)/[handle]/settings/invite.tsx:159 msgid "Please correct the errors and try again." msgstr "오류를 수정하고 다시 시도해주세요." @@ -650,17 +650,17 @@ msgstr "저장" msgid "Saving…" msgstr "저장 중…" -#: src/routes/(root)/[handle]/settings/invite.tsx:331 +#: src/routes/(root)/[handle]/settings/invite.tsx:333 msgid "Send" msgstr "보내기" -#: src/routes/(root)/[handle]/settings/invite.tsx:330 +#: src/routes/(root)/[handle]/settings/invite.tsx:332 msgid "Sending…" msgstr "보내는 중…" #: src/components/AppSidebar.tsx:288 #: src/routes/(root)/[handle]/settings/index.tsx:152 -#: src/routes/(root)/[handle]/settings/invite.tsx:215 +#: src/routes/(root)/[handle]/settings/invite.tsx:217 #: src/routes/(root)/[handle]/settings/passkeys.tsx:379 #: src/routes/(root)/[handle]/settings/preferences.tsx:181 msgid "Settings" @@ -732,11 +732,11 @@ msgstr "단문의 기본 공개 설정입니다." msgid "The default privacy setting for your shares." msgstr "공유의 기본 공개 설정입니다." -#: src/routes/(root)/[handle]/settings/invite.tsx:272 +#: src/routes/(root)/[handle]/settings/invite.tsx:274 msgid "The email address is invalid." msgstr "이메일 주소가 유효하지 않습니다." -#: src/routes/(root)/[handle]/settings/invite.tsx:267 +#: src/routes/(root)/[handle]/settings/invite.tsx:269 msgid "The email address is not only used for receiving the invitation, but also for signing in to the account." msgstr "이메일 주소는 초대장을 받을 때 뿐만 아니라, 계정에 로그인할 때도 사용됩니다." @@ -744,7 +744,7 @@ msgstr "이메일 주소는 초대장을 받을 때 뿐만 아니라, 계정에 msgid "The following passkeys are registered to your account. You can use them to sign in to your account." msgstr "다음 패스키들이 계정에 등록되어 있습니다. 이들을 사용하여 계정에 로그인할 수 있습니다." -#: src/routes/(root)/[handle]/settings/invite.tsx:167 +#: src/routes/(root)/[handle]/settings/invite.tsx:169 msgid "The invitation has been sent successfully." msgstr "초대장이 성공적으로 발송되었습니다." @@ -846,7 +846,7 @@ msgstr "아이디는 단 한 번만 변경할 수 있으며, 변경하기 전 msgid "You can leave this empty to remove the link." msgstr "링크를 삭제하려면 이곳을 비워두세요." -#: src/routes/(root)/[handle]/settings/invite.tsx:316 +#: src/routes/(root)/[handle]/settings/invite.tsx:318 msgid "You can leave this field empty." msgstr "이 필드는 비워둘 수 있습니다." @@ -854,8 +854,8 @@ msgstr "이 필드는 비워둘 수 있습니다." msgid "You don't have any passkeys registered yet." msgstr "등록된 패스키가 아직 없습니다." -#: src/routes/(root)/[handle]/settings/invite.tsx:235 -#: src/routes/(root)/[handle]/settings/invite.tsx:337 +#: src/routes/(root)/[handle]/settings/invite.tsx:237 +#: src/routes/(root)/[handle]/settings/invite.tsx:339 msgid "You have no invitations left. Please wait until you receive more." msgstr "남은 초대장이 없습니다. 추가로 받을 때까지 기다려주세요." @@ -883,7 +883,7 @@ msgstr "약력은 프로필에 표시됩니다. Markdown을 사용할 수 있습 msgid "Your email address will be used to sign in to your account." msgstr "이메일 주소는 계정에 로그인할 때 사용됩니다." -#: src/routes/(root)/[handle]/settings/invite.tsx:319 +#: src/routes/(root)/[handle]/settings/invite.tsx:321 msgid "Your friend will see this message in the invitation email." msgstr "초대장을 받는 친구가 볼 수 있는 메시지입니다." diff --git a/web-next/src/locales/zh-CN/messages.po b/web-next/src/locales/zh-CN/messages.po index 211ce708..4d25f978 100644 --- a/web-next/src/locales/zh-CN/messages.po +++ b/web-next/src/locales/zh-CN/messages.po @@ -24,7 +24,7 @@ msgid "{0, plural, one {# following} other {# following}}" msgstr "{0, plural, other {关注 # 人}}" #. placeholder {0}: account().invitationsLeft -#: src/routes/(root)/[handle]/settings/invite.tsx:237 +#: src/routes/(root)/[handle]/settings/invite.tsx:239 msgid "{0, plural, one {Invite your friends to Hackers' Pub. You can invite up to # person.} other {Invite your friends to Hackers' Pub. You can invite up to # people.}}" msgstr "{0, plural, other {邀请你的朋友加入 Hackers' Pub。你可以邀请最多 # 个人。}}" @@ -71,7 +71,7 @@ msgid "{0} followed you" msgstr "{0} 关注了你" #. placeholder {0}: "USER" -#: src/routes/(root)/[handle]/settings/invite.tsx:281 +#: src/routes/(root)/[handle]/settings/invite.tsx:283 msgid "{0} is already a member of Hackers' Pub." msgstr "{0} 已经是 Hackers' Pub 的成员。" @@ -170,7 +170,7 @@ msgstr "保存设置时出现错误。请重试,如果问题持续存在,请 msgid "An error occurred while saving your settings. Please try again, or contact support if the problem persists." msgstr "保存设置时发生错误。请重试,如果问题仍然存在,请联系客服。" -#: src/routes/(root)/[handle]/settings/invite.tsx:178 +#: src/routes/(root)/[handle]/settings/invite.tsx:180 msgid "An unexpected error occurred. Please try again later." msgstr "发生了意外错误。请稍后再试。" @@ -215,7 +215,7 @@ msgstr "个人简介太长。不能长于 512 字符。" msgid "Cancel" msgstr "取消" -#: src/routes/(root)/[handle]/settings/invite.tsx:305 +#: src/routes/(root)/[handle]/settings/invite.tsx:307 msgid "Choose the language your friend prefers. This language will only be used for the invitation." msgstr "选择你的朋友使用的语言。这个语言只会用于邀请。" @@ -263,7 +263,7 @@ msgstr "需要创建账户吗?Hackers' Pub 仅限邀请,请联系朋友邀 msgid "Drag to select the area you want to keep, then click “Crop” to update your avatar." msgstr "拖动选择要保留的区域,然后点击「裁剪」来更新你的头像。" -#: src/routes/(root)/[handle]/settings/invite.tsx:254 +#: src/routes/(root)/[handle]/settings/invite.tsx:256 #: src/routes/(root)/sign/up/[token].tsx:337 msgid "Email address" msgstr "电子邮件地址" @@ -284,7 +284,7 @@ msgstr "错误" msgid "ex) My key" msgstr "例如:我的通行密钥" -#: src/routes/(root)/[handle]/settings/invite.tsx:310 +#: src/routes/(root)/[handle]/settings/invite.tsx:312 msgid "Extra message" msgstr "额外消息" @@ -330,8 +330,8 @@ msgstr "保存设置失败" msgid "Failed to save settings" msgstr "保存设置失败" -#: src/routes/(root)/[handle]/settings/invite.tsx:156 -#: src/routes/(root)/[handle]/settings/invite.tsx:176 +#: src/routes/(root)/[handle]/settings/invite.tsx:158 +#: src/routes/(root)/[handle]/settings/invite.tsx:178 msgid "Failed to send invitation" msgstr "发送邀请失败" @@ -388,17 +388,17 @@ msgstr "我同意 Hackers' Pub 的行为准则。" msgid "If enabled, the AI will generate a summary of the article for you. Otherwise, the first few lines of the article will be used as the summary." msgstr "启用后,AI 将为您生成文章摘要。否则,将使用文章的前几行作为摘要。" -#: src/routes/(root)/[handle]/settings/invite.tsx:298 +#: src/routes/(root)/[handle]/settings/invite.tsx:300 msgid "Invitation language" msgstr "邀请语言" -#: src/routes/(root)/[handle]/settings/invite.tsx:166 +#: src/routes/(root)/[handle]/settings/invite.tsx:168 msgid "Invitation sent" msgstr "邀请已发送" #: src/components/SettingsTabs.tsx:51 -#: src/routes/(root)/[handle]/settings/invite.tsx:210 -#: src/routes/(root)/[handle]/settings/invite.tsx:221 +#: src/routes/(root)/[handle]/settings/invite.tsx:212 +#: src/routes/(root)/[handle]/settings/invite.tsx:223 msgid "Invite" msgstr "邀请" @@ -494,7 +494,7 @@ msgstr "从未使用过" msgid "No followers found" msgstr "未找到粉丝" -#: src/routes/(root)/[handle]/settings/invite.tsx:328 +#: src/routes/(root)/[handle]/settings/invite.tsx:330 msgid "No invitations left" msgstr "没有剩余邀请名额" @@ -570,7 +570,7 @@ msgstr "通行密钥" msgid "Please choose an image file smaller than 5 MiB." msgstr "请选择小于 5 MiB 的图片文件。" -#: src/routes/(root)/[handle]/settings/invite.tsx:157 +#: src/routes/(root)/[handle]/settings/invite.tsx:159 msgid "Please correct the errors and try again." msgstr "请修正错误并重试。" @@ -650,17 +650,17 @@ msgstr "保存" msgid "Saving…" msgstr "保存中…" -#: src/routes/(root)/[handle]/settings/invite.tsx:331 +#: src/routes/(root)/[handle]/settings/invite.tsx:333 msgid "Send" msgstr "发送" -#: src/routes/(root)/[handle]/settings/invite.tsx:330 +#: src/routes/(root)/[handle]/settings/invite.tsx:332 msgid "Sending…" msgstr "发送中…" #: src/components/AppSidebar.tsx:288 #: src/routes/(root)/[handle]/settings/index.tsx:152 -#: src/routes/(root)/[handle]/settings/invite.tsx:215 +#: src/routes/(root)/[handle]/settings/invite.tsx:217 #: src/routes/(root)/[handle]/settings/passkeys.tsx:379 #: src/routes/(root)/[handle]/settings/preferences.tsx:181 msgid "Settings" @@ -732,11 +732,11 @@ msgstr "您帖子的默认隐私设置。" msgid "The default privacy setting for your shares." msgstr "您转帖的默认隐私设置。" -#: src/routes/(root)/[handle]/settings/invite.tsx:272 +#: src/routes/(root)/[handle]/settings/invite.tsx:274 msgid "The email address is invalid." msgstr "电子邮件地址无效。" -#: src/routes/(root)/[handle]/settings/invite.tsx:267 +#: src/routes/(root)/[handle]/settings/invite.tsx:269 msgid "The email address is not only used for receiving the invitation, but also for signing in to the account." msgstr "电子邮件地址不仅用于接收邀请,还用于登录账户。" @@ -744,7 +744,7 @@ msgstr "电子邮件地址不仅用于接收邀请,还用于登录账户。" msgid "The following passkeys are registered to your account. You can use them to sign in to your account." msgstr "以下通行密钥已注册到你的账户。你可以使用它们登录你的账户。" -#: src/routes/(root)/[handle]/settings/invite.tsx:167 +#: src/routes/(root)/[handle]/settings/invite.tsx:169 msgid "The invitation has been sent successfully." msgstr "邀请已成功发送。" @@ -846,7 +846,7 @@ msgstr "你只能更改一次用户名,而旧的用户名会公开为别人使 msgid "You can leave this empty to remove the link." msgstr "您可以将此处留空以删除链接。" -#: src/routes/(root)/[handle]/settings/invite.tsx:316 +#: src/routes/(root)/[handle]/settings/invite.tsx:318 msgid "You can leave this field empty." msgstr "你可以留空此字段。" @@ -854,8 +854,8 @@ msgstr "你可以留空此字段。" msgid "You don't have any passkeys registered yet." msgstr "您尚未注册任何通行密钥。" -#: src/routes/(root)/[handle]/settings/invite.tsx:235 -#: src/routes/(root)/[handle]/settings/invite.tsx:337 +#: src/routes/(root)/[handle]/settings/invite.tsx:237 +#: src/routes/(root)/[handle]/settings/invite.tsx:339 msgid "You have no invitations left. Please wait until you receive more." msgstr "你没有剩余邀请名额。请稍后再试。" @@ -883,7 +883,7 @@ msgstr "你的个人简介将在你的个人资料页面显示。你可以用 Ma msgid "Your email address will be used to sign in to your account." msgstr "你的电子邮件地址将用于登录。" -#: src/routes/(root)/[handle]/settings/invite.tsx:319 +#: src/routes/(root)/[handle]/settings/invite.tsx:321 msgid "Your friend will see this message in the invitation email." msgstr "你的朋友将在邀请邮件中看到此信息。" diff --git a/web-next/src/locales/zh-TW/messages.po b/web-next/src/locales/zh-TW/messages.po index b8aefa53..5767f093 100644 --- a/web-next/src/locales/zh-TW/messages.po +++ b/web-next/src/locales/zh-TW/messages.po @@ -24,7 +24,7 @@ msgid "{0, plural, one {# following} other {# following}}" msgstr "{0, plural, other {關注 # 人}}" #. placeholder {0}: account().invitationsLeft -#: src/routes/(root)/[handle]/settings/invite.tsx:237 +#: src/routes/(root)/[handle]/settings/invite.tsx:239 msgid "{0, plural, one {Invite your friends to Hackers' Pub. You can invite up to # person.} other {Invite your friends to Hackers' Pub. You can invite up to # people.}}" msgstr "{0, plural, other {邀請你的朋友加入 Hackers' Pub。你可以邀請最多 # 個人。}}" @@ -71,7 +71,7 @@ msgid "{0} followed you" msgstr "{0} 關注了你" #. placeholder {0}: "USER" -#: src/routes/(root)/[handle]/settings/invite.tsx:281 +#: src/routes/(root)/[handle]/settings/invite.tsx:283 msgid "{0} is already a member of Hackers' Pub." msgstr "{0} 已經是 Hackers' Pub 的成員。" @@ -170,7 +170,7 @@ msgstr "儲存設定時發生錯誤。請重試,如果問題持續存在,請 msgid "An error occurred while saving your settings. Please try again, or contact support if the problem persists." msgstr "儲存設定時發生錯誤。請重試,如果問題仍然存在,請聯繫客服。" -#: src/routes/(root)/[handle]/settings/invite.tsx:178 +#: src/routes/(root)/[handle]/settings/invite.tsx:180 msgid "An unexpected error occurred. Please try again later." msgstr "發生了意外錯誤。請稍後再試。" @@ -215,7 +215,7 @@ msgstr "個人簡介太長。不能長於 512 字元。" msgid "Cancel" msgstr "取消" -#: src/routes/(root)/[handle]/settings/invite.tsx:305 +#: src/routes/(root)/[handle]/settings/invite.tsx:307 msgid "Choose the language your friend prefers. This language will only be used for the invitation." msgstr "選擇你的朋友使用的語言。這個語言只會用於邀請。" @@ -263,7 +263,7 @@ msgstr "需要建立帳戶嗎?Hackers' Pub 僅限邀請,請聯繫朋友邀 msgid "Drag to select the area you want to keep, then click “Crop” to update your avatar." msgstr "拖動選擇要保留的區域,然後點擊「裁剪」來更新你的頭像。" -#: src/routes/(root)/[handle]/settings/invite.tsx:254 +#: src/routes/(root)/[handle]/settings/invite.tsx:256 #: src/routes/(root)/sign/up/[token].tsx:337 msgid "Email address" msgstr "電子郵件地址" @@ -284,7 +284,7 @@ msgstr "錯誤" msgid "ex) My key" msgstr "例如:我的通行金鑰" -#: src/routes/(root)/[handle]/settings/invite.tsx:310 +#: src/routes/(root)/[handle]/settings/invite.tsx:312 msgid "Extra message" msgstr "額外訊息" @@ -330,8 +330,8 @@ msgstr "儲存設定失敗" msgid "Failed to save settings" msgstr "儲存設定失敗" -#: src/routes/(root)/[handle]/settings/invite.tsx:156 -#: src/routes/(root)/[handle]/settings/invite.tsx:176 +#: src/routes/(root)/[handle]/settings/invite.tsx:158 +#: src/routes/(root)/[handle]/settings/invite.tsx:178 msgid "Failed to send invitation" msgstr "發送邀請失敗" @@ -388,17 +388,17 @@ msgstr "我同意 Hackers' Pub 的行為準則。" msgid "If enabled, the AI will generate a summary of the article for you. Otherwise, the first few lines of the article will be used as the summary." msgstr "啟用後,AI 將為您生成文章摘要。否則,將使用文章的前幾行作為摘要。" -#: src/routes/(root)/[handle]/settings/invite.tsx:298 +#: src/routes/(root)/[handle]/settings/invite.tsx:300 msgid "Invitation language" msgstr "邀請語言" -#: src/routes/(root)/[handle]/settings/invite.tsx:166 +#: src/routes/(root)/[handle]/settings/invite.tsx:168 msgid "Invitation sent" msgstr "邀請已發送" #: src/components/SettingsTabs.tsx:51 -#: src/routes/(root)/[handle]/settings/invite.tsx:210 -#: src/routes/(root)/[handle]/settings/invite.tsx:221 +#: src/routes/(root)/[handle]/settings/invite.tsx:212 +#: src/routes/(root)/[handle]/settings/invite.tsx:223 msgid "Invite" msgstr "邀請" @@ -494,7 +494,7 @@ msgstr "從未使用過" msgid "No followers found" msgstr "未找到粉絲" -#: src/routes/(root)/[handle]/settings/invite.tsx:328 +#: src/routes/(root)/[handle]/settings/invite.tsx:330 msgid "No invitations left" msgstr "沒有剩餘邀請名額" @@ -570,7 +570,7 @@ msgstr "通行金鑰" msgid "Please choose an image file smaller than 5 MiB." msgstr "請選擇小於 5 MiB 的圖片檔案。" -#: src/routes/(root)/[handle]/settings/invite.tsx:157 +#: src/routes/(root)/[handle]/settings/invite.tsx:159 msgid "Please correct the errors and try again." msgstr "請修正錯誤並重試。" @@ -625,7 +625,7 @@ msgstr "為你的帳戶註冊通行金鑰。你可以使用通行金鑰登入, #: src/routes/(root)/[handle]/settings/passkeys.tsx:427 msgid "Registered passkeys" -msgstr "已註冊的通行通行金鑰" +msgstr "已註冊的通行金鑰" #: src/routes/(root)/[handle]/settings/passkeys.tsx:420 msgid "Registering…" @@ -650,17 +650,17 @@ msgstr "儲存" msgid "Saving…" msgstr "儲存中…" -#: src/routes/(root)/[handle]/settings/invite.tsx:331 +#: src/routes/(root)/[handle]/settings/invite.tsx:333 msgid "Send" msgstr "發送" -#: src/routes/(root)/[handle]/settings/invite.tsx:330 +#: src/routes/(root)/[handle]/settings/invite.tsx:332 msgid "Sending…" msgstr "發送中…" #: src/components/AppSidebar.tsx:288 #: src/routes/(root)/[handle]/settings/index.tsx:152 -#: src/routes/(root)/[handle]/settings/invite.tsx:215 +#: src/routes/(root)/[handle]/settings/invite.tsx:217 #: src/routes/(root)/[handle]/settings/passkeys.tsx:379 #: src/routes/(root)/[handle]/settings/preferences.tsx:181 msgid "Settings" @@ -732,19 +732,19 @@ msgstr "您貼文的預設隱私設定。" msgid "The default privacy setting for your shares." msgstr "您轉貼的預設隱私設定。" -#: src/routes/(root)/[handle]/settings/invite.tsx:272 +#: src/routes/(root)/[handle]/settings/invite.tsx:274 msgid "The email address is invalid." msgstr "電子郵件地址無效。" -#: src/routes/(root)/[handle]/settings/invite.tsx:267 +#: src/routes/(root)/[handle]/settings/invite.tsx:269 msgid "The email address is not only used for receiving the invitation, but also for signing in to the account." msgstr "電子郵件地址不僅用於接收邀請,還用於登入帳戶。" #: src/routes/(root)/[handle]/settings/passkeys.tsx:429 msgid "The following passkeys are registered to your account. You can use them to sign in to your account." -msgstr "以下通行通行金鑰已註冊到你的帳戶。你可以使用它們登入你的帳戶。" +msgstr "以下通行金鑰已註冊到你的帳戶。你可以使用它們登入你的帳戶。" -#: src/routes/(root)/[handle]/settings/invite.tsx:167 +#: src/routes/(root)/[handle]/settings/invite.tsx:169 msgid "The invitation has been sent successfully." msgstr "邀請已成功發送。" @@ -846,7 +846,7 @@ msgstr "你只能更改一次使用者名稱,而舊的使用者名稱會公開 msgid "You can leave this empty to remove the link." msgstr "您可以將此處留空以刪除連結。" -#: src/routes/(root)/[handle]/settings/invite.tsx:316 +#: src/routes/(root)/[handle]/settings/invite.tsx:318 msgid "You can leave this field empty." msgstr "你可以留空此欄位。" @@ -854,8 +854,8 @@ msgstr "你可以留空此欄位。" msgid "You don't have any passkeys registered yet." msgstr "您尚未註冊任何通行金鑰。" -#: src/routes/(root)/[handle]/settings/invite.tsx:235 -#: src/routes/(root)/[handle]/settings/invite.tsx:337 +#: src/routes/(root)/[handle]/settings/invite.tsx:237 +#: src/routes/(root)/[handle]/settings/invite.tsx:339 msgid "You have no invitations left. Please wait until you receive more." msgstr "你沒有剩餘邀請名額。請稍後再試。" @@ -883,7 +883,7 @@ msgstr "你的個人簡介將在你的個人資料頁面顯示。你可以用 Ma msgid "Your email address will be used to sign in to your account." msgstr "你的電子郵件地址將用於登入。" -#: src/routes/(root)/[handle]/settings/invite.tsx:319 +#: src/routes/(root)/[handle]/settings/invite.tsx:321 msgid "Your friend will see this message in the invitation email." msgstr "你的朋友將在邀請郵件中看到此訊息。" diff --git a/web-next/src/routes/(root)/[handle]/settings/preferences.tsx b/web-next/src/routes/(root)/[handle]/settings/preferences.tsx index 03700ac4..cc499874 100644 --- a/web-next/src/routes/(root)/[handle]/settings/preferences.tsx +++ b/web-next/src/routes/(root)/[handle]/settings/preferences.tsx @@ -202,7 +202,7 @@ export default function PreferencesPage() { defaultChecked={account().preferAiSummary} />
-