From 5878fa81fb87e409193bc8eb059b1b5666299a23 Mon Sep 17 00:00:00 2001 From: lewis Date: Mon, 22 Dec 2025 18:02:32 +0200 Subject: [PATCH] Locales in frontend, why not --- ...278467cba319b1fbe312d9220253610c4fcd.json} | 22 +- ...7861fea2e0e757c40b4d43bafa1beb156c2f1.json | 23 + ...cdda4f8037c7feba39610e3dd4d4d2f7ee8c.json} | 8 +- ...0d1236b5f8c876de016eb358bdcfc383671d.json} | 12 +- README.md | 2 +- frontend/deno.lock | 344 ++++++++- frontend/package.json | 3 + frontend/src/App.svelte | 76 +- frontend/src/components/ReauthModal.svelte | 1 + frontend/src/components/ui/Button.svelte | 75 ++ frontend/src/components/ui/Card.svelte | 49 ++ frontend/src/components/ui/Input.svelte | 42 ++ frontend/src/components/ui/Message.svelte | 46 ++ frontend/src/components/ui/Page.svelte | 82 ++ frontend/src/components/ui/Section.svelte | 59 ++ frontend/src/components/ui/index.ts | 6 + frontend/src/lib/api.ts | 8 + frontend/src/lib/auth.svelte.ts | 30 +- frontend/src/lib/date.ts | 17 + frontend/src/lib/i18n.ts | 54 ++ frontend/src/locales/en.json | 702 ++++++++++++++++++ frontend/src/locales/ja.json | 702 ++++++++++++++++++ frontend/src/locales/ko.json | 702 ++++++++++++++++++ frontend/src/locales/zh.json | 702 ++++++++++++++++++ frontend/src/main.ts | 1 + frontend/src/routes/Admin.svelte | 222 ++++-- frontend/src/routes/AppPasswords.svelte | 150 ++-- .../{Notifications.svelte => Comms.svelte} | 448 +++++------ frontend/src/routes/Dashboard.svelte | 237 +++--- frontend/src/routes/Home.svelte | 146 ++++ frontend/src/routes/InviteCodes.svelte | 161 ++-- frontend/src/routes/Login.svelte | 253 +++---- frontend/src/routes/OAuth2FA.svelte | 68 +- frontend/src/routes/OAuthAccounts.svelte | 57 +- frontend/src/routes/OAuthConsent.svelte | 129 ++-- frontend/src/routes/OAuthError.svelte | 34 +- frontend/src/routes/OAuthLogin.svelte | 123 +-- frontend/src/routes/OAuthPasskey.svelte | 33 +- frontend/src/routes/OAuthTotp.svelte | 85 +-- frontend/src/routes/RecoverPasskey.svelte | 155 ++-- frontend/src/routes/Register.svelte | 589 +++++++-------- frontend/src/routes/RegisterPasskey.svelte | 487 +++--------- frontend/src/routes/RepoExplorer.svelte | 311 +++++--- .../src/routes/RequestPasskeyRecovery.svelte | 135 +--- frontend/src/routes/ResetPassword.svelte | 142 ++-- frontend/src/routes/Security.svelte | 517 ++++++------- frontend/src/routes/Sessions.svelte | 156 ++-- frontend/src/routes/Settings.svelte | 404 +++++----- frontend/src/routes/TrustedDevices.svelte | 157 ++-- frontend/src/routes/Verify.svelte | 163 ++-- frontend/src/styles/base.css | 349 +++++++++ frontend/src/styles/tokens.css | 120 +++ .../{Notifications.test.ts => Comms.test.ts} | 50 +- frontend/src/tests/Dashboard.test.ts | 2 +- migrations/20251230_add_preferred_locale.sql | 1 + src/api/identity/account.rs | 1 + src/api/server/mod.rs | 2 +- src/api/server/passkey_account.rs | 1 + src/api/server/session.rs | 67 +- src/auth/token.rs | 8 +- src/comms/locale.rs | 174 +++++ src/comms/mod.rs | 1 + src/comms/service.rs | 128 ++-- src/lib.rs | 4 + src/oauth/endpoints/metadata.rs | 5 +- src/oauth/endpoints/token/grants.rs | 22 +- src/oauth/endpoints/token/helpers.rs | 2 +- tests/oauth_client_metadata.rs | 4 +- tests/session_management.rs | 2 +- 69 files changed, 7097 insertions(+), 2976 deletions(-) rename .sqlx/{query-de72338f80b4f7b5bc7c9fc44100b6eb9e75f442b5b37a5a1cd761cd3b6950d9.json => query-0fe621daeeb56e4be363ce96df73278467cba319b1fbe312d9220253610c4fcd.json} (71%) create mode 100644 .sqlx/query-50fef1f4f739c42622f2d23c8057861fea2e0e757c40b4d43bafa1beb156c2f1.json rename .sqlx/{query-17da8b6f6b46eae067bd8842a369a406699888f689122d2bae8bef13b532bcd2.json => query-7fea217210a7a97f02d981692ba1cdda4f8037c7feba39610e3dd4d4d2f7ee8c.json} (80%) rename .sqlx/{query-8c69c5f98e3ee59b50346094ff39eed73bb602f0b5ab48c11e53c82839a66721.json => query-b554241c510dae200a0de3050da30d1236b5f8c876de016eb358bdcfc383671d.json} (74%) create mode 100644 frontend/src/components/ui/Button.svelte create mode 100644 frontend/src/components/ui/Card.svelte create mode 100644 frontend/src/components/ui/Input.svelte create mode 100644 frontend/src/components/ui/Message.svelte create mode 100644 frontend/src/components/ui/Page.svelte create mode 100644 frontend/src/components/ui/Section.svelte create mode 100644 frontend/src/components/ui/index.ts create mode 100644 frontend/src/lib/date.ts create mode 100644 frontend/src/lib/i18n.ts create mode 100644 frontend/src/locales/en.json create mode 100644 frontend/src/locales/ja.json create mode 100644 frontend/src/locales/ko.json create mode 100644 frontend/src/locales/zh.json rename frontend/src/routes/{Notifications.svelte => Comms.svelte} (60%) create mode 100644 frontend/src/routes/Home.svelte create mode 100644 frontend/src/styles/base.css create mode 100644 frontend/src/styles/tokens.css rename frontend/src/tests/{Notifications.test.ts => Comms.test.ts} (94%) create mode 100644 migrations/20251230_add_preferred_locale.sql create mode 100644 src/comms/locale.rs diff --git a/.sqlx/query-de72338f80b4f7b5bc7c9fc44100b6eb9e75f442b5b37a5a1cd761cd3b6950d9.json b/.sqlx/query-0fe621daeeb56e4be363ce96df73278467cba319b1fbe312d9220253610c4fcd.json similarity index 71% rename from .sqlx/query-de72338f80b4f7b5bc7c9fc44100b6eb9e75f442b5b37a5a1cd761cd3b6950d9.json rename to .sqlx/query-0fe621daeeb56e4be363ce96df73278467cba319b1fbe312d9220253610c4fcd.json index 64edd35..f511c6b 100644 --- a/.sqlx/query-de72338f80b4f7b5bc7c9fc44100b6eb9e75f442b5b37a5a1cd761cd3b6950d9.json +++ b/.sqlx/query-0fe621daeeb56e4be363ce96df73278467cba319b1fbe312d9220253610c4fcd.json @@ -1,6 +1,6 @@ { "db_name": "PostgreSQL", - "query": "SELECT\n handle, email, email_verified, is_admin,\n preferred_comms_channel as \"preferred_channel: crate::comms::CommsChannel\",\n discord_verified, telegram_verified, signal_verified\n FROM users WHERE did = $1", + "query": "SELECT\n handle, email, email_verified, is_admin, deactivated_at, preferred_locale,\n preferred_comms_channel as \"preferred_channel: crate::comms::CommsChannel\",\n discord_verified, telegram_verified, signal_verified\n FROM users WHERE did = $1", "describe": { "columns": [ { @@ -25,6 +25,16 @@ }, { "ordinal": 4, + "name": "deactivated_at", + "type_info": "Timestamptz" + }, + { + "ordinal": 5, + "name": "preferred_locale", + "type_info": "Varchar" + }, + { + "ordinal": 6, "name": "preferred_channel: crate::comms::CommsChannel", "type_info": { "Custom": { @@ -41,17 +51,17 @@ } }, { - "ordinal": 5, + "ordinal": 7, "name": "discord_verified", "type_info": "Bool" }, { - "ordinal": 6, + "ordinal": 8, "name": "telegram_verified", "type_info": "Bool" }, { - "ordinal": 7, + "ordinal": 9, "name": "signal_verified", "type_info": "Bool" } @@ -66,11 +76,13 @@ true, false, false, + true, + true, false, false, false, false ] }, - "hash": "de72338f80b4f7b5bc7c9fc44100b6eb9e75f442b5b37a5a1cd761cd3b6950d9" + "hash": "0fe621daeeb56e4be363ce96df73278467cba319b1fbe312d9220253610c4fcd" } diff --git a/.sqlx/query-50fef1f4f739c42622f2d23c8057861fea2e0e757c40b4d43bafa1beb156c2f1.json b/.sqlx/query-50fef1f4f739c42622f2d23c8057861fea2e0e757c40b4d43bafa1beb156c2f1.json new file mode 100644 index 0000000..0924497 --- /dev/null +++ b/.sqlx/query-50fef1f4f739c42622f2d23c8057861fea2e0e757c40b4d43bafa1beb156c2f1.json @@ -0,0 +1,23 @@ +{ + "db_name": "PostgreSQL", + "query": "UPDATE users SET preferred_locale = $1 WHERE did = $2 RETURNING did", + "describe": { + "columns": [ + { + "ordinal": 0, + "name": "did", + "type_info": "Text" + } + ], + "parameters": { + "Left": [ + "Varchar", + "Text" + ] + }, + "nullable": [ + false + ] + }, + "hash": "50fef1f4f739c42622f2d23c8057861fea2e0e757c40b4d43bafa1beb156c2f1" +} diff --git a/.sqlx/query-17da8b6f6b46eae067bd8842a369a406699888f689122d2bae8bef13b532bcd2.json b/.sqlx/query-7fea217210a7a97f02d981692ba1cdda4f8037c7feba39610e3dd4d4d2f7ee8c.json similarity index 80% rename from .sqlx/query-17da8b6f6b46eae067bd8842a369a406699888f689122d2bae8bef13b532bcd2.json rename to .sqlx/query-7fea217210a7a97f02d981692ba1cdda4f8037c7feba39610e3dd4d4d2f7ee8c.json index 25d150d..28de5f1 100644 --- a/.sqlx/query-17da8b6f6b46eae067bd8842a369a406699888f689122d2bae8bef13b532bcd2.json +++ b/.sqlx/query-7fea217210a7a97f02d981692ba1cdda4f8037c7feba39610e3dd4d4d2f7ee8c.json @@ -1,6 +1,6 @@ { "db_name": "PostgreSQL", - "query": "SELECT\n handle, email, email_verified, is_admin, deactivated_at,\n preferred_comms_channel as \"preferred_channel: crate::comms::CommsChannel\",\n discord_verified, telegram_verified, signal_verified\n FROM users WHERE did = $1", + "query": "SELECT\n handle, email, email_verified, is_admin, preferred_locale,\n preferred_comms_channel as \"preferred_channel: crate::comms::CommsChannel\",\n discord_verified, telegram_verified, signal_verified\n FROM users WHERE did = $1", "describe": { "columns": [ { @@ -25,8 +25,8 @@ }, { "ordinal": 4, - "name": "deactivated_at", - "type_info": "Timestamptz" + "name": "preferred_locale", + "type_info": "Varchar" }, { "ordinal": 5, @@ -78,5 +78,5 @@ false ] }, - "hash": "17da8b6f6b46eae067bd8842a369a406699888f689122d2bae8bef13b532bcd2" + "hash": "7fea217210a7a97f02d981692ba1cdda4f8037c7feba39610e3dd4d4d2f7ee8c" } diff --git a/.sqlx/query-8c69c5f98e3ee59b50346094ff39eed73bb602f0b5ab48c11e53c82839a66721.json b/.sqlx/query-b554241c510dae200a0de3050da30d1236b5f8c876de016eb358bdcfc383671d.json similarity index 74% rename from .sqlx/query-8c69c5f98e3ee59b50346094ff39eed73bb602f0b5ab48c11e53c82839a66721.json rename to .sqlx/query-b554241c510dae200a0de3050da30d1236b5f8c876de016eb358bdcfc383671d.json index 4dfc823..19219c2 100644 --- a/.sqlx/query-8c69c5f98e3ee59b50346094ff39eed73bb602f0b5ab48c11e53c82839a66721.json +++ b/.sqlx/query-b554241c510dae200a0de3050da30d1236b5f8c876de016eb358bdcfc383671d.json @@ -1,6 +1,6 @@ { "db_name": "PostgreSQL", - "query": "\n SELECT\n email,\n handle,\n preferred_comms_channel as \"channel: CommsChannel\"\n FROM users\n WHERE id = $1\n ", + "query": "\n SELECT\n email,\n handle,\n preferred_comms_channel as \"channel: CommsChannel\",\n preferred_locale\n FROM users\n WHERE id = $1\n ", "describe": { "columns": [ { @@ -29,6 +29,11 @@ } } } + }, + { + "ordinal": 3, + "name": "preferred_locale", + "type_info": "Varchar" } ], "parameters": { @@ -39,8 +44,9 @@ "nullable": [ true, false, - false + false, + true ] }, - "hash": "8c69c5f98e3ee59b50346094ff39eed73bb602f0b5ab48c11e53c82839a66721" + "hash": "b554241c510dae200a0de3050da30d1236b5f8c876de016eb358bdcfc383671d" } diff --git a/README.md b/README.md index 347cd98..413aa0b 100644 --- a/README.md +++ b/README.md @@ -14,7 +14,7 @@ Another excellent PDS is [Cocoon](https://github.com/haileyok/cocoon), written i This software isn't an afterthought by a company with limited resources. -It is a superset of the reference PDS, including: multi-channel communication (email, discord, telegram, signal) for verification and alerts. Built-in web UI for account management, OAuth consent, repo browsing, and admin. Granular OAuth scopes with UI support such that users choose exactly what apps can access. +It is a superset of the reference PDS, including: passkeys and 2FA (WebAuthn/FIDO2, TOTP, backup codes, trusted devices), did:web support (PDS-hosted subdomains or bring-your-own), multi-channel communication (email, discord, telegram, signal) for verification and alerts, granular OAuth scopes with a consent UI showing human-readable descriptions, and a built-in web UI for account management, OAuth consent, repo browsing, and admin. The PDS itself is a single small binary with no node/npm runtime. It does require postgres, valkey, and s3-compatible storage, which makes setup heavier than the reference PDS's sqlite. The tradeoff is that these are battle-tested pieces of infra that we already know how to scale, back up, and monitor. diff --git a/frontend/deno.lock b/frontend/deno.lock index 0736697..4bf6511 100644 --- a/frontend/deno.lock +++ b/frontend/deno.lock @@ -6,6 +6,7 @@ "npm:@testing-library/svelte@^5.2.6": "5.2.9_svelte@5.45.10__acorn@8.15.0_vite@6.4.1__picomatch@4.0.3_vitest@2.1.9__jsdom@25.0.1__vite@5.4.21_jsdom@25.0.1", "npm:@testing-library/user-event@^14.5.2": "14.6.1_@testing-library+dom@10.4.1", "npm:jsdom@^25.0.1": "25.0.1", + "npm:svelte-i18n@^4.0.1": "4.0.1_svelte@5.45.10__acorn@8.15.0", "npm:svelte@5": "5.45.10_acorn@8.15.0", "npm:vite@*": "6.4.1_picomatch@4.0.3", "npm:vite@6": "6.4.1_picomatch@4.0.3", @@ -68,6 +69,11 @@ "@csstools/css-tokenizer@3.0.4": { "integrity": "sha512-Vd/9EVDiu6PPJt9yAh6roZP6El1xHrdvIVGjyBsHR0RYwNHgL7FJPyIIW4fANJNG6FtyZfvlRPpFI4ZM/lubvw==" }, + "@esbuild/aix-ppc64@0.19.12": { + "integrity": "sha512-bmoCYyWdEL3wDQIVbcyzRyeKLgk2WtWLTWz1ZIAZF/EGbNOwSA6ew3PftJ1PqMiOOGu0OyFMzG53L0zqIpPeNA==", + "os": ["aix"], + "cpu": ["ppc64"] + }, "@esbuild/aix-ppc64@0.21.5": { "integrity": "sha512-1SDgH6ZSPTlggy1yI6+Dbkiz8xzpHJEVAlF/AM1tHPLsf5STom9rwtjE4hKAF20FfXXNTFqEYXyJNWh1GiZedQ==", "os": ["aix"], @@ -78,6 +84,11 @@ "os": ["aix"], "cpu": ["ppc64"] }, + "@esbuild/android-arm64@0.19.12": { + "integrity": "sha512-P0UVNGIienjZv3f5zq0DP3Nt2IE/3plFzuaS96vihvD0Hd6H/q4WXUGpCxD/E8YrSXfNyRPbpTq+T8ZQioSuPA==", + "os": ["android"], + "cpu": ["arm64"] + }, "@esbuild/android-arm64@0.21.5": { "integrity": "sha512-c0uX9VAUBQ7dTDCjq+wdyGLowMdtR/GoC2U5IYk/7D1H1JYC0qseD7+11iMP2mRLN9RcCMRcjC4YMclCzGwS/A==", "os": ["android"], @@ -88,6 +99,11 @@ "os": ["android"], "cpu": ["arm64"] }, + "@esbuild/android-arm@0.19.12": { + "integrity": "sha512-qg/Lj1mu3CdQlDEEiWrlC4eaPZ1KztwGJ9B6J+/6G+/4ewxJg7gqj8eVYWvao1bXrqGiW2rsBZFSX3q2lcW05w==", + "os": ["android"], + "cpu": ["arm"] + }, "@esbuild/android-arm@0.21.5": { "integrity": "sha512-vCPvzSjpPHEi1siZdlvAlsPxXl7WbOVUBBAowWug4rJHb68Ox8KualB+1ocNvT5fjv6wpkX6o/iEpbDrf68zcg==", "os": ["android"], @@ -98,6 +114,11 @@ "os": ["android"], "cpu": ["arm"] }, + "@esbuild/android-x64@0.19.12": { + "integrity": "sha512-3k7ZoUW6Q6YqhdhIaq/WZ7HwBpnFBlW905Fa4s4qWJyiNOgT1dOqDiVAQFwBH7gBRZr17gLrlFCRzF6jFh7Kew==", + "os": ["android"], + "cpu": ["x64"] + }, "@esbuild/android-x64@0.21.5": { "integrity": "sha512-D7aPRUUNHRBwHxzxRvp856rjUHRFW1SdQATKXH2hqA0kAZb1hKmi02OpYRacl0TxIGz/ZmXWlbZgjwWYaCakTA==", "os": ["android"], @@ -108,6 +129,11 @@ "os": ["android"], "cpu": ["x64"] }, + "@esbuild/darwin-arm64@0.19.12": { + "integrity": "sha512-B6IeSgZgtEzGC42jsI+YYu9Z3HKRxp8ZT3cqhvliEHovq8HSX2YX8lNocDn79gCKJXOSaEot9MVYky7AKjCs8g==", + "os": ["darwin"], + "cpu": ["arm64"] + }, "@esbuild/darwin-arm64@0.21.5": { "integrity": "sha512-DwqXqZyuk5AiWWf3UfLiRDJ5EDd49zg6O9wclZ7kUMv2WRFr4HKjXp/5t8JZ11QbQfUS6/cRCKGwYhtNAY88kQ==", "os": ["darwin"], @@ -118,6 +144,11 @@ "os": ["darwin"], "cpu": ["arm64"] }, + "@esbuild/darwin-x64@0.19.12": { + "integrity": "sha512-hKoVkKzFiToTgn+41qGhsUJXFlIjxI/jSYeZf3ugemDYZldIXIxhvwN6erJGlX4t5h417iFuheZ7l+YVn05N3A==", + "os": ["darwin"], + "cpu": ["x64"] + }, "@esbuild/darwin-x64@0.21.5": { "integrity": "sha512-se/JjF8NlmKVG4kNIuyWMV/22ZaerB+qaSi5MdrXtd6R08kvs2qCN4C09miupktDitvh8jRFflwGFBQcxZRjbw==", "os": ["darwin"], @@ -128,6 +159,11 @@ "os": ["darwin"], "cpu": ["x64"] }, + "@esbuild/freebsd-arm64@0.19.12": { + "integrity": "sha512-4aRvFIXmwAcDBw9AueDQ2YnGmz5L6obe5kmPT8Vd+/+x/JMVKCgdcRwH6APrbpNXsPz+K653Qg8HB/oXvXVukA==", + "os": ["freebsd"], + "cpu": ["arm64"] + }, "@esbuild/freebsd-arm64@0.21.5": { "integrity": "sha512-5JcRxxRDUJLX8JXp/wcBCy3pENnCgBR9bN6JsY4OmhfUtIHe3ZW0mawA7+RDAcMLrMIZaf03NlQiX9DGyB8h4g==", "os": ["freebsd"], @@ -138,6 +174,11 @@ "os": ["freebsd"], "cpu": ["arm64"] }, + "@esbuild/freebsd-x64@0.19.12": { + "integrity": "sha512-EYoXZ4d8xtBoVN7CEwWY2IN4ho76xjYXqSXMNccFSx2lgqOG/1TBPW0yPx1bJZk94qu3tX0fycJeeQsKovA8gg==", + "os": ["freebsd"], + "cpu": ["x64"] + }, "@esbuild/freebsd-x64@0.21.5": { "integrity": "sha512-J95kNBj1zkbMXtHVH29bBriQygMXqoVQOQYA+ISs0/2l3T9/kj42ow2mpqerRBxDJnmkUDCaQT/dfNXWX/ZZCQ==", "os": ["freebsd"], @@ -148,6 +189,11 @@ "os": ["freebsd"], "cpu": ["x64"] }, + "@esbuild/linux-arm64@0.19.12": { + "integrity": "sha512-EoTjyYyLuVPfdPLsGVVVC8a0p1BFFvtpQDB/YLEhaXyf/5bczaGeN15QkR+O4S5LeJ92Tqotve7i1jn35qwvdA==", + "os": ["linux"], + "cpu": ["arm64"] + }, "@esbuild/linux-arm64@0.21.5": { "integrity": "sha512-ibKvmyYzKsBeX8d8I7MH/TMfWDXBF3db4qM6sy+7re0YXya+K1cem3on9XgdT2EQGMu4hQyZhan7TeQ8XkGp4Q==", "os": ["linux"], @@ -158,6 +204,11 @@ "os": ["linux"], "cpu": ["arm64"] }, + "@esbuild/linux-arm@0.19.12": { + "integrity": "sha512-J5jPms//KhSNv+LO1S1TX1UWp1ucM6N6XuL6ITdKWElCu8wXP72l9MM0zDTzzeikVyqFE6U8YAV9/tFyj0ti+w==", + "os": ["linux"], + "cpu": ["arm"] + }, "@esbuild/linux-arm@0.21.5": { "integrity": "sha512-bPb5AHZtbeNGjCKVZ9UGqGwo8EUu4cLq68E95A53KlxAPRmUyYv2D6F0uUI65XisGOL1hBP5mTronbgo+0bFcA==", "os": ["linux"], @@ -168,6 +219,11 @@ "os": ["linux"], "cpu": ["arm"] }, + "@esbuild/linux-ia32@0.19.12": { + "integrity": "sha512-Thsa42rrP1+UIGaWz47uydHSBOgTUnwBwNq59khgIwktK6x60Hivfbux9iNR0eHCHzOLjLMLfUMLCypBkZXMHA==", + "os": ["linux"], + "cpu": ["ia32"] + }, "@esbuild/linux-ia32@0.21.5": { "integrity": "sha512-YvjXDqLRqPDl2dvRODYmmhz4rPeVKYvppfGYKSNGdyZkA01046pLWyRKKI3ax8fbJoK5QbxblURkwK/MWY18Tg==", "os": ["linux"], @@ -178,6 +234,11 @@ "os": ["linux"], "cpu": ["ia32"] }, + "@esbuild/linux-loong64@0.19.12": { + "integrity": "sha512-LiXdXA0s3IqRRjm6rV6XaWATScKAXjI4R4LoDlvO7+yQqFdlr1Bax62sRwkVvRIrwXxvtYEHHI4dm50jAXkuAA==", + "os": ["linux"], + "cpu": ["loong64"] + }, "@esbuild/linux-loong64@0.21.5": { "integrity": "sha512-uHf1BmMG8qEvzdrzAqg2SIG/02+4/DHB6a9Kbya0XDvwDEKCoC8ZRWI5JJvNdUjtciBGFQ5PuBlpEOXQj+JQSg==", "os": ["linux"], @@ -188,6 +249,11 @@ "os": ["linux"], "cpu": ["loong64"] }, + "@esbuild/linux-mips64el@0.19.12": { + "integrity": "sha512-fEnAuj5VGTanfJ07ff0gOA6IPsvrVHLVb6Lyd1g2/ed67oU1eFzL0r9WL7ZzscD+/N6i3dWumGE1Un4f7Amf+w==", + "os": ["linux"], + "cpu": ["mips64el"] + }, "@esbuild/linux-mips64el@0.21.5": { "integrity": "sha512-IajOmO+KJK23bj52dFSNCMsz1QP1DqM6cwLUv3W1QwyxkyIWecfafnI555fvSGqEKwjMXVLokcV5ygHW5b3Jbg==", "os": ["linux"], @@ -198,6 +264,11 @@ "os": ["linux"], "cpu": ["mips64el"] }, + "@esbuild/linux-ppc64@0.19.12": { + "integrity": "sha512-nYJA2/QPimDQOh1rKWedNOe3Gfc8PabU7HT3iXWtNUbRzXS9+vgB0Fjaqr//XNbd82mCxHzik2qotuI89cfixg==", + "os": ["linux"], + "cpu": ["ppc64"] + }, "@esbuild/linux-ppc64@0.21.5": { "integrity": "sha512-1hHV/Z4OEfMwpLO8rp7CvlhBDnjsC3CttJXIhBi+5Aj5r+MBvy4egg7wCbe//hSsT+RvDAG7s81tAvpL2XAE4w==", "os": ["linux"], @@ -208,6 +279,11 @@ "os": ["linux"], "cpu": ["ppc64"] }, + "@esbuild/linux-riscv64@0.19.12": { + "integrity": "sha512-2MueBrlPQCw5dVJJpQdUYgeqIzDQgw3QtiAHUC4RBz9FXPrskyyU3VI1hw7C0BSKB9OduwSJ79FTCqtGMWqJHg==", + "os": ["linux"], + "cpu": ["riscv64"] + }, "@esbuild/linux-riscv64@0.21.5": { "integrity": "sha512-2HdXDMd9GMgTGrPWnJzP2ALSokE/0O5HhTUvWIbD3YdjME8JwvSCnNGBnTThKGEB91OZhzrJ4qIIxk/SBmyDDA==", "os": ["linux"], @@ -218,6 +294,11 @@ "os": ["linux"], "cpu": ["riscv64"] }, + "@esbuild/linux-s390x@0.19.12": { + "integrity": "sha512-+Pil1Nv3Umes4m3AZKqA2anfhJiVmNCYkPchwFJNEJN5QxmTs1uzyy4TvmDrCRNT2ApwSari7ZIgrPeUx4UZDg==", + "os": ["linux"], + "cpu": ["s390x"] + }, "@esbuild/linux-s390x@0.21.5": { "integrity": "sha512-zus5sxzqBJD3eXxwvjN1yQkRepANgxE9lgOW2qLnmr8ikMTphkjgXu1HR01K4FJg8h1kEEDAqDcZQtbrRnB41A==", "os": ["linux"], @@ -228,6 +309,11 @@ "os": ["linux"], "cpu": ["s390x"] }, + "@esbuild/linux-x64@0.19.12": { + "integrity": "sha512-B71g1QpxfwBvNrfyJdVDexenDIt1CiDN1TIXLbhOw0KhJzE78KIFGX6OJ9MrtC0oOqMWf+0xop4qEU8JrJTwCg==", + "os": ["linux"], + "cpu": ["x64"] + }, "@esbuild/linux-x64@0.21.5": { "integrity": "sha512-1rYdTpyv03iycF1+BhzrzQJCdOuAOtaqHTWJZCWvijKD2N5Xu0TtVC8/+1faWqcP9iBCWOmjmhoH94dH82BxPQ==", "os": ["linux"], @@ -243,6 +329,11 @@ "os": ["netbsd"], "cpu": ["arm64"] }, + "@esbuild/netbsd-x64@0.19.12": { + "integrity": "sha512-3ltjQ7n1owJgFbuC61Oj++XhtzmymoCihNFgT84UAmJnxJfm4sYCiSLTXZtE00VWYpPMYc+ZQmB6xbSdVh0JWA==", + "os": ["netbsd"], + "cpu": ["x64"] + }, "@esbuild/netbsd-x64@0.21.5": { "integrity": "sha512-Woi2MXzXjMULccIwMnLciyZH4nCIMpWQAs049KEeMvOcNADVxo0UBIQPfSmxB3CWKedngg7sWZdLvLczpe0tLg==", "os": ["netbsd"], @@ -258,6 +349,11 @@ "os": ["openbsd"], "cpu": ["arm64"] }, + "@esbuild/openbsd-x64@0.19.12": { + "integrity": "sha512-RbrfTB9SWsr0kWmb9srfF+L933uMDdu9BIzdA7os2t0TXhCRjrQyCeOt6wVxr79CKD4c+p+YhCj31HBkYcXebw==", + "os": ["openbsd"], + "cpu": ["x64"] + }, "@esbuild/openbsd-x64@0.21.5": { "integrity": "sha512-HLNNw99xsvx12lFBUwoT8EVCsSvRNDVxNpjZ7bPn947b8gJPzeHWyNVhFsaerc0n3TsbOINvRP2byTZ5LKezow==", "os": ["openbsd"], @@ -273,6 +369,11 @@ "os": ["openharmony"], "cpu": ["arm64"] }, + "@esbuild/sunos-x64@0.19.12": { + "integrity": "sha512-HKjJwRrW8uWtCQnQOz9qcU3mUZhTUQvi56Q8DPTLLB+DawoiQdjsYq+j+D3s9I8VFtDr+F9CjgXKKC4ss89IeA==", + "os": ["sunos"], + "cpu": ["x64"] + }, "@esbuild/sunos-x64@0.21.5": { "integrity": "sha512-6+gjmFpfy0BHU5Tpptkuh8+uw3mnrvgs+dSPQXQOv3ekbordwnzTVEb4qnIvQcYXq6gzkyTnoZ9dZG+D4garKg==", "os": ["sunos"], @@ -283,6 +384,11 @@ "os": ["sunos"], "cpu": ["x64"] }, + "@esbuild/win32-arm64@0.19.12": { + "integrity": "sha512-URgtR1dJnmGvX864pn1B2YUYNzjmXkuJOIqG2HdU62MVS4EHpU2946OZoTMnRUHklGtJdJZ33QfzdjGACXhn1A==", + "os": ["win32"], + "cpu": ["arm64"] + }, "@esbuild/win32-arm64@0.21.5": { "integrity": "sha512-Z0gOTd75VvXqyq7nsl93zwahcTROgqvuAcYDUr+vOv8uHhNSKROyU961kgtCD1e95IqPKSQKH7tBTslnS3tA8A==", "os": ["win32"], @@ -293,6 +399,11 @@ "os": ["win32"], "cpu": ["arm64"] }, + "@esbuild/win32-ia32@0.19.12": { + "integrity": "sha512-+ZOE6pUkMOJfmxmBZElNOx72NKpIa/HFOMGzu8fqzQJ5kgf6aTGrcJaFsNiVMH4JKpMipyK+7k0n2UXN7a8YKQ==", + "os": ["win32"], + "cpu": ["ia32"] + }, "@esbuild/win32-ia32@0.21.5": { "integrity": "sha512-SWXFF1CL2RVNMaVs+BBClwtfZSvDgtL//G/smwAc5oVK/UPu2Gu9tIaRgFmYFFKrmg3SyAjSrElf0TiJ1v8fYA==", "os": ["win32"], @@ -303,6 +414,11 @@ "os": ["win32"], "cpu": ["ia32"] }, + "@esbuild/win32-x64@0.19.12": { + "integrity": "sha512-T1QyPSDCyMXaO3pzBkF96E8xMkiRYbUEZADd29SyPGabqxMViNoii+NcK7eWJAEoU6RZyEm5lVSIjTmcdoB9HA==", + "os": ["win32"], + "cpu": ["x64"] + }, "@esbuild/win32-x64@0.21.5": { "integrity": "sha512-tQd/1efJuzPC6rCFwEvLtci/xNFcTZknmXs98FYDfGE4wP9ClFV98nyKrzJKVPMhdDnjzLhdUyMX4PsQAPjwIw==", "os": ["win32"], @@ -313,6 +429,42 @@ "os": ["win32"], "cpu": ["x64"] }, + "@formatjs/ecma402-abstract@2.3.6": { + "integrity": "sha512-HJnTFeRM2kVFVr5gr5kH1XP6K0JcJtE7Lzvtr3FS/so5f1kpsqqqxy5JF+FRaO6H2qmcMfAUIox7AJteieRtVw==", + "dependencies": [ + "@formatjs/fast-memoize", + "@formatjs/intl-localematcher", + "decimal.js", + "tslib" + ] + }, + "@formatjs/fast-memoize@2.2.7": { + "integrity": "sha512-Yabmi9nSvyOMrlSeGGWDiH7rf3a7sIwplbvo/dlz9WCIjzIQAfy1RMf4S0X3yG724n5Ghu2GmEl5NJIV6O9sZQ==", + "dependencies": [ + "tslib" + ] + }, + "@formatjs/icu-messageformat-parser@2.11.4": { + "integrity": "sha512-7kR78cRrPNB4fjGFZg3Rmj5aah8rQj9KPzuLsmcSn4ipLXQvC04keycTI1F7kJYDwIXtT2+7IDEto842CfZBtw==", + "dependencies": [ + "@formatjs/ecma402-abstract", + "@formatjs/icu-skeleton-parser", + "tslib" + ] + }, + "@formatjs/icu-skeleton-parser@1.8.16": { + "integrity": "sha512-H13E9Xl+PxBd8D5/6TVUluSpxGNvFSlN/b3coUp0e0JpuWXXnQDiavIpY3NnvSp4xhEMoXyyBvVfdFX8jglOHQ==", + "dependencies": [ + "@formatjs/ecma402-abstract", + "tslib" + ] + }, + "@formatjs/intl-localematcher@0.6.2": { + "integrity": "sha512-XOMO2Hupl0wdd172Y06h6kLpBz6Dv+J4okPLl4LPtzbr8f66WbIoy4ev98EBuZ6ZK4h5ydTN6XneT4QVpD7cdA==", + "dependencies": [ + "tslib" + ] + }, "@jridgewell/gen-mapping@0.3.13": { "integrity": "sha512-2kkt/7niJ6MgEPxF0bYdQ6etZaA+fQvDcLKckhy1yIQOzaoKjBBjSj63/aLVjYE3qhRt5dvM+uUyfCg6UKCBbA==", "dependencies": [ @@ -540,7 +692,7 @@ "integrity": "sha512-tVL6uJgoUdi6icpxmdrn5YNo3g3Dxv+IHJBr0GXHaEdTcw3F+cPKnsXFhli6nO+f/6SDKPHEK1UN+k+TQv0Ehg==", "dependencies": [ "@vitest/spy", - "estree-walker", + "estree-walker@3.0.3", "magic-string", "vite@5.4.21" ], @@ -637,6 +789,16 @@ "check-error@2.1.1": { "integrity": "sha512-OAlb+T7V4Op9OwdkjmguYRqncdlx5JiofwOAUkmTF+jNdHwzTaTs4sRAGpzLF3oOz5xAyDGrPgeIDFQmDOTiJw==" }, + "cli-color@2.0.4": { + "integrity": "sha512-zlnpg0jNcibNrO7GG9IeHH7maWFeCz+Ja1wx/7tZNU5ASSSSZ+/qZciM0/LHCYxSdqv5h2sdbQ/PXYdOuetXvA==", + "dependencies": [ + "d", + "es5-ext", + "es6-iterator", + "memoizee", + "timers-ext" + ] + }, "clsx@2.1.1": { "integrity": "sha512-eYm0QWBtUrBWZWG0d386OGAw16Z995PiOVo2B7bjWSbHedGl5e0ZWaq65kOGgUSNesEIDkB9ISbTg/JK9dhCZA==" }, @@ -656,6 +818,13 @@ "rrweb-cssom@0.8.0" ] }, + "d@1.0.2": { + "integrity": "sha512-MOqHvMWF9/9MX6nza0KgvFH4HpMU0EF5uUDXqX/BtxtU8NfB0QzRtJ8Oe/6SuS4kbhyzVJwjd97EA4PKrzJ8bw==", + "dependencies": [ + "es5-ext", + "type" + ] + }, "data-urls@5.0.0": { "integrity": "sha512-ZYP5VBHshaDAiVZxjbRVcFJpc+4xGgT0bK3vzy1HLN8jTO975HEbuYzZJcHoQEY5K1a0z8YayJkyVETa08eNTg==", "dependencies": [ @@ -728,6 +897,70 @@ "hasown" ] }, + "es5-ext@0.10.64": { + "integrity": "sha512-p2snDhiLaXe6dahss1LddxqEm+SkuDvV8dnIQG0MWjyHpcMNfXKPE+/Cc0y+PhxJX3A4xGNeFCj5oc0BUh6deg==", + "dependencies": [ + "es6-iterator", + "es6-symbol", + "esniff", + "next-tick" + ], + "scripts": true + }, + "es6-iterator@2.0.3": { + "integrity": "sha512-zw4SRzoUkd+cl+ZoE15A9o1oQd920Bb0iOJMQkQhl3jNc03YqVjAhG7scf9C5KWRU/R13Orf588uCC6525o02g==", + "dependencies": [ + "d", + "es5-ext", + "es6-symbol" + ] + }, + "es6-symbol@3.1.4": { + "integrity": "sha512-U9bFFjX8tFiATgtkJ1zg25+KviIXpgRvRHS8sau3GfhVzThRQrOeksPeT0BWW2MNZs1OEWJ1DPXOQMn0KKRkvg==", + "dependencies": [ + "d", + "ext" + ] + }, + "es6-weak-map@2.0.3": { + "integrity": "sha512-p5um32HOTO1kP+w7PRnB+5lQ43Z6muuMuIMffvDN8ZB4GcnjLBV6zGStpbASIMk4DCAvEaamhe2zhyCb/QXXsA==", + "dependencies": [ + "d", + "es5-ext", + "es6-iterator", + "es6-symbol" + ] + }, + "esbuild@0.19.12": { + "integrity": "sha512-aARqgq8roFBj054KvQr5f1sFu0D65G+miZRCuJyJ0G13Zwx7vRar5Zhn2tkQNzIXcBrNVsv/8stehpj+GAjgbg==", + "optionalDependencies": [ + "@esbuild/aix-ppc64@0.19.12", + "@esbuild/android-arm@0.19.12", + "@esbuild/android-arm64@0.19.12", + "@esbuild/android-x64@0.19.12", + "@esbuild/darwin-arm64@0.19.12", + "@esbuild/darwin-x64@0.19.12", + "@esbuild/freebsd-arm64@0.19.12", + "@esbuild/freebsd-x64@0.19.12", + "@esbuild/linux-arm@0.19.12", + "@esbuild/linux-arm64@0.19.12", + "@esbuild/linux-ia32@0.19.12", + "@esbuild/linux-loong64@0.19.12", + "@esbuild/linux-mips64el@0.19.12", + "@esbuild/linux-ppc64@0.19.12", + "@esbuild/linux-riscv64@0.19.12", + "@esbuild/linux-s390x@0.19.12", + "@esbuild/linux-x64@0.19.12", + "@esbuild/netbsd-x64@0.19.12", + "@esbuild/openbsd-x64@0.19.12", + "@esbuild/sunos-x64@0.19.12", + "@esbuild/win32-arm64@0.19.12", + "@esbuild/win32-ia32@0.19.12", + "@esbuild/win32-x64@0.19.12" + ], + "scripts": true, + "bin": true + }, "esbuild@0.21.5": { "integrity": "sha512-mg3OPMV4hXywwpoDxu3Qda5xCKQi+vCTZq8S9J/EpkhB2HzKXq4SNFZE3+NK93JYxc8VMSep+lOUSC/RVKaBqw==", "optionalDependencies": [ @@ -794,21 +1027,46 @@ "esm-env@1.2.2": { "integrity": "sha512-Epxrv+Nr/CaL4ZcFGPJIYLWFom+YeV1DqMLHJoEd9SYRxNbaFruBwfEX/kkHUJf55j2+TUbmDcmuilbP1TmXHA==" }, + "esniff@2.0.1": { + "integrity": "sha512-kTUIGKQ/mDPFoJ0oVfcmyJn4iBDRptjNVIzwIFR7tqWXdVI9xfA2RMwY/gbSpJG3lkdWNEjLap/NqVHZiJsdfg==", + "dependencies": [ + "d", + "es5-ext", + "event-emitter", + "type" + ] + }, "esrap@2.2.1": { "integrity": "sha512-GiYWG34AN/4CUyaWAgunGt0Rxvr1PTMlGC0vvEov/uOQYWne2bpN03Um+k8jT+q3op33mKouP2zeJ6OlM+qeUg==", "dependencies": [ "@jridgewell/sourcemap-codec" ] }, + "estree-walker@2.0.2": { + "integrity": "sha512-Rfkk/Mp/DL7JVje3u18FxFujQlTNR2q6QfMSMB7AvCBx91NGj/ba3kCfza0f6dVDbw7YlRf/nDrn7pQrCCyQ/w==" + }, "estree-walker@3.0.3": { "integrity": "sha512-7RUKfXgSMMkzt6ZuXmqapOurLGPPfgj6l9uRZ7lRGolvk0y2yocc35LdcxKC5PQZdn2DMqioAQ2NoWcrTKmm6g==", "dependencies": [ "@types/estree" ] }, + "event-emitter@0.3.5": { + "integrity": "sha512-D9rRn9y7kLPnJ+hMq7S/nhvoKwwvVJahBi2BPmx3bvbsEdK3W9ii8cBSGjP+72/LnM4n6fo3+dkCX5FeTQruXA==", + "dependencies": [ + "d", + "es5-ext" + ] + }, "expect-type@1.3.0": { "integrity": "sha512-knvyeauYhqjOYvQ66MznSMs83wmHrCycNEN6Ao+2AeYEfxUIkuiVxdEa1qlGEPK+We3n0THiDciYSsCcgW/DoA==" }, + "ext@1.7.0": { + "integrity": "sha512-6hxeJYaL110a9b5TEJSj0gojyHQAmA2ch5Os+ySCiA1QGdS697XWY1pzsrSjqA9LDEEgdB/KypIlR59RcLuHYw==", + "dependencies": [ + "type" + ] + }, "fdir@6.5.0_picomatch@4.0.3": { "integrity": "sha512-tIbYtZbucOs0BRGqPJkshJUYdL+SDH7dVM8gjy+ERp3WAUjLEFJE+02kanyHtwjWOnwrKYBiwAmM0p4kLJAnXg==", "dependencies": [ @@ -858,6 +1116,12 @@ "es-object-atoms" ] }, + "globalyzer@0.1.0": { + "integrity": "sha512-40oNTM9UfG6aBmuKxk/giHn5nQ8RVz/SS4Ir6zgzOv9/qC3kKZ9v4etGTcJbEl/NyVQH7FGU7d+X1egr57Md2Q==" + }, + "globrex@0.1.2": { + "integrity": "sha512-uHJgbwAMwNFf5mLst7IWLNg14x1CkeqglJb/K3doi4dw6q2IvAAmM/Y81kevy83wP+Sst+nutFTYOGg3d1lsxg==" + }, "gopd@1.2.0": { "integrity": "sha512-ZUKRh6/kUFoAiTAtTYPZJ3hw9wNxx+BIBOijnlG9PnrJsCcSjs1wyyD6vJpaYtgnzDrKYRSqf3OO6Rfa93xsRg==" }, @@ -905,9 +1169,21 @@ "indent-string@4.0.0": { "integrity": "sha512-EdDDZu4A2OyIK7Lr/2zG+w5jmbuk1DVBnEwREQvBzspBJkCEbRa8GxU1lghYcaGJCnRWibjDXlq779X1/y5xwg==" }, + "intl-messageformat@10.7.18": { + "integrity": "sha512-m3Ofv/X/tV8Y3tHXLohcuVuhWKo7BBq62cqY15etqmLxg2DZ34AGGgQDeR+SCta2+zICb1NX83af0GJmbQ1++g==", + "dependencies": [ + "@formatjs/ecma402-abstract", + "@formatjs/fast-memoize", + "@formatjs/icu-messageformat-parser", + "tslib" + ] + }, "is-potential-custom-element-name@1.0.1": { "integrity": "sha512-bCYeRA2rVibKZd+s2625gGnGF/t7DSqDs4dP7CrLA1m7jKWz6pps0LpYLJN8Q64HtmPKJ1hrN3nzPNKFEKOUiQ==" }, + "is-promise@2.2.2": { + "integrity": "sha512-+lP4/6lKUBfQjZ2pdxThZvLUAafmZb8OAxFb8XXtiQmS35INgr85hdOGoEs124ez1FCnZJt6jau/T+alh58QFQ==" + }, "is-reference@3.0.3": { "integrity": "sha512-ixkJoqQvAP88E6wLydLGGqCJsrFUnqoH6HnaczB8XmDH1oaWU+xxdptvikTgaEhtZ53Ky6YXiBuUI2WXLMCwjw==", "dependencies": [ @@ -955,6 +1231,12 @@ "lru-cache@10.4.3": { "integrity": "sha512-JNAzZcXrCt42VGLuYz0zfAzDfAvJWW6AfYlDBQyDV5DClI2m5sAmK+OIO7s59XfsRsWHp02jAJrRadPRGTt6SQ==" }, + "lru-queue@0.1.0": { + "integrity": "sha512-BpdYkt9EvGl8OfWHDQPISVpcl5xZthb+XPsbELj5AQXxIC8IriDZIQYjBJPEm5rS420sjZ0TLEzRcq5KdBhYrQ==", + "dependencies": [ + "es5-ext" + ] + }, "lz-string@1.5.0": { "integrity": "sha512-h5bgJWpxJNswbU7qCrV0tIKQCaS3blPDrqKWx+QxzuzL1zGUzij9XCWLrSLsJPu5t+eWA/ycetzYAO5IOMcWAQ==", "bin": true @@ -968,6 +1250,19 @@ "math-intrinsics@1.1.0": { "integrity": "sha512-/IXtbwEk5HTPyEwyKX6hGkYXxM9nbj64B+ilVJnC/R6B0pH5G4V3b0pVbL7DBj4tkhBAppbQUlf6F6Xl9LHu1g==" }, + "memoizee@0.4.17": { + "integrity": "sha512-DGqD7Hjpi/1or4F/aYAspXKNm5Yili0QDAFAY4QYvpqpgiY6+1jOfqpmByzjxbWd/T9mChbCArXAbDAsTm5oXA==", + "dependencies": [ + "d", + "es5-ext", + "es6-weak-map", + "event-emitter", + "is-promise", + "lru-queue", + "next-tick", + "timers-ext" + ] + }, "mime-db@1.52.0": { "integrity": "sha512-sPU4uV7dYlvtWJxwwxHD0PuihVNiE7TyAbQ5SWxDCB9mUYvOgroQOwYQQOKPJ8CIbE+1ETVlOoK1UC2nU3gYvg==" }, @@ -980,6 +1275,9 @@ "min-indent@1.0.1": { "integrity": "sha512-I9jwMn07Sy/IwOj3zVkVik2JTvgpaykDZEigL6Rx6N9LbMywwUSMtxET+7lVoDLLd3O3IXwJwvuuns8UB/HeAg==" }, + "mri@1.2.0": { + "integrity": "sha512-tzzskb3bG8LvYGFF/mDTpq3jpI6Q9wc3LEmBaghu+DdCssd1FakN7Bc0hVNmEyGq1bq3RgfkCb3cmQLpNPOroA==" + }, "ms@2.1.3": { "integrity": "sha512-6FlzubTLZG3J2a/NVCAleEhjzq5oxgHyaCU9yYXvcLsvoVaHJq/s5xXI6/XXP6tz7R9xAOtHnSO/tXtF3WRTlA==" }, @@ -987,6 +1285,9 @@ "integrity": "sha512-N8SpfPUnUp1bK+PMYW8qSWdl9U+wwNWI4QKxOYDy9JAro3WMX7p2OeVRF9v+347pnakNevPmiHhNmZ2HbFA76w==", "bin": true }, + "next-tick@1.1.0": { + "integrity": "sha512-CXdUiJembsNjuToQvxayPZF9Vqht7hewsvy2sOWafLvi2awflj9mOC6bHIg50orX8IJvWKY9wYQ/zB2kogPslQ==" + }, "nwsapi@2.2.23": { "integrity": "sha512-7wfH4sLbt4M0gCDzGE6vzQBo0bfTKjU7Sfpqy/7gs1qBfYz2vEJH6vXcBKpO3+6Yu1telwd0t9HpyOoLEQQbIQ==" }, @@ -1075,6 +1376,12 @@ "rrweb-cssom@0.8.0": { "integrity": "sha512-guoltQEx+9aMf2gDZ0s62EcV8lsXR+0w8915TC3ITdn2YueuNjdAYh/levpU9nFaoChh9RUS5ZdQMrKfVEN9tw==" }, + "sade@1.8.1": { + "integrity": "sha512-xal3CZX1Xlo/k4ApwCFrHVACi9fBqJ7V+mwhBsuf/1IOKbBy098Fex+Wa/5QMubw09pSZ/u8EY8PWgevJsXp1A==", + "dependencies": [ + "mri" + ] + }, "safer-buffer@2.1.2": { "integrity": "sha512-YZo3K82SD7Riyi0E1EQPojLz7kpepnSQI9IyPbHHg1XXXevb5dJI7tpyN2ADxGcQbHG7vcyRHk0cbwqcQriUtg==" }, @@ -1102,6 +1409,20 @@ "min-indent" ] }, + "svelte-i18n@4.0.1_svelte@5.45.10__acorn@8.15.0": { + "integrity": "sha512-jaykGlGT5PUaaq04JWbJREvivlCnALtT+m87Kbm0fxyYHynkQaxQMnIKHLm2WeIuBRoljzwgyvz0Z6/CMwfdmQ==", + "dependencies": [ + "cli-color", + "deepmerge", + "esbuild@0.19.12", + "estree-walker@2.0.2", + "intl-messageformat", + "sade", + "svelte", + "tiny-glob" + ], + "bin": true + }, "svelte@5.45.10_acorn@8.15.0": { "integrity": "sha512-GiWXq6akkEN3zVDMQ1BVlRolmks5JkEdzD/67mvXOz6drRfuddT5JwsGZjMGSnsTRv/PjAXX8fqBcOr2g2qc/Q==", "dependencies": [ @@ -1125,6 +1446,20 @@ "symbol-tree@3.2.4": { "integrity": "sha512-9QNk5KwDF+Bvz+PyObkmSYjI5ksVUYtjW7AU22r2NKcfLJcXp96hkDWU3+XndOsUb+AQ9QhfzfCT2O+CNWT5Tw==" }, + "timers-ext@0.1.8": { + "integrity": "sha512-wFH7+SEAcKfJpfLPkrgMPvvwnEtj8W4IurvEyrKsDleXnKLCDw71w8jltvfLa8Rm4qQxxT4jmDBYbJG/z7qoww==", + "dependencies": [ + "es5-ext", + "next-tick" + ] + }, + "tiny-glob@0.2.9": { + "integrity": "sha512-g/55ssRPUjShh+xkfx9UPDXqhckHEsHr4Vd9zX55oSdGZc/MD0m3sferOkwWtp98bv+kcVfEHtRJgBVJzelrzg==", + "dependencies": [ + "globalyzer", + "globrex" + ] + }, "tinybench@2.9.0": { "integrity": "sha512-0+DUvqWMValLmha6lr4kD8iAMK1HzV0/aKnCtWb9v9641TnP/MFb7Pc2bxoxQjTXAErryXVgUOfv2YqNllqGeg==" }, @@ -1169,6 +1504,12 @@ "punycode" ] }, + "tslib@2.8.1": { + "integrity": "sha512-oJFu94HQb+KVduSUQL7wnpmqnfmLsOA/nAh6b6EH0wCEoK0/mPeXU6c3wKDV83MkOuHPRHtSXKKU99IBazS/2w==" + }, + "type@2.7.3": { + "integrity": "sha512-8j+1QmAbPvLZow5Qpi6NCaN8FB60p/6x8/vfNqOk/hC+HuvFZhL4+WfekuhQLiqFZXOgQdrs3B+XxEmCc6b3FQ==" + }, "vite-node@2.1.9": { "integrity": "sha512-AM9aQ/IPrW/6ENLQg3AGY4K1N2TGZdR5e4gu/MmmR2xR3Ll1+dib+nook92g4TV3PXVyeyxdWwtaCAiUL0hMxA==", "dependencies": [ @@ -1300,6 +1641,7 @@ "npm:@testing-library/svelte@^5.2.6", "npm:@testing-library/user-event@^14.5.2", "npm:jsdom@^25.0.1", + "npm:svelte-i18n@^4.0.1", "npm:svelte@5", "npm:vite@6", "npm:vitest@^2.1.8" diff --git a/frontend/package.json b/frontend/package.json index ad2361b..4fe7c98 100644 --- a/frontend/package.json +++ b/frontend/package.json @@ -11,6 +11,9 @@ "test:run": "vitest run", "test:coverage": "vitest run --coverage" }, + "dependencies": { + "svelte-i18n": "^4.0.1" + }, "devDependencies": { "@sveltejs/vite-plugin-svelte": "^5.0.0", "@testing-library/jest-dom": "^6.6.3", diff --git a/frontend/src/App.svelte b/frontend/src/App.svelte index 0586495..1bf3b54 100644 --- a/frontend/src/App.svelte +++ b/frontend/src/App.svelte @@ -1,6 +1,8 @@
- {#if auth.loading} + {#if auth.loading || $i18nLoading}

Loading...

@@ -102,69 +107,8 @@
diff --git a/frontend/src/components/ui/Card.svelte b/frontend/src/components/ui/Card.svelte new file mode 100644 index 0000000..d858c53 --- /dev/null +++ b/frontend/src/components/ui/Card.svelte @@ -0,0 +1,49 @@ + + +
+ {@render children()} +
+ + diff --git a/frontend/src/components/ui/Input.svelte b/frontend/src/components/ui/Input.svelte new file mode 100644 index 0000000..9c4bb3e --- /dev/null +++ b/frontend/src/components/ui/Input.svelte @@ -0,0 +1,42 @@ + + +
+ {#if label} + + {/if} + + {#if error} + {error} + {:else if hint} + {hint} + {/if} +
+ + diff --git a/frontend/src/components/ui/Message.svelte b/frontend/src/components/ui/Message.svelte new file mode 100644 index 0000000..05a5e6e --- /dev/null +++ b/frontend/src/components/ui/Message.svelte @@ -0,0 +1,46 @@ + + +
+ {@render children()} +
+ + diff --git a/frontend/src/components/ui/Page.svelte b/frontend/src/components/ui/Page.svelte new file mode 100644 index 0000000..2fcfed1 --- /dev/null +++ b/frontend/src/components/ui/Page.svelte @@ -0,0 +1,82 @@ + + +
+
+ {#if backHref} + ← {backLabel || $_('common.backToDashboard')} + {/if} +
+

{title}

+ {#if actions} +
+ {@render actions()} +
+ {/if} +
+
+ {@render children()} +
+ + diff --git a/frontend/src/components/ui/Section.svelte b/frontend/src/components/ui/Section.svelte new file mode 100644 index 0000000..fad9878 --- /dev/null +++ b/frontend/src/components/ui/Section.svelte @@ -0,0 +1,59 @@ + + +
+ {#if title} +

{title}

+ {/if} + {#if description} +

{description}

+ {/if} + {@render children()} +
+ + diff --git a/frontend/src/components/ui/index.ts b/frontend/src/components/ui/index.ts new file mode 100644 index 0000000..083a3ce --- /dev/null +++ b/frontend/src/components/ui/index.ts @@ -0,0 +1,6 @@ +export { default as Button } from './Button.svelte' +export { default as Card } from './Card.svelte' +export { default as Input } from './Input.svelte' +export { default as Message } from './Message.svelte' +export { default as Page } from './Page.svelte' +export { default as Section } from './Section.svelte' diff --git a/frontend/src/lib/api.ts b/frontend/src/lib/api.ts index b93f5bf..68c3be8 100644 --- a/frontend/src/lib/api.ts +++ b/frontend/src/lib/api.ts @@ -343,6 +343,14 @@ export const api = { }) }, + async updateLocale(token: string, preferredLocale: string): Promise<{ preferredLocale: string }> { + return xrpc('com.tranquil.account.updateLocale', { + method: 'POST', + token, + body: { preferredLocale }, + }) + }, + async listSessions(token: string): Promise<{ sessions: Array<{ id: string diff --git a/frontend/src/lib/auth.svelte.ts b/frontend/src/lib/auth.svelte.ts index 9442dc4..9986a3b 100644 --- a/frontend/src/lib/auth.svelte.ts +++ b/frontend/src/lib/auth.svelte.ts @@ -1,5 +1,12 @@ import { api, type Session, type CreateAccountParams, type CreateAccountResult, ApiError } from './api' import { startOAuthLogin, handleOAuthCallback, checkForOAuthCallback, clearOAuthCallbackParams, refreshOAuthToken } from './oauth' +import { setLocale, type SupportedLocale } from './i18n' + +function applyLocaleFromSession(sessionInfo: { preferredLocale?: string | null }) { + if (sessionInfo.preferredLocale) { + setLocale(sessionInfo.preferredLocale as SupportedLocale) + } +} const STORAGE_KEY = 'tranquil_pds_session' const ACCOUNTS_KEY = 'tranquil_pds_accounts' @@ -104,6 +111,7 @@ export async function initAuth() { state.session = session saveSession(session) addOrUpdateSavedAccount(session) + applyLocaleFromSession(sessionInfo) state.loading = false return } catch (e) { @@ -116,9 +124,10 @@ export async function initAuth() { const stored = loadSession() if (stored) { try { - const session = await api.getSession(stored.accessJwt) - state.session = { ...session, accessJwt: stored.accessJwt, refreshJwt: stored.refreshJwt } + const sessionInfo = await api.getSession(stored.accessJwt) + state.session = { ...sessionInfo, accessJwt: stored.accessJwt, refreshJwt: stored.refreshJwt } addOrUpdateSavedAccount(state.session) + applyLocaleFromSession(sessionInfo) } catch (e) { if (e instanceof ApiError && e.status === 401) { try { @@ -132,6 +141,7 @@ export async function initAuth() { state.session = session saveSession(session) addOrUpdateSavedAccount(session) + applyLocaleFromSession(sessionInfo) } catch { saveSession(null) state.session = null @@ -291,6 +301,22 @@ export function getAuthState() { return state } +export async function refreshSession(): Promise { + if (!state.session) return + try { + const sessionInfo = await api.getSession(state.session.accessJwt) + state.session = { + ...sessionInfo, + accessJwt: state.session.accessJwt, + refreshJwt: state.session.refreshJwt, + } + saveSession(state.session) + addOrUpdateSavedAccount(state.session) + } catch (e) { + console.error('Failed to refresh session:', e) + } +} + export function getToken(): string | null { return state.session?.accessJwt ?? null } diff --git a/frontend/src/lib/date.ts b/frontend/src/lib/date.ts new file mode 100644 index 0000000..07f13a2 --- /dev/null +++ b/frontend/src/lib/date.ts @@ -0,0 +1,17 @@ +export function formatDate(dateStr: string): string { + const date = new Date(dateStr) + const year = date.getFullYear() + const month = String(date.getMonth() + 1).padStart(2, '0') + const day = String(date.getDate()).padStart(2, '0') + return `${year}-${month}-${day}` +} + +export function formatDateTime(dateStr: string): string { + const date = new Date(dateStr) + const year = date.getFullYear() + const month = String(date.getMonth() + 1).padStart(2, '0') + const day = String(date.getDate()).padStart(2, '0') + const hours = String(date.getHours()).padStart(2, '0') + const minutes = String(date.getMinutes()).padStart(2, '0') + return `${year}-${month}-${day} ${hours}:${minutes}` +} diff --git a/frontend/src/lib/i18n.ts b/frontend/src/lib/i18n.ts new file mode 100644 index 0000000..199ff21 --- /dev/null +++ b/frontend/src/lib/i18n.ts @@ -0,0 +1,54 @@ +import { register, init, getLocaleFromNavigator, locale, _ } from 'svelte-i18n' + +const LOCALE_STORAGE_KEY = 'tranquil-pds-locale' + +const SUPPORTED_LOCALES = ['en', 'zh', 'ja', 'ko'] as const +export type SupportedLocale = typeof SUPPORTED_LOCALES[number] + +export const localeNames: Record = { + en: 'English', + zh: '中文', + ja: '日本語', + ko: '한국어' +} + +register('en', () => import('../locales/en.json')) +register('zh', () => import('../locales/zh.json')) +register('ja', () => import('../locales/ja.json')) +register('ko', () => import('../locales/ko.json')) + +function getInitialLocale(): string { + const stored = localStorage.getItem(LOCALE_STORAGE_KEY) + if (stored && SUPPORTED_LOCALES.includes(stored as SupportedLocale)) { + return stored + } + + const browserLocale = getLocaleFromNavigator() + if (browserLocale) { + const lang = browserLocale.split('-')[0] + if (SUPPORTED_LOCALES.includes(lang as SupportedLocale)) { + return lang + } + } + + return 'en' +} + +export function initI18n() { + init({ + fallbackLocale: 'en', + initialLocale: getInitialLocale() + }) +} + +export function setLocale(newLocale: SupportedLocale) { + locale.set(newLocale) + localStorage.setItem(LOCALE_STORAGE_KEY, newLocale) + document.documentElement.lang = newLocale +} + +export function getSupportedLocales(): SupportedLocale[] { + return [...SUPPORTED_LOCALES] +} + +export { locale, _ } diff --git a/frontend/src/locales/en.json b/frontend/src/locales/en.json new file mode 100644 index 0000000..412b365 --- /dev/null +++ b/frontend/src/locales/en.json @@ -0,0 +1,702 @@ +{ + "common": { + "loading": "Loading...", + "error": "Error", + "save": "Save", + "cancel": "Cancel", + "back": "Back", + "done": "Done", + "refresh": "Refresh", + "create": "Create", + "delete": "Delete", + "confirm": "Confirm", + "created": "Created", + "expires": "Expires", + "name": "Name", + "dashboard": "Dashboard", + "backToDashboard": "← Dashboard" + }, + "login": { + "title": "Sign In", + "subtitle": "Sign in to manage your PDS account", + "button": "Sign In", + "redirecting": "Redirecting...", + "chooseAccount": "Choose an account", + "signInToAnother": "Sign in to another account", + "backToSaved": "← Back to saved accounts", + "forgotPassword": "Forgot password?", + "lostPasskey": "Lost passkey?", + "noAccount": "Don't have an account?", + "createAccount": "Create account", + "removeAccount": "Remove from saved accounts" + }, + "verification": { + "title": "Verify Your Account", + "subtitle": "Your account needs verification. Enter the code sent to your verification method.", + "codeLabel": "Verification Code", + "codePlaceholder": "Enter 6-digit code", + "verifyButton": "Verify Account", + "verifying": "Verifying...", + "resendButton": "Resend Code", + "resending": "Resending...", + "resent": "Verification code resent!", + "backToLogin": "Back to Login" + }, + "register": { + "title": "Create Account", + "subtitle": "Create a new account on this PDS", + "handle": "Handle", + "handlePlaceholder": "yourname", + "handleHint": "Your full handle will be: @{handle}", + "handleDotWarning": "Custom domain handles can be set up after account creation in Settings.", + "password": "Password", + "passwordPlaceholder": "At least 8 characters", + "confirmPassword": "Confirm Password", + "confirmPasswordPlaceholder": "Confirm your password", + "identityType": "Identity Type", + "identityHint": "Choose how your decentralized identity will be managed.", + "didPlc": "did:plc", + "didPlcRecommended": "(Recommended)", + "didPlcHint": "Portable identity managed by PLC Directory", + "didWeb": "did:web", + "didWebHint": "Identity hosted on this PDS (read warning below)", + "didWebBYOD": "did:web (BYOD)", + "didWebBYODHint": "Bring your own domain", + "didWebWarningTitle": "Important: Understand the trade-offs", + "didWebWarning1": "Permanent tie to this PDS:", + "didWebWarning1Detail": "Your identity will be {did}. Even if you migrate to another PDS later, this server must continue hosting your DID document.", + "didWebWarning2": "No recovery mechanism:", + "didWebWarning2Detail": "Unlike did:plc, did:web has no rotation keys. If this PDS goes offline permanently, your identity cannot be recovered.", + "didWebWarning3": "We commit to you:", + "didWebWarning3Detail": "If you migrate away, we will continue serving a minimal DID document pointing to your new PDS. Your identity will remain functional.", + "didWebWarning4": "Recommendation:", + "didWebWarning4Detail": "Choose did:plc unless you have a specific reason to prefer did:web.", + "externalDid": "Your did:web", + "externalDidPlaceholder": "did:web:yourdomain.com", + "externalDidHint": "Your domain must serve a valid DID document at /.well-known/did.json pointing to this PDS", + "contactMethod": "Contact Method", + "contactMethodHint": "Choose how you'd like to verify your account and receive notifications. You only need one.", + "verificationMethod": "Verification Method", + "email": "Email", + "emailAddress": "Email Address", + "emailPlaceholder": "you@example.com", + "discord": "Discord", + "discordId": "Discord User ID", + "discordIdPlaceholder": "Your Discord user ID", + "discordIdHint": "Your numeric Discord user ID (enable Developer Mode to find it)", + "telegram": "Telegram", + "telegramUsername": "Telegram Username", + "telegramUsernamePlaceholder": "@yourusername", + "signal": "Signal", + "signalNumber": "Signal Phone Number", + "signalNumberPlaceholder": "+1234567890", + "signalNumberHint": "Include country code (e.g., +1 for US)", + "inviteCode": "Invite Code", + "inviteCodePlaceholder": "Enter your invite code", + "inviteCodeRequired": "required", + "createButton": "Create Account", + "creating": "Creating account...", + "alreadyHaveAccount": "Already have an account?", + "signIn": "Sign in", + "wantPasswordless": "Want passwordless security?", + "createPasskeyAccount": "Create a passkey account", + "validation": { + "handleRequired": "Handle is required", + "handleNoDots": "Handle cannot contain dots. You can set up a custom domain handle after creating your account.", + "passwordRequired": "Password is required", + "passwordLength": "Password must be at least 8 characters", + "passwordsMismatch": "Passwords do not match", + "inviteCodeRequired": "Invite code is required", + "externalDidRequired": "External did:web is required", + "externalDidFormat": "External DID must start with did:web:", + "emailRequired": "Email is required for email verification", + "discordIdRequired": "Discord ID is required for Discord verification", + "telegramRequired": "Telegram username is required for Telegram verification", + "signalRequired": "Phone number is required for Signal verification" + } + }, + "dashboard": { + "title": "Dashboard", + "switchAccount": "Switch Account", + "addAnotherAccount": "Add another account", + "signOut": "Sign out @{handle}", + "deactivatedTitle": "Account Deactivated", + "deactivatedMessage": "Your account is currently deactivated. This typically happens during account migration. Some features may be limited until your account is reactivated.", + "accountOverview": "Account Overview", + "handle": "Handle", + "did": "DID", + "primaryContact": "Primary Contact", + "admin": "Admin", + "deactivated": "Deactivated", + "verified": "Verified", + "unverified": "Unverified", + "navAppPasswords": "App Passwords", + "navAppPasswordsDesc": "Manage passwords for third-party apps", + "navSessions": "Active Sessions", + "navSessionsDesc": "View and manage your login sessions", + "navInviteCodes": "Invite Codes", + "navInviteCodesDesc": "View and create invite codes", + "navSettings": "Account Settings", + "navSettingsDesc": "Email, password, handle, and more", + "navSecurity": "Security", + "navSecurityDesc": "Two-factor authentication", + "navComms": "Communication Preferences", + "navCommsDesc": "Discord, Telegram, Signal channels", + "navRepo": "Repository Explorer", + "navRepoDesc": "Browse and manage raw AT Protocol records", + "navAdmin": "Admin Panel", + "navAdminDesc": "Server stats and admin operations" + }, + "settings": { + "title": "Account Settings", + "language": "Language", + "languageDescription": "Choose your preferred language", + "changeEmail": "Change Email", + "currentEmail": "Current: {email}", + "newEmail": "New Email", + "newEmailPlaceholder": "new@example.com", + "changeEmailButton": "Change Email", + "requesting": "Requesting...", + "verificationCode": "Verification Code", + "verificationCodePlaceholder": "Enter code from email", + "confirmEmailChange": "Confirm Email Change", + "updating": "Updating...", + "changeHandle": "Change Handle", + "currentHandle": "Current: @{handle}", + "pdsHandle": "PDS Handle", + "customDomain": "Custom Domain", + "customDomainDescription": "Use your own domain as your handle. You need to verify domain ownership first.", + "setupInstructions": "Setup Instructions", + "setupMethodsIntro": "Choose one of these verification methods:", + "dnsMethod": "Option 1: DNS TXT Record (Recommended)", + "dnsMethodDesc": "Add this TXT record to your domain:", + "httpMethod": "Option 2: HTTP Well-Known File", + "httpMethodDesc": "Serve your DID at this URL:", + "httpMethodContent": "The file should contain only:", + "yourDomain": "Your Domain", + "yourDomainPlaceholder": "example.com", + "verifyAndUpdate": "Verify & Update Handle", + "verifying": "Verifying...", + "newHandle": "New Handle", + "newHandlePlaceholder": "yourhandle", + "changeHandleButton": "Change Handle", + "changePassword": "Change Password", + "currentPassword": "Current Password", + "currentPasswordPlaceholder": "Enter current password", + "newPassword": "New Password", + "newPasswordPlaceholder": "At least 8 characters", + "confirmNewPassword": "Confirm New Password", + "confirmNewPasswordPlaceholder": "Confirm new password", + "changePasswordButton": "Change Password", + "changing": "Changing...", + "exportData": "Export Data", + "exportDataDescription": "Download your entire repository as a CAR (Content Addressable Archive) file. This includes all your posts, likes, follows, and other data.", + "downloadRepo": "Download Repository", + "exporting": "Exporting...", + "deleteAccount": "Delete Account", + "deleteWarning": "This action is irreversible. All your data will be permanently deleted.", + "requestDeletion": "Request Account Deletion", + "confirmationCode": "Confirmation Code (from email)", + "confirmationCodePlaceholder": "Enter confirmation code", + "yourPassword": "Your Password", + "yourPasswordPlaceholder": "Enter your password", + "permanentlyDelete": "Permanently Delete Account", + "deleting": "Deleting...", + "messages": { + "emailCodeSent": "Verification code sent to your current email", + "emailUpdated": "Email updated successfully", + "handleUpdated": "Handle updated successfully", + "passwordChanged": "Password changed successfully", + "passwordsMismatch": "Passwords do not match", + "passwordLength": "Password must be at least 8 characters", + "deletionCodeSent": "Deletion confirmation sent to your email", + "repoExported": "Repository exported successfully", + "confirmDelete": "Are you absolutely sure you want to delete your account? This cannot be undone." + } + }, + "appPasswords": { + "title": "App Passwords", + "description": "App passwords let you sign in to third-party apps without giving them your main password. Each app password can be revoked individually.", + "createNew": "Create New App Password", + "appNamePlaceholder": "App name (e.g., Graysky, Skeets)", + "created": "App Password Created", + "createdMessage": "Copy this password now. You won't be able to see it again.", + "yourPasswords": "Your App Passwords", + "noPasswords": "No app passwords yet", + "revoke": "Revoke", + "revoking": "Revoking...", + "creating": "Creating...", + "revokeConfirm": "Revoke app password \"{name}\"? Apps using this password will no longer be able to access your account." + }, + "sessions": { + "title": "Active Sessions", + "loadingSessions": "Loading sessions...", + "noSessions": "No active sessions found.", + "current": "Current", + "oauth": "OAuth", + "session": "Session", + "signOut": "Sign Out", + "revoke": "Revoke", + "revokeAll": "Revoke All Other Sessions", + "revokeCurrentConfirm": "This will log you out of this session. Continue?", + "revokeConfirm": "Revoke this session?", + "revokeAllConfirm": "This will revoke {count} other session(s). Continue?", + "noOtherSessions": "No other sessions to revoke", + "failedToLoad": "Failed to load sessions", + "failedToRevoke": "Failed to revoke session", + "failedToRevokeAll": "Failed to revoke sessions", + "created": "Created:", + "expires": "Expires:", + "daysAgo": "{count} day(s) ago", + "hoursAgo": "{count} hour(s) ago", + "minutesAgo": "{count} minute(s) ago", + "justNow": "Just now" + }, + "inviteCodes": { + "title": "Invite Codes", + "description": "Invite codes let you invite friends to join. Each code can be used once.", + "createNew": "Create New Invite Code", + "uses": "Uses", + "usesPlaceholder": "Number of uses (1-100)", + "yourCodes": "Your Invite Codes", + "noCodes": "No invite codes yet", + "available": "Available", + "used": "Used by @{handle}", + "disabled": "Disabled", + "usedBy": "Used by", + "creating": "Creating...", + "disableConfirm": "Disable this invite code? It can no longer be used.", + "created": "Invite Code Created", + "copy": "Copy", + "createdOn": "Created {date}" + }, + "security": { + "title": "Security", + "passkeys": "Passkeys", + "passkeysDescription": "Passkeys provide secure, passwordless authentication using your device's built-in security (fingerprint, face, or PIN).", + "addPasskey": "Add Passkey", + "adding": "Adding...", + "noPasskeys": "No passkeys registered", + "passkeyName": "Passkey name", + "passkeyNamePlaceholder": "e.g., MacBook Pro, iPhone", + "register": "Register", + "registering": "Registering...", + "rename": "Rename", + "renaming": "Renaming...", + "deletePasskey": "Delete", + "deletePasskeyConfirm": "Delete passkey \"{name}\"? You won't be able to use it to sign in anymore.", + "totp": "Authenticator App (TOTP)", + "totpDescription": "Use an authenticator app like Google Authenticator, Authy, or 1Password for two-factor authentication.", + "totpEnabled": "TOTP is enabled", + "totpDisabled": "TOTP is not enabled", + "enableTotp": "Enable TOTP", + "disableTotp": "Disable TOTP", + "disabling": "Disabling...", + "totpSetup": "Set up Authenticator App", + "totpSetupInstructions": "Scan this QR code with your authenticator app, then enter the 6-digit code to verify.", + "totpCode": "Verification Code", + "totpCodePlaceholder": "Enter 6-digit code", + "verifyAndEnable": "Verify & Enable", + "backupCodes": "Backup Codes", + "backupCodesDescription": "Use these codes to sign in if you lose access to your authenticator app. Each code can only be used once.", + "regenerateBackupCodes": "Regenerate Backup Codes", + "regenerating": "Regenerating...", + "regenerateConfirm": "Regenerate backup codes? Your current codes will no longer work.", + "legacyLogin": "Legacy Login", + "legacyLoginDescription": "Allow signing in with username/password directly (legacy mode). When disabled, you must use OAuth with MFA.", + "legacyLoginOn": "Legacy login is enabled", + "legacyLoginOff": "Legacy login is disabled", + "legacyLoginWarning": "Warning: Enabling legacy login bypasses MFA for direct password logins. Only enable if needed for app compatibility.", + "totpPasswordWarning": "With TOTP enabled, changing your password from the Bluesky app (or other legacy apps) will be blocked. To change your password, you have two options:", + "totpPasswordOption1Label": "Change it here:", + "totpPasswordOption1Text": "Use this website's", + "totpPasswordOption1Link": "Settings page", + "totpPasswordOption1Suffix": "where you can verify with your authenticator app.", + "totpPasswordOption2Label": "Verify your session first:", + "totpPasswordOption2Text": "Use the", + "totpPasswordOption2Link": "re-authenticate option", + "totpPasswordOption2Suffix": "to verify your Bluesky session with TOTP, then password changes will work temporarily.", + "legacyAppsTitle": "What are legacy apps?", + "legacyAppsDescription": "Some apps (like the official Bluesky app) use older authentication that only requires your password. When you have MFA enabled, these apps bypass your second factor. Disabling legacy login forces all apps to use OAuth, which properly enforces MFA.", + "password": "Password", + "passwordStatus": "You have a password set", + "noPassword": "No password set (passkey-only account)", + "setPassword": "Set Password", + "removePassword": "Remove Password", + "removePasswordConfirm": "Remove your password? You'll need to use passkeys to sign in.", + "removing": "Removing...", + "loading": "Loading...", + "loadingPasskeys": "Loading passkeys...", + "cancel": "Cancel", + "save": "Save", + "back": "Back", + "next": "Next: Verify Code", + "copyToClipboard": "Copy to Clipboard", + "savedMyCodes": "I've Saved My Codes", + "cantScan": "Can't scan? Enter manually", + "unnamedPasskey": "Unnamed passkey", + "added": "Added", + "lastUsed": "Last used", + "passwordDescription": "Manage your account password. If you have passkeys set up, you can optionally remove your password for a fully passwordless experience.", + "disableTotpWarning": "This will make your account less secure.", + "removePasswordWarning": "This will make your account passkey-only. You'll only be able to sign in using your registered passkeys. If you lose access to all your passkeys, you can recover your account using your notification channel.", + "beforeProceeding": "Before proceeding:", + "beforeProceedingItem1": "Make sure you have at least one reliable passkey registered", + "beforeProceedingItem2": "Consider registering passkeys on multiple devices", + "beforeProceedingItem3": "Ensure your recovery notification channel is up to date", + "addPasskeyFirst": "Add at least one passkey before you can remove your password.", + "passkeyOnlyHint": "You sign in using passkeys only. If you ever lose access to your passkeys, you can recover your account using the \"Lost passkey?\" link on the login page.", + "trustedDevices": "Trusted Devices", + "trustedDevicesDescription": "Manage devices that can skip two-factor authentication when signing in. Trust is granted for 30 days and automatically extends when you use the device.", + "manageTrustedDevices": "Manage Trusted Devices", + "appCompatibility": "App Compatibility", + "enterPassword": "Enter your password", + "legacyLoginEnabled": "Legacy app login enabled", + "legacyLoginDisabled": "Legacy app login disabled - only OAuth apps can sign in", + "failedToUpdatePreference": "Failed to update preference", + "passwordRemoved": "Password removed. Your account is now passkey-only.", + "failedToRemovePassword": "Failed to remove password", + "failedToLoadTotpStatus": "Failed to load TOTP status", + "totpEnabledSuccess": "Two-factor authentication enabled successfully", + "totpDisabledSuccess": "Two-factor authentication disabled", + "backupCodesCopied": "Backup codes copied to clipboard", + "failedToLoadPasskeys": "Failed to load passkeys", + "passkeysNotSupported": "Passkeys are not supported in this browser", + "passkeyCreationCancelled": "Passkey creation was cancelled", + "passkeyAddedSuccess": "Passkey added successfully", + "passkeyDeleted": "Passkey deleted", + "passkeyRenamed": "Passkey renamed" + }, + "comms": { + "title": "Communication Preferences", + "description": "Choose how you want to receive important messages like password resets, security alerts, and account updates.", + "preferredChannel": "Preferred Channel", + "preferredChannelDescription": "Select your preferred way to receive messages. You must configure a channel before you can select it.", + "channelConfiguration": "Channel Configuration", + "emailVia": "Receive messages via email", + "discordVia": "Receive messages via Discord DM", + "telegramVia": "Receive messages via Telegram", + "signalVia": "Receive messages via Signal", + "configureToEnable": "Configure below to enable", + "emailManagedInSettings": "Your email is managed in Account Settings", + "discordIdHint": "Your Discord user ID (not username). Enable Developer Mode in Discord to copy it.", + "telegramHint": "Your Telegram username without the @ symbol", + "signalHint": "Your Signal phone number with country code", + "primary": "Primary", + "verified": "Verified", + "notVerified": "Not verified", + "verifyButton": "Verify", + "verifyCodePlaceholder": "Enter verification code", + "submit": "Submit", + "saving": "Saving...", + "savePreferences": "Save Preferences", + "preferencesSaved": "Communication preferences saved", + "verifiedSuccess": "{channel} verified successfully", + "messageHistory": "Message History", + "historyDescription": "View recent messages sent to your account.", + "loadHistory": "Load History", + "hideHistory": "Hide History", + "noMessages": "No messages found.", + "sent": "sent", + "failed": "failed" + }, + "repoExplorer": { + "title": "Repository Explorer", + "description": "Browse and manage your raw AT Protocol records.", + "collections": "Collections", + "noCollections": "No collections found", + "records": "Records", + "noRecords": "No records in this collection", + "recordDetails": "Record Details", + "rkey": "Record Key", + "cid": "CID", + "value": "Value", + "deleteRecord": "Delete Record", + "deleteConfirm": "Delete record {rkey}? This cannot be undone.", + "unknownError": "An unknown error occurred", + "invalidJson": "Invalid JSON", + "collectionRequired": "Collection is required", + "recordCreated": "Record created: {uri}", + "recordUpdated": "Record updated", + "recordDeleted": "Record deleted", + "newRecord": "New Record", + "createRecord": "Create Record", + "filterCollections": "Filter collections...", + "filterRecords": "Filter records...", + "noCollectionsYet": "No collections yet. Create your first record to get started.", + "loadMore": "Load More", + "recordJson": "Record JSON", + "saving": "Saving...", + "updateRecord": "Update Record", + "collectionNsid": "Collection (NSID)", + "recordKeyOptional": "Record Key (optional)", + "autoGenerated": "Auto-generated if empty (TID)", + "autoGeneratedHint": "Leave empty to auto-generate a TID-based key", + "creating": "Creating...", + "demoPostText": "Hello from my PDS! This is my first post.", + "demoDisplayName": "Your Display Name", + "demoBio": "A short bio about yourself." + }, + "admin": { + "title": "Admin Panel", + "serverStats": "Server Statistics", + "users": "Users", + "repos": "Repositories", + "records": "Records", + "blobStorage": "Blob Storage", + "refreshStats": "Refresh Stats", + "userManagement": "User Management", + "searchPlaceholder": "Search by handle (optional)", + "searchUsers": "Search Users", + "noUsers": "No users found", + "handle": "Handle", + "email": "Email", + "status": "Status", + "created": "Created", + "loadMore": "Load More", + "inviteCodes": "Invite Codes", + "loadInviteCodes": "Load Invite Codes", + "refresh": "Refresh", + "noInvites": "No invite codes found", + "code": "Code", + "available": "Available", + "uses": "Uses", + "actions": "Actions", + "disable": "Disable", + "disableInviteConfirm": "Disable invite code {code}?", + "active": "Active", + "exhausted": "Exhausted", + "disabled": "Disabled", + "userDetails": "User Details", + "did": "DID", + "invites": "Invites", + "enabled": "Enabled", + "enableInvites": "Enable Invites", + "disableInvites": "Disable Invites", + "deleteAccount": "Delete Account", + "deleteConfirm": "Delete account @{handle}? This cannot be undone.", + "verified": "Verified", + "unverified": "Unverified", + "deactivated": "Deactivated" + }, + "oauth": { + "login": { + "title": "Sign In", + "subtitle": "Sign in to continue to the application", + "signingIn": "Signing in...", + "authenticating": "Authenticating...", + "checkingPasskey": "Checking passkey...", + "signInWithPasskey": "Sign in with passkey", + "passkeyNotSetUp": "Passkey not set up", + "orUsePassword": "or use password", + "password": "Password", + "rememberDevice": "Remember this device", + "passkeyHintChecking": "Checking passkey status...", + "passkeyHintAvailable": "Sign in with your passkey", + "passkeyHintNotAvailable": "No passkeys registered for this account" + }, + "consent": { + "title": "Authorize Application", + "appWantsAccess": "{app} wants to access your account", + "permissions": "This application will be able to:", + "readProfile": "Read your profile information", + "readPosts": "Read your posts and content", + "writePosts": "Create and delete posts on your behalf", + "readNotifications": "Read your notifications", + "fullAccess": "Full access to your account", + "authorize": "Authorize", + "deny": "Deny", + "authorizing": "Authorizing...", + "rememberChoice": "Remember this choice", + "signingInAs": "Signing in as:", + "permissionsRequested": "Permissions Requested", + "required": "Required", + "rememberChoiceLabel": "Remember my choice for this application" + }, + "accounts": { + "title": "Choose Account", + "subtitle": "Select an account to continue", + "useAnother": "Use a different account" + }, + "twoFactor": { + "title": "Two-Factor Authentication", + "subtitle": "Additional verification is required", + "usePasskey": "Use Passkey", + "useTotp": "Use Authenticator App", + "verifying": "Verifying..." + }, + "twoFactorCode": { + "title": "Two-Factor Authentication", + "subtitle": "A verification code has been sent to your {channel}. Enter the code below to continue.", + "codeLabel": "Verification Code", + "codePlaceholder": "Enter 6-digit code", + "verify": "Verify", + "verifying": "Verifying...", + "errors": { + "missingRequestUri": "Missing request_uri parameter", + "verificationFailed": "Verification failed", + "connectionFailed": "Failed to connect to server", + "unexpectedResponse": "Unexpected response from server" + } + }, + "totp": { + "title": "Enter Authenticator Code", + "subtitle": "Enter the 6-digit code from your authenticator app", + "codePlaceholder": "Enter 6-digit code", + "verify": "Verify", + "verifying": "Verifying...", + "useBackupCode": "Use backup code instead", + "backupCodePlaceholder": "Enter backup code", + "trustDevice": "Trust this device for 30 days", + "hintBackupCode": "Using backup code", + "hintTotpCode": "Using authenticator code", + "hintDefault": "6 digits for authenticator, 8 characters for backup code" + }, + "passkey": { + "title": "Passkey Verification", + "subtitle": "Use your passkey to verify your identity", + "waiting": "Waiting for passkey...", + "useTotp": "Use authenticator app instead" + }, + "error": { + "title": "Authorization Error", + "genericError": "An error occurred during authorization.", + "tryAgain": "Try Again", + "backToApp": "Back to Application" + } + }, + "verify": { + "title": "Verify Your Account", + "subtitle": "We've sent a verification code to your {channel}. Enter it below to complete registration.", + "codePlaceholder": "Enter 6-digit code", + "codeLabel": "Verification Code", + "verifyButton": "Verify Account", + "verifying": "Verifying...", + "resendCode": "Resend Code", + "resending": "Resending...", + "codeResent": "Verification code resent!", + "backToLogin": "Back to Login", + "verifyingAccount": "Verifying account: @{handle}", + "startOver": "Start over with a different account", + "noPending": "No pending verification found.", + "noPendingInfo": "If you recently created an account and need to verify it, you may need to create a new account. If you already verified your account, you can sign in.", + "createAccount": "Create Account", + "signIn": "Sign In" + }, + "resetPassword": { + "title": "Reset Password", + "forgotTitle": "Forgot Password", + "subtitle": "Enter the code you received and choose a new password.", + "forgotSubtitle": "Enter your handle or email and we'll send you a code to reset your password.", + "handleOrEmail": "Handle or Email", + "emailPlaceholder": "handle or you@example.com", + "sendCode": "Send Reset Code", + "sending": "Sending...", + "codeSent": "Password reset code sent! Check your preferred notification channel.", + "enterCode": "Enter the code from your email and your new password.", + "code": "Reset Code", + "codePlaceholder": "Enter reset code", + "newPassword": "New Password", + "newPasswordPlaceholder": "At least 8 characters", + "confirmPassword": "Confirm Password", + "confirmPasswordPlaceholder": "Confirm new password", + "resetButton": "Reset Password", + "resetting": "Resetting...", + "success": "Password reset successfully!", + "backToLogin": "Back to Sign In", + "requestNewCode": "Request New Code", + "passwordsMismatch": "Passwords do not match", + "passwordLength": "Password must be at least 8 characters" + }, + "recoverPasskey": { + "title": "Recover Your Account", + "invalidLinkTitle": "Invalid Recovery Link", + "invalidLinkMessage": "This recovery link is invalid or has been corrupted. Please request a new recovery email.", + "goToLogin": "Go to Login", + "successTitle": "Password Set!", + "successMessage": "Your temporary password has been set. You can now sign in with this password.", + "successNextSteps": "After signing in, we recommend adding a new passkey in your security settings to restore passkey-only authentication.", + "signIn": "Sign In", + "subtitle": "Set a temporary password to regain access to your passkey-only account.", + "newPassword": "New Password", + "newPasswordPlaceholder": "At least 8 characters", + "confirmPassword": "Confirm Password", + "confirmPasswordPlaceholder": "Confirm your password", + "whatHappensNext": "What happens next?", + "whatHappensNextDetail": "After setting this password, you can sign in and add a new passkey in your security settings. Once you have a new passkey, you can optionally remove the temporary password.", + "setPassword": "Set Password", + "settingPassword": "Setting password...", + "validation": { + "passwordRequired": "New password is required", + "passwordLength": "Password must be at least 8 characters", + "passwordsMismatch": "Passwords do not match" + }, + "errors": { + "invalidLink": "Invalid recovery link. Please request a new one.", + "expired": "This recovery link has expired. Please request a new one." + } + }, + "requestPasskeyRecovery": { + "title": "Recover Passkey Account", + "subtitle": "Lost access to your passkey? Enter your handle or email and we'll send you a recovery link.", + "successTitle": "Recovery Link Sent", + "successMessage": "If your account exists and is a passkey-only account, you'll receive a recovery link at your preferred notification channel.", + "successInfo": "The link will expire in 1 hour. Check your email, Discord, Telegram, or Signal depending on your account settings.", + "handleOrEmail": "Handle or Email", + "emailPlaceholder": "handle or you@example.com", + "howItWorks": "How it works", + "howItWorksDetail": "We'll send a secure link to your registered notification channel. Click the link to set a temporary password. Then you can sign in and add a new passkey.", + "sendRecoveryLink": "Send Recovery Link", + "sending": "Sending...", + "backToLogin": "Back to Sign In" + }, + "registerPasskey": { + "title": "Create Passkey Account", + "subtitle": "Create a passwordless account using a passkey.", + "handle": "Handle", + "handlePlaceholder": "yourname", + "handleHint": "Your full handle will be: @{handle}", + "email": "Email Address", + "emailPlaceholder": "you@example.com", + "inviteCode": "Invite Code", + "inviteCodePlaceholder": "Enter your invite code", + "createButton": "Create Account", + "creating": "Creating...", + "alreadyHaveAccount": "Already have an account?", + "signIn": "Sign in", + "wantPassword": "Want to use a password?", + "createPasswordAccount": "Create a password account" + }, + "trustedDevices": { + "title": "Trusted Devices", + "backToSecurity": "← Security Settings", + "description": "Trusted devices can skip two-factor authentication when logging in. Trust is granted for 30 days and automatically extends when you use the device.", + "noDevices": "No trusted devices yet.", + "noDevicesHint": "When you log in with two-factor authentication enabled, you can choose to trust the device for 30 days.", + "lastSeen": "Last seen:", + "trustedSince": "Trusted since:", + "trustExpires": "Trust expires:", + "expired": "Expired", + "tomorrow": "Tomorrow", + "inDays": "In {days} days", + "revoke": "Revoke Trust", + "revokeConfirm": "Are you sure you want to revoke trust for this device? You will need to enter your 2FA code next time you log in from this device.", + "deviceRevoked": "Device trust revoked", + "deviceRenamed": "Device renamed", + "deviceNamePlaceholder": "Device name", + "browser": "Browser:", + "unknownDevice": "Unknown device" + }, + "reauth": { + "title": "Re-authentication Required", + "subtitle": "Please verify your identity to continue.", + "usePassword": "Use Password", + "usePasskey": "Use Passkey", + "useTotp": "Use Authenticator", + "passwordPlaceholder": "Enter your password", + "totpPlaceholder": "Enter 6-digit code", + "verify": "Verify", + "verifying": "Verifying...", + "cancel": "Cancel" + } +} diff --git a/frontend/src/locales/ja.json b/frontend/src/locales/ja.json new file mode 100644 index 0000000..86e647b --- /dev/null +++ b/frontend/src/locales/ja.json @@ -0,0 +1,702 @@ +{ + "common": { + "loading": "読み込み中...", + "error": "エラー", + "save": "保存", + "cancel": "キャンセル", + "back": "戻る", + "done": "完了", + "refresh": "更新", + "create": "作成", + "delete": "削除", + "confirm": "確認", + "created": "作成日時", + "expires": "有効期限", + "name": "名前", + "dashboard": "ダッシュボード", + "backToDashboard": "← ダッシュボード" + }, + "login": { + "title": "サインイン", + "subtitle": "PDS アカウントを管理するにはサインインしてください", + "button": "サインイン", + "redirecting": "リダイレクト中...", + "chooseAccount": "アカウントを選択", + "signInToAnother": "別のアカウントでサインイン", + "backToSaved": "← 保存済みアカウントに戻る", + "forgotPassword": "パスワードをお忘れですか?", + "lostPasskey": "パスキーを紛失しましたか?", + "noAccount": "アカウントをお持ちでないですか?", + "createAccount": "アカウントを作成", + "removeAccount": "保存済みアカウントから削除" + }, + "verification": { + "title": "アカウント確認", + "subtitle": "アカウントの確認が必要です。確認方法に送信されたコードを入力してください。", + "codeLabel": "確認コード", + "codePlaceholder": "6桁のコードを入力", + "verifyButton": "確認する", + "verifying": "確認中...", + "resendButton": "コードを再送信", + "resending": "送信中...", + "resent": "確認コードを再送信しました!", + "backToLogin": "ログインに戻る" + }, + "register": { + "title": "アカウント作成", + "subtitle": "この PDS で新規アカウントを作成", + "handle": "ハンドル", + "handlePlaceholder": "あなたの名前", + "handleHint": "完全なハンドル: @{handle}", + "handleDotWarning": "カスタムドメインハンドルはアカウント作成後に設定で構成できます。", + "password": "パスワード", + "passwordPlaceholder": "8文字以上", + "confirmPassword": "パスワード確認", + "confirmPasswordPlaceholder": "パスワードを再入力", + "identityType": "アイデンティティタイプ", + "identityHint": "分散型アイデンティティの管理方法を選択してください。", + "didPlc": "did:plc", + "didPlcRecommended": "(推奨)", + "didPlcHint": "PLC ディレクトリで管理されるポータブルアイデンティティ", + "didWeb": "did:web", + "didWebHint": "この PDS でホストされるアイデンティティ(下記の警告をお読みください)", + "didWebBYOD": "did:web (BYOD)", + "didWebBYODHint": "独自ドメインを持ち込む", + "didWebWarningTitle": "重要: トレードオフをご理解ください", + "didWebWarning1": "この PDS への永続的な紐付け:", + "didWebWarning1Detail": "あなたのアイデンティティは {did} になります。後で別の PDS に移行しても、このサーバーは DID ドキュメントをホストし続ける必要があります。", + "didWebWarning2": "復旧手段がありません:", + "didWebWarning2Detail": "did:plc と異なり、did:web にはローテーションキーがありません。この PDS が永久にオフラインになると、アイデンティティは復旧できません。", + "didWebWarning3": "私たちの約束:", + "didWebWarning3Detail": "移行する場合、新しい PDS を指す最小限の DID ドキュメントを引き続き提供します。アイデンティティは機能し続けます。", + "didWebWarning4": "推奨:", + "didWebWarning4Detail": "did:web を希望する特定の理由がない限り、did:plc を選択してください。", + "externalDid": "あなたの did:web", + "externalDidPlaceholder": "did:web:yourdomain.com", + "externalDidHint": "ドメインは /.well-known/did.json でこの PDS を指す有効な DID ドキュメントを提供する必要があります", + "contactMethod": "連絡方法", + "contactMethodHint": "アカウントの確認と通知の受信方法を選択してください。1つだけ必要です。", + "verificationMethod": "確認方法", + "email": "メール", + "emailAddress": "メールアドレス", + "emailPlaceholder": "you@example.com", + "discord": "Discord", + "discordId": "Discord ユーザー ID", + "discordIdPlaceholder": "Discord ユーザー ID", + "discordIdHint": "数値の Discord ユーザー ID(開発者モードを有効にして確認)", + "telegram": "Telegram", + "telegramUsername": "Telegram ユーザー名", + "telegramUsernamePlaceholder": "@yourusername", + "signal": "Signal", + "signalNumber": "Signal 電話番号", + "signalNumberPlaceholder": "+81XXXXXXXXXX", + "signalNumberHint": "国番号を含めてください(例: 日本は +81)", + "inviteCode": "招待コード", + "inviteCodePlaceholder": "招待コードを入力", + "inviteCodeRequired": "必須", + "createButton": "アカウントを作成", + "creating": "作成中...", + "alreadyHaveAccount": "すでにアカウントをお持ちですか?", + "signIn": "サインイン", + "wantPasswordless": "パスワードレスをご希望ですか?", + "createPasskeyAccount": "パスキーアカウントを作成", + "validation": { + "handleRequired": "ハンドルは必須です", + "handleNoDots": "ハンドルにドットは使用できません。アカウント作成後にカスタムドメインを設定できます。", + "passwordRequired": "パスワードは必須です", + "passwordLength": "パスワードは8文字以上である必要があります", + "passwordsMismatch": "パスワードが一致しません", + "inviteCodeRequired": "招待コードは必須です", + "externalDidRequired": "外部 did:web は必須です", + "externalDidFormat": "外部 DID は did:web: で始まる必要があります", + "emailRequired": "メール認証にはメールアドレスが必要です", + "discordIdRequired": "Discord 認証には Discord ID が必要です", + "telegramRequired": "Telegram 認証には Telegram ユーザー名が必要です", + "signalRequired": "Signal 認証には電話番号が必要です" + } + }, + "dashboard": { + "title": "ダッシュボード", + "switchAccount": "アカウント切替", + "addAnotherAccount": "別のアカウントを追加", + "signOut": "@{handle} からサインアウト", + "deactivatedTitle": "アカウント無効化", + "deactivatedMessage": "アカウントは現在無効化されています。これは通常、アカウント移行中に発生します。アカウントが再有効化されるまで、一部の機能が制限される場合があります。", + "accountOverview": "アカウント概要", + "handle": "ハンドル", + "did": "DID", + "primaryContact": "主要連絡先", + "admin": "管理者", + "deactivated": "無効化", + "verified": "認証済み", + "unverified": "未認証", + "navAppPasswords": "アプリパスワード", + "navAppPasswordsDesc": "サードパーティアプリのパスワードを管理", + "navSessions": "アクティブセッション", + "navSessionsDesc": "ログインセッションを表示・管理", + "navInviteCodes": "招待コード", + "navInviteCodesDesc": "招待コードを表示・作成", + "navSettings": "アカウント設定", + "navSettingsDesc": "メール、パスワード、ハンドルなど", + "navSecurity": "セキュリティ", + "navSecurityDesc": "二要素認証", + "navComms": "連絡設定", + "navCommsDesc": "Discord、Telegram、Signal チャンネル", + "navRepo": "リポジトリエクスプローラー", + "navRepoDesc": "AT Protocol レコードを閲覧・管理", + "navAdmin": "管理パネル", + "navAdminDesc": "サーバー統計と管理操作" + }, + "settings": { + "title": "アカウント設定", + "language": "言語", + "languageDescription": "お好みの言語を選択", + "changeEmail": "メール変更", + "currentEmail": "現在: {email}", + "newEmail": "新しいメール", + "newEmailPlaceholder": "new@example.com", + "changeEmailButton": "メールを変更", + "requesting": "リクエスト中...", + "verificationCode": "確認コード", + "verificationCodePlaceholder": "メールから受け取ったコードを入力", + "confirmEmailChange": "メール変更を確認", + "updating": "更新中...", + "changeHandle": "ハンドル変更", + "currentHandle": "現在: @{handle}", + "pdsHandle": "PDS ハンドル", + "customDomain": "カスタムドメイン", + "customDomainDescription": "独自のドメインをハンドルとして使用します。まずドメインの所有権を確認する必要があります。", + "setupInstructions": "設定手順", + "setupMethodsIntro": "以下の確認方法のいずれかを選択してください:", + "dnsMethod": "方法 1: DNS TXT レコード(推奨)", + "dnsMethodDesc": "ドメインにこの TXT レコードを追加:", + "httpMethod": "方法 2: HTTP Well-Known ファイル", + "httpMethodDesc": "この URL で DID を提供:", + "httpMethodContent": "ファイルには以下の内容のみを含める:", + "yourDomain": "ドメイン", + "yourDomainPlaceholder": "example.com", + "verifyAndUpdate": "確認してハンドルを更新", + "verifying": "確認中...", + "newHandle": "新しいハンドル", + "newHandlePlaceholder": "yourhandle", + "changeHandleButton": "ハンドルを変更", + "changePassword": "パスワード変更", + "currentPassword": "現在のパスワード", + "currentPasswordPlaceholder": "現在のパスワードを入力", + "newPassword": "新しいパスワード", + "newPasswordPlaceholder": "8文字以上", + "confirmNewPassword": "新しいパスワードの確認", + "confirmNewPasswordPlaceholder": "新しいパスワードを再入力", + "changePasswordButton": "パスワードを変更", + "changing": "変更中...", + "exportData": "データエクスポート", + "exportDataDescription": "リポジトリ全体を CAR(Content Addressable Archive)ファイルとしてダウンロードします。投稿、いいね、フォローなどすべてのデータが含まれます。", + "downloadRepo": "リポジトリをダウンロード", + "exporting": "エクスポート中...", + "deleteAccount": "アカウント削除", + "deleteWarning": "この操作は取り消せません。すべてのデータが完全に削除されます。", + "requestDeletion": "アカウント削除をリクエスト", + "confirmationCode": "確認コード(メールから)", + "confirmationCodePlaceholder": "確認コードを入力", + "yourPassword": "パスワード", + "yourPasswordPlaceholder": "パスワードを入力", + "permanentlyDelete": "アカウントを完全に削除", + "deleting": "削除中...", + "messages": { + "emailCodeSent": "現在のメールに確認コードを送信しました", + "emailUpdated": "メールを更新しました", + "handleUpdated": "ハンドルを更新しました", + "passwordChanged": "パスワードを変更しました", + "passwordsMismatch": "パスワードが一致しません", + "passwordLength": "パスワードは8文字以上である必要があります", + "deletionCodeSent": "削除確認をメールに送信しました", + "repoExported": "リポジトリをエクスポートしました", + "confirmDelete": "本当にアカウントを削除しますか?この操作は取り消せません。" + } + }, + "appPasswords": { + "title": "アプリパスワード", + "description": "アプリパスワードを使用すると、メインパスワードを提供せずにサードパーティアプリにサインインできます。各アプリパスワードは個別に取り消すことができます。", + "createNew": "新しいアプリパスワードを作成", + "appNamePlaceholder": "アプリ名(例: Graysky、Skeets)", + "created": "アプリパスワードを作成しました", + "createdMessage": "このパスワードを今すぐコピーしてください。再度表示することはできません。", + "yourPasswords": "アプリパスワード一覧", + "noPasswords": "アプリパスワードはまだありません", + "revoke": "取り消す", + "revoking": "取り消し中...", + "creating": "作成中...", + "revokeConfirm": "アプリパスワード「{name}」を取り消しますか?このパスワードを使用しているアプリはアカウントにアクセスできなくなります。" + }, + "sessions": { + "title": "アクティブセッション", + "loadingSessions": "セッションを読み込み中...", + "noSessions": "アクティブなセッションが見つかりません。", + "current": "現在", + "oauth": "OAuth", + "session": "セッション", + "signOut": "サインアウト", + "revoke": "取り消す", + "revokeAll": "他のすべてのセッションを取り消す", + "revokeCurrentConfirm": "このセッションからサインアウトされます。続行しますか?", + "revokeConfirm": "このセッションを取り消しますか?", + "revokeAllConfirm": "他の {count} 件のセッションを取り消します。続行しますか?", + "noOtherSessions": "取り消す他のセッションはありません", + "failedToLoad": "セッションの読み込みに失敗しました", + "failedToRevoke": "セッションの取り消しに失敗しました", + "failedToRevokeAll": "セッションの取り消しに失敗しました", + "created": "作成日時:", + "expires": "有効期限:", + "daysAgo": "{count}日前", + "hoursAgo": "{count}時間前", + "minutesAgo": "{count}分前", + "justNow": "たった今" + }, + "inviteCodes": { + "title": "招待コード", + "description": "招待コードで友人をこの PDS に招待できます。各コードは1回のみ使用可能です。", + "createNew": "新しい招待コードを作成", + "uses": "使用回数", + "usesPlaceholder": "使用回数(1-100)", + "yourCodes": "招待コード一覧", + "noCodes": "招待コードはまだありません", + "available": "利用可能", + "used": "@{handle} が使用済み", + "disabled": "無効", + "usedBy": "使用者", + "creating": "作成中...", + "disableConfirm": "この招待コードを無効にしますか?使用できなくなります。", + "created": "招待コードを作成しました", + "copy": "コピー", + "createdOn": "{date} に作成" + }, + "security": { + "title": "セキュリティ", + "passkeys": "パスキー", + "passkeysDescription": "パスキーは、デバイスの内蔵セキュリティ(指紋、顔、または PIN)を使用して、安全なパスワードレス認証を提供します。", + "addPasskey": "パスキーを追加", + "adding": "追加中...", + "noPasskeys": "登録されたパスキーはありません", + "passkeyName": "パスキー名", + "passkeyNamePlaceholder": "例: MacBook Pro、iPhone", + "register": "登録", + "registering": "登録中...", + "rename": "名前変更", + "renaming": "名前変更中...", + "deletePasskey": "削除", + "deletePasskeyConfirm": "パスキー「{name}」を削除しますか?サインインに使用できなくなります。", + "totp": "認証アプリ (TOTP)", + "totpDescription": "Google Authenticator、Authy、1Password などの認証アプリを二要素認証に使用します。", + "totpEnabled": "TOTP は有効です", + "totpDisabled": "TOTP は無効です", + "enableTotp": "TOTP を有効化", + "disableTotp": "TOTP を無効化", + "disabling": "無効化中...", + "totpSetup": "認証アプリの設定", + "totpSetupInstructions": "認証アプリでこの QR コードをスキャンし、6桁のコードを入力して確認してください。", + "totpCode": "確認コード", + "totpCodePlaceholder": "6桁のコードを入力", + "verifyAndEnable": "確認して有効化", + "backupCodes": "バックアップコード", + "backupCodesDescription": "認証アプリにアクセスできなくなった場合、これらのコードを使用してサインインします。各コードは1回のみ使用可能です。", + "regenerateBackupCodes": "バックアップコードを再生成", + "regenerating": "再生成中...", + "regenerateConfirm": "バックアップコードを再生成しますか?現在のコードは使用できなくなります。", + "legacyLogin": "レガシーログイン", + "legacyLoginDescription": "ユーザー名/パスワードでの直接ログイン(レガシーモード)を許可します。無効にすると、MFA 付きの OAuth を使用する必要があります。", + "legacyLoginOn": "レガシーログインは有効です", + "legacyLoginOff": "レガシーログインは無効です", + "legacyLoginWarning": "警告: レガシーログインを有効にすると、直接パスワードログインの MFA がバイパスされます。アプリの互換性が必要な場合にのみ有効にしてください。", + "totpPasswordWarning": "TOTP が有効な場合、Bluesky アプリ(または他のレガシーアプリ)からパスワードを変更することはできません。パスワードを変更するには、2つの方法があります:", + "totpPasswordOption1Label": "ここで変更する:", + "totpPasswordOption1Text": "このウェブサイトの", + "totpPasswordOption1Link": "設定ページ", + "totpPasswordOption1Suffix": "を使用して、認証アプリで確認できます。", + "totpPasswordOption2Label": "まずセッションを確認する:", + "totpPasswordOption2Text": "", + "totpPasswordOption2Link": "再認証オプション", + "totpPasswordOption2Suffix": "を使用して Bluesky セッションを TOTP で確認すると、一時的にパスワード変更が可能になります。", + "legacyAppsTitle": "レガシーアプリとは?", + "legacyAppsDescription": "一部のアプリ(公式 Bluesky アプリなど)は、パスワードのみを必要とする古い認証を使用します。MFA を有効にしている場合、これらのアプリは二要素認証をバイパスします。レガシーログインを無効にすると、すべてのアプリが OAuth を使用するよう強制され、MFA が適切に適用されます。", + "password": "パスワード", + "passwordStatus": "パスワードが設定されています", + "noPassword": "パスワードは設定されていません(パスキーのみのアカウント)", + "setPassword": "パスワードを設定", + "removePassword": "パスワードを削除", + "removePasswordConfirm": "パスワードを削除しますか?サインインにパスキーが必要になります。", + "removing": "削除中...", + "loading": "読み込み中...", + "loadingPasskeys": "パスキーを読み込み中...", + "cancel": "キャンセル", + "save": "保存", + "back": "戻る", + "next": "次へ: コードを確認", + "copyToClipboard": "クリップボードにコピー", + "savedMyCodes": "コードを保存しました", + "cantScan": "スキャンできませんか?手動で入力", + "unnamedPasskey": "名前のないパスキー", + "added": "追加日", + "lastUsed": "最終使用日", + "passwordDescription": "アカウントパスワードを管理します。パスキーを設定している場合、完全にパスワードレスな体験のためにパスワードを削除することもできます。", + "disableTotpWarning": "これによりアカウントのセキュリティが低下します。", + "removePasswordWarning": "これによりアカウントはパスキーのみになります。登録済みのパスキーでのみサインインできます。すべてのパスキーにアクセスできなくなった場合、通知チャンネルを使用してアカウントを復旧できます。", + "beforeProceeding": "続行する前に:", + "beforeProceedingItem1": "少なくとも1つの信頼できるパスキーが登録されていることを確認", + "beforeProceedingItem2": "複数のデバイスにパスキーを登録することを検討", + "beforeProceedingItem3": "復旧用の通知チャンネルが最新であることを確認", + "addPasskeyFirst": "パスワードを削除する前に、少なくとも1つのパスキーを追加してください。", + "passkeyOnlyHint": "パスキーのみでサインインしています。パスキーにアクセスできなくなった場合、ログインページの「パスキーを紛失しましたか?」リンクからアカウントを復旧できます。", + "trustedDevices": "信頼済みデバイス", + "trustedDevicesDescription": "サインイン時に二要素認証をスキップできるデバイスを管理します。信頼は30日間有効で、デバイスを使用すると自動的に延長されます。", + "manageTrustedDevices": "信頼済みデバイスを管理", + "appCompatibility": "アプリ互換性", + "enterPassword": "パスワードを入力", + "legacyLoginEnabled": "レガシーアプリログインが有効", + "legacyLoginDisabled": "レガシーアプリログインが無効 - OAuth アプリのみサインイン可能", + "failedToUpdatePreference": "設定の更新に失敗しました", + "passwordRemoved": "パスワードが削除されました。アカウントはパスキーのみになりました。", + "failedToRemovePassword": "パスワードの削除に失敗しました", + "failedToLoadTotpStatus": "TOTP ステータスの読み込みに失敗しました", + "totpEnabledSuccess": "二要素認証が正常に有効化されました", + "totpDisabledSuccess": "二要素認証が無効化されました", + "backupCodesCopied": "バックアップコードをクリップボードにコピーしました", + "failedToLoadPasskeys": "パスキーの読み込みに失敗しました", + "passkeysNotSupported": "このブラウザではパスキーがサポートされていません", + "passkeyCreationCancelled": "パスキーの作成がキャンセルされました", + "passkeyAddedSuccess": "パスキーが追加されました", + "passkeyDeleted": "パスキーが削除されました", + "passkeyRenamed": "パスキーの名前が変更されました" + }, + "comms": { + "title": "連絡設定", + "description": "パスワードリセット、セキュリティアラート、アカウント更新などの重要なメッセージの受信方法を選択してください。", + "preferredChannel": "優先チャンネル", + "preferredChannelDescription": "メッセージの優先受信方法を選択してください。選択する前にチャンネルを設定する必要があります。", + "channelConfiguration": "チャンネル設定", + "emailVia": "メールでメッセージを受信", + "discordVia": "Discord DM でメッセージを受信", + "telegramVia": "Telegram でメッセージを受信", + "signalVia": "Signal でメッセージを受信", + "configureToEnable": "有効にするには下記で設定", + "emailManagedInSettings": "メールはアカウント設定で管理されています", + "discordIdHint": "Discord ユーザー ID(ユーザー名ではありません)。Discord で開発者モードを有効にしてコピーしてください。", + "telegramHint": "@ 記号なしの Telegram ユーザー名", + "signalHint": "国番号付きの Signal 電話番号", + "primary": "優先", + "verified": "確認済み", + "notVerified": "未確認", + "verifyButton": "確認", + "verifyCodePlaceholder": "確認コードを入力", + "submit": "送信", + "saving": "保存中...", + "savePreferences": "設定を保存", + "preferencesSaved": "連絡設定を保存しました", + "verifiedSuccess": "{channel} を確認しました", + "messageHistory": "メッセージ履歴", + "historyDescription": "アカウントに送信された最近のメッセージを表示します。", + "loadHistory": "履歴を読み込む", + "hideHistory": "履歴を隠す", + "noMessages": "メッセージが見つかりません。", + "sent": "送信済み", + "failed": "失敗" + }, + "repoExplorer": { + "title": "リポジトリエクスプローラー", + "description": "AT Protocol レコードを閲覧・管理します。", + "collections": "コレクション", + "noCollections": "コレクションが見つかりません", + "records": "レコード", + "noRecords": "このコレクションにレコードはありません", + "recordDetails": "レコード詳細", + "rkey": "レコードキー", + "cid": "CID", + "value": "値", + "deleteRecord": "レコードを削除", + "deleteConfirm": "レコード {rkey} を削除しますか?この操作は取り消せません。", + "unknownError": "不明なエラーが発生しました", + "invalidJson": "無効な JSON", + "collectionRequired": "コレクションは必須です", + "recordCreated": "レコードを作成しました: {uri}", + "recordUpdated": "レコードを更新しました", + "recordDeleted": "レコードを削除しました", + "newRecord": "新規レコード", + "createRecord": "レコードを作成", + "filterCollections": "コレクションを検索...", + "filterRecords": "レコードを検索...", + "noCollectionsYet": "コレクションがまだありません。最初のレコードを作成して開始しましょう。", + "loadMore": "さらに読み込む", + "recordJson": "レコード JSON", + "saving": "保存中...", + "updateRecord": "レコードを更新", + "collectionNsid": "コレクション (NSID)", + "recordKeyOptional": "レコードキー(任意)", + "autoGenerated": "空白で自動生成 (TID)", + "autoGeneratedHint": "空白にすると TID ベースのキーが自動生成されます", + "creating": "作成中...", + "demoPostText": "こんにちは、私の PDS からの初投稿です!", + "demoDisplayName": "表示名", + "demoBio": "自己紹介を書いてください。" + }, + "admin": { + "title": "管理パネル", + "serverStats": "サーバー統計", + "users": "ユーザー", + "repos": "リポジトリ", + "records": "レコード", + "blobStorage": "Blob ストレージ", + "refreshStats": "統計を更新", + "userManagement": "ユーザー管理", + "searchPlaceholder": "ハンドルで検索(任意)", + "searchUsers": "ユーザーを検索", + "noUsers": "ユーザーが見つかりません", + "handle": "ハンドル", + "email": "メール", + "status": "ステータス", + "created": "作成日時", + "loadMore": "さらに読み込む", + "inviteCodes": "招待コード", + "loadInviteCodes": "招待コードを読み込む", + "refresh": "更新", + "noInvites": "招待コードが見つかりません", + "code": "コード", + "available": "利用可能", + "uses": "使用回数", + "actions": "アクション", + "disable": "無効化", + "disableInviteConfirm": "招待コード {code} を無効にしますか?", + "active": "アクティブ", + "exhausted": "使用済み", + "disabled": "無効", + "userDetails": "ユーザー詳細", + "did": "DID", + "invites": "招待", + "enabled": "有効", + "enableInvites": "招待を有効化", + "disableInvites": "招待を無効化", + "deleteAccount": "アカウント削除", + "deleteConfirm": "アカウント @{handle} を削除しますか?この操作は取り消せません。", + "verified": "確認済み", + "unverified": "未確認", + "deactivated": "無効化" + }, + "oauth": { + "login": { + "title": "サインイン", + "subtitle": "アプリを続行するにはサインインしてください", + "signingIn": "サインイン中...", + "authenticating": "認証中...", + "checkingPasskey": "パスキーを確認中...", + "signInWithPasskey": "パスキーでサインイン", + "passkeyNotSetUp": "パスキーは設定されていません", + "orUsePassword": "またはパスワードを使用", + "password": "パスワード", + "rememberDevice": "このデバイスを記憶する", + "passkeyHintChecking": "パスキーの状態を確認中...", + "passkeyHintAvailable": "パスキーでサインイン", + "passkeyHintNotAvailable": "このアカウントにはパスキーが登録されていません" + }, + "consent": { + "title": "アプリを承認", + "appWantsAccess": "{app} があなたのアカウントにアクセスしようとしています", + "permissions": "このアプリは以下のことができるようになります:", + "readProfile": "プロフィール情報を読み取る", + "readPosts": "投稿とコンテンツを読み取る", + "writePosts": "あなたに代わって投稿を作成・削除する", + "readNotifications": "通知を読み取る", + "fullAccess": "アカウントへのフルアクセス", + "authorize": "承認", + "deny": "拒否", + "authorizing": "承認中...", + "rememberChoice": "この選択を記憶", + "signingInAs": "サインイン中のアカウント:", + "permissionsRequested": "リクエストされた権限", + "required": "必須", + "rememberChoiceLabel": "このアプリに対する選択を記憶する" + }, + "accounts": { + "title": "アカウントを選択", + "subtitle": "続行するアカウントを選択", + "useAnother": "別のアカウントを使用" + }, + "twoFactor": { + "title": "二要素認証", + "subtitle": "追加の確認が必要です", + "usePasskey": "パスキーを使用", + "useTotp": "認証アプリを使用", + "verifying": "確認中..." + }, + "twoFactorCode": { + "title": "二要素認証", + "subtitle": "{channel} に確認コードを送信しました。以下にコードを入力して続行してください。", + "codeLabel": "確認コード", + "codePlaceholder": "6桁のコードを入力", + "verify": "確認", + "verifying": "確認中...", + "errors": { + "missingRequestUri": "request_uri パラメータがありません", + "verificationFailed": "確認に失敗しました", + "connectionFailed": "サーバーへの接続に失敗しました", + "unexpectedResponse": "サーバーからの予期しない応答" + } + }, + "totp": { + "title": "認証コードを入力", + "subtitle": "認証アプリの6桁のコードを入力", + "codePlaceholder": "6桁のコードを入力", + "verify": "確認", + "verifying": "確認中...", + "useBackupCode": "バックアップコードを使用", + "backupCodePlaceholder": "バックアップコードを入力", + "trustDevice": "このデバイスを30日間信頼する", + "hintBackupCode": "バックアップコードを使用中", + "hintTotpCode": "認証コードを使用中", + "hintDefault": "認証アプリは6桁、バックアップコードは8文字" + }, + "passkey": { + "title": "パスキー確認", + "subtitle": "パスキーで本人確認を行います", + "waiting": "パスキーを待機中...", + "useTotp": "認証アプリを使用" + }, + "error": { + "title": "承認エラー", + "genericError": "承認中にエラーが発生しました。", + "tryAgain": "再試行", + "backToApp": "アプリに戻る" + } + }, + "verify": { + "title": "アカウント確認", + "subtitle": "{channel} に確認コードを送信しました。以下に入力して登録を完了してください。", + "codePlaceholder": "6桁のコードを入力", + "codeLabel": "確認コード", + "verifyButton": "アカウントを確認", + "verifying": "確認中...", + "resendCode": "コードを再送信", + "resending": "送信中...", + "codeResent": "確認コードを再送信しました!", + "backToLogin": "ログインに戻る", + "verifyingAccount": "確認中のアカウント: @{handle}", + "startOver": "別のアカウントでやり直す", + "noPending": "保留中の確認が見つかりません。", + "noPendingInfo": "最近アカウントを作成して確認が必要な場合は、新しいアカウントを作成する必要があります。すでにアカウントを確認した場合は、サインインできます。", + "createAccount": "アカウントを作成", + "signIn": "サインイン" + }, + "resetPassword": { + "title": "パスワードリセット", + "forgotTitle": "パスワードをお忘れですか", + "subtitle": "受け取ったコードを入力して、新しいパスワードを選択してください。", + "forgotSubtitle": "ハンドルまたはメールアドレスを入力すると、パスワードリセットコードを送信します。", + "handleOrEmail": "ハンドルまたはメール", + "emailPlaceholder": "ハンドルまたは you@example.com", + "sendCode": "リセットコードを送信", + "sending": "送信中...", + "codeSent": "パスワードリセットコードを送信しました!優先通知チャンネルを確認してください。", + "enterCode": "メールからのコードと新しいパスワードを入力してください。", + "code": "リセットコード", + "codePlaceholder": "リセットコードを入力", + "newPassword": "新しいパスワード", + "newPasswordPlaceholder": "8文字以上", + "confirmPassword": "パスワード確認", + "confirmPasswordPlaceholder": "新しいパスワードを再入力", + "resetButton": "パスワードをリセット", + "resetting": "リセット中...", + "success": "パスワードをリセットしました!", + "backToLogin": "サインインに戻る", + "requestNewCode": "新しいコードをリクエスト", + "passwordsMismatch": "パスワードが一致しません", + "passwordLength": "パスワードは8文字以上である必要があります" + }, + "recoverPasskey": { + "title": "アカウントを復旧", + "invalidLinkTitle": "無効な復旧リンク", + "invalidLinkMessage": "この復旧リンクは無効または破損しています。新しい復旧メールをリクエストしてください。", + "goToLogin": "ログインへ", + "successTitle": "パスワードを設定しました!", + "successMessage": "一時パスワードを設定しました。このパスワードでサインインできます。", + "successNextSteps": "サインイン後、セキュリティ設定で新しいパスキーを追加して、パスキーのみの認証を復元することをお勧めします。", + "signIn": "サインイン", + "subtitle": "パスキーのみのアカウントへのアクセスを回復するために一時パスワードを設定します。", + "newPassword": "新しいパスワード", + "newPasswordPlaceholder": "8文字以上", + "confirmPassword": "パスワード確認", + "confirmPasswordPlaceholder": "パスワードを再入力", + "whatHappensNext": "次のステップ", + "whatHappensNextDetail": "このパスワードを設定後、サインインしてセキュリティ設定で新しいパスキーを追加できます。新しいパスキーを追加したら、一時パスワードを削除することもできます。", + "setPassword": "パスワードを設定", + "settingPassword": "パスワードを設定中...", + "validation": { + "passwordRequired": "新しいパスワードは必須です", + "passwordLength": "パスワードは8文字以上である必要があります", + "passwordsMismatch": "パスワードが一致しません" + }, + "errors": { + "invalidLink": "無効な復旧リンクです。新しいリンクをリクエストしてください。", + "expired": "この復旧リンクは期限切れです。新しいリンクをリクエストしてください。" + } + }, + "requestPasskeyRecovery": { + "title": "パスキーアカウントを復旧", + "subtitle": "パスキーにアクセスできなくなりましたか?ハンドルまたはメールを入力すると、復旧リンクを送信します。", + "successTitle": "復旧リンクを送信しました", + "successMessage": "アカウントが存在し、パスキーのみのアカウントの場合、優先通知チャンネルに復旧リンクが届きます。", + "successInfo": "リンクは1時間で期限切れになります。アカウント設定に応じて、メール、Discord、Telegram、または Signal を確認してください。", + "handleOrEmail": "ハンドルまたはメール", + "emailPlaceholder": "ハンドルまたは you@example.com", + "howItWorks": "仕組み", + "howItWorksDetail": "登録された通知チャンネルに安全なリンクを送信します。リンクをクリックして一時パスワードを設定します。その後サインインして新しいパスキーを追加できます。", + "sendRecoveryLink": "復旧リンクを送信", + "sending": "送信中...", + "backToLogin": "サインインに戻る" + }, + "registerPasskey": { + "title": "パスキーアカウントを作成", + "subtitle": "パスキーを使用してパスワードレスアカウントを作成します。", + "handle": "ハンドル", + "handlePlaceholder": "あなたの名前", + "handleHint": "完全なハンドル: @{handle}", + "email": "メールアドレス", + "emailPlaceholder": "you@example.com", + "inviteCode": "招待コード", + "inviteCodePlaceholder": "招待コードを入力", + "createButton": "アカウントを作成", + "creating": "作成中...", + "alreadyHaveAccount": "すでにアカウントをお持ちですか?", + "signIn": "サインイン", + "wantPassword": "パスワードを使用しますか?", + "createPasswordAccount": "パスワードアカウントを作成" + }, + "trustedDevices": { + "title": "信頼済みデバイス", + "backToSecurity": "← セキュリティ設定", + "description": "信頼済みデバイスはログイン時に二要素認証をスキップできます。信頼は30日間有効で、デバイスを使用すると自動的に延長されます。", + "noDevices": "信頼済みデバイスはまだありません。", + "noDevicesHint": "二要素認証を有効にしてログインする際に、デバイスを30日間信頼することを選択できます。", + "lastSeen": "最終使用:", + "trustedSince": "信頼開始:", + "trustExpires": "信頼期限:", + "expired": "期限切れ", + "tomorrow": "明日", + "inDays": "あと{days}日", + "revoke": "信頼を取り消す", + "revokeConfirm": "このデバイスへの信頼を取り消しますか?次回このデバイスからログインする際に2FAコードの入力が必要になります。", + "deviceRevoked": "デバイスの信頼を取り消しました", + "deviceRenamed": "デバイス名を変更しました", + "deviceNamePlaceholder": "デバイス名", + "browser": "ブラウザ:", + "unknownDevice": "不明なデバイス" + }, + "reauth": { + "title": "再認証が必要です", + "subtitle": "続行するには本人確認を行ってください。", + "usePassword": "パスワードを使用", + "usePasskey": "パスキーを使用", + "useTotp": "認証アプリを使用", + "passwordPlaceholder": "パスワードを入力", + "totpPlaceholder": "6桁のコードを入力", + "verify": "確認", + "verifying": "確認中...", + "cancel": "キャンセル" + } +} diff --git a/frontend/src/locales/ko.json b/frontend/src/locales/ko.json new file mode 100644 index 0000000..23dfc97 --- /dev/null +++ b/frontend/src/locales/ko.json @@ -0,0 +1,702 @@ +{ + "common": { + "loading": "로딩 중...", + "error": "오류", + "save": "저장", + "cancel": "취소", + "back": "뒤로", + "done": "완료", + "refresh": "새로고침", + "create": "생성", + "delete": "삭제", + "confirm": "확인", + "created": "생성일", + "expires": "만료일", + "name": "이름", + "dashboard": "대시보드", + "backToDashboard": "← 대시보드" + }, + "login": { + "title": "로그인", + "subtitle": "PDS 계정을 관리하려면 로그인하세요", + "button": "로그인", + "redirecting": "리디렉션 중...", + "chooseAccount": "계정 선택", + "signInToAnother": "다른 계정으로 로그인", + "backToSaved": "← 저장된 계정으로 돌아가기", + "forgotPassword": "비밀번호를 잊으셨나요?", + "lostPasskey": "패스키를 분실하셨나요?", + "noAccount": "계정이 없으신가요?", + "createAccount": "계정 만들기", + "removeAccount": "저장된 계정에서 삭제" + }, + "verification": { + "title": "계정 인증", + "subtitle": "계정 인증이 필요합니다. 인증 방법으로 전송된 코드를 입력하세요.", + "codeLabel": "인증 코드", + "codePlaceholder": "6자리 코드 입력", + "verifyButton": "계정 인증", + "verifying": "인증 중...", + "resendButton": "코드 다시 보내기", + "resending": "전송 중...", + "resent": "인증 코드를 다시 보냈습니다!", + "backToLogin": "로그인으로 돌아가기" + }, + "register": { + "title": "계정 만들기", + "subtitle": "이 PDS에 새 계정을 만듭니다", + "handle": "핸들", + "handlePlaceholder": "사용자 이름", + "handleHint": "전체 핸들: @{handle}", + "handleDotWarning": "사용자 정의 도메인 핸들은 계정 생성 후 설정에서 구성할 수 있습니다.", + "password": "비밀번호", + "passwordPlaceholder": "8자 이상", + "confirmPassword": "비밀번호 확인", + "confirmPasswordPlaceholder": "비밀번호 재입력", + "identityType": "ID 유형", + "identityHint": "분산 ID를 관리하는 방법을 선택하세요.", + "didPlc": "did:plc", + "didPlcRecommended": "(권장)", + "didPlcHint": "PLC 디렉토리에서 관리하는 이동 가능한 ID", + "didWeb": "did:web", + "didWebHint": "이 PDS에서 호스팅되는 ID (아래 경고 참조)", + "didWebBYOD": "did:web (BYOD)", + "didWebBYODHint": "자체 도메인 사용", + "didWebWarningTitle": "중요: 장단점을 이해하세요", + "didWebWarning1": "이 PDS에 영구 연결:", + "didWebWarning1Detail": "ID는 {did}가 됩니다. 나중에 다른 PDS로 마이그레이션하더라도 이 서버는 계속 DID 문서를 호스팅해야 합니다.", + "didWebWarning2": "복구 메커니즘 없음:", + "didWebWarning2Detail": "did:plc와 달리 did:web에는 순환 키가 없습니다. 이 PDS가 영구적으로 오프라인이 되면 ID를 복구할 수 없습니다.", + "didWebWarning3": "우리의 약속:", + "didWebWarning3Detail": "마이그레이션하면 새 PDS를 가리키는 최소한의 DID 문서를 계속 제공합니다. ID는 계속 작동합니다.", + "didWebWarning4": "권장:", + "didWebWarning4Detail": "did:web을 선호하는 특별한 이유가 없다면 did:plc를 선택하세요.", + "externalDid": "귀하의 did:web", + "externalDidPlaceholder": "did:web:yourdomain.com", + "externalDidHint": "도메인은 /.well-known/did.json에서 이 PDS를 가리키는 유효한 DID 문서를 제공해야 합니다", + "contactMethod": "연락 방법", + "contactMethodHint": "계정 인증 및 알림 수신 방법을 선택하세요. 하나만 필요합니다.", + "verificationMethod": "인증 방법", + "email": "이메일", + "emailAddress": "이메일 주소", + "emailPlaceholder": "you@example.com", + "discord": "Discord", + "discordId": "Discord 사용자 ID", + "discordIdPlaceholder": "Discord 사용자 ID", + "discordIdHint": "숫자 Discord 사용자 ID (개발자 모드를 활성화하여 찾기)", + "telegram": "Telegram", + "telegramUsername": "Telegram 사용자 이름", + "telegramUsernamePlaceholder": "@yourusername", + "signal": "Signal", + "signalNumber": "Signal 전화번호", + "signalNumberPlaceholder": "+821012345678", + "signalNumberHint": "국가 코드 포함 (예: 한국 +82)", + "inviteCode": "초대 코드", + "inviteCodePlaceholder": "초대 코드 입력", + "inviteCodeRequired": "필수", + "createButton": "계정 만들기", + "creating": "계정 생성 중...", + "alreadyHaveAccount": "이미 계정이 있으신가요?", + "signIn": "로그인", + "wantPasswordless": "비밀번호 없는 보안을 원하시나요?", + "createPasskeyAccount": "패스키 계정 만들기", + "validation": { + "handleRequired": "핸들은 필수입니다", + "handleNoDots": "핸들에 점을 포함할 수 없습니다. 계정 생성 후 사용자 정의 도메인을 설정할 수 있습니다.", + "passwordRequired": "비밀번호는 필수입니다", + "passwordLength": "비밀번호는 8자 이상이어야 합니다", + "passwordsMismatch": "비밀번호가 일치하지 않습니다", + "inviteCodeRequired": "초대 코드는 필수입니다", + "externalDidRequired": "외부 did:web은 필수입니다", + "externalDidFormat": "외부 DID는 did:web:으로 시작해야 합니다", + "emailRequired": "이메일 인증에는 이메일이 필요합니다", + "discordIdRequired": "Discord 인증에는 Discord ID가 필요합니다", + "telegramRequired": "Telegram 인증에는 Telegram 사용자 이름이 필요합니다", + "signalRequired": "Signal 인증에는 전화번호가 필요합니다" + } + }, + "dashboard": { + "title": "대시보드", + "switchAccount": "계정 전환", + "addAnotherAccount": "다른 계정 추가", + "signOut": "@{handle} 로그아웃", + "deactivatedTitle": "계정 비활성화됨", + "deactivatedMessage": "계정이 현재 비활성화되어 있습니다. 이는 일반적으로 계정 마이그레이션 중에 발생합니다. 계정이 다시 활성화될 때까지 일부 기능이 제한될 수 있습니다.", + "accountOverview": "계정 개요", + "handle": "핸들", + "did": "DID", + "primaryContact": "주요 연락처", + "admin": "관리자", + "deactivated": "비활성화됨", + "verified": "인증됨", + "unverified": "미인증", + "navAppPasswords": "앱 비밀번호", + "navAppPasswordsDesc": "타사 앱의 비밀번호 관리", + "navSessions": "활성 세션", + "navSessionsDesc": "로그인 세션 보기 및 관리", + "navInviteCodes": "초대 코드", + "navInviteCodesDesc": "초대 코드 보기 및 생성", + "navSettings": "계정 설정", + "navSettingsDesc": "이메일, 비밀번호, 핸들 등", + "navSecurity": "보안", + "navSecurityDesc": "2단계 인증", + "navComms": "통신 설정", + "navCommsDesc": "Discord, Telegram, Signal 채널", + "navRepo": "저장소 탐색기", + "navRepoDesc": "AT Protocol 레코드 탐색 및 관리", + "navAdmin": "관리 패널", + "navAdminDesc": "서버 통계 및 관리 작업" + }, + "settings": { + "title": "계정 설정", + "language": "언어", + "languageDescription": "선호하는 언어를 선택하세요", + "changeEmail": "이메일 변경", + "currentEmail": "현재: {email}", + "newEmail": "새 이메일", + "newEmailPlaceholder": "new@example.com", + "changeEmailButton": "이메일 변경", + "requesting": "요청 중...", + "verificationCode": "인증 코드", + "verificationCodePlaceholder": "이메일의 코드 입력", + "confirmEmailChange": "이메일 변경 확인", + "updating": "업데이트 중...", + "changeHandle": "핸들 변경", + "currentHandle": "현재: @{handle}", + "pdsHandle": "PDS 핸들", + "customDomain": "사용자 정의 도메인", + "customDomainDescription": "자체 도메인을 핸들로 사용합니다. 먼저 도메인 소유권을 확인해야 합니다.", + "setupInstructions": "설정 지침", + "setupMethodsIntro": "다음 인증 방법 중 하나를 선택하세요:", + "dnsMethod": "방법 1: DNS TXT 레코드 (권장)", + "dnsMethodDesc": "도메인에 이 TXT 레코드 추가:", + "httpMethod": "방법 2: HTTP Well-Known 파일", + "httpMethodDesc": "이 URL에서 DID 제공:", + "httpMethodContent": "파일에는 다음만 포함:", + "yourDomain": "도메인", + "yourDomainPlaceholder": "example.com", + "verifyAndUpdate": "확인 후 핸들 업데이트", + "verifying": "확인 중...", + "newHandle": "새 핸들", + "newHandlePlaceholder": "yourhandle", + "changeHandleButton": "핸들 변경", + "changePassword": "비밀번호 변경", + "currentPassword": "현재 비밀번호", + "currentPasswordPlaceholder": "현재 비밀번호 입력", + "newPassword": "새 비밀번호", + "newPasswordPlaceholder": "8자 이상", + "confirmNewPassword": "새 비밀번호 확인", + "confirmNewPasswordPlaceholder": "새 비밀번호 재입력", + "changePasswordButton": "비밀번호 변경", + "changing": "변경 중...", + "exportData": "데이터 내보내기", + "exportDataDescription": "전체 저장소를 CAR (Content Addressable Archive) 파일로 다운로드합니다. 모든 게시물, 좋아요, 팔로우 및 기타 데이터가 포함됩니다.", + "downloadRepo": "저장소 다운로드", + "exporting": "내보내기 중...", + "deleteAccount": "계정 삭제", + "deleteWarning": "이 작업은 되돌릴 수 없습니다. 모든 데이터가 영구적으로 삭제됩니다.", + "requestDeletion": "계정 삭제 요청", + "confirmationCode": "확인 코드 (이메일에서)", + "confirmationCodePlaceholder": "확인 코드 입력", + "yourPassword": "비밀번호", + "yourPasswordPlaceholder": "비밀번호 입력", + "permanentlyDelete": "계정 영구 삭제", + "deleting": "삭제 중...", + "messages": { + "emailCodeSent": "현재 이메일로 인증 코드를 보냈습니다", + "emailUpdated": "이메일이 업데이트되었습니다", + "handleUpdated": "핸들이 업데이트되었습니다", + "passwordChanged": "비밀번호가 변경되었습니다", + "passwordsMismatch": "비밀번호가 일치하지 않습니다", + "passwordLength": "비밀번호는 8자 이상이어야 합니다", + "deletionCodeSent": "이메일로 삭제 확인을 보냈습니다", + "repoExported": "저장소를 내보냈습니다", + "confirmDelete": "정말로 계정을 삭제하시겠습니까? 이 작업은 되돌릴 수 없습니다." + } + }, + "appPasswords": { + "title": "앱 비밀번호", + "description": "앱 비밀번호를 사용하면 기본 비밀번호를 제공하지 않고 타사 앱에 로그인할 수 있습니다. 각 앱 비밀번호는 개별적으로 취소할 수 있습니다.", + "createNew": "새 앱 비밀번호 만들기", + "appNamePlaceholder": "앱 이름 (예: Graysky, Skeets)", + "created": "앱 비밀번호가 생성되었습니다", + "createdMessage": "지금 이 비밀번호를 복사하세요. 다시 볼 수 없습니다.", + "yourPasswords": "앱 비밀번호 목록", + "noPasswords": "앱 비밀번호가 아직 없습니다", + "revoke": "취소", + "revoking": "취소 중...", + "creating": "생성 중...", + "revokeConfirm": "앱 비밀번호 \"{name}\"을(를) 취소하시겠습니까? 이 비밀번호를 사용하는 앱은 더 이상 계정에 액세스할 수 없습니다." + }, + "sessions": { + "title": "활성 세션", + "loadingSessions": "세션 로딩 중...", + "noSessions": "활성 세션이 없습니다.", + "current": "현재", + "oauth": "OAuth", + "session": "세션", + "signOut": "로그아웃", + "revoke": "취소", + "revokeAll": "다른 모든 세션 취소", + "revokeCurrentConfirm": "이 세션에서 로그아웃됩니다. 계속하시겠습니까?", + "revokeConfirm": "이 세션을 취소하시겠습니까?", + "revokeAllConfirm": "{count}개의 다른 세션을 취소합니다. 계속하시겠습니까?", + "noOtherSessions": "취소할 다른 세션이 없습니다", + "failedToLoad": "세션 로딩에 실패했습니다", + "failedToRevoke": "세션 취소에 실패했습니다", + "failedToRevokeAll": "세션 취소에 실패했습니다", + "created": "생성일:", + "expires": "만료일:", + "daysAgo": "{count}일 전", + "hoursAgo": "{count}시간 전", + "minutesAgo": "{count}분 전", + "justNow": "방금" + }, + "inviteCodes": { + "title": "초대 코드", + "description": "초대 코드로 친구를 이 PDS에 초대할 수 있습니다. 각 코드는 한 번만 사용할 수 있습니다.", + "createNew": "새 초대 코드 만들기", + "uses": "사용 횟수", + "usesPlaceholder": "사용 횟수 (1-100)", + "yourCodes": "초대 코드 목록", + "noCodes": "초대 코드가 아직 없습니다", + "available": "사용 가능", + "used": "@{handle}이(가) 사용함", + "disabled": "비활성화됨", + "usedBy": "사용자", + "creating": "생성 중...", + "disableConfirm": "이 초대 코드를 비활성화하시겠습니까? 더 이상 사용할 수 없습니다.", + "created": "초대 코드가 생성되었습니다", + "copy": "복사", + "createdOn": "{date}에 생성됨" + }, + "security": { + "title": "보안", + "passkeys": "패스키", + "passkeysDescription": "패스키는 기기의 내장 보안(지문, 얼굴 또는 PIN)을 사용하여 안전한 비밀번호 없는 인증을 제공합니다.", + "addPasskey": "패스키 추가", + "adding": "추가 중...", + "noPasskeys": "등록된 패스키가 없습니다", + "passkeyName": "패스키 이름", + "passkeyNamePlaceholder": "예: MacBook Pro, iPhone", + "register": "등록", + "registering": "등록 중...", + "rename": "이름 변경", + "renaming": "이름 변경 중...", + "deletePasskey": "삭제", + "deletePasskeyConfirm": "패스키 \"{name}\"을(를) 삭제하시겠습니까? 더 이상 로그인에 사용할 수 없습니다.", + "totp": "인증 앱 (TOTP)", + "totpDescription": "Google Authenticator, Authy 또는 1Password와 같은 인증 앱을 2단계 인증에 사용합니다.", + "totpEnabled": "TOTP가 활성화되었습니다", + "totpDisabled": "TOTP가 비활성화되었습니다", + "enableTotp": "TOTP 활성화", + "disableTotp": "TOTP 비활성화", + "disabling": "비활성화 중...", + "totpSetup": "인증 앱 설정", + "totpSetupInstructions": "인증 앱으로 이 QR 코드를 스캔한 다음 6자리 코드를 입력하여 확인합니다.", + "totpCode": "인증 코드", + "totpCodePlaceholder": "6자리 코드 입력", + "verifyAndEnable": "확인 후 활성화", + "backupCodes": "백업 코드", + "backupCodesDescription": "인증 앱에 액세스할 수 없는 경우 이 코드를 사용하여 로그인합니다. 각 코드는 한 번만 사용할 수 있습니다.", + "regenerateBackupCodes": "백업 코드 재생성", + "regenerating": "재생성 중...", + "regenerateConfirm": "백업 코드를 재생성하시겠습니까? 현재 코드는 더 이상 작동하지 않습니다.", + "legacyLogin": "레거시 로그인", + "legacyLoginDescription": "사용자 이름/비밀번호로 직접 로그인(레거시 모드)을 허용합니다. 비활성화하면 MFA가 있는 OAuth를 사용해야 합니다.", + "legacyLoginOn": "레거시 로그인이 활성화되었습니다", + "legacyLoginOff": "레거시 로그인이 비활성화되었습니다", + "legacyLoginWarning": "경고: 레거시 로그인을 활성화하면 직접 비밀번호 로그인에 대한 MFA가 우회됩니다. 앱 호환성이 필요한 경우에만 활성화하세요.", + "totpPasswordWarning": "TOTP가 활성화되면 Bluesky 앱(또는 기타 레거시 앱)에서 비밀번호를 변경할 수 없습니다. 비밀번호를 변경하려면 두 가지 방법이 있습니다:", + "totpPasswordOption1Label": "여기에서 변경:", + "totpPasswordOption1Text": "이 웹사이트의", + "totpPasswordOption1Link": "설정 페이지", + "totpPasswordOption1Suffix": "에서 인증 앱으로 확인할 수 있습니다.", + "totpPasswordOption2Label": "먼저 세션 확인:", + "totpPasswordOption2Text": "", + "totpPasswordOption2Link": "재인증 옵션", + "totpPasswordOption2Suffix": "을 사용하여 TOTP로 Bluesky 세션을 확인하면 일시적으로 비밀번호 변경이 가능합니다.", + "legacyAppsTitle": "레거시 앱이란?", + "legacyAppsDescription": "일부 앱(공식 Bluesky 앱 등)은 비밀번호만 필요한 이전 인증을 사용합니다. MFA가 활성화되어 있으면 이러한 앱은 두 번째 인증 요소를 우회합니다. 레거시 로그인을 비활성화하면 모든 앱이 OAuth를 사용하도록 강제되어 MFA가 적절히 적용됩니다.", + "password": "비밀번호", + "passwordStatus": "비밀번호가 설정되었습니다", + "noPassword": "비밀번호가 설정되지 않음 (패스키 전용 계정)", + "setPassword": "비밀번호 설정", + "removePassword": "비밀번호 제거", + "removePasswordConfirm": "비밀번호를 제거하시겠습니까? 로그인에 패스키가 필요합니다.", + "removing": "제거 중...", + "loading": "로딩 중...", + "loadingPasskeys": "패스키 로딩 중...", + "cancel": "취소", + "save": "저장", + "back": "뒤로", + "next": "다음: 코드 확인", + "copyToClipboard": "클립보드에 복사", + "savedMyCodes": "코드를 저장했습니다", + "cantScan": "스캔할 수 없나요? 수동 입력", + "unnamedPasskey": "이름 없는 패스키", + "added": "추가됨", + "lastUsed": "마지막 사용", + "passwordDescription": "계정 비밀번호를 관리합니다. 패스키를 설정한 경우 완전한 비밀번호 없는 경험을 위해 비밀번호를 제거할 수 있습니다.", + "disableTotpWarning": "이렇게 하면 계정 보안이 약해집니다.", + "removePasswordWarning": "이렇게 하면 계정이 패스키 전용이 됩니다. 등록된 패스키로만 로그인할 수 있습니다. 모든 패스키에 액세스할 수 없게 되면 알림 채널을 사용하여 계정을 복구할 수 있습니다.", + "beforeProceeding": "계속하기 전에:", + "beforeProceedingItem1": "최소 하나의 신뢰할 수 있는 패스키가 등록되어 있는지 확인", + "beforeProceedingItem2": "여러 기기에 패스키 등록을 고려", + "beforeProceedingItem3": "복구 알림 채널이 최신인지 확인", + "addPasskeyFirst": "비밀번호를 제거하려면 먼저 최소 하나의 패스키를 추가하세요.", + "passkeyOnlyHint": "패스키로만 로그인합니다. 패스키에 액세스할 수 없게 되면 로그인 페이지의 '패스키를 분실하셨나요?' 링크를 사용하여 계정을 복구할 수 있습니다.", + "trustedDevices": "신뢰할 수 있는 기기", + "trustedDevicesDescription": "로그인 시 2단계 인증을 건너뛸 수 있는 기기를 관리합니다. 신뢰는 30일간 유효하며 기기를 사용하면 자동으로 연장됩니다.", + "manageTrustedDevices": "신뢰할 수 있는 기기 관리", + "appCompatibility": "앱 호환성", + "enterPassword": "비밀번호를 입력하세요", + "legacyLoginEnabled": "레거시 앱 로그인 활성화됨", + "legacyLoginDisabled": "레거시 앱 로그인 비활성화됨 - OAuth 앱만 로그인 가능", + "failedToUpdatePreference": "설정 업데이트에 실패했습니다", + "passwordRemoved": "비밀번호가 제거되었습니다. 이제 계정은 패스키 전용입니다.", + "failedToRemovePassword": "비밀번호 제거에 실패했습니다", + "failedToLoadTotpStatus": "TOTP 상태 로딩에 실패했습니다", + "totpEnabledSuccess": "2단계 인증이 활성화되었습니다", + "totpDisabledSuccess": "2단계 인증이 비활성화되었습니다", + "backupCodesCopied": "백업 코드가 클립보드에 복사되었습니다", + "failedToLoadPasskeys": "패스키 로딩에 실패했습니다", + "passkeysNotSupported": "이 브라우저에서 패스키가 지원되지 않습니다", + "passkeyCreationCancelled": "패스키 생성이 취소되었습니다", + "passkeyAddedSuccess": "패스키가 추가되었습니다", + "passkeyDeleted": "패스키가 삭제되었습니다", + "passkeyRenamed": "패스키 이름이 변경되었습니다" + }, + "comms": { + "title": "통신 설정", + "description": "비밀번호 재설정, 보안 알림, 계정 업데이트 등 중요한 메시지를 받는 방법을 선택하세요.", + "preferredChannel": "선호 채널", + "preferredChannelDescription": "메시지 수신 방법을 선택하세요. 선택하기 전에 채널을 설정해야 합니다.", + "channelConfiguration": "채널 설정", + "emailVia": "이메일로 메시지 받기", + "discordVia": "Discord DM으로 메시지 받기", + "telegramVia": "Telegram으로 메시지 받기", + "signalVia": "Signal로 메시지 받기", + "configureToEnable": "활성화하려면 아래에서 설정", + "emailManagedInSettings": "이메일은 계정 설정에서 관리됩니다", + "discordIdHint": "Discord 사용자 ID (사용자 이름 아님). Discord에서 개발자 모드를 활성화하여 복사하세요.", + "telegramHint": "@ 기호 없이 Telegram 사용자 이름", + "signalHint": "국가 코드가 포함된 Signal 전화번호", + "primary": "기본", + "verified": "인증됨", + "notVerified": "미인증", + "verifyButton": "인증", + "verifyCodePlaceholder": "인증 코드 입력", + "submit": "제출", + "saving": "저장 중...", + "savePreferences": "설정 저장", + "preferencesSaved": "통신 설정이 저장되었습니다", + "verifiedSuccess": "{channel} 인증 완료", + "messageHistory": "메시지 기록", + "historyDescription": "계정에 전송된 최근 메시지를 확인합니다.", + "loadHistory": "기록 불러오기", + "hideHistory": "기록 숨기기", + "noMessages": "메시지가 없습니다.", + "sent": "전송됨", + "failed": "실패" + }, + "repoExplorer": { + "title": "저장소 탐색기", + "description": "AT Protocol 레코드를 탐색하고 관리합니다.", + "collections": "컬렉션", + "noCollections": "컬렉션을 찾을 수 없습니다", + "records": "레코드", + "noRecords": "이 컬렉션에 레코드가 없습니다", + "recordDetails": "레코드 세부 정보", + "rkey": "레코드 키", + "cid": "CID", + "value": "값", + "deleteRecord": "레코드 삭제", + "deleteConfirm": "레코드 {rkey}을(를) 삭제하시겠습니까? 이 작업은 되돌릴 수 없습니다.", + "unknownError": "알 수 없는 오류가 발생했습니다", + "invalidJson": "잘못된 JSON", + "collectionRequired": "컬렉션은 필수입니다", + "recordCreated": "레코드 생성됨: {uri}", + "recordUpdated": "레코드가 업데이트되었습니다", + "recordDeleted": "레코드가 삭제되었습니다", + "newRecord": "새 레코드", + "createRecord": "레코드 생성", + "filterCollections": "컬렉션 검색...", + "filterRecords": "레코드 검색...", + "noCollectionsYet": "컬렉션이 아직 없습니다. 첫 번째 레코드를 만들어 시작하세요.", + "loadMore": "더 불러오기", + "recordJson": "레코드 JSON", + "saving": "저장 중...", + "updateRecord": "레코드 업데이트", + "collectionNsid": "컬렉션 (NSID)", + "recordKeyOptional": "레코드 키 (선택사항)", + "autoGenerated": "비워두면 자동 생성 (TID)", + "autoGeneratedHint": "비워두면 TID 기반 키가 자동 생성됩니다", + "creating": "생성 중...", + "demoPostText": "안녕하세요, 제 PDS에서 보내는 첫 번째 게시물입니다!", + "demoDisplayName": "표시 이름", + "demoBio": "간단한 자기소개를 작성하세요." + }, + "admin": { + "title": "관리 패널", + "serverStats": "서버 통계", + "users": "사용자", + "repos": "저장소", + "records": "레코드", + "blobStorage": "Blob 저장소", + "refreshStats": "통계 새로고침", + "userManagement": "사용자 관리", + "searchPlaceholder": "핸들로 검색 (선택사항)", + "searchUsers": "사용자 검색", + "noUsers": "사용자를 찾을 수 없습니다", + "handle": "핸들", + "email": "이메일", + "status": "상태", + "created": "생성일", + "loadMore": "더 불러오기", + "inviteCodes": "초대 코드", + "loadInviteCodes": "초대 코드 불러오기", + "refresh": "새로고침", + "noInvites": "초대 코드가 없습니다", + "code": "코드", + "available": "사용 가능", + "uses": "사용 횟수", + "actions": "작업", + "disable": "비활성화", + "disableInviteConfirm": "초대 코드 {code}을(를) 비활성화하시겠습니까?", + "active": "활성", + "exhausted": "소진됨", + "disabled": "비활성화됨", + "userDetails": "사용자 세부 정보", + "did": "DID", + "invites": "초대", + "enabled": "활성화됨", + "enableInvites": "초대 활성화", + "disableInvites": "초대 비활성화", + "deleteAccount": "계정 삭제", + "deleteConfirm": "계정 @{handle}을(를) 삭제하시겠습니까? 이 작업은 되돌릴 수 없습니다.", + "verified": "인증됨", + "unverified": "미인증", + "deactivated": "비활성화됨" + }, + "oauth": { + "login": { + "title": "로그인", + "subtitle": "앱을 계속하려면 로그인하세요", + "signingIn": "로그인 중...", + "authenticating": "인증 중...", + "checkingPasskey": "패스키 확인 중...", + "signInWithPasskey": "패스키로 로그인", + "passkeyNotSetUp": "패스키가 설정되지 않음", + "orUsePassword": "또는 비밀번호 사용", + "password": "비밀번호", + "rememberDevice": "이 기기 기억하기", + "passkeyHintChecking": "패스키 상태 확인 중...", + "passkeyHintAvailable": "패스키로 로그인", + "passkeyHintNotAvailable": "이 계정에 등록된 패스키가 없습니다" + }, + "consent": { + "title": "앱 승인", + "appWantsAccess": "{app}이(가) 계정에 액세스하려고 합니다", + "permissions": "이 앱은 다음을 수행할 수 있습니다:", + "readProfile": "프로필 정보 읽기", + "readPosts": "게시물 및 콘텐츠 읽기", + "writePosts": "대신 게시물 작성 및 삭제", + "readNotifications": "알림 읽기", + "fullAccess": "계정에 대한 전체 액세스", + "authorize": "승인", + "deny": "거부", + "authorizing": "승인 중...", + "rememberChoice": "이 선택 기억", + "signingInAs": "로그인 계정:", + "permissionsRequested": "요청된 권한", + "required": "필수", + "rememberChoiceLabel": "이 앱에 대한 선택 기억하기" + }, + "accounts": { + "title": "계정 선택", + "subtitle": "계속할 계정 선택", + "useAnother": "다른 계정 사용" + }, + "twoFactor": { + "title": "2단계 인증", + "subtitle": "추가 확인이 필요합니다", + "usePasskey": "패스키 사용", + "useTotp": "인증 앱 사용", + "verifying": "확인 중..." + }, + "twoFactorCode": { + "title": "2단계 인증", + "subtitle": "{channel}(으)로 인증 코드를 보냈습니다. 아래에 코드를 입력하여 계속하세요.", + "codeLabel": "인증 코드", + "codePlaceholder": "6자리 코드 입력", + "verify": "확인", + "verifying": "확인 중...", + "errors": { + "missingRequestUri": "request_uri 매개변수가 없습니다", + "verificationFailed": "인증에 실패했습니다", + "connectionFailed": "서버에 연결하지 못했습니다", + "unexpectedResponse": "서버로부터 예기치 않은 응답" + } + }, + "totp": { + "title": "인증 코드 입력", + "subtitle": "인증 앱의 6자리 코드를 입력하세요", + "codePlaceholder": "6자리 코드 입력", + "verify": "확인", + "verifying": "확인 중...", + "useBackupCode": "백업 코드 사용", + "backupCodePlaceholder": "백업 코드 입력", + "trustDevice": "이 기기를 30일간 신뢰", + "hintBackupCode": "백업 코드 사용 중", + "hintTotpCode": "인증 코드 사용 중", + "hintDefault": "인증 앱은 6자리, 백업 코드는 8자" + }, + "passkey": { + "title": "패스키 확인", + "subtitle": "패스키를 사용하여 본인 확인", + "waiting": "패스키 대기 중...", + "useTotp": "인증 앱 사용" + }, + "error": { + "title": "승인 오류", + "genericError": "승인 중 오류가 발생했습니다.", + "tryAgain": "다시 시도", + "backToApp": "앱으로 돌아가기" + } + }, + "verify": { + "title": "계정 인증", + "subtitle": "{channel}(으)로 인증 코드를 보냈습니다. 아래에 입력하여 등록을 완료하세요.", + "codePlaceholder": "6자리 코드 입력", + "codeLabel": "인증 코드", + "verifyButton": "계정 인증", + "verifying": "인증 중...", + "resendCode": "코드 다시 보내기", + "resending": "전송 중...", + "codeResent": "인증 코드를 다시 보냈습니다!", + "backToLogin": "로그인으로 돌아가기", + "verifyingAccount": "인증 중인 계정: @{handle}", + "startOver": "다른 계정으로 다시 시작", + "noPending": "보류 중인 인증이 없습니다.", + "noPendingInfo": "최근에 계정을 만들고 인증이 필요한 경우 새 계정을 만들어야 합니다. 이미 계정을 인증한 경우 로그인할 수 있습니다.", + "createAccount": "계정 만들기", + "signIn": "로그인" + }, + "resetPassword": { + "title": "비밀번호 재설정", + "forgotTitle": "비밀번호를 잊으셨나요", + "subtitle": "받은 코드를 입력하고 새 비밀번호를 선택하세요.", + "forgotSubtitle": "핸들 또는 이메일을 입력하면 비밀번호 재설정 코드를 보내드립니다.", + "handleOrEmail": "핸들 또는 이메일", + "emailPlaceholder": "핸들 또는 you@example.com", + "sendCode": "재설정 코드 보내기", + "sending": "전송 중...", + "codeSent": "비밀번호 재설정 코드를 보냈습니다! 선호하는 알림 채널을 확인하세요.", + "enterCode": "이메일의 코드와 새 비밀번호를 입력하세요.", + "code": "재설정 코드", + "codePlaceholder": "재설정 코드 입력", + "newPassword": "새 비밀번호", + "newPasswordPlaceholder": "8자 이상", + "confirmPassword": "비밀번호 확인", + "confirmPasswordPlaceholder": "새 비밀번호 재입력", + "resetButton": "비밀번호 재설정", + "resetting": "재설정 중...", + "success": "비밀번호가 재설정되었습니다!", + "backToLogin": "로그인으로 돌아가기", + "requestNewCode": "새 코드 요청", + "passwordsMismatch": "비밀번호가 일치하지 않습니다", + "passwordLength": "비밀번호는 8자 이상이어야 합니다" + }, + "recoverPasskey": { + "title": "계정 복구", + "invalidLinkTitle": "잘못된 복구 링크", + "invalidLinkMessage": "이 복구 링크가 잘못되었거나 손상되었습니다. 새 복구 이메일을 요청하세요.", + "goToLogin": "로그인으로 이동", + "successTitle": "비밀번호가 설정되었습니다!", + "successMessage": "임시 비밀번호가 설정되었습니다. 이 비밀번호로 로그인할 수 있습니다.", + "successNextSteps": "로그인 후 보안 설정에서 새 패스키를 추가하여 패스키 전용 인증을 복원하는 것이 좋습니다.", + "signIn": "로그인", + "subtitle": "패스키 전용 계정에 대한 액세스를 복구하기 위해 임시 비밀번호를 설정합니다.", + "newPassword": "새 비밀번호", + "newPasswordPlaceholder": "8자 이상", + "confirmPassword": "비밀번호 확인", + "confirmPasswordPlaceholder": "비밀번호 재입력", + "whatHappensNext": "다음 단계", + "whatHappensNextDetail": "이 비밀번호를 설정한 후 로그인하여 보안 설정에서 새 패스키를 추가할 수 있습니다. 새 패스키를 추가한 후 임시 비밀번호를 제거할 수 있습니다.", + "setPassword": "비밀번호 설정", + "settingPassword": "비밀번호 설정 중...", + "validation": { + "passwordRequired": "새 비밀번호는 필수입니다", + "passwordLength": "비밀번호는 8자 이상이어야 합니다", + "passwordsMismatch": "비밀번호가 일치하지 않습니다" + }, + "errors": { + "invalidLink": "잘못된 복구 링크입니다. 새 링크를 요청하세요.", + "expired": "이 복구 링크가 만료되었습니다. 새 링크를 요청하세요." + } + }, + "requestPasskeyRecovery": { + "title": "패스키 계정 복구", + "subtitle": "패스키에 액세스할 수 없나요? 핸들 또는 이메일을 입력하면 복구 링크를 보내드립니다.", + "successTitle": "복구 링크 전송됨", + "successMessage": "계정이 존재하고 패스키 전용 계정인 경우 선호하는 알림 채널로 복구 링크를 받게 됩니다.", + "successInfo": "링크는 1시간 후 만료됩니다. 계정 설정에 따라 이메일, Discord, Telegram 또는 Signal을 확인하세요.", + "handleOrEmail": "핸들 또는 이메일", + "emailPlaceholder": "핸들 또는 you@example.com", + "howItWorks": "작동 방식", + "howItWorksDetail": "등록된 알림 채널로 보안 링크를 보냅니다. 링크를 클릭하여 임시 비밀번호를 설정합니다. 그런 다음 로그인하여 새 패스키를 추가할 수 있습니다.", + "sendRecoveryLink": "복구 링크 보내기", + "sending": "전송 중...", + "backToLogin": "로그인으로 돌아가기" + }, + "registerPasskey": { + "title": "패스키 계정 만들기", + "subtitle": "패스키를 사용하여 비밀번호 없는 계정을 만듭니다.", + "handle": "핸들", + "handlePlaceholder": "사용자 이름", + "handleHint": "전체 핸들: @{handle}", + "email": "이메일 주소", + "emailPlaceholder": "you@example.com", + "inviteCode": "초대 코드", + "inviteCodePlaceholder": "초대 코드 입력", + "createButton": "계정 만들기", + "creating": "생성 중...", + "alreadyHaveAccount": "이미 계정이 있으신가요?", + "signIn": "로그인", + "wantPassword": "비밀번호를 사용하시겠습니까?", + "createPasswordAccount": "비밀번호 계정 만들기" + }, + "trustedDevices": { + "title": "신뢰할 수 있는 기기", + "backToSecurity": "← 보안 설정", + "description": "신뢰할 수 있는 기기는 로그인 시 2단계 인증을 건너뛸 수 있습니다. 신뢰는 30일간 유효하며 기기를 사용할 때 자동으로 연장됩니다.", + "noDevices": "신뢰할 수 있는 기기가 아직 없습니다.", + "noDevicesHint": "2단계 인증이 활성화된 상태로 로그인할 때 기기를 30일간 신뢰하도록 선택할 수 있습니다.", + "lastSeen": "마지막 접속:", + "trustedSince": "신뢰 시작:", + "trustExpires": "신뢰 만료:", + "expired": "만료됨", + "tomorrow": "내일", + "inDays": "{days}일 후", + "revoke": "신뢰 취소", + "revokeConfirm": "이 기기에 대한 신뢰를 취소하시겠습니까? 다음에 이 기기에서 로그인할 때 2FA 코드를 입력해야 합니다.", + "deviceRevoked": "기기 신뢰가 취소되었습니다", + "deviceRenamed": "기기 이름이 변경되었습니다", + "deviceNamePlaceholder": "기기 이름", + "browser": "브라우저:", + "unknownDevice": "알 수 없는 기기" + }, + "reauth": { + "title": "재인증 필요", + "subtitle": "계속하려면 본인 확인을 해주세요.", + "usePassword": "비밀번호 사용", + "usePasskey": "패스키 사용", + "useTotp": "인증 앱 사용", + "passwordPlaceholder": "비밀번호 입력", + "totpPlaceholder": "6자리 코드 입력", + "verify": "확인", + "verifying": "확인 중...", + "cancel": "취소" + } +} diff --git a/frontend/src/locales/zh.json b/frontend/src/locales/zh.json new file mode 100644 index 0000000..d3c79e4 --- /dev/null +++ b/frontend/src/locales/zh.json @@ -0,0 +1,702 @@ +{ + "common": { + "loading": "加载中...", + "error": "错误", + "save": "保存", + "cancel": "取消", + "back": "返回", + "done": "完成", + "refresh": "刷新", + "create": "创建", + "delete": "删除", + "confirm": "确认", + "created": "创建时间", + "expires": "过期时间", + "name": "名称", + "dashboard": "控制台", + "backToDashboard": "← 返回控制台" + }, + "login": { + "title": "登录", + "subtitle": "登录以管理您的 PDS 账户", + "button": "登录", + "redirecting": "跳转中...", + "chooseAccount": "选择账户", + "signInToAnother": "登录其他账户", + "backToSaved": "← 返回已保存账户", + "forgotPassword": "忘记密码?", + "lostPasskey": "丢失通行密钥?", + "noAccount": "还没有账户?", + "createAccount": "立即注册", + "removeAccount": "从已保存账户中移除" + }, + "verification": { + "title": "验证账户", + "subtitle": "您的账户需要验证。请输入发送到您验证方式的验证码。", + "codeLabel": "验证码", + "codePlaceholder": "输入6位验证码", + "verifyButton": "验证账户", + "verifying": "验证中...", + "resendButton": "重新发送验证码", + "resending": "发送中...", + "resent": "验证码已重新发送!", + "backToLogin": "返回登录" + }, + "register": { + "title": "创建账户", + "subtitle": "在此 PDS 上创建新账户", + "handle": "用户名", + "handlePlaceholder": "您的用户名", + "handleHint": "您的完整用户名将是:@{handle}", + "handleDotWarning": "自定义域名可以在创建账户后在设置中配置。", + "password": "密码", + "passwordPlaceholder": "至少8位字符", + "confirmPassword": "确认密码", + "confirmPasswordPlaceholder": "再次输入密码", + "identityType": "身份类型", + "identityHint": "选择如何管理您的去中心化身份。", + "didPlc": "did:plc", + "didPlcRecommended": "(推荐)", + "didPlcHint": "由 PLC 目录管理的可迁移身份", + "didWeb": "did:web", + "didWebHint": "托管在此 PDS 上的身份(请阅读下方警告)", + "didWebBYOD": "did:web(自带域名)", + "didWebBYODHint": "使用您自己的域名", + "didWebWarningTitle": "重要提示:了解利弊", + "didWebWarning1": "永久绑定此 PDS:", + "didWebWarning1Detail": "您的身份将是 {did}。即使您以后迁移到另一个 PDS,此服务器也必须继续托管您的 DID 文档。", + "didWebWarning2": "无法恢复:", + "didWebWarning2Detail": "与 did:plc 不同,did:web 没有密钥轮换机制。如果此 PDS 永久下线,您的身份将无法恢复。", + "didWebWarning3": "我们的承诺:", + "didWebWarning3Detail": "如果您迁移到其他 PDS,我们将继续提供指向您新 PDS 的最小 DID 文档。您的身份将保持可用。", + "didWebWarning4": "建议:", + "didWebWarning4Detail": "除非您有特定原因需要 did:web,否则请选择 did:plc。", + "externalDid": "您的 did:web", + "externalDidPlaceholder": "did:web:yourdomain.com", + "externalDidHint": "您的域名必须在 /.well-known/did.json 提供指向此 PDS 的有效 DID 文档", + "contactMethod": "联系方式", + "contactMethodHint": "选择您希望如何验证账户和接收通知。您只需选择一种。", + "verificationMethod": "验证方式", + "email": "电子邮件", + "emailAddress": "电子邮件地址", + "emailPlaceholder": "you@example.com", + "discord": "Discord", + "discordId": "Discord 用户 ID", + "discordIdPlaceholder": "您的 Discord 用户 ID", + "discordIdHint": "您的 Discord 数字用户 ID(开启开发者模式后可以复制)", + "telegram": "Telegram", + "telegramUsername": "Telegram 用户名", + "telegramUsernamePlaceholder": "@yourusername", + "signal": "Signal", + "signalNumber": "Signal 电话号码", + "signalNumberPlaceholder": "+1234567890", + "signalNumberHint": "包含国家代码(例如中国为 +86)", + "inviteCode": "邀请码", + "inviteCodePlaceholder": "输入您的邀请码", + "inviteCodeRequired": "必填", + "createButton": "创建账户", + "creating": "正在创建...", + "alreadyHaveAccount": "已有账户?", + "signIn": "立即登录", + "wantPasswordless": "想要无密码登录?", + "createPasskeyAccount": "创建通行密钥账户", + "validation": { + "handleRequired": "请输入用户名", + "handleNoDots": "用户名不能包含点号。您可以在创建账户后设置自定义域名。", + "passwordRequired": "请输入密码", + "passwordLength": "密码至少需要8位字符", + "passwordsMismatch": "两次输入的密码不一致", + "inviteCodeRequired": "请输入邀请码", + "externalDidRequired": "请输入您的 did:web", + "externalDidFormat": "DID 必须以 did:web: 开头", + "emailRequired": "使用邮箱验证需要填写邮箱地址", + "discordIdRequired": "使用 Discord 验证需要填写 Discord ID", + "telegramRequired": "使用 Telegram 验证需要填写用户名", + "signalRequired": "使用 Signal 验证需要填写电话号码" + } + }, + "dashboard": { + "title": "控制台", + "switchAccount": "切换账户", + "addAnotherAccount": "添加其他账户", + "signOut": "退出 @{handle}", + "deactivatedTitle": "账户已停用", + "deactivatedMessage": "您的账户目前已停用。这通常发生在账户迁移期间。在账户重新激活之前,部分功能可能受限。", + "accountOverview": "账户概览", + "handle": "用户名", + "did": "DID", + "primaryContact": "主要联系方式", + "admin": "管理员", + "deactivated": "已停用", + "verified": "已验证", + "unverified": "未验证", + "navAppPasswords": "应用专用密码", + "navAppPasswordsDesc": "管理第三方应用的专用密码", + "navSessions": "登录会话", + "navSessionsDesc": "查看和管理您的登录会话", + "navInviteCodes": "邀请码", + "navInviteCodesDesc": "查看和创建邀请码", + "navSettings": "账户设置", + "navSettingsDesc": "邮箱、密码、用户名等", + "navSecurity": "安全设置", + "navSecurityDesc": "双重身份验证", + "navComms": "通讯偏好", + "navCommsDesc": "Discord、Telegram、Signal 渠道设置", + "navRepo": "数据浏览器", + "navRepoDesc": "浏览和管理原始 AT Protocol 记录", + "navAdmin": "管理后台", + "navAdminDesc": "服务器统计和管理操作" + }, + "settings": { + "title": "账户设置", + "language": "语言", + "languageDescription": "选择您的首选语言", + "changeEmail": "更改邮箱", + "currentEmail": "当前:{email}", + "newEmail": "新邮箱", + "newEmailPlaceholder": "new@example.com", + "changeEmailButton": "更改邮箱", + "requesting": "请求中...", + "verificationCode": "验证码", + "verificationCodePlaceholder": "输入邮件中的验证码", + "confirmEmailChange": "确认更改邮箱", + "updating": "更新中...", + "changeHandle": "更改用户名", + "currentHandle": "当前:@{handle}", + "pdsHandle": "PDS 用户名", + "customDomain": "自定义域名", + "customDomainDescription": "使用您自己的域名作为用户名。需要先验证域名所有权。", + "setupInstructions": "设置说明", + "setupMethodsIntro": "选择以下验证方式之一:", + "dnsMethod": "方式一:DNS TXT 记录(推荐)", + "dnsMethodDesc": "在您的域名中添加此 TXT 记录:", + "httpMethod": "方式二:HTTP Well-Known 文件", + "httpMethodDesc": "在此 URL 提供您的 DID:", + "httpMethodContent": "文件内容应为:", + "yourDomain": "您的域名", + "yourDomainPlaceholder": "example.com", + "verifyAndUpdate": "验证并更新用户名", + "verifying": "验证中...", + "newHandle": "新用户名", + "newHandlePlaceholder": "yourhandle", + "changeHandleButton": "更改用户名", + "changePassword": "更改密码", + "currentPassword": "当前密码", + "currentPasswordPlaceholder": "输入当前密码", + "newPassword": "新密码", + "newPasswordPlaceholder": "至少8位字符", + "confirmNewPassword": "确认新密码", + "confirmNewPasswordPlaceholder": "再次输入新密码", + "changePasswordButton": "更改密码", + "changing": "更改中...", + "exportData": "导出数据", + "exportDataDescription": "将您的所有数据下载为 CAR 文件。包括您的所有帖子、点赞、关注等数据。", + "downloadRepo": "下载数据", + "exporting": "导出中...", + "deleteAccount": "删除账户", + "deleteWarning": "此操作不可逆。您的所有数据将被永久删除。", + "requestDeletion": "请求删除账户", + "confirmationCode": "确认码(来自邮件)", + "confirmationCodePlaceholder": "输入确认码", + "yourPassword": "您的密码", + "yourPasswordPlaceholder": "输入您的密码", + "permanentlyDelete": "永久删除账户", + "deleting": "删除中...", + "messages": { + "emailCodeSent": "验证码已发送到您当前的邮箱", + "emailUpdated": "邮箱更新成功", + "handleUpdated": "用户名更新成功", + "passwordChanged": "密码更改成功", + "passwordsMismatch": "两次输入的密码不一致", + "passwordLength": "密码至少需要8位字符", + "deletionCodeSent": "删除确认码已发送到您的邮箱", + "repoExported": "数据导出成功", + "confirmDelete": "您确定要删除账户吗?此操作无法撤销。" + } + }, + "appPasswords": { + "title": "应用专用密码", + "description": "应用专用密码可让您登录第三方应用而无需提供主密码。每个密码都可以单独撤销。", + "createNew": "创建新密码", + "appNamePlaceholder": "应用名称(如 Graysky、Skeets)", + "created": "应用专用密码已创建", + "createdMessage": "请立即复制此密码,您将无法再次查看。", + "yourPasswords": "您的应用专用密码", + "noPasswords": "暂无应用专用密码", + "revoke": "撤销", + "revoking": "撤销中...", + "creating": "创建中...", + "revokeConfirm": "撤销「{name}」的密码?使用此密码的应用将无法再访问您的账户。" + }, + "sessions": { + "title": "登录会话", + "loadingSessions": "加载会话中...", + "noSessions": "没有活跃的登录会话", + "current": "当前", + "oauth": "OAuth", + "session": "会话", + "signOut": "退出", + "revoke": "撤销", + "revokeAll": "撤销所有其他会话", + "revokeCurrentConfirm": "这将使您退出当前会话,确定继续?", + "revokeConfirm": "确定撤销此会话?", + "revokeAllConfirm": "这将撤销 {count} 个其他会话,确定继续?", + "noOtherSessions": "没有其他可撤销的会话", + "failedToLoad": "加载会话失败", + "failedToRevoke": "撤销会话失败", + "failedToRevokeAll": "撤销会话失败", + "created": "创建时间:", + "expires": "过期时间:", + "daysAgo": "{count} 天前", + "hoursAgo": "{count} 小时前", + "minutesAgo": "{count} 分钟前", + "justNow": "刚刚" + }, + "inviteCodes": { + "title": "邀请码", + "description": "邀请码可让您邀请朋友加入。每个邀请码只能使用一次。", + "createNew": "创建新邀请码", + "uses": "使用次数", + "usesPlaceholder": "使用次数(1-100)", + "yourCodes": "您的邀请码", + "noCodes": "暂无邀请码", + "available": "可用", + "used": "已被 @{handle} 使用", + "disabled": "已禁用", + "usedBy": "使用者", + "creating": "创建中...", + "disableConfirm": "禁用此邀请码?它将无法再被使用。", + "created": "邀请码已创建", + "copy": "复制", + "createdOn": "创建于 {date}" + }, + "security": { + "title": "安全设置", + "passkeys": "通行密钥", + "passkeysDescription": "通行密钥使用您设备的安全功能(指纹、面容或 PIN)提供安全的无密码登录。", + "addPasskey": "添加通行密钥", + "adding": "添加中...", + "noPasskeys": "未注册通行密钥", + "passkeyName": "通行密钥名称", + "passkeyNamePlaceholder": "如 MacBook Pro、iPhone", + "register": "注册", + "registering": "注册中...", + "rename": "重命名", + "renaming": "重命名中...", + "deletePasskey": "删除", + "deletePasskeyConfirm": "删除通行密钥「{name}」?您将无法再使用它登录。", + "totp": "身份验证器(TOTP)", + "totpDescription": "使用 Google Authenticator、Authy 或 1Password 等应用进行双重身份验证。", + "totpEnabled": "已启用身份验证器", + "totpDisabled": "未启用身份验证器", + "enableTotp": "启用身份验证器", + "disableTotp": "禁用身份验证器", + "disabling": "禁用中...", + "totpSetup": "设置身份验证器", + "totpSetupInstructions": "使用身份验证器应用扫描此二维码,然后输入6位验证码完成验证。", + "totpCode": "验证码", + "totpCodePlaceholder": "输入6位验证码", + "verifyAndEnable": "验证并启用", + "backupCodes": "备用验证码", + "backupCodesDescription": "如果无法使用身份验证器,可以使用这些备用码登录。每个验证码只能使用一次。", + "regenerateBackupCodes": "重新生成备用码", + "regenerating": "生成中...", + "regenerateConfirm": "重新生成备用码?当前的验证码将失效。", + "legacyLogin": "传统登录", + "legacyLoginDescription": "允许使用用户名/密码直接登录(传统模式)。禁用后必须使用 OAuth + 双重验证。", + "legacyLoginOn": "传统登录已启用", + "legacyLoginOff": "传统登录已禁用", + "legacyLoginWarning": "警告:启用传统登录会绕过双重身份验证。仅在需要兼容旧版应用时启用。", + "totpPasswordWarning": "启用 TOTP 后,将无法从 Bluesky 应用(或其他旧版应用)更改密码。要更改密码,您有两个选择:", + "totpPasswordOption1Label": "在这里更改:", + "totpPasswordOption1Text": "使用本网站的", + "totpPasswordOption1Link": "设置页面", + "totpPasswordOption1Suffix": ",您可以使用身份验证器应用进行验证。", + "totpPasswordOption2Label": "先验证您的会话:", + "totpPasswordOption2Text": "使用", + "totpPasswordOption2Link": "重新验证选项", + "totpPasswordOption2Suffix": "用 TOTP 验证您的 Bluesky 会话,然后密码更改将暂时有效。", + "legacyAppsTitle": "什么是旧版应用?", + "legacyAppsDescription": "某些应用(如官方 Bluesky 应用)使用仅需密码的旧版身份验证。启用双重验证后,这些应用会绕过您的第二重验证。禁用传统登录会强制所有应用使用 OAuth,从而正确执行双重验证。", + "password": "密码", + "passwordStatus": "已设置密码", + "noPassword": "未设置密码(仅通行密钥账户)", + "setPassword": "设置密码", + "removePassword": "移除密码", + "removePasswordConfirm": "移除密码后需要使用通行密钥登录,确定继续?", + "removing": "移除中...", + "loading": "加载中...", + "loadingPasskeys": "加载通行密钥中...", + "cancel": "取消", + "save": "保存", + "back": "返回", + "next": "下一步:验证代码", + "copyToClipboard": "复制到剪贴板", + "savedMyCodes": "我已保存备用码", + "cantScan": "无法扫描?手动输入", + "unnamedPasskey": "未命名的通行密钥", + "added": "添加于", + "lastUsed": "上次使用", + "passwordDescription": "管理您的账户密码。如果您已设置通行密钥,可以选择移除密码以获得完全无密码的体验。", + "disableTotpWarning": "这将降低您的账户安全性。", + "removePasswordWarning": "这将使您的账户变为仅通行密钥模式。您只能使用已注册的通行密钥登录。如果您丢失了所有通行密钥,可以通过通知渠道恢复账户。", + "beforeProceeding": "继续之前:", + "beforeProceedingItem1": "确保您至少注册了一个可靠的通行密钥", + "beforeProceedingItem2": "考虑在多个设备上注册通行密钥", + "beforeProceedingItem3": "确保您的恢复通知渠道是最新的", + "addPasskeyFirst": "请先添加至少一个通行密钥才能移除密码。", + "passkeyOnlyHint": "您使用通行密钥登录。如果您丢失了通行密钥,可以使用登录页面上的「丢失通行密钥?」链接恢复账户。", + "trustedDevices": "受信任设备", + "trustedDevicesDescription": "管理可以跳过双重身份验证的设备。信任有效期为30天,使用设备时自动延长。", + "manageTrustedDevices": "管理受信任设备", + "appCompatibility": "应用兼容性", + "enterPassword": "输入您的密码", + "legacyLoginEnabled": "已启用传统应用登录", + "legacyLoginDisabled": "已禁用传统应用登录 - 仅 OAuth 应用可登录", + "failedToUpdatePreference": "更新偏好设置失败", + "passwordRemoved": "密码已移除。您的账户现在仅支持通行密钥。", + "failedToRemovePassword": "移除密码失败", + "failedToLoadTotpStatus": "加载 TOTP 状态失败", + "totpEnabledSuccess": "双重身份验证已成功启用", + "totpDisabledSuccess": "双重身份验证已禁用", + "backupCodesCopied": "备用码已复制到剪贴板", + "failedToLoadPasskeys": "加载通行密钥失败", + "passkeysNotSupported": "此浏览器不支持通行密钥", + "passkeyCreationCancelled": "通行密钥创建已取消", + "passkeyAddedSuccess": "通行密钥添加成功", + "passkeyDeleted": "通行密钥已删除", + "passkeyRenamed": "通行密钥已重命名" + }, + "comms": { + "title": "通讯偏好", + "description": "选择您希望如何接收重要消息,如密码重置、安全提醒和账户更新。", + "preferredChannel": "首选渠道", + "preferredChannelDescription": "选择您首选的消息接收方式。必须先配置好渠道才能选择。", + "channelConfiguration": "渠道配置", + "emailVia": "通过邮件接收消息", + "discordVia": "通过 Discord 私信接收消息", + "telegramVia": "通过 Telegram 接收消息", + "signalVia": "通过 Signal 接收消息", + "configureToEnable": "请先在下方配置", + "emailManagedInSettings": "邮箱在账户设置中管理", + "discordIdHint": "您的 Discord 数字用户 ID(非用户名)。在 Discord 中开启开发者模式即可复制。", + "telegramHint": "您的 Telegram 用户名,不含 @ 符号", + "signalHint": "您的 Signal 电话号码,需包含国家代码", + "primary": "主要", + "verified": "已验证", + "notVerified": "未验证", + "verifyButton": "验证", + "verifyCodePlaceholder": "输入验证码", + "submit": "提交", + "saving": "保存中...", + "savePreferences": "保存偏好设置", + "preferencesSaved": "通讯偏好已保存", + "verifiedSuccess": "{channel} 验证成功", + "messageHistory": "消息历史", + "historyDescription": "查看发送到您账户的最近消息。", + "loadHistory": "加载历史", + "hideHistory": "隐藏历史", + "noMessages": "暂无消息记录", + "sent": "已发送", + "failed": "发送失败" + }, + "repoExplorer": { + "title": "数据浏览器", + "description": "浏览和管理您的原始 AT Protocol 记录。", + "collections": "集合", + "noCollections": "暂无集合", + "records": "记录", + "noRecords": "此集合中暂无记录", + "recordDetails": "记录详情", + "rkey": "记录键", + "cid": "CID", + "value": "值", + "deleteRecord": "删除记录", + "deleteConfirm": "删除记录 {rkey}?此操作无法撤销。", + "unknownError": "发生未知错误", + "invalidJson": "无效的 JSON", + "collectionRequired": "集合是必填项", + "recordCreated": "记录已创建:{uri}", + "recordUpdated": "记录已更新", + "recordDeleted": "记录已删除", + "newRecord": "新建记录", + "createRecord": "创建记录", + "filterCollections": "筛选集合...", + "filterRecords": "筛选记录...", + "noCollectionsYet": "暂无集合。创建您的第一条记录开始使用。", + "loadMore": "加载更多", + "recordJson": "记录 JSON", + "saving": "保存中...", + "updateRecord": "更新记录", + "collectionNsid": "集合 (NSID)", + "recordKeyOptional": "记录键(可选)", + "autoGenerated": "留空自动生成 (TID)", + "autoGeneratedHint": "留空将自动生成基于 TID 的键", + "creating": "创建中...", + "demoPostText": "你好,这是我的第一条帖子!来自我的 PDS。", + "demoDisplayName": "你的显示名称", + "demoBio": "写一段简短的自我介绍。" + }, + "admin": { + "title": "管理后台", + "serverStats": "服务器统计", + "users": "用户", + "repos": "仓库", + "records": "记录", + "blobStorage": "文件存储", + "refreshStats": "刷新统计", + "userManagement": "用户管理", + "searchPlaceholder": "按用户名搜索(可选)", + "searchUsers": "搜索用户", + "noUsers": "未找到用户", + "handle": "用户名", + "email": "邮箱", + "status": "状态", + "created": "创建时间", + "loadMore": "加载更多", + "inviteCodes": "邀请码", + "loadInviteCodes": "加载邀请码", + "refresh": "刷新", + "noInvites": "暂无邀请码", + "code": "邀请码", + "available": "可用", + "uses": "使用次数", + "actions": "操作", + "disable": "禁用", + "disableInviteConfirm": "禁用邀请码 {code}?", + "active": "活跃", + "exhausted": "已用完", + "disabled": "已禁用", + "userDetails": "用户详情", + "did": "DID", + "invites": "邀请", + "enabled": "已启用", + "enableInvites": "启用邀请", + "disableInvites": "禁用邀请", + "deleteAccount": "删除账户", + "deleteConfirm": "删除账户 @{handle}?此操作无法撤销。", + "verified": "已验证", + "unverified": "未验证", + "deactivated": "已停用" + }, + "oauth": { + "login": { + "title": "登录", + "subtitle": "登录以继续使用应用", + "signingIn": "登录中...", + "authenticating": "验证中...", + "checkingPasskey": "检查通行密钥...", + "signInWithPasskey": "使用通行密钥登录", + "passkeyNotSetUp": "未设置通行密钥", + "orUsePassword": "或使用密码", + "password": "密码", + "rememberDevice": "记住此设备", + "passkeyHintChecking": "正在检查通行密钥状态...", + "passkeyHintAvailable": "使用您的通行密钥登录", + "passkeyHintNotAvailable": "此账户未注册通行密钥" + }, + "consent": { + "title": "授权应用", + "appWantsAccess": "{app} 想要访问您的账户", + "permissions": "此应用将能够:", + "readProfile": "读取您的个人资料", + "readPosts": "读取您的帖子和内容", + "writePosts": "代表您发布和删除帖子", + "readNotifications": "读取您的通知", + "fullAccess": "完全访问您的账户", + "authorize": "授权", + "deny": "拒绝", + "authorizing": "授权中...", + "rememberChoice": "记住此选择", + "signingInAs": "登录账户:", + "permissionsRequested": "请求的权限", + "required": "必需", + "rememberChoiceLabel": "记住对此应用的授权选择" + }, + "accounts": { + "title": "选择账户", + "subtitle": "选择一个账户继续", + "useAnother": "使用其他账户" + }, + "twoFactor": { + "title": "双重身份验证", + "subtitle": "需要额外验证", + "usePasskey": "使用通行密钥", + "useTotp": "使用身份验证器", + "verifying": "验证中..." + }, + "twoFactorCode": { + "title": "双重身份验证", + "subtitle": "验证码已发送到您的 {channel}。请在下方输入验证码继续。", + "codeLabel": "验证码", + "codePlaceholder": "输入6位验证码", + "verify": "验证", + "verifying": "验证中...", + "errors": { + "missingRequestUri": "缺少 request_uri 参数", + "verificationFailed": "验证失败", + "connectionFailed": "无法连接到服务器", + "unexpectedResponse": "服务器返回意外响应" + } + }, + "totp": { + "title": "输入验证码", + "subtitle": "请输入身份验证器应用中的6位验证码", + "codePlaceholder": "输入6位验证码", + "verify": "验证", + "verifying": "验证中...", + "useBackupCode": "使用备用验证码", + "backupCodePlaceholder": "输入备用验证码", + "trustDevice": "信任此设备30天", + "hintBackupCode": "正在使用备用验证码", + "hintTotpCode": "正在使用身份验证器验证码", + "hintDefault": "身份验证器为6位数字,备用码为8位字符" + }, + "passkey": { + "title": "通行密钥验证", + "subtitle": "使用您的通行密钥验证身份", + "waiting": "等待通行密钥...", + "useTotp": "改用身份验证器" + }, + "error": { + "title": "授权错误", + "genericError": "授权过程中发生错误。", + "tryAgain": "重试", + "backToApp": "返回应用" + } + }, + "verify": { + "title": "验证账户", + "subtitle": "我们已将验证码发送到您的{channel}。请在下方输入以完成注册。", + "codePlaceholder": "输入6位验证码", + "codeLabel": "验证码", + "verifyButton": "验证账户", + "verifying": "验证中...", + "resendCode": "重新发送验证码", + "resending": "发送中...", + "codeResent": "验证码已重新发送!", + "backToLogin": "返回登录", + "verifyingAccount": "正在验证账户:@{handle}", + "startOver": "使用其他账户重新开始", + "noPending": "未找到待验证的账户", + "noPendingInfo": "如果您最近创建了账户需要验证,可能需要重新创建账户。如果您已完成验证,可以直接登录。", + "createAccount": "创建账户", + "signIn": "登录" + }, + "resetPassword": { + "title": "重置密码", + "forgotTitle": "忘记密码", + "subtitle": "输入您收到的验证码和新密码。", + "forgotSubtitle": "输入您的用户名或邮箱,我们将发送重置密码的验证码。", + "handleOrEmail": "用户名或邮箱", + "emailPlaceholder": "用户名或 you@example.com", + "sendCode": "发送重置验证码", + "sending": "发送中...", + "codeSent": "重置验证码已发送!请检查您的首选通知渠道。", + "enterCode": "输入邮件中的验证码和新密码。", + "code": "重置验证码", + "codePlaceholder": "输入重置验证码", + "newPassword": "新密码", + "newPasswordPlaceholder": "至少8位字符", + "confirmPassword": "确认密码", + "confirmPasswordPlaceholder": "再次输入新密码", + "resetButton": "重置密码", + "resetting": "重置中...", + "success": "密码重置成功!", + "backToLogin": "返回登录", + "requestNewCode": "重新获取验证码", + "passwordsMismatch": "两次输入的密码不一致", + "passwordLength": "密码至少需要8位字符" + }, + "recoverPasskey": { + "title": "恢复账户", + "invalidLinkTitle": "无效的恢复链接", + "invalidLinkMessage": "此恢复链接无效或已损坏。请重新申请恢复邮件。", + "goToLogin": "前往登录", + "successTitle": "密码设置成功!", + "successMessage": "您的临时密码已设置成功。您现在可以使用此密码登录。", + "successNextSteps": "登录后,建议您在安全设置中添加新的通行密钥以恢复无密码登录。", + "signIn": "登录", + "subtitle": "设置临时密码以恢复您的通行密钥账户访问权限。", + "newPassword": "新密码", + "newPasswordPlaceholder": "至少8位字符", + "confirmPassword": "确认密码", + "confirmPasswordPlaceholder": "再次输入密码", + "whatHappensNext": "接下来会发生什么?", + "whatHappensNextDetail": "设置密码后,您可以登录并在安全设置中添加新的通行密钥。添加通行密钥后,您可以选择移除临时密码。", + "setPassword": "设置密码", + "settingPassword": "设置中...", + "validation": { + "passwordRequired": "请输入新密码", + "passwordLength": "密码至少需要8位字符", + "passwordsMismatch": "两次输入的密码不一致" + }, + "errors": { + "invalidLink": "恢复链接无效,请重新申请。", + "expired": "恢复链接已过期,请重新申请。" + } + }, + "requestPasskeyRecovery": { + "title": "恢复通行密钥账户", + "subtitle": "丢失了通行密钥?输入您的用户名或邮箱,我们将发送恢复链接。", + "successTitle": "恢复链接已发送", + "successMessage": "如果账户存在且为通行密钥账户,您将在首选通知渠道收到恢复链接。", + "successInfo": "链接将在1小时后过期。请根据您的账户设置检查邮箱、Discord、Telegram 或 Signal。", + "handleOrEmail": "用户名或邮箱", + "emailPlaceholder": "用户名或 you@example.com", + "howItWorks": "如何恢复", + "howItWorksDetail": "我们将向您注册的通知渠道发送安全链接。点击链接设置临时密码,然后您就可以登录并添加新的通行密钥。", + "sendRecoveryLink": "发送恢复链接", + "sending": "发送中...", + "backToLogin": "返回登录" + }, + "registerPasskey": { + "title": "创建通行密钥账户", + "subtitle": "使用通行密钥创建无密码账户。", + "handle": "用户名", + "handlePlaceholder": "您的用户名", + "handleHint": "您的完整用户名将是:@{handle}", + "email": "邮箱地址", + "emailPlaceholder": "you@example.com", + "inviteCode": "邀请码", + "inviteCodePlaceholder": "输入您的邀请码", + "createButton": "创建账户", + "creating": "创建中...", + "alreadyHaveAccount": "已有账户?", + "signIn": "立即登录", + "wantPassword": "想使用密码?", + "createPasswordAccount": "创建密码账户" + }, + "trustedDevices": { + "title": "受信任设备", + "backToSecurity": "← 安全设置", + "description": "受信任设备可以跳过双重身份验证。信任有效期为30天,使用设备时自动延长。", + "noDevices": "暂无受信任设备", + "noDevicesHint": "开启双重身份验证后登录时,可以选择信任设备30天。", + "lastSeen": "最后使用:", + "trustedSince": "信任时间:", + "trustExpires": "信任过期:", + "expired": "已过期", + "tomorrow": "明天", + "inDays": "{days}天后", + "revoke": "撤销信任", + "revokeConfirm": "确定撤销对此设备的信任?下次从此设备登录时需要输入双重验证码。", + "deviceRevoked": "设备信任已撤销", + "deviceRenamed": "设备已重命名", + "deviceNamePlaceholder": "设备名称", + "browser": "浏览器:", + "unknownDevice": "未知设备" + }, + "reauth": { + "title": "需要重新验证", + "subtitle": "请验证您的身份以继续。", + "usePassword": "使用密码", + "usePasskey": "使用通行密钥", + "useTotp": "使用身份验证器", + "passwordPlaceholder": "输入您的密码", + "totpPlaceholder": "输入6位验证码", + "verify": "验证", + "verifying": "验证中...", + "cancel": "取消" + } +} diff --git a/frontend/src/main.ts b/frontend/src/main.ts index 1a85b5f..77f4a08 100644 --- a/frontend/src/main.ts +++ b/frontend/src/main.ts @@ -1,3 +1,4 @@ +import './styles/base.css' import App from './App.svelte' import { mount } from 'svelte' diff --git a/frontend/src/routes/Admin.svelte b/frontend/src/routes/Admin.svelte index 8a8a5a1..ed37000 100644 --- a/frontend/src/routes/Admin.svelte +++ b/frontend/src/routes/Admin.svelte @@ -2,6 +2,8 @@ import { getAuthState } from '../lib/auth.svelte' import { navigate } from '../lib/router.svelte' import { api, ApiError } from '../lib/api' + import { _ } from '../lib/i18n' + import { formatDate, formatDateTime } from '../lib/date' const auth = getAuthState() let loading = $state(true) let error = $state(null) @@ -123,7 +125,7 @@ } async function disableInvite(code: string) { if (!auth.session) return - if (!confirm(`Disable invite code ${code}?`)) return + if (!confirm($_('admin.disableInviteConfirm', { values: { code } }))) return try { await api.disableInviteCodes(auth.session.accessJwt, [code]) invites = invites.map(inv => inv.code === code ? { ...inv, disabled: true } : inv) @@ -164,7 +166,7 @@ } async function deleteUser() { if (!auth.session || !selectedUser) return - if (!confirm(`Delete account @${selectedUser.handle}? This cannot be undone.`)) return + if (!confirm($_('admin.deleteConfirm', { values: { handle: selectedUser.handle } }))) return userActionLoading = true try { await api.adminDeleteAccount(auth.session.accessJwt, selectedUser.did) @@ -267,7 +269,7 @@ Unverified {/if} - {new Date(user.indexedAt).toLocaleDateString()} + {formatDate(user.indexedAt)} {/each} @@ -322,7 +324,7 @@ Active {/if} - {new Date(invite.createdAt).toLocaleDateString()} + {formatDate(invite.createdAt)} {#if !invite.disabled} + {$_('comms.notVerified')} + {/if} {/if} -

Your Discord user ID (not username). Enable Developer Mode in Discord to copy it.

+

{$_('comms.discordIdHint')}

{#if verifyingChannel === 'discord'}
- - + +
{/if}
- +
{#if telegramUsername} {#if telegramVerified} - Verified + {$_('comms.verified')} {:else} - Not verified - + {$_('comms.notVerified')} + {/if} {/if}
-

Your Telegram username without the @ symbol

+

{$_('comms.telegramHint')}

{#if verifyingChannel === 'telegram'}
- - + +
{/if}
- +
{#if signalNumber} {#if signalVerified} - Verified + {$_('comms.verified')} {:else} - Not verified - + {$_('comms.notVerified')} + {/if} {/if}
-

Your Signal phone number with country code

+

{$_('comms.signalHint')}

{#if verifyingChannel === 'signal'}
- - + +
{/if}
@@ -303,37 +319,37 @@
-

Notification History

-

View recent notifications sent to your account.

+

{$_('comms.messageHistory')}

+

{$_('comms.historyDescription')}

{#if !showHistory} {:else} - + {#if historyError}
{historyError}
- {:else if notifications.length === 0} -

No notifications found.

+ {:else if messages.length === 0} +

{$_('comms.noMessages')}

{:else} -
- {#each notifications as notification} -
-
- {notification.notificationType} - {notification.channel} - {notification.status} +
+ {#each messages as msg} +
+
+ {msg.notificationType} + {msg.channel} + {msg.status}
- {#if notification.subject} -
{notification.subject}
+ {#if msg.subject} +
{msg.subject}
{/if} -
{notification.body}
-
{formatDate(notification.createdAt)}
+
{msg.body}
+
{formatDate(msg.createdAt)}
{/each}
@@ -344,318 +360,332 @@
diff --git a/frontend/src/routes/Dashboard.svelte b/frontend/src/routes/Dashboard.svelte index 4c30130..1c4a2d5 100644 --- a/frontend/src/routes/Dashboard.svelte +++ b/frontend/src/routes/Dashboard.svelte @@ -1,18 +1,23 @@ + {#if auth.session}
-

Dashboard

+

{$_('dashboard.title')}

{/if} -
{/if}
+ {#if auth.session.status === 'deactivated' || auth.session.active === false}
- Account Deactivated -

Your account is currently deactivated. This typically happens during account migration. Some features may be limited until your account is reactivated.

+ {$_('dashboard.deactivatedTitle')} +

{$_('dashboard.deactivatedMessage')}

{/if} + +
{:else if auth.loading} -
Loading...
+
{$_('common.loading')}
{/if} + diff --git a/frontend/src/routes/Home.svelte b/frontend/src/routes/Home.svelte new file mode 100644 index 0000000..8fa1489 --- /dev/null +++ b/frontend/src/routes/Home.svelte @@ -0,0 +1,146 @@ + +
+
+

Tranquil PDS

+

A Personal Data Server for the AT Protocol

+
+
+

What is a PDS?

+

+ Bluesky runs on a federated protocol called AT Protocol. Your account lives on a PDS, + a server that stores your posts, profile, follows, and cryptographic keys. Bluesky hosts + one for you at bsky.social, but you can run your own. Self-hosting means you control your + data; you're not dependent on any company's servers, and your account + data is actually yours. +

+
+
+

What's different about Tranquil?

+

+ This software isn't an afterthought by a company with limited resources. + It is a superset of the reference PDS, including: +

+
    +
  • Passkeys and 2FA (WebAuthn/FIDO2, TOTP, backup codes, trusted devices)
  • +
  • did:web support (PDS-hosted subdomains or bring-your-own)
  • +
  • Multi-channel notifications (email, discord, telegram, signal)
  • +
  • Granular OAuth scopes with a consent UI
  • +
  • Built-in web UI for account management, repo browsing, and admin
  • +
+

+ Full compatibility with Bluesky's reference PDS: same endpoints, same behavior, + same client compatibility. Everything works. +

+
+
+ {#if auth.session} + @{auth.session.handle} + {:else} + {$_('login.button')} + {$_('login.createAccount')} + {/if} +
+ +
+ diff --git a/frontend/src/routes/InviteCodes.svelte b/frontend/src/routes/InviteCodes.svelte index 289b6ea..fdb13c9 100644 --- a/frontend/src/routes/InviteCodes.svelte +++ b/frontend/src/routes/InviteCodes.svelte @@ -2,6 +2,8 @@ import { getAuthState } from '../lib/auth.svelte' import { navigate } from '../lib/router.svelte' import { api, type InviteCode, ApiError } from '../lib/api' + import { _ } from '../lib/i18n' + import { formatDate } from '../lib/date' const auth = getAuthState() let codes = $state([]) let loading = $state(true) @@ -54,54 +56,54 @@
- ← Dashboard -

Invite Codes

+ {$_('common.backToDashboard')} +

{$_('inviteCodes.title')}

- Invite codes let you invite friends to join. Each code can be used once. + {$_('inviteCodes.description')}

{#if error}
{error}
{/if} {#if createdCode}
-

Invite Code Created

+

{$_('inviteCodes.created')}

{createdCode} - +
- +
{/if}
-

Your Invite Codes

+

{$_('inviteCodes.yourCodes')}

{#if loading} -

Loading...

+

{$_('common.loading')}

{:else if codes.length === 0} -

No invite codes yet

+

{$_('inviteCodes.noCodes')}

{:else}
    {#each codes as code}
  • 0 && code.available === 0}>
    {code.code} -
    - Created {new Date(code.createdAt).toLocaleDateString()} + {$_('inviteCodes.createdOn', { values: { date: formatDate(code.createdAt) } })} {#if code.disabled} - Disabled + {$_('inviteCodes.disabled')} {:else if code.uses.length > 0} - Used by @{code.uses[0].usedBy.split(':').pop()} + {$_('inviteCodes.used', { values: { handle: code.uses[0].usedBy.split(':').pop() } })} {:else} - Available + {$_('inviteCodes.available')} {/if}
  • @@ -112,163 +114,176 @@
diff --git a/frontend/src/routes/Login.svelte b/frontend/src/routes/Login.svelte index d79e524..6c786b8 100644 --- a/frontend/src/routes/Login.svelte +++ b/frontend/src/routes/Login.svelte @@ -1,6 +1,8 @@ -