From 442ca1434f81d1fe2164846d2391f0e33bea47a4 Mon Sep 17 00:00:00 2001 From: lewis Date: Mon, 2 Feb 2026 20:25:41 +0100 Subject: [PATCH] fix: better dashboard UX --- ...52cd25f6359a9d1bfe0471caf6c3e3285f88e.json | 106 - ...0b6c6fbec8566a4adbb595c91606fdfa3bedc.json | 112 -- ...7e92e2e8752970586725d4b2cb55f4998a9a9.json | 16 - ...92a6cc41b94b04009b5602f1ef72e0138edc.json} | 4 +- ...81ce1d5bcc7269c8a89aa9c86a30eec90b50d.json | 16 - ...04b92c9e6b3462744a4c91530a31bc81e763b.json | 130 -- crates/tranquil-db-traits/src/infra.rs | 1 + crates/tranquil-db/src/postgres/backup.rs | 1 + crates/tranquil-pds/src/auth/extractor.rs | 9 +- crates/tranquil-pds/src/sso/endpoints.rs | 6 +- frontend/src/App.svelte | 59 +- .../src/components/LoadMoreSentinel.svelte | 50 + frontend/src/components/ReauthModal.svelte | 11 - .../dashboard/AccountOverview.svelte | 141 ++ .../components/dashboard/AdminContent.svelte | 1047 ++++++++++ .../dashboard/AppPasswordsContent.svelte | 427 ++++ .../components/dashboard/CommsContent.svelte | 609 ++++++ .../dashboard/ControllersContent.svelte | 655 +++++++ .../dashboard/DelegationAuditContent.svelte | 282 +++ .../dashboard/DidDocumentContent.svelte | 415 ++++ .../dashboard/InviteCodesContent.svelte} | 204 +- .../dashboard/MigrationContent.svelte | 113 ++ .../dashboard/RepoContent.svelte} | 431 ++--- .../dashboard/SecurityContent.svelte | 1722 +++++++++++++++++ .../dashboard/SessionsContent.svelte} | 214 +- .../dashboard/SettingsContent.svelte} | 1107 ++++------- .../migration/ChooseHandleStep.svelte | 8 +- frontend/src/lib/api.ts | 122 +- frontend/src/lib/types/api.ts | 1 + frontend/src/lib/types/routes.ts | 1 - frontend/src/locales/en.json | 433 +---- frontend/src/locales/fi.json | 457 +---- frontend/src/locales/ja.json | 455 +---- frontend/src/locales/ko.json | 459 +---- frontend/src/locales/sv.json | 459 +---- frontend/src/locales/zh.json | 457 +---- frontend/src/routes/ActAs.svelte | 2 +- frontend/src/routes/Admin.svelte | 1155 ----------- frontend/src/routes/AppPasswords.svelte | 472 ----- frontend/src/routes/Comms.svelte | 814 -------- frontend/src/routes/Controllers.svelte | 699 ------- frontend/src/routes/Dashboard.svelte | 873 +++++---- frontend/src/routes/DelegationAudit.svelte | 311 --- frontend/src/routes/DidDocumentEditor.svelte | 478 ----- frontend/src/routes/Security.svelte | 1512 --------------- frontend/src/routes/TrustedDevices.svelte | 327 ---- frontend/src/tests/AppPasswords.test.ts | 404 ---- frontend/src/tests/Comms.test.ts | 508 ----- frontend/src/tests/Dashboard.test.ts | 36 +- frontend/src/tests/Login.test.ts | 2 +- frontend/src/tests/Settings.test.ts | 579 ------ .../20260122_backup_storage_key_unique.sql | 8 + 52 files changed, 7356 insertions(+), 11564 deletions(-) delete mode 100644 .sqlx/query-2605e4c41794f3201bbbd4fb37b52cd25f6359a9d1bfe0471caf6c3e3285f88e.json delete mode 100644 .sqlx/query-297fcbb356d65aae3faae5430000b6c6fbec8566a4adbb595c91606fdfa3bedc.json delete mode 100644 .sqlx/query-3b32503a945ead2f146e62b0b017e92e2e8752970586725d4b2cb55f4998a9a9.json rename .sqlx/{query-964645277bd31cf07225c46cf29561162bf85c10ac985afe7025c5b992019f59.json => query-6c7d5b62546ed7581edbfdb6946492a6cc41b94b04009b5602f1ef72e0138edc.json} (66%) delete mode 100644 .sqlx/query-75b0817f1bb79a4c7dcab683d0e81ce1d5bcc7269c8a89aa9c86a30eec90b50d.json delete mode 100644 .sqlx/query-aae64b1be442f522565b8a4ff2304b92c9e6b3462744a4c91530a31bc81e763b.json create mode 100644 frontend/src/components/LoadMoreSentinel.svelte create mode 100644 frontend/src/components/dashboard/AccountOverview.svelte create mode 100644 frontend/src/components/dashboard/AdminContent.svelte create mode 100644 frontend/src/components/dashboard/AppPasswordsContent.svelte create mode 100644 frontend/src/components/dashboard/CommsContent.svelte create mode 100644 frontend/src/components/dashboard/ControllersContent.svelte create mode 100644 frontend/src/components/dashboard/DelegationAuditContent.svelte create mode 100644 frontend/src/components/dashboard/DidDocumentContent.svelte rename frontend/src/{routes/InviteCodes.svelte => components/dashboard/InviteCodesContent.svelte} (59%) create mode 100644 frontend/src/components/dashboard/MigrationContent.svelte rename frontend/src/{routes/RepoExplorer.svelte => components/dashboard/RepoContent.svelte} (68%) create mode 100644 frontend/src/components/dashboard/SecurityContent.svelte rename frontend/src/{routes/Sessions.svelte => components/dashboard/SessionsContent.svelte} (59%) rename frontend/src/{routes/Settings.svelte => components/dashboard/SettingsContent.svelte} (54%) delete mode 100644 frontend/src/routes/Admin.svelte delete mode 100644 frontend/src/routes/AppPasswords.svelte delete mode 100644 frontend/src/routes/Comms.svelte delete mode 100644 frontend/src/routes/Controllers.svelte delete mode 100644 frontend/src/routes/DelegationAudit.svelte delete mode 100644 frontend/src/routes/DidDocumentEditor.svelte delete mode 100644 frontend/src/routes/Security.svelte delete mode 100644 frontend/src/routes/TrustedDevices.svelte delete mode 100644 frontend/src/tests/AppPasswords.test.ts delete mode 100644 frontend/src/tests/Comms.test.ts delete mode 100644 frontend/src/tests/Settings.test.ts create mode 100644 migrations/20260122_backup_storage_key_unique.sql diff --git a/.sqlx/query-2605e4c41794f3201bbbd4fb37b52cd25f6359a9d1bfe0471caf6c3e3285f88e.json b/.sqlx/query-2605e4c41794f3201bbbd4fb37b52cd25f6359a9d1bfe0471caf6c3e3285f88e.json deleted file mode 100644 index 747b1e0..0000000 --- a/.sqlx/query-2605e4c41794f3201bbbd4fb37b52cd25f6359a9d1bfe0471caf6c3e3285f88e.json +++ /dev/null @@ -1,106 +0,0 @@ -{ - "db_name": "PostgreSQL", - "query": "\n SELECT handle, email, email_verified, is_admin, deactivated_at, takedown_ref,\n preferred_locale,\n preferred_comms_channel as \"preferred_comms_channel!: CommsChannel\",\n discord_verified, telegram_verified, signal_verified,\n migrated_to_pds, migrated_at\n FROM users\n WHERE did = $1\n ", - "describe": { - "columns": [ - { - "ordinal": 0, - "name": "handle", - "type_info": "Text" - }, - { - "ordinal": 1, - "name": "email", - "type_info": "Text" - }, - { - "ordinal": 2, - "name": "email_verified", - "type_info": "Bool" - }, - { - "ordinal": 3, - "name": "is_admin", - "type_info": "Bool" - }, - { - "ordinal": 4, - "name": "deactivated_at", - "type_info": "Timestamptz" - }, - { - "ordinal": 5, - "name": "takedown_ref", - "type_info": "Text" - }, - { - "ordinal": 6, - "name": "preferred_locale", - "type_info": "Varchar" - }, - { - "ordinal": 7, - "name": "preferred_comms_channel!: CommsChannel", - "type_info": { - "Custom": { - "name": "comms_channel", - "kind": { - "Enum": [ - "email", - "discord", - "telegram", - "signal" - ] - } - } - } - }, - { - "ordinal": 8, - "name": "discord_verified", - "type_info": "Bool" - }, - { - "ordinal": 9, - "name": "telegram_verified", - "type_info": "Bool" - }, - { - "ordinal": 10, - "name": "signal_verified", - "type_info": "Bool" - }, - { - "ordinal": 11, - "name": "migrated_to_pds", - "type_info": "Text" - }, - { - "ordinal": 12, - "name": "migrated_at", - "type_info": "Timestamptz" - } - ], - "parameters": { - "Left": [ - "Text" - ] - }, - "nullable": [ - false, - true, - false, - false, - true, - true, - true, - false, - false, - false, - false, - true, - true - ] - }, - "hash": "2605e4c41794f3201bbbd4fb37b52cd25f6359a9d1bfe0471caf6c3e3285f88e" -} diff --git a/.sqlx/query-297fcbb356d65aae3faae5430000b6c6fbec8566a4adbb595c91606fdfa3bedc.json b/.sqlx/query-297fcbb356d65aae3faae5430000b6c6fbec8566a4adbb595c91606fdfa3bedc.json deleted file mode 100644 index d14cb47..0000000 --- a/.sqlx/query-297fcbb356d65aae3faae5430000b6c6fbec8566a4adbb595c91606fdfa3bedc.json +++ /dev/null @@ -1,112 +0,0 @@ -{ - "db_name": "PostgreSQL", - "query": "\n SELECT handle, email, email_verified, is_admin, deactivated_at, takedown_ref,\n preferred_locale,\n preferred_comms_channel as \"preferred_comms_channel!: CommsChannel\",\n discord_verified, telegram_verified, signal_verified,\n migrated_to_pds, migrated_at,\n (SELECT verified FROM user_totp WHERE did = users.did) as totp_enabled\n FROM users\n WHERE did = $1\n ", - "describe": { - "columns": [ - { - "ordinal": 0, - "name": "handle", - "type_info": "Text" - }, - { - "ordinal": 1, - "name": "email", - "type_info": "Text" - }, - { - "ordinal": 2, - "name": "email_verified", - "type_info": "Bool" - }, - { - "ordinal": 3, - "name": "is_admin", - "type_info": "Bool" - }, - { - "ordinal": 4, - "name": "deactivated_at", - "type_info": "Timestamptz" - }, - { - "ordinal": 5, - "name": "takedown_ref", - "type_info": "Text" - }, - { - "ordinal": 6, - "name": "preferred_locale", - "type_info": "Varchar" - }, - { - "ordinal": 7, - "name": "preferred_comms_channel!: CommsChannel", - "type_info": { - "Custom": { - "name": "comms_channel", - "kind": { - "Enum": [ - "email", - "discord", - "telegram", - "signal" - ] - } - } - } - }, - { - "ordinal": 8, - "name": "discord_verified", - "type_info": "Bool" - }, - { - "ordinal": 9, - "name": "telegram_verified", - "type_info": "Bool" - }, - { - "ordinal": 10, - "name": "signal_verified", - "type_info": "Bool" - }, - { - "ordinal": 11, - "name": "migrated_to_pds", - "type_info": "Text" - }, - { - "ordinal": 12, - "name": "migrated_at", - "type_info": "Timestamptz" - }, - { - "ordinal": 13, - "name": "totp_enabled", - "type_info": "Bool" - } - ], - "parameters": { - "Left": [ - "Text" - ] - }, - "nullable": [ - false, - true, - false, - false, - true, - true, - true, - false, - false, - false, - false, - true, - true, - null - ] - }, - "hash": "297fcbb356d65aae3faae5430000b6c6fbec8566a4adbb595c91606fdfa3bedc" -} diff --git a/.sqlx/query-3b32503a945ead2f146e62b0b017e92e2e8752970586725d4b2cb55f4998a9a9.json b/.sqlx/query-3b32503a945ead2f146e62b0b017e92e2e8752970586725d4b2cb55f4998a9a9.json deleted file mode 100644 index c5ec3fc..0000000 --- a/.sqlx/query-3b32503a945ead2f146e62b0b017e92e2e8752970586725d4b2cb55f4998a9a9.json +++ /dev/null @@ -1,16 +0,0 @@ -{ - "db_name": "PostgreSQL", - "query": "INSERT INTO account_preferences (user_id, name, value_json) VALUES ($1, $2, $3)\n ON CONFLICT (user_id, name) DO UPDATE SET value_json = $3", - "describe": { - "columns": [], - "parameters": { - "Left": [ - "Uuid", - "Text", - "Jsonb" - ] - }, - "nullable": [] - }, - "hash": "3b32503a945ead2f146e62b0b017e92e2e8752970586725d4b2cb55f4998a9a9" -} diff --git a/.sqlx/query-964645277bd31cf07225c46cf29561162bf85c10ac985afe7025c5b992019f59.json b/.sqlx/query-6c7d5b62546ed7581edbfdb6946492a6cc41b94b04009b5602f1ef72e0138edc.json similarity index 66% rename from .sqlx/query-964645277bd31cf07225c46cf29561162bf85c10ac985afe7025c5b992019f59.json rename to .sqlx/query-6c7d5b62546ed7581edbfdb6946492a6cc41b94b04009b5602f1ef72e0138edc.json index b69aa31..3d4eed4 100644 --- a/.sqlx/query-964645277bd31cf07225c46cf29561162bf85c10ac985afe7025c5b992019f59.json +++ b/.sqlx/query-6c7d5b62546ed7581edbfdb6946492a6cc41b94b04009b5602f1ef72e0138edc.json @@ -1,6 +1,6 @@ { "db_name": "PostgreSQL", - "query": "\n INSERT INTO account_backups (user_id, storage_key, repo_root_cid, repo_rev, block_count, size_bytes)\n VALUES ($1, $2, $3, $4, $5, $6)\n RETURNING id\n ", + "query": "\n INSERT INTO account_backups (user_id, storage_key, repo_root_cid, repo_rev, block_count, size_bytes)\n VALUES ($1, $2, $3, $4, $5, $6)\n ON CONFLICT (storage_key) DO UPDATE SET created_at = NOW()\n RETURNING id\n ", "describe": { "columns": [ { @@ -23,5 +23,5 @@ false ] }, - "hash": "964645277bd31cf07225c46cf29561162bf85c10ac985afe7025c5b992019f59" + "hash": "6c7d5b62546ed7581edbfdb6946492a6cc41b94b04009b5602f1ef72e0138edc" } diff --git a/.sqlx/query-75b0817f1bb79a4c7dcab683d0e81ce1d5bcc7269c8a89aa9c86a30eec90b50d.json b/.sqlx/query-75b0817f1bb79a4c7dcab683d0e81ce1d5bcc7269c8a89aa9c86a30eec90b50d.json deleted file mode 100644 index bcd36de..0000000 --- a/.sqlx/query-75b0817f1bb79a4c7dcab683d0e81ce1d5bcc7269c8a89aa9c86a30eec90b50d.json +++ /dev/null @@ -1,16 +0,0 @@ -{ - "db_name": "PostgreSQL", - "query": "INSERT INTO account_preferences (user_id, name, value_json) VALUES ($1, $2, $3)\n ON CONFLICT (user_id, name) DO NOTHING", - "describe": { - "columns": [], - "parameters": { - "Left": [ - "Uuid", - "Text", - "Jsonb" - ] - }, - "nullable": [] - }, - "hash": "75b0817f1bb79a4c7dcab683d0e81ce1d5bcc7269c8a89aa9c86a30eec90b50d" -} diff --git a/.sqlx/query-aae64b1be442f522565b8a4ff2304b92c9e6b3462744a4c91530a31bc81e763b.json b/.sqlx/query-aae64b1be442f522565b8a4ff2304b92c9e6b3462744a4c91530a31bc81e763b.json deleted file mode 100644 index 677b2e7..0000000 --- a/.sqlx/query-aae64b1be442f522565b8a4ff2304b92c9e6b3462744a4c91530a31bc81e763b.json +++ /dev/null @@ -1,130 +0,0 @@ -{ - "db_name": "PostgreSQL", - "query": "SELECT\n u.id, u.did, u.handle, u.password_hash, u.email, u.deactivated_at, u.takedown_ref,\n u.email_verified, u.discord_verified, u.telegram_verified, u.signal_verified,\n u.allow_legacy_login, u.migrated_to_pds,\n u.preferred_comms_channel as \"preferred_comms_channel: CommsChannel\",\n k.key_bytes, k.encryption_version,\n (SELECT verified FROM user_totp WHERE did = u.did) as totp_enabled\n FROM users u\n JOIN user_keys k ON u.id = k.user_id\n WHERE u.handle = $1 OR u.email = $1 OR u.did = $1", - "describe": { - "columns": [ - { - "ordinal": 0, - "name": "id", - "type_info": "Uuid" - }, - { - "ordinal": 1, - "name": "did", - "type_info": "Text" - }, - { - "ordinal": 2, - "name": "handle", - "type_info": "Text" - }, - { - "ordinal": 3, - "name": "password_hash", - "type_info": "Text" - }, - { - "ordinal": 4, - "name": "email", - "type_info": "Text" - }, - { - "ordinal": 5, - "name": "deactivated_at", - "type_info": "Timestamptz" - }, - { - "ordinal": 6, - "name": "takedown_ref", - "type_info": "Text" - }, - { - "ordinal": 7, - "name": "email_verified", - "type_info": "Bool" - }, - { - "ordinal": 8, - "name": "discord_verified", - "type_info": "Bool" - }, - { - "ordinal": 9, - "name": "telegram_verified", - "type_info": "Bool" - }, - { - "ordinal": 10, - "name": "signal_verified", - "type_info": "Bool" - }, - { - "ordinal": 11, - "name": "allow_legacy_login", - "type_info": "Bool" - }, - { - "ordinal": 12, - "name": "migrated_to_pds", - "type_info": "Text" - }, - { - "ordinal": 13, - "name": "preferred_comms_channel: CommsChannel", - "type_info": { - "Custom": { - "name": "comms_channel", - "kind": { - "Enum": [ - "email", - "discord", - "telegram", - "signal" - ] - } - } - } - }, - { - "ordinal": 14, - "name": "key_bytes", - "type_info": "Bytea" - }, - { - "ordinal": 15, - "name": "encryption_version", - "type_info": "Int4" - }, - { - "ordinal": 16, - "name": "totp_enabled", - "type_info": "Bool" - } - ], - "parameters": { - "Left": [ - "Text" - ] - }, - "nullable": [ - false, - false, - false, - true, - true, - true, - true, - false, - false, - false, - false, - false, - true, - false, - false, - true, - null - ] - }, - "hash": "aae64b1be442f522565b8a4ff2304b92c9e6b3462744a4c91530a31bc81e763b" -} diff --git a/crates/tranquil-db-traits/src/infra.rs b/crates/tranquil-db-traits/src/infra.rs index 6745411..82a11b1 100644 --- a/crates/tranquil-db-traits/src/infra.rs +++ b/crates/tranquil-db-traits/src/infra.rs @@ -54,6 +54,7 @@ impl From for bool { } #[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, Serialize, Deserialize, sqlx::Type)] +#[serde(rename_all = "lowercase")] #[sqlx(type_name = "comms_channel", rename_all = "snake_case")] pub enum CommsChannel { Email, diff --git a/crates/tranquil-db/src/postgres/backup.rs b/crates/tranquil-db/src/postgres/backup.rs index 437451b..91cd509 100644 --- a/crates/tranquil-db/src/postgres/backup.rs +++ b/crates/tranquil-db/src/postgres/backup.rs @@ -114,6 +114,7 @@ impl BackupRepository for PostgresBackupRepository { r#" INSERT INTO account_backups (user_id, storage_key, repo_root_cid, repo_rev, block_count, size_bytes) VALUES ($1, $2, $3, $4, $5, $6) + ON CONFLICT (storage_key) DO UPDATE SET created_at = NOW() RETURNING id "#, user_id, diff --git a/crates/tranquil-pds/src/auth/extractor.rs b/crates/tranquil-pds/src/auth/extractor.rs index 9016dd5..02cf737 100644 --- a/crates/tranquil-pds/src/auth/extractor.rs +++ b/crates/tranquil-pds/src/auth/extractor.rs @@ -1,7 +1,7 @@ use std::marker::PhantomData; use axum::{ - extract::{FromRequestParts, OptionalFromRequestParts}, + extract::{FromRequestParts, OptionalFromRequestParts, OriginalUri}, http::{StatusCode, header::AUTHORIZATION, request::Parts}, response::{IntoResponse, Response}, }; @@ -295,7 +295,12 @@ async fn extract_auth_internal( let dpop_proof = crate::util::get_header_str(&parts.headers, "DPoP"); let method = parts.method.as_str(); - let uri = build_full_url(&parts.uri.to_string()); + let original_uri = parts + .extensions + .get::() + .map(|u| u.0.path().to_string()) + .unwrap_or_else(|| parts.uri.path().to_string()); + let uri = build_full_url(&original_uri); match validate_bearer_token_for_service_auth(state.user_repo.as_ref(), &extracted.token).await { Ok(user) if !user.auth_source.is_oauth() => { diff --git a/crates/tranquil-pds/src/sso/endpoints.rs b/crates/tranquil-pds/src/sso/endpoints.rs index 5a0ab10..0fcb7ed 100644 --- a/crates/tranquil-pds/src/sso/endpoints.rs +++ b/crates/tranquil-pds/src/sso/endpoints.rs @@ -11,7 +11,7 @@ use tranquil_types::RequestId; use super::config::SsoConfig; use crate::api::error::ApiError; -use crate::auth::extractor::extract_bearer_token_from_header; +use crate::auth::extractor::extract_auth_token_from_header; use crate::auth::{generate_app_password, validate_bearer_token_cached}; use crate::rate_limit::{ AccountCreationLimit, RateLimited, SsoCallbackLimit, SsoInitiateLimit, SsoUnlinkLimit, @@ -119,12 +119,12 @@ pub async fn sso_initiate( let auth_header = headers .get(axum::http::header::AUTHORIZATION) .and_then(|v| v.to_str().ok()); - let token = extract_bearer_token_from_header(auth_header) + let extracted = extract_auth_token_from_header(auth_header) .ok_or(ApiError::SsoNotAuthenticated)?; let auth_user = validate_bearer_token_cached( state.user_repo.as_ref(), state.cache.as_ref(), - &token, + &extracted.token, ) .await .map_err(|_| ApiError::SsoNotAuthenticated)?; diff --git a/frontend/src/App.svelte b/frontend/src/App.svelte index 6c7bde7..88fab77 100644 --- a/frontend/src/App.svelte +++ b/frontend/src/App.svelte @@ -12,13 +12,6 @@ import RecoverPasskey from './routes/RecoverPasskey.svelte' import RequestPasskeyRecovery from './routes/RequestPasskeyRecovery.svelte' import Dashboard from './routes/Dashboard.svelte' - import AppPasswords from './routes/AppPasswords.svelte' - import InviteCodes from './routes/InviteCodes.svelte' - import Settings from './routes/Settings.svelte' - import Sessions from './routes/Sessions.svelte' - import Comms from './routes/Comms.svelte' - import RepoExplorer from './routes/RepoExplorer.svelte' - import Admin from './routes/Admin.svelte' import OAuthConsent from './routes/OAuthConsent.svelte' import OAuthLogin from './routes/OAuthLogin.svelte' import OAuthAccounts from './routes/OAuthAccounts.svelte' @@ -30,13 +23,8 @@ import SsoRegisterComplete from './routes/SsoRegisterComplete.svelte' import Register from './routes/Register.svelte' import RegisterPassword from './routes/RegisterPassword.svelte' - import Security from './routes/Security.svelte' - import TrustedDevices from './routes/TrustedDevices.svelte' - import Controllers from './routes/Controllers.svelte' - import DelegationAudit from './routes/DelegationAudit.svelte' import ActAs from './routes/ActAs.svelte' import Migration from './routes/Migration.svelte' - import DidDocumentEditor from './routes/DidDocumentEditor.svelte' import { _ } from './lib/i18n' initI18n() @@ -94,8 +82,27 @@ } }) + const dashboardRoutes = new Set([ + '/dashboard', + '/settings', + '/security', + '/sessions', + '/app-passwords', + '/comms', + '/repo', + '/controllers', + '/delegation-audit', + '/invite-codes', + '/did-document', + '/admin', + ]) + function getComponent(path: string) { - switch (path) { + const pathWithoutQuery = path.split('?')[0] + if (dashboardRoutes.has(pathWithoutQuery)) { + return Dashboard + } + switch (pathWithoutQuery) { case '/login': return Login case '/verify': @@ -106,22 +113,6 @@ return RecoverPasskey case '/request-passkey-recovery': return RequestPasskeyRecovery - case '/dashboard': - return Dashboard - case '/app-passwords': - return AppPasswords - case '/invite-codes': - return InviteCodes - case '/settings': - return Settings - case '/sessions': - return Sessions - case '/comms': - return Comms - case '/repo': - return RepoExplorer - case '/admin': - return Admin case '/oauth/consent': return OAuthConsent case '/oauth/login': @@ -147,20 +138,10 @@ return RegisterSso case '/oauth/register-password': return RegisterPassword - case '/security': - return Security - case '/trusted-devices': - return TrustedDevices - case '/controllers': - return Controllers - case '/delegation-audit': - return DelegationAudit case '/act-as': return ActAs case '/migrate': return Migration - case '/did-document': - return DidDocumentEditor default: return Login } diff --git a/frontend/src/components/LoadMoreSentinel.svelte b/frontend/src/components/LoadMoreSentinel.svelte new file mode 100644 index 0000000..aaf0faf --- /dev/null +++ b/frontend/src/components/LoadMoreSentinel.svelte @@ -0,0 +1,50 @@ + + +{#if hasMore} +
+ {#if loading} + {$_('common.loading')} + {/if} +
+{/if} + + diff --git a/frontend/src/components/ReauthModal.svelte b/frontend/src/components/ReauthModal.svelte index b935634..c15231c 100644 --- a/frontend/src/components/ReauthModal.svelte +++ b/frontend/src/components/ReauthModal.svelte @@ -281,12 +281,6 @@ color: var(--text-primary); } - .modal-description { - padding: var(--space-4) var(--space-6) 0; - margin: 0; - color: var(--text-secondary); - } - .error-message { margin: var(--space-4) var(--space-6) 0; padding: var(--space-3); @@ -336,11 +330,6 @@ text-align: center; } - .passkey-auth p { - margin-bottom: var(--space-4); - color: var(--text-secondary); - } - .modal-content button:not(.tab) { width: 100%; } diff --git a/frontend/src/components/dashboard/AccountOverview.svelte b/frontend/src/components/dashboard/AccountOverview.svelte new file mode 100644 index 0000000..71250d7 --- /dev/null +++ b/frontend/src/components/dashboard/AccountOverview.svelte @@ -0,0 +1,141 @@ + + +
+
+
{$_('dashboard.handle')}
+
+ @{session.handle} + {#if session.isAdmin} + {$_('dashboard.admin')} + {/if} + {#if session.accountKind === 'migrated'} + {$_('dashboard.migrated')} + {:else if session.accountKind === 'deactivated'} + {$_('dashboard.deactivated')} + {/if} +
+
{$_('dashboard.did')}
+
{session.did}
+ {#if session.contactKind === 'channel'} +
{$_('dashboard.primaryContact')}
+
+ {#if session.preferredChannel === 'email'} + {session.email || $_('register.email')} + {:else if session.preferredChannel === 'discord'} + {$_('register.discord')} + {:else if session.preferredChannel === 'telegram'} + {$_('register.telegram')} + {:else if session.preferredChannel === 'signal'} + {$_('register.signal')} + {:else} + {session.preferredChannel} + {/if} + {#if session.preferredChannelVerified} + {$_('dashboard.verified')} + {:else} + {$_('dashboard.unverified')} + {/if} +
+ {:else if session.contactKind === 'email'} +
{$_('register.email')}
+
+ {session.email} + {#if session.emailConfirmed} + {$_('dashboard.verified')} + {:else} + {$_('dashboard.unverified')} + {/if} +
+ {/if} +
+
+ + diff --git a/frontend/src/components/dashboard/AdminContent.svelte b/frontend/src/components/dashboard/AdminContent.svelte new file mode 100644 index 0000000..005d3f3 --- /dev/null +++ b/frontend/src/components/dashboard/AdminContent.svelte @@ -0,0 +1,1047 @@ + + +
+
+

{$_('admin.serverConfig')}

+
+
+ + + {$_('admin.serverNameHelp')} +
+ +
+ +
+ {#if logoPreview} +
+ {$_('admin.logoPreview')} + +
+ {/if} + +
+ {$_('admin.logoHelp')} +
+ +
+

{$_('admin.themeColors')}

+ {$_('admin.themeColorsHint')} +
+
+ +
+ + +
+
+
+ +
+ + +
+
+
+ +
+ + +
+
+
+ +
+ + +
+
+
+
+ + +
+
+ +
+
+

{$_('admin.serverStats')}

+ +
+ {#if loading} +
{$_('common.loading')}
+ {:else if stats} +
+
+ {formatNumber(stats.userCount)} + {$_('admin.users')} +
+
+ {formatNumber(stats.repoCount)} + {$_('admin.repos')} +
+
+ {formatNumber(stats.recordCount)} + {$_('admin.records')} +
+
+ {formatBytes(stats.blobStorageBytes)} + {$_('admin.blobStorage')} +
+
+ {/if} +
+ +
+

{$_('admin.userManagement')}

+ + + + {#if users.length === 0 && !usersLoading} +

{$_('admin.searchToSeeUsers')}

+ {:else} +
    + {#each users as user} +
  • + +
  • + {/each} +
+ {#if usersCursor} + + {/if} + {/if} +
+ +
+

{$_('admin.inviteCodes')}

+
+ +
+ {#if showInvites} + {#if invites.length === 0} +

{$_('admin.noInvites')}

+ {:else} +
    + {#each invites as invite} +
  • +
    + {invite.code} + + {$_('admin.available')}: {invite.available} - {$_('admin.uses')}: {invite.uses.length} - {$_('admin.created')}: {formatDate(invite.createdAt)} + +
    +
    + {#if invite.disabled} + {$_('admin.disabled')} + {:else if invite.available === 0} + {$_('admin.exhausted')} + {:else} + {$_('admin.active')} + {/if} +
    +
    + {#if !invite.disabled} + + {/if} +
    +
  • + {/each} +
+ {#if invitesCursor} + + {/if} + {/if} + {/if} +
+
+ +{#if selectedUser} + +{/if} + + diff --git a/frontend/src/components/dashboard/AppPasswordsContent.svelte b/frontend/src/components/dashboard/AppPasswordsContent.svelte new file mode 100644 index 0000000..76ca20b --- /dev/null +++ b/frontend/src/components/dashboard/AppPasswordsContent.svelte @@ -0,0 +1,427 @@ + + +
+ {#if newPassword} +
+ {#if newPasswordName} +
{$_('common.name')}: {newPasswordName}
+ {/if} +

{$_('appPasswords.saveWarning')}

+
+ {newPassword} + +
+ + +
+ {/if} + +
+
+ + +
+
+ {$_('appPasswords.permissions')}: +
+ {#each SCOPE_PRESETS as preset} + + {/each} +
+
+ +
+ + {#if loading} +
{$_('common.loading')}
+ {:else if appPasswords.length === 0} +

{$_('appPasswords.noPasswords')}

+ {:else} +
    + {#each appPasswords as pw} +
  • +
    + {pw.name} + + {getScopeLabel(pw.scopes)} + {#if pw.createdByController} + {$_('appPasswords.byController')} + {/if} + {$_('common.created')} {formatDate(pw.createdAt)} + +
    + +
  • + {/each} +
+ {/if} +
+ + diff --git a/frontend/src/components/dashboard/CommsContent.svelte b/frontend/src/components/dashboard/CommsContent.svelte new file mode 100644 index 0000000..cfc723c --- /dev/null +++ b/frontend/src/components/dashboard/CommsContent.svelte @@ -0,0 +1,609 @@ + + +
+ {#if loading} +
{$_('common.loading')}
+ {:else} +
+
+

{$_('comms.preferredChannel')}

+
+ {#each channels as channelId} + + {/each} +
+
+ +
+

{$_('comms.channelConfiguration')}

+
+
+
+ + {$_('comms.primary')} +
+ +
+ + {#if isChannelAvailableOnServer('discord')} +
+
+ + {#if discordId} + + {discordVerified ? $_('comms.verified') : $_('comms.notVerified')} + + {/if} +
+
+ checkChannelInUse('discord', discordId)} + placeholder={$_('register.discordIdPlaceholder')} + disabled={saving} + /> + {#if discordId && !discordVerified} + + {/if} +
+ {#if discordInUse} +

{$_('comms.discordInUseWarning')}

+ {/if} + {#if verifyingChannel === 'discord'} +
+ + + +
+ {/if} +
+ {/if} + + {#if isChannelAvailableOnServer('telegram')} +
+
+ + {#if telegramUsername} + + {telegramVerified ? $_('comms.verified') : $_('comms.notVerified')} + + {/if} +
+
+ checkChannelInUse('telegram', telegramUsername)} + placeholder={$_('register.telegramUsernamePlaceholder')} + disabled={saving} + /> + {#if telegramUsername && !telegramVerified} + + {/if} +
+ {#if telegramInUse} +

{$_('comms.telegramInUseWarning')}

+ {/if} + {#if verifyingChannel === 'telegram'} +
+ + + +
+ {/if} +
+ {/if} + + {#if isChannelAvailableOnServer('signal')} +
+
+ + {#if signalNumber} + + {signalVerified ? $_('comms.verified') : $_('comms.notVerified')} + + {/if} +
+
+ checkChannelInUse('signal', signalNumber)} + placeholder={$_('register.signalNumberPlaceholder')} + disabled={saving} + /> + {#if signalNumber && !signalVerified} + + {/if} +
+ {#if signalInUse} +

{$_('comms.signalInUseWarning')}

+ {/if} + {#if verifyingChannel === 'signal'} +
+ + + +
+ {/if} +
+ {/if} +
+
+ +
+ +
+
+ +
+

{$_('comms.messageHistory')}

+ {#if historyLoading} +
{$_('common.loading')}
+ {:else if messages.length === 0} +

{$_('comms.noMessages')}

+ {:else} +
+ {#each messages as msg} +
+
+ {msg.notificationType} + {msg.channel} + {msg.status} +
+ {#if msg.subject} +
{msg.subject}
+ {/if} +
{msg.body}
+
{formatDate(msg.createdAt)}
+
+ {/each} +
+ {/if} +
+ {/if} +
+ + diff --git a/frontend/src/components/dashboard/ControllersContent.svelte b/frontend/src/components/dashboard/ControllersContent.svelte new file mode 100644 index 0000000..c65b6e6 --- /dev/null +++ b/frontend/src/components/dashboard/ControllersContent.svelte @@ -0,0 +1,655 @@ + + +
+ {#if loading} +
{$_('common.loading')}
+ {:else} +
+
+

{$_('delegation.controllers')}

+

{$_('delegation.controllersDesc')}

+
+ + {#if controllers.length === 0} +

{$_('delegation.noControllers')}

+ {:else} +
+ {#each controllers as controller} +
+
+
+ @{controller.handle || controller.did} + {getScopeLabel(controller.grantedScopes)} + {#if !controller.isActive} + {$_('delegation.inactive')} + {/if} +
+
+
+ {$_('delegation.did')} + {controller.did} +
+
+ {$_('delegation.granted')} + {formatDateTime(controller.grantedAt)} +
+
+
+
+ +
+
+ {/each} +
+ {/if} + + {#if !canAddControllers} +
+

{$_('delegation.cannotAddControllers')}

+
+ {:else if showAddController} +
+

{$_('delegation.addController')}

+ +
+
{$_('delegation.addControllerWarningTitle')}
+

{$_('delegation.addControllerWarningText')}

+
    +
  • {$_('delegation.addControllerWarningBullet1')}
  • +
  • {$_('delegation.addControllerWarningBullet2')}
  • +
  • {$_('delegation.addControllerWarningBullet3')}
  • +
+
+ +
+ + +
+
+ + +
+ +
+ + +
+
+ {:else} + + {/if} +
+ +
+
+

{$_('delegation.controlledAccounts')}

+

{$_('delegation.controlledAccountsDesc')}

+
+ + {#if controlledAccounts.length === 0} +

{$_('delegation.noControlledAccounts')}

+ {:else} +
+ {#each controlledAccounts as account} +
+
+
+ @{account.handle} + {getScopeLabel(account.grantedScopes)} +
+
+
+ {$_('delegation.did')} + {account.did} +
+
+ {$_('delegation.granted')} + {formatDateTime(account.grantedAt)} +
+
+
+ +
+ {/each} +
+ {/if} + + {#if !canControlAccounts} +
+

{$_('delegation.cannotControlAccounts')}

+
+ {:else if showCreateDelegated} +
+

{$_('delegation.createDelegatedAccount')}

+
+ + +
+
+ + +
+
+ + +
+
+ + +
+
+ {:else} + + {/if} +
+ +
+
+

{$_('delegation.auditLog')}

+

{$_('delegation.auditLogDesc')}

+
+ {$_('delegation.viewAuditLog')} +
+ {/if} +
+ + diff --git a/frontend/src/components/dashboard/DelegationAuditContent.svelte b/frontend/src/components/dashboard/DelegationAuditContent.svelte new file mode 100644 index 0000000..beb1f3b --- /dev/null +++ b/frontend/src/components/dashboard/DelegationAuditContent.svelte @@ -0,0 +1,282 @@ + + +
+
+ +
+ + {#if loading} +
{$_('common.loading')}
+ {:else if entries.length === 0} +

{$_('delegation.noAuditEntries')}

+ {:else} +
+ {#each entries as entry} +
+
+ {formatActionType(entry.actionType)} + +
+
+
+ {$_('delegation.actor')} + {truncateDid(entry.actorDid)} +
+ {#if entry.delegatedDid} +
+ {$_('delegation.target')} + {truncateDid(entry.delegatedDid)} +
+ {/if} + {#if entry.actionDetails} +
+ {$_('delegation.details')} + {formatActionDetails(entry.actionDetails)} +
+ {/if} +
+
+ {/each} +
+ + + {/if} +
+ + diff --git a/frontend/src/components/dashboard/DidDocumentContent.svelte b/frontend/src/components/dashboard/DidDocumentContent.svelte new file mode 100644 index 0000000..3470e1b --- /dev/null +++ b/frontend/src/components/dashboard/DidDocumentContent.svelte @@ -0,0 +1,415 @@ + + +
+ {#if loading} +
{$_('common.loading')}
+ {:else} +
+

{$_('didEditor.helpTitle')}

+

{$_('didEditor.helpText')}

+
+ +
+

{$_('didEditor.verificationMethods')}

+

{$_('didEditor.verificationMethodsDesc')}

+ + {#if verificationMethods.length === 0} +

{$_('didEditor.noKeys')}

+ {:else} +
    + {#each verificationMethods as vm} +
  • +
    +
    + {vm.id} + {vm.type} +
    + {vm.publicKeyMultibase} +
    + +
  • + {/each} +
+ {/if} + +
+
+ + +
+
+ + +
+ +
+
+ +
+

{$_('didEditor.alsoKnownAs')}

+

{$_('didEditor.alsoKnownAsDesc')}

+ + {#if alsoKnownAs.length === 0} +

{$_('didEditor.noHandles')}

+ {:else} +
    + {#each alsoKnownAs as handle} +
  • + {handle} + +
  • + {/each} +
+ {/if} + +
+
+ + +
+ +
+
+ +
+

{$_('didEditor.serviceEndpoint')}

+

{$_('didEditor.serviceEndpointDesc')}

+
+ + +
+
+ +
+

{$_('didEditor.preview')}

+
{previewJson}
+
+ +
+ +
+ {/if} +
+ + diff --git a/frontend/src/routes/InviteCodes.svelte b/frontend/src/components/dashboard/InviteCodesContent.svelte similarity index 59% rename from frontend/src/routes/InviteCodes.svelte rename to frontend/src/components/dashboard/InviteCodesContent.svelte index 7109639..b3e987e 100644 --- a/frontend/src/routes/InviteCodes.svelte +++ b/frontend/src/components/dashboard/InviteCodesContent.svelte @@ -1,90 +1,65 @@ -
-
- {$_('common.backToDashboard')} -

{$_('inviteCodes.title')}

-
-

- {$_('inviteCodes.description')} -

+ +
{#if createdCode}

{$_('inviteCodes.created')}

{createdCode} -
- +
{/if} - {#if session?.isAdmin} -
+ + {#if session.isAdmin} +
-
+
{/if} +

{$_('inviteCodes.yourCodes')}

{#if loading} -
    - {#each Array(2) as _} -
  • - {/each} -
+
{$_('common.loading')}
{:else if codes.length === 0}

{$_('inviteCodes.noCodes')}

{:else}
    {#each codes as code} -
  • 0 && code.available === 0}> +
  • 0 && code.available === 0}>
    - {code.code} + {code.code} @@ -164,42 +130,18 @@ {/if}
+ diff --git a/frontend/src/components/dashboard/MigrationContent.svelte b/frontend/src/components/dashboard/MigrationContent.svelte new file mode 100644 index 0000000..eee2287 --- /dev/null +++ b/frontend/src/components/dashboard/MigrationContent.svelte @@ -0,0 +1,113 @@ + + +
+
+

{$_('migration.migrateHere')}

+

{$_('migration.migrateHereDesc')}

+
    +
  • {$_('migration.bringDid')}
  • +
  • {$_('migration.transferData')}
  • +
  • {$_('migration.keepFollowers')}
  • +
+ +
+ +
+

{$_('migration.offlineRestore')}

+

{$_('migration.offlineRestoreDesc')}

+
    +
  • {$_('migration.offlineFeature1')}
  • +
  • {$_('migration.offlineFeature2')}
  • +
  • {$_('migration.offlineFeature3')}
  • +
+ +
+ + {#if session.accountKind === 'migrated'} +
+

{$_('dashboard.migratedTitle')}

+

{$_('dashboard.migratedMessage', { values: { pds: session.migratedToPds || 'another PDS' } })}

+
+ {/if} +
+ + diff --git a/frontend/src/routes/RepoExplorer.svelte b/frontend/src/components/dashboard/RepoContent.svelte similarity index 68% rename from frontend/src/routes/RepoExplorer.svelte rename to frontend/src/components/dashboard/RepoContent.svelte index e8af920..0e95647 100644 --- a/frontend/src/routes/RepoExplorer.svelte +++ b/frontend/src/components/dashboard/RepoContent.svelte @@ -1,23 +1,18 @@ -
-
-
+ + {#if error}
{#if error.code} @@ -324,15 +312,13 @@ {error.message}
{/if} + {#if success}
{success}
{/if} + {#if loading} -
- {#each Array(4) as _} -
- {/each} -
+
{$_('common.loading')}
{:else if view === 'collections'}
- +
+ {#if collections.length === 0}

{$_('repoExplorer.noCollectionsYet')}

{:else}
{#each [...groupedCollections.entries()] as [authority, nsids]}
-

{authority}

+

{authority}

    {#each nsids as nsid}
  • -
  • {/each} @@ -364,6 +351,7 @@ {/each}
{/if} + {:else if view === 'records'}
- +
+ {#if records.length === 0}

{$_('repoExplorer.noRecords')}

{:else}
    {#each filteredRecords as record}
  • -
  • {/each}
- {#if loadingMore} -
- {#each [1, 2, 3] as _} -
-
-
-
-
-
-
- {/each} -
- {/if} + {/if} + {:else if view === 'record' && selectedRecord}
@@ -429,15 +407,16 @@ {/if}
- -
+ {:else if view === 'create'}
@@ -476,25 +455,20 @@ {/if}
- -
{/if}
- diff --git a/frontend/src/components/dashboard/SecurityContent.svelte b/frontend/src/components/dashboard/SecurityContent.svelte new file mode 100644 index 0000000..d862964 --- /dev/null +++ b/frontend/src/components/dashboard/SecurityContent.svelte @@ -0,0 +1,1722 @@ + + +
+ {#if loading} +
{$_('common.loading')}
+ {:else} +
+

{$_('security.passkeys')}

+ + {#if !passkeysLoading} + {#if passkeys.length > 0} +
    + {#each passkeys as passkey} +
  • + {#if editingPasskeyId === passkey.id} +
    + + + +
    + {:else} +
    + {passkey.friendlyName || $_('security.unnamedPasskey')} + + {$_('security.added')} {formatDate(passkey.createdAt)} + {#if passkey.lastUsed} + - {$_('security.lastUsed')} {formatDate(passkey.lastUsed)} + {/if} + +
    +
    + + {#if hasPassword || passkeys.length > 1} + + {/if} +
    + {/if} +
  • + {/each} +
+ {:else} +
{$_('security.noPasskeys')}
+ {/if} + +
+ + +
+ + {/if} +
+ +
+

{$_('security.totp')}

+ + {#if totpSetup.step === 'idle'} + {#if totpEnabled} +
{$_('security.totpEnabled')}
+ + {#if !showDisableForm && !showRegenForm} +
+ + +
+ {/if} + + {#if showRegenForm} +
+

{$_('security.regenerateBackupCodes')}

+

{$_('security.regenerateConfirm')}

+
+ + +
+
+ + +
+
+ + +
+
+ {/if} + + {#if showDisableForm} +
+

{$_('security.disableTotp')}

+

{$_('security.disableTotpWarning')}

+
+ + +
+
+ + +
+
+ + +
+
+ {/if} + {:else} +
{$_('security.totpDisabled')}
+ + {/if} + {:else if totpSetup.step === 'qr'} + {@const qrData = totpSetup as TotpQr} +
+

{$_('security.totpSetupInstructions')}

+
+ TOTP QR Code +
+
+ {$_('security.cantScan')} + {qrData.totpUri.split('secret=')[1]?.split('&')[0] || ''} +
+ +
+ {:else if totpSetup.step === 'verify'} + {@const verifyData = totpSetup} +
+

{$_('security.totpCodePlaceholder')}

+
+ +
+ + +
+
+
+ {:else if totpSetup.step === 'backup'} +
+

{$_('security.backupCodes')}

+

{$_('security.backupCodesDescription')}

+
+ {#each totpSetup.backupCodes as code} + {code} + {/each} +
+
+ + +
+
+ {/if} +
+ +
+

{$_('security.password')}

+ {#if !passwordLoading} + {#if hasPassword} +
{$_('security.passwordStatus')}
+ + {#if !showChangePasswordForm && !showRemovePasswordForm} +
+ + {#if passkeys.length > 0} + + {/if} +
+ {/if} + + {#if showChangePasswordForm} +
+

{$_('security.changePassword')}

+
+ + +
+
+ + +
+
+ + +
+
+ + +
+
+ {/if} + + {#if showRemovePasswordForm} +
+

{$_('security.removePasswordWarning')}

+
+ + +
+
+ {/if} + {:else} +
{$_('security.noPassword')}
+ + {#if !showSetPasswordForm} + + {:else} +
+

{$_('security.setPassword')}

+
+ + +
+
+ + +
+
+ + +
+
+ {/if} + {/if} + {/if} +
+ + {#if ssoProviders.length > 0} +
+

{$_('oauth.sso.linkedAccounts')}

+ + {#if linkedAccountsLoading} +
{$_('common.loading')}
+ {:else} + {#if linkedAccounts.length > 0} +
    + {#each linkedAccounts as account} +
  • +
    + {account.provider_name} + {account.provider_username} + {$_('oauth.sso.linkedAt')} {formatDate(account.created_at)} +
    + +
  • + {/each} +
+ {:else} +

{$_('oauth.sso.noLinkedAccounts')}

+ {/if} + +
+

{$_('oauth.sso.linkNewAccount')}

+
+ {#each ssoProviders as provider} + {@const isLinked = linkedAccounts.some(a => a.provider === provider.provider)} + + {/each} +
+
+ {/if} +
+ {/if} + + {#if hasMfa} +
+

{$_('security.appCompatibility')}

+

{$_('security.legacyLoginDescription')}

+ + {#if !legacyLoginLoading} +
+
+ {$_('security.legacyLogin')} + + {#if allowLegacyLogin} + {$_('security.legacyLoginOn')} + {:else} + {$_('security.legacyLoginOff')} + {/if} + +
+ +
+ + {#if totpEnabled && allowLegacyLogin} +
+ {$_('security.legacyLoginWarning')} +
+ {/if} + +
+ {$_('security.legacyAppsTitle')} +

{$_('security.legacyAppsDescription')}

+
+ {/if} +
+ {/if} + +
+

{$_('security.trustedDevices')}

+

{$_('security.trustedDevicesDescription')}

+ + {#if trustedDevicesLoading} +
{$_('common.loading')}
+ {:else if trustedDevices.length === 0} +

{$_('trustedDevices.noDevices')}

+

{$_('trustedDevices.noDevicesHint')}

+ {:else} +
+ {#each trustedDevices as device} +
+
+ {#if editingDeviceId === device.id} + +
+ + +
+ {:else} + {device.friendlyName || parseUserAgent(device.userAgent)} + + {/if} +
+ +
+ {#if device.userAgent} + {parseUserAgent(device.userAgent)} + {/if} + {#if device.trustedAt} + {$_('trustedDevices.trustedSince')} {formatDate(device.trustedAt)} + {/if} + {$_('trustedDevices.lastSeen')} {formatDate(device.lastSeenAt)} + {#if device.trustedUntil} + {@const daysRemaining = getDaysRemaining(device.trustedUntil)} + + {#if daysRemaining <= 0} + {$_('trustedDevices.expired')} + {:else if daysRemaining === 1} + {$_('trustedDevices.tomorrow')} + {:else} + {$_('trustedDevices.inDays', { values: { days: daysRemaining } })} + {/if} + + {/if} +
+ + +
+ {/each} +
+ {/if} +
+ {/if} +
+ + + + diff --git a/frontend/src/routes/Sessions.svelte b/frontend/src/components/dashboard/SessionsContent.svelte similarity index 59% rename from frontend/src/routes/Sessions.svelte rename to frontend/src/components/dashboard/SessionsContent.svelte index 74c74dc..6ae5f12 100644 --- a/frontend/src/routes/Sessions.svelte +++ b/frontend/src/components/dashboard/SessionsContent.svelte @@ -1,45 +1,35 @@ -
-
- {$_('common.backToDashboard')} -

{$_('sessions.title')}

-
+ +
{#if loading} -
- {#each Array(3) as _} -
- {/each} -
+
{$_('common.loading')}
+ {:else if sessions.length === 0} +

{$_('sessions.noSessions')}

{:else} - {#if sessions.length === 0} -

{$_('sessions.noSessions')}

- {:else} -
- {#each sessions as session} -
-
-
- {#if session.isCurrent} - {$_('sessions.current')} - {/if} - - {session.sessionType === 'oauth' ? $_('sessions.oauth') : $_('sessions.session')} - - {#if session.clientName} - {session.clientName} - {/if} -
-
-
- {$_('sessions.created')} - {timeAgo(session.createdAt)} -
-
- {$_('sessions.expires')} - {formatDate(session.expiresAt)} -
-
+
+ {#each sessions as s} +
+
+
+ {#if s.isCurrent} + {$_('sessions.current')} + {/if} + + {s.sessionType === 'oauth' ? $_('sessions.oauth') : $_('sessions.session')} + + {#if s.clientName} + {s.clientName} + {/if}
-
- +
+
+ {$_('sessions.created')} + {timeAgo(s.createdAt)} +
+
+ {$_('sessions.expires')} + {formatDate(s.expiresAt)} +
- {/each} -
-
- - {#if sessions.filter(s => !s.isCurrent).length > 0} - - {/if} -
- {/if} + +
+ {/each} +
+
+ + {#if sessions.filter(s => !s.isCurrent).length > 0} + + {/if} +
{/if}
+ diff --git a/frontend/src/routes/Settings.svelte b/frontend/src/components/dashboard/SettingsContent.svelte similarity index 54% rename from frontend/src/routes/Settings.svelte rename to frontend/src/components/dashboard/SettingsContent.svelte index 4073478..8e6ec37 100644 --- a/frontend/src/routes/Settings.svelte +++ b/frontend/src/components/dashboard/SettingsContent.svelte @@ -1,57 +1,46 @@ -
-
- {$_('common.backToDashboard')} -

{$_('settings.title')}

-
-
+ +
-

{$_('settings.language')}

-

{$_('settings.languageDescription')}

+

{$_('settings.language')}

+
- - - {:else} -
-

{$_('settings.setPassword')}

-

{$_('settings.setPasswordDescription')}

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

{$_('settings.exportData')}

-

{$_('settings.exportDataDescription')}

+

{$_('settings.exportData')}

-
-
-

{$_('settings.backups.title')}

-

{$_('settings.backups.description')}

- - - {#if !backupsLoading && backups.length > 0} -
    - {#each backups as backup} -
  • -
    - {formatDate(backup.createdAt)} - {formatBytes(backup.sizeBytes)} - {backup.blockCount} {$_('settings.backups.blocks')} -
    -
    - - -
    -
  • - {/each} -
+
+

{$_('settings.backups.title')}

+ {#if backupsLoading} +
{$_('common.loading')}
{:else} -

{$_('settings.backups.noBackups')}

- {/if} - - -
-
-

{$_('settings.backups.restoreTitle')}

-

{$_('settings.backups.restoreDescription')}

- -
- - -
- - {#if restoreFile} -
-

{$_('settings.backups.selectedFile')}: {restoreFile.name} ({formatBytes(restoreFile.size)})

- + +
+ + {/each} + + {:else} +

{$_('settings.backups.noBackups')}

+ {/if} +
+ +
+
+
+ +
+

{$_('settings.backups.restoreTitle')}

+

{$_('settings.backups.restoreHint')}

+
+ + +
+ {#if restoreFile} +
+ {$_('settings.backups.selectedFile')}: {restoreFile.name} + ({formatBytes(restoreFile.size)}) +
+ {/if} +
{/if}
-
+
-

{$_('settings.deleteAccount')}

-

{$_('settings.deleteWarning')}

+

{$_('settings.deleteAccount')}

+

{$_('settings.deleteWarning')}

{#if deleteTokenSent}
@@ -871,109 +615,48 @@
-{#if showReauthModal && session} - -{/if} diff --git a/frontend/src/components/migration/ChooseHandleStep.svelte b/frontend/src/components/migration/ChooseHandleStep.svelte index 48298fb..8803656 100644 --- a/frontend/src/components/migration/ChooseHandleStep.svelte +++ b/frontend/src/components/migration/ChooseHandleStep.svelte @@ -50,8 +50,10 @@ onContinue, }: Props = $props() + const handleTooShort = $derived(handleInput.trim().length > 0 && handleInput.trim().length < 3) + const canContinue = $derived( - handleInput.trim() && + handleInput.trim().length >= 3 && email && (authMethod === 'passkey' || password) && handleAvailable !== false @@ -87,7 +89,9 @@ {/if}
- {#if checkingHandle} + {#if handleTooShort} +

{$_('migration.inbound.chooseHandle.handleTooShort')}

+ {:else if checkingHandle}

{$_('migration.inbound.chooseHandle.checkingAvailability')}

{:else if handleAvailable === true}

{$_('migration.inbound.chooseHandle.handleAvailable')}

diff --git a/frontend/src/lib/api.ts b/frontend/src/lib/api.ts index 86782fd..1ebc2e0 100644 --- a/frontend/src/lib/api.ts +++ b/frontend/src/lib/api.ts @@ -325,8 +325,10 @@ function _castDelegationController(raw: unknown): DelegationController { const c = raw as Record; return { did: unsafeAsDid(c.did as string), - granted_scopes: unsafeAsScopeSet(c.granted_scopes as string), - added_at: unsafeAsISODate(c.added_at as string), + handle: unsafeAsHandle(c.handle as string), + grantedScopes: unsafeAsScopeSet((c.granted_scopes ?? c.grantedScopes) as string), + grantedAt: unsafeAsISODate((c.granted_at ?? c.grantedAt ?? c.added_at) as string), + isActive: (c.is_active ?? c.isActive ?? true) as boolean, }; } @@ -337,19 +339,28 @@ function _castDelegationControlledAccount( return { did: unsafeAsDid(a.did as string), handle: unsafeAsHandle(a.handle as string), - granted_scopes: unsafeAsScopeSet(a.granted_scopes as string), + grantedScopes: unsafeAsScopeSet((a.granted_scopes ?? a.grantedScopes) as string), + grantedAt: unsafeAsISODate((a.granted_at ?? a.grantedAt ?? a.added_at) as string), }; } function _castDelegationAuditEntry(raw: unknown): DelegationAuditEntry { const e = raw as Record; + const actorDid = (e.actor_did ?? e.actorDid) as string; + const targetDid = (e.target_did ?? e.targetDid ?? e.delegatedDid) as string | undefined; + const createdAt = (e.created_at ?? e.createdAt) as string; + const action = (e.action ?? e.actionType) as string; + const details = e.details ?? e.actionDetails; + const detailsStr = details + ? (typeof details === "string" ? details : JSON.stringify(details)) + : undefined; return { id: e.id as string, - action: e.action as string, - actor_did: unsafeAsDid(e.actor_did as string), - target_did: e.target_did ? unsafeAsDid(e.target_did as string) : undefined, - details: e.details as string | undefined, - created_at: unsafeAsISODate(e.created_at as string), + action, + actor_did: unsafeAsDid(actorDid), + target_did: targetDid ? unsafeAsDid(targetDid) : undefined, + details: detailsStr, + created_at: unsafeAsISODate(createdAt), }; } @@ -1348,16 +1359,85 @@ export const api = { return res.json(); }, - listDelegationControllers( + async initiateSsoLink( token: AccessToken, - ): Promise> { - return xrpcResult("_delegation.listControllers", { token }); + provider: string, + requestUri: string, + ): Promise<{ redirect_url: string }> { + const res = await authenticatedFetch("/oauth/sso/initiate", { + method: "POST", + token, + headers: { "Content-Type": "application/json" }, + body: JSON.stringify({ + provider, + request_uri: requestUri, + action: "link", + }), + }); + if (!res.ok) { + const errData = await res.json().catch(() => ({ + error: "Unknown", + message: res.statusText, + })); + throw new ApiError( + res.status, + errData.error, + errData.error_description ?? errData.message, + errData.reauthMethods, + ); + } + return res.json(); }, - listDelegationControlledAccounts( + async unlinkSsoAccount( + token: AccessToken, + id: string, + ): Promise<{ success: boolean }> { + const res = await authenticatedFetch("/oauth/sso/unlink", { + method: "POST", + token, + headers: { "Content-Type": "application/json" }, + body: JSON.stringify({ id }), + }); + if (!res.ok) { + const errData = await res.json().catch(() => ({ + error: "Unknown", + message: res.statusText, + })); + throw new ApiError( + res.status, + errData.error, + errData.error_description ?? errData.message, + errData.reauthMethods, + ); + } + return res.json(); + }, + + async listDelegationControllers( + token: AccessToken, + ): Promise> { + const result = await xrpcResult<{ controllers: unknown[] }>( + "_delegation.listControllers", + { token }, + ); + if (!result.ok) return result; + return ok({ + controllers: (result.value.controllers ?? []).map(_castDelegationController), + }); + }, + + async listDelegationControlledAccounts( token: AccessToken, ): Promise> { - return xrpcResult("_delegation.listControlledAccounts", { token }); + const result = await xrpcResult<{ accounts: unknown[] }>( + "_delegation.listControlledAccounts", + { token }, + ); + if (!result.ok) return result; + return ok({ + accounts: (result.value.accounts ?? []).map(_castDelegationControlledAccount), + }); }, getDelegationScopePresets(): Promise< @@ -1402,16 +1482,24 @@ export const api = { }); }, - getDelegationAuditLog( + async getDelegationAuditLog( token: AccessToken, limit: number, offset: number, ): Promise< Result<{ entries: DelegationAuditEntry[]; total: number }, ApiError> > { - return xrpcResult("_delegation.getAuditLog", { - token, - params: { limit: String(limit), offset: String(offset) }, + const result = await xrpcResult<{ entries: unknown[]; total: number }>( + "_delegation.getAuditLog", + { + token, + params: { limit: String(limit), offset: String(offset) }, + }, + ); + if (!result.ok) return result; + return ok({ + entries: (result.value.entries ?? []).map(_castDelegationAuditEntry), + total: result.value.total ?? 0, }); }, diff --git a/frontend/src/lib/types/api.ts b/frontend/src/lib/types/api.ts index ebae420..8601722 100644 --- a/frontend/src/lib/types/api.ts +++ b/frontend/src/lib/types/api.ts @@ -562,6 +562,7 @@ export interface SsoLinkedAccount { export interface DelegationController { did: Did; + handle: Handle; grantedScopes: ScopeSet; grantedAt: ISODateString; isActive: boolean; diff --git a/frontend/src/lib/types/routes.ts b/frontend/src/lib/types/routes.ts index d72860a..4118de2 100644 --- a/frontend/src/lib/types/routes.ts +++ b/frontend/src/lib/types/routes.ts @@ -5,7 +5,6 @@ export const routes = { security: "/security", sessions: "/sessions", appPasswords: "/app-passwords", - trustedDevices: "/trusted-devices", inviteCodes: "/invite-codes", comms: "/comms", repo: "/repo", diff --git a/frontend/src/locales/en.json b/frontend/src/locales/en.json index c1ab992..4f45f49 100644 --- a/frontend/src/locales/en.json +++ b/frontend/src/locales/en.json @@ -8,55 +8,23 @@ "done": "Done", "continue": "Continue", "refresh": "Refresh", - "create": "Create", "delete": "Delete", - "confirm": "Confirm", "created": "Created", - "expires": "Expires", "name": "Name", - "dashboard": "Dashboard", "backToDashboard": "← Dashboard", "copied": "Copied", "copyToClipboard": "Copy", "verifying": "Verifying", "saving": "Saving", "creating": "Creating", - "updating": "Updating", "sending": "Sending", - "authenticating": "Authenticating", "checking": "Checking", - "redirecting": "Redirecting", "signIn": "Sign in", "verify": "Verify", - "remove": "Remove", "revoke": "Revoke", "resendCode": "Resend code", - "startOver": "Start over", - "tryAgain": "Retry", - "password": "Password", - "email": "Email", - "emailAddress": "Email Address", - "handle": "Handle", - "did": "DID", - "verificationCode": "Verification Code", - "inviteCode": "Invite Code", - "newPassword": "New Password", - "confirmPassword": "Confirm Password", - "enterSixDigitCode": "Enter 6-digit code", - "passwordHint": "At least 8 characters", - "enterPassword": "Enter your password", - "emailPlaceholder": "you@example.com", - "verified": "Verified", - "disabled": "Disabled", - "available": "Available", - "deactivated": "Deactivated", - "unverified": "Unverified", "backToLogin": "Back to Login", - "backToSettings": "Back to Settings", - "alreadyHaveAccount": "Already have an account?", - "createAccount": "Create account", - "passwordsMismatch": "Passwords do not match", - "passwordTooShort": "Password must be at least 8 characters" + "backToSettings": "Back to Settings" }, "login": { "title": "Sign In", @@ -75,18 +43,10 @@ "subtitle": "Enter the code sent to your contact method", "codeLabel": "Code", "codePlaceholder": "6-digit code", - "verifyButton": "Verify", "resent": "Code resent" }, "register": { "title": "Create Account", - "subtitle": "Create a new account on this PDS", - "subtitleKeyChoice": "Set up your did:web identity", - "subtitleInitialDidDoc": "Upload your DID document", - "subtitleVerify": "Verify your {channel}", - "subtitleUpdatedDidDoc": "Update your DID document", - "subtitleActivating": "Activating", - "subtitleComplete": "Account created", "redirecting": "Redirecting", "migrateTitle": "Already have an account?", "migrateDescription": "Migrate instead of creating a new account", @@ -115,8 +75,6 @@ "didWebWarning2Detail": "No rotation keys like did:plc", "didWebWarning3": "Our commitment:", "didWebWarning3Detail": "We will continue hosting your DID document if you migrate", - "didWebWarning4": "", - "didWebWarning4Detail": "", "externalDid": "Your did:web", "externalDidPlaceholder": "did:web:yourdomain.com", "externalDidHint": "Serve DID document at", @@ -125,7 +83,6 @@ "email": "Email", "emailAddress": "Email Address", "emailPlaceholder": "you@example.com", - "emailInUseWarning": "Email in use by another account", "discord": "Discord", "discordId": "Discord User ID", "discordIdPlaceholder": "123456789012345678", @@ -144,13 +101,11 @@ "inviteCode": "Invite Code", "inviteCodePlaceholder": "Enter your invite code", "inviteCodeRequired": "required", - "createButton": "Create Account", "alreadyHaveAccount": "Already have an account?", "signIn": "Sign in", "passkeyAccount": "Passkey", "passwordAccount": "Password", "ssoAccount": "SSO", - "ssoSubtitle": "Create account with external provider", "noSsoProviders": "No SSO providers configured", "continueWith": "Continue with {provider}", "validation": { @@ -170,12 +125,12 @@ }, "dashboard": { "title": "Dashboard", + "accountManager": "Account manager", "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", @@ -184,38 +139,21 @@ "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", + "navSettings": "General", "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", "navDelegation": "Delegation", - "navDelegationDesc": "Manage account controllers and delegated accounts", + "navDelegationAudit": "Delegation Audit", "navAdmin": "Admin Panel", - "navAdminDesc": "Server stats and admin operations", "navDidDocument": "DID Document", - "navDidDocumentDesc": "Manage your DID document for external migrations", - "navDidDocumentDescActive": "Edit your DID document settings", - "navBackup": "Download Backup", - "navBackupDesc": "Download your repository as a CAR file", - "downloadingBackup": "Downloading...", - "backupFailed": "Failed to download backup", "migrated": "Migrated", "migratedTitle": "Account Migrated", - "migratedMessage": "Your account has migrated to {pds}. Your DID document is still hosted here, and you can update it for future migrations.", - "navMigrateAgain": "Migrate Again", - "navMigrateAgainDesc": "Move to another PDS and update your DID document" + "migratedMessage": "Your account has migrated to {pds}. Your DID document is still hosted here, and you can update it for future migrations." }, "didEditor": { - "title": "DID Document Editor", "preview": "Current DID Document", "verificationMethods": "Verification Methods", "verificationMethodsDesc": "Signing keys that can act on behalf of your DID. When you migrate to a new PDS, add their signing key here.", @@ -241,14 +179,11 @@ "saveFailed": "Failed to save DID document", "loadFailed": "Failed to load DID document", "invalidMultibase": "Public key must be a valid multibase string starting with 'z'", - "invalidHandle": "Handle must be an at:// URI (eg., at://handle.example.com)", "helpTitle": "What is this?", "helpText": "When you migrate to another PDS, that PDS generates new signing keys. Update your DID document here so it points to your new keys and location. This enables multi-hop migrations (PDS 1 → PDS 2 → PDS 3)." }, "settings": { - "title": "Account Settings", "language": "Language", - "languageDescription": "Choose your preferred language", "changeEmail": "Change Email", "currentEmail": "Current: {email}", "newEmail": "New Email", @@ -266,7 +201,6 @@ "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)", @@ -280,33 +214,17 @@ "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...", - "setPassword": "Set Password", - "setPasswordDescription": "Your account is currently passkey-only. You can add a password to enable traditional login alongside your passkeys.", - "setPasswordButton": "Set Password", - "setting": "Setting...", "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", "downloadBlobs": "Download Media", "exporting": "Exporting...", "backups": { "title": "Backups", - "description": "Your repository is automatically backed up daily. You can also create manual backups or restore from a previous backup.", - "enableAutomatic": "Enable automatic backups", + "autoBackup": "Automatic backups", "enabled": "Automatic backups enabled", "disabled": "Automatic backups disabled", "toggleFailed": "Failed to update backup setting", "noBackups": "No backups available yet.", - "blocks": "blocks", "download": "Download", "delete": "Delete", "createNow": "Create Backup Now", @@ -316,8 +234,7 @@ "deleted": "Backup deleted", "deleteFailed": "Failed to delete backup", "restoreTitle": "Restore from Backup", - "restoreDescription": "Upload a CAR file to restore your repository. This will overwrite your current data.", - "selectFile": "Select CAR file", + "restoreHint": "Upload a CAR file to restore your repository", "selectedFile": "Selected file", "restore": "Restore", "restoring": "Restoring...", @@ -334,21 +251,11 @@ "permanentlyDelete": "Permanently Delete Account", "deleting": "Deleting...", "messages": { - "emailCodeSent": "Verification code sent to your notification channel", "emailCodeSentToCurrent": "Verification code sent to your current email address", "emailUpdated": "Email updated successfully", "emailUpdateFailed": "Failed to update email", "handleUpdated": "Handle updated successfully", "handleUpdateFailed": "Failed to update handle", - "passwordChanged": "Password changed successfully", - "passwordChangeFailed": "Failed to change password", - "passwordSet": "Password set successfully", - "passwordSetFailed": "Failed to set password", - "passwordsMismatch": "Passwords do not match", - "passwordsDoNotMatch": "Passwords do not match", - "passwordLength": "Password must be at least 8 characters", - "passwordTooShort": "Password must be at least 8 characters", - "deletionCodeSent": "Deletion confirmation sent to your email", "deletionConfirmationSent": "Deletion confirmation sent to your email", "deletionRequestFailed": "Failed to request account deletion", "deleteConfirmation": "Are you absolutely sure you want to delete your account? This cannot be undone.", @@ -356,22 +263,22 @@ "repoExported": "Repository exported successfully", "blobsExported": "Media files exported successfully", "noBlobsToExport": "No media files to export", - "exportFailed": "Failed to export", - "confirmDelete": "Are you absolutely sure you want to delete your account? This cannot be undone." + "exportFailed": "Failed to export" } }, "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 (eg., Graysky, Skeets)", + "create": "Create", + "name": "Name", + "namePlaceholder": "App name (eg., Graysky)", "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...", - "revokeConfirm": "Revoke app password \"{name}\"? Apps using this password will no longer be able to access your account.", + "deleteConfirm": "Revoke app password \"{name}\"?", + "deleted": "App password revoked", + "loadFailed": "Failed to load app passwords", + "createFailed": "Failed to create app password", + "deleteFailed": "Failed to revoke app password", + "saveWarning": "Save this password now - you will not see it again", "saveWarningTitle": "Important: Save this app password!", "saveWarningMessage": "This password is required to sign into apps that don't support passkeys or OAuth. You will only see it once.", "acknowledgeLabel": "I have saved my app password in a secure location", @@ -383,8 +290,6 @@ "byController": "By Controller" }, "sessions": { - "title": "Active Sessions", - "loadingSessions": "Loading sessions...", "noSessions": "No active sessions found.", "current": "Current", "oauth": "OAuth", @@ -396,6 +301,8 @@ "revokeConfirm": "Revoke this session?", "revokeAllConfirm": "This will revoke {count} other session(s). Continue?", "noOtherSessions": "No other sessions to revoke", + "sessionRevoked": "Session revoked", + "allSessionsRevoked": "All other sessions revoked", "failedToLoad": "Failed to load sessions", "failedToRevoke": "Failed to revoke session", "failedToRevokeAll": "Failed to revoke sessions", @@ -407,49 +314,38 @@ "justNow": "Just now" }, "inviteCodes": { - "title": "Invite Codes", - "description": "Invite codes let you invite friends to join. Each code can be used once.", + "loadFailed": "Failed to load invite codes", + "createFailed": "Failed to create invite code", "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}", "spent": "Spent", "disabled": "Disabled", - "usedBy": "Used by", - "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": "eg., 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", + "totpCode": "TOTP code", + "totpCodePlaceholder": "6 digits", "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.", @@ -463,15 +359,6 @@ "enableLegacyLogin": "Enable legacy login", "disableLegacyLogin": "Disable legacy login", "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", @@ -479,13 +366,7 @@ "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", @@ -493,20 +374,10 @@ "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.", - "addPasswordHint": "Want to add a password? Go to Settings to set one up.", - "goToSettings": "Go to Settings", "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", + "trustedDevicesDescription": "Devices that can skip two-factor authentication when signing in. Trust is granted for 30 days and automatically extends when you use the device.", "appCompatibility": "App Compatibility", "enterPassword": "Enter your password", "sessionExpired": "Session expired. Please log in again.", @@ -524,13 +395,30 @@ "passkeyCreationCancelled": "Passkey creation was cancelled", "passkeyAddedSuccess": "Passkey added successfully", "passkeyDeleted": "Passkey deleted", - "passkeyRenamed": "Passkey renamed" + "passkeyRenamed": "Passkey renamed", + "changePassword": "Change Password", + "currentPassword": "Current Password", + "currentPasswordPlaceholder": "Enter current password", + "newPassword": "New Password", + "newPasswordPlaceholder": "Enter new password", + "confirmPassword": "Confirm New Password", + "confirmPasswordPlaceholder": "Confirm new password", + "passwordsDoNotMatch": "Passwords do not match", + "passwordTooShort": "Password must be at least 8 characters", + "passwordChanged": "Password changed successfully", + "failedToChangePassword": "Failed to change password", + "changing": "Changing...", + "setting": "Setting...", + "passwordSet": "Password set successfully", + "failedToSetPassword": "Failed to set password", + "failedToDisableTotp": "Failed to disable TOTP" }, "comms": { - "title": "Communication Preferences", - "description": "Choose how you want to receive important messages like password resets, security alerts, and account updates.", + "failedToLoad": "Failed to load preferences", + "failedToSave": "Failed to save preferences", + "failedToVerify": "Verification failed", + "failedToLoadHistory": "Failed to load message history", "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", @@ -538,10 +426,6 @@ "signalVia": "Receive messages via Signal", "configureToEnable": "Configure below to enable", "notConfiguredOnServer": "Not configured on this server", - "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", @@ -552,29 +436,16 @@ "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", "discordInUseWarning": "This Discord ID is already associated with another account.", "telegramInUseWarning": "This Telegram username is already associated with another account.", "signalInUseWarning": "This Signal number is already associated with another account." }, "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", "uri": "URI", "cid": "CID", - "value": "Value", - "deleteRecord": "Delete Record", "deleteConfirm": "Delete record {rkey}? This cannot be undone.", "unknownError": "An unknown error occurred", "invalidJson": "Invalid JSON", @@ -587,7 +458,6 @@ "filterCollections": "Filter collections...", "filterRecords": "Filter records...", "noCollectionsYet": "No collections yet. Create your first record to get started.", - "loadMore": "Load More", "recordJson": "Record JSON", "updateRecord": "Update Record", "collectionNsid": "Collection (NSID)", @@ -599,8 +469,10 @@ "demoBio": "A short bio about yourself." }, "admin": { - "title": "Admin Panel", - "loading": "Loading...", + "failedToLoadStats": "Failed to load server stats", + "failedToLoadUsers": "Failed to load users", + "searchToSeeUsers": "Search to view users", + "search": "Search", "serverConfig": "Server Configuration", "serverName": "Server Name", "serverNamePlaceholder": "My PDS", @@ -612,7 +484,6 @@ "themeColors": "Theme Colors", "themeColorsHint": "Leave blank to use default colors.", "primaryLight": "Primary (Light Mode)", - "colorDefault": "{color} (default)", "primaryDark": "Primary (Dark Mode)", "secondaryLight": "Secondary (Light Mode)", "secondaryDark": "Secondary (Dark Mode)", @@ -626,8 +497,6 @@ "refreshStats": "Refresh Stats", "userManagement": "User Management", "searchPlaceholder": "Search by handle (optional)", - "searchUsers": "Search Users", - "noUsers": "No users found", "handle": "Handle", "email": "Email", "status": "Status", @@ -637,10 +506,8 @@ "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", @@ -656,7 +523,12 @@ "deleteConfirm": "Delete account @{handle}? This cannot be undone.", "verified": "Verified", "unverified": "Unverified", - "deactivated": "Deactivated" + "deactivated": "Deactivated", + "inviteDisabled": "Invite code disabled", + "invitesEnabled": "User invites enabled", + "invitesDisabled": "User invites disabled", + "userDeleted": "User account deleted", + "failedToLoadConfig": "Failed to load server configuration" }, "oauth": { "login": { @@ -675,82 +547,35 @@ "passkeyHintNotAvailable": "No passkey registered", "passwordPlaceholder": "Password", "usePasskey": "Use passkey", - "orContinueWith": "or", "orUseCredentials": "or" }, "sso": { "linkedAccounts": "Linked Accounts", - "linkedAccountsDesc": "External accounts linked to your identity for single sign-on.", "noLinkedAccounts": "No linked accounts", - "noLinkedAccountsDesc": "Link an external account to enable quick sign-in with that provider.", - "linkAccount": "Link Account", - "unlinkAccount": "Unlink", + "unlink": "Unlink", "unlinkConfirm": "Are you sure you want to unlink this account?", "unlinked": "Unlinked {provider}", - "lastLoginAt": "Last used", - "linkedAt": "Linked" + "linkedAt": "Linked", + "linkNewAccount": "Link Account", + "linked": "Linked", + "linkSuccess": "Account linked successfully", + "linkFailed": "Failed to link account", + "unlinkFailed": "Failed to unlink 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", "scopes": { - "atproto": { - "name": "Full Account Access", - "description": "Full access to read, write, and manage this account" - }, "atprotoWithGranular": { "name": "AT Protocol Access", "description": "AT Protocol baseline scope (permissions determined by selected options below)" - }, - "transitionGeneric": { - "name": "Transition Access", - "description": "Generic transition scope for compatibility" - }, - "transitionChat": { - "name": "Chat Access", - "description": "Access to Bluesky chat features" - }, - "transitionEmail": { - "name": "Email Access", - "description": "Read your account email address" - }, - "repoCreate": { - "name": "Create Records", - "description": "Create new records in your repository" - }, - "repoUpdate": { - "name": "Update Records", - "description": "Update existing records in your repository" - }, - "repoDelete": { - "name": "Delete Records", - "description": "Delete records from your repository" - }, - "blobAll": { - "name": "Upload Media", - "description": "Upload images, videos, and other media files" - }, - "repoFull": { - "name": "Full Repository Access", - "description": "Full read and write access to all repository records" - }, - "accountManage": { - "name": "Manage Account", - "description": "Manage account settings and preferences" } }, "unexpectedState": { @@ -769,11 +594,6 @@ "subtitleGeneric": "Create an account", "haveAccount": "Have an account? Sign in" }, - "twoFactor": { - "title": "Verification", - "usePasskey": "Use passkey", - "useTotp": "Use authenticator" - }, "twoFactorCode": { "title": "Verification", "subtitle": "Code sent to {channel}", @@ -789,21 +609,14 @@ "totp": { "title": "Authenticator code", "codePlaceholder": "6-digit code", - "useBackupCode": "Use backup code", "backupCodePlaceholder": "Backup code", "trustDevice": "Trust this device for 30 days", "hintBackupCode": "Backup code", "hintTotpCode": "Authenticator code" }, - "passkey": { - "title": "Passkey", - "waiting": "Waiting", - "useTotp": "Use authenticator" - }, "error": { "title": "Authorization failed", - "tryAgain": "Retry", - "backToApp": "Back" + "tryAgain": "Retry" } }, "sso_register": { @@ -832,7 +645,6 @@ "codePlaceholder": "Paste verification code", "codeLabel": "Verification Code", "codeHelp": "Copy the entire code from your message", - "verifyButton": "Verify Account", "pleaseWait": "Please wait...", "codeResent": "Verification code resent!", "codeResentDetail": "Verification code sent! Check your inbox.", @@ -874,8 +686,6 @@ "sendCode": "Send Reset Code", "sending": "Sending...", "codeSent": "Password reset code sent! Check your preferred notification channel.", - "multipleAccountsWarning": "Multiple accounts share this email. The reset code was sent to the most recently created account. Use your handle instead for a specific account.", - "enterCode": "Enter the code you received and your new password.", "code": "Reset Code", "codePlaceholder": "Enter reset code", "newPassword": "New Password", @@ -931,35 +741,6 @@ "sending": "Sending..." }, "registerPasskey": { - "title": "Create Passkey Account", - "subtitleKeyChoice": "Set up your did:web identity", - "subtitleInitialDidDoc": "Upload your DID document", - "subtitleCreating": "Creating account", - "subtitlePasskey": "Register your passkey", - "subtitleAppPassword": "Save your app password", - "subtitleVerify": "Verify your {channel}", - "subtitleUpdatedDidDoc": "Update your DID document", - "subtitleActivating": "Activating", - "subtitleComplete": "Account created", - "handle": "Handle", - "handlePlaceholder": "yourname", - "handleHint": "Your full handle: @{handle}", - "handleDotWarning": "Custom domains can be set up in Settings", - "email": "Email Address", - "emailPlaceholder": "you@example.com", - "inviteCode": "Invite Code", - "inviteCodePlaceholder": "Enter your invite code", - "createButton": "Create Account", - "continue": "Continue", - "back": "Back", - "alreadyHaveAccount": "Already have an account?", - "signIn": "Sign in", - "wantPassword": "Use a password instead?", - "createPasswordAccount": "Password account", - "wantTraditional": "Use a password instead?", - "registerWithPassword": "Password account", - "contactMethod": "Contact Method", - "verificationMethod": "Verification Method", "identityType": "Identity Type", "identityTypeHint": "How your decentralized identity is managed", "didPlcRecommended": "did:plc (Recommended)", @@ -987,11 +768,8 @@ "setupPasskey": "Create Passkey", "passkeyDescription": "Register a passkey for this account", "createPasskey": "Create Passkey", - "creatingPasskey": "Creating", "creatingAccount": "Creating account", "activatingAccount": "Activating", - "redirecting": "Redirecting", - "loading": "Loading", "errors": { "handleRequired": "Handle is required", "handleNoDots": "Handle cannot contain dots. You can set up a custom domain handle after creating your account.", @@ -1008,13 +786,9 @@ } }, "trustedDevices": { - "title": "Trusted Devices", - "backToSecurity": "← Security", - "failedToLoad": "Could not load trusted devices", "noDevices": "No trusted devices", "lastSeen": "Last seen:", "trustedSince": "Trusted since:", - "trustExpires": "Expires:", "expired": "Expired", "tomorrow": "Tomorrow", "inDays": "In {days} days", @@ -1023,9 +797,7 @@ "deviceRevoked": "Trust revoked", "deviceRenamed": "Device renamed", "deviceNamePlaceholder": "Device name", - "browser": "Browser:", "unknownDevice": "Unknown device", - "description": "Trusted devices can skip two-factor authentication when signing in. Trust is granted for 30 days and automatically extends when you use the device.", "noDevicesHint": "When you sign in with two-factor authentication enabled, you can choose to trust the device for 30 days." }, "reauth": { @@ -1034,17 +806,11 @@ "totp": "TOTP", "passkey": "Passkey", "authenticatorCode": "Authenticator Code", - "usePassword": "Password", "usePasskey": "Passkey", - "useTotp": "Authenticator", - "passwordPlaceholder": "Enter your password", - "totpPlaceholder": "6-digit code", "authenticating": "Authenticating", "cancel": "Cancel" }, "delegation": { - "title": "Account Delegation", - "loading": "Loading", "controllers": "Controllers", "controllersDesc": "Accounts that can act on your behalf", "noControllers": "No controllers", @@ -1067,8 +833,6 @@ "addControllerConfirm": "I understand that I will no longer be able to log in directly", "controllerAdded": "Controller added successfully", "controllerRemoved": "Controller removed successfully", - "failedToAddController": "Failed to add controller", - "failedToRemoveController": "Failed to remove controller", "controlledAccounts": "Controlled Accounts", "controlledAccountsDesc": "Accounts you can act on behalf of", "noControlledAccounts": "You do not have access to any delegated accounts.", @@ -1081,40 +845,34 @@ "createAccount": "Create Account", "createDelegatedAccountButton": "+ Create Delegated Account", "accountCreated": "Created delegated account: {handle}", - "failedToCreateAccount": "Failed to create delegated account", "auditLog": "Audit Log", "auditLogDesc": "View all delegation activity", "viewAuditLog": "View Audit Log", "scopeOwner": "Owner", "scopeViewer": "Viewer", "scopeCustom": "Custom", - "backToControllers": "Back to Controllers", - "auditLogTitle": "Delegation Audit Log", - "noActivity": "No delegation activity recorded.", "actor": "Actor", - "controller": "Controller", - "account": "Account", "details": "Details", "previous": "Previous", "next": "Next", - "showing": "Showing {start} - {end} of {total}", "refresh": "Refresh", - "failedToLoadAuditLog": "Failed to load audit log", "actionGrantCreated": "Grant Created", "actionGrantRevoked": "Grant Revoked", "actionScopesModified": "Scopes Modified", "actionTokenIssued": "Token Issued", "actionRepoWrite": "Repository Write", "actionBlobUpload": "Blob Upload", - "actionAccountAction": "Account Action" + "actionAccountAction": "Account Action", + "noAuditEntries": "No audit entries", + "target": "Target", + "pageInfo": "{start} - {end} of {total}", + "failedToLoadAudit": "Failed to load audit log" }, "actAs": { "noAccountSpecified": "No account DID specified", - "failedToVerify": "Failed to verify delegation access", "noAccess": "You do not have access to this account", "failedToInitiate": "Failed to initiate OAuth flow", "invalidResponse": "Invalid OAuth response", - "failedError": "Failed to initiate act-as: {error}", "preparing": "Preparing to switch accounts...", "title": "Act As", "backToControllers": "Back to Controllers" @@ -1162,31 +920,9 @@ "viewerLimitedDesc": "As a Viewer, you have read-only access. This app will not be able to create, update, or delete content on this account.", "editorLimitedDesc": "As an Editor, you can create and edit content but cannot manage account settings or security." }, - "verifyChannel": { - "title": "Verify Channel", - "subtitle": "Enter the verification code sent to your notification channel.", - "signInRequired": "Sign In Required", - "signInRequiredDesc": "You must be signed in to verify a channel.", - "signIn": "Sign In", - "verifying": "Verifying...", - "pleaseWait": "Please wait while we verify your channel.", - "successTitle": "Verified!", - "successDesc": "Your {channel} has been verified successfully.", - "backToSettings": "Back to Settings", - "channelLabel": "Channel", - "selectChannel": "Select channel...", - "identifierLabel": "Identifier", - "identifierPlaceholder": "Email, Discord ID, etc.", - "identifierHelp": "The email address, Discord ID, Telegram username, or Signal number being verified.", - "codeLabel": "Verification Code", - "codeHelp": "Copy the entire code from your message, including dashes.", - "verifyButton": "Verify" - }, "migration": { "title": "Account Migration", "subtitle": "Move your AT Protocol identity between servers", - "navTitle": "Migration", - "navDesc": "Move your account to or from another PDS", "migrateHere": "Migrate Here", "migrateHereDesc": "Move your existing AT Protocol account to this PDS from another server.", "bringDid": "Bring your DID and identity", @@ -1252,6 +988,7 @@ "checkingAvailability": "Checking availability...", "handleAvailable": "Handle is available!", "handleTaken": "Handle is already taken", + "handleTooShort": "Handle must be at least 3 characters", "handleHint": "You can also use your own domain by entering the full handle (eg., alice.mydomain.com)", "email": "Email Address", "authMethod": "Authentication Method", @@ -1276,7 +1013,6 @@ "authentication": "Authentication", "authPasskey": "Passkey (passwordless)", "authPassword": "Password", - "inviteCode": "Invite Code", "warning": "After you click \"Start Migration\", your repository and data will begin transferring. This process cannot be easily undone.", "startMigration": "Start Migration", "starting": "Starting..." @@ -1313,9 +1049,7 @@ "hint": "Enter the code below, or click the link in the email to continue automatically.", "tokenLabel": "Verification Code", "tokenPlaceholder": "Enter code from email", - "resend": "Resend Code", - "verify": "Verify Email", - "verifying": "Verifying..." + "resend": "Resend Code" }, "plcToken": { "title": "Verify Migration", @@ -1423,7 +1157,6 @@ "desc": "Please confirm the details of your offline restoration.", "carFile": "CAR File", "rotationKey": "Rotation Key", - "warning": "After you click \"Start Migration\", your repository will be imported and your DID will be updated to point to this PDS.", "plcWarningTitle": "Point of No Return", "plcWarning": "Once you start, your DID document will be updated to point to this PDS. If something goes wrong, you can use your rotation key to recover, but you should complete the migration to avoid a broken identity state." }, @@ -1431,40 +1164,18 @@ "title": "Restoring Account", "desc": "Please wait while your account is being restored...", "creating": "Creating account", - "importing": "Importing repository", - "plcSigning": "Signing identity update", - "activating": "Activating account" + "importing": "Importing repository" }, "blobs": { "title": "Migrating Blobs", "desc": "Attempting to recover images and media from your old PDS...", "migrating": "Migrating blobs", "failedTitle": "Some blobs could not be migrated", - "failedDesc": "{count} blobs could not be fetched from your old PDS. This may happen if the server is unreachable or the files were deleted.", - "sourceUnreachableTitle": "Source PDS Unreachable", - "sourceUnreachable": "Could not connect to your old PDS to fetch media files. This is common when migrating from a shut-down server. Your posts will work, but some images may be missing." + "failedDesc": "{count} blobs could not be fetched from your old PDS. This may happen if the server is unreachable or the files were deleted." }, "success": { "desc": "Your account has been successfully restored to this PDS." } - }, - "progress": { - "repoExported": "Repository exported", - "repoImported": "Repository imported", - "blobsMigrated": "{count} blobs migrated", - "prefsMigrated": "Preferences migrated", - "plcSigned": "Identity updated", - "activated": "Account activated", - "deactivated": "Old account deactivated" - }, - "errors": { - "connectionFailed": "Could not connect to PDS", - "invalidCredentials": "Invalid credentials", - "twoFactorRequired": "Two-factor authentication required", - "accountExists": "Account already exists on target PDS", - "plcFailed": "PLC operation failed", - "blobFailed": "Failed to migrate blob: {cid}", - "networkError": "Network error. Please try again." } } } diff --git a/frontend/src/locales/fi.json b/frontend/src/locales/fi.json index 4f30d43..699903b 100644 --- a/frontend/src/locales/fi.json +++ b/frontend/src/locales/fi.json @@ -8,55 +8,23 @@ "done": "Valmis", "continue": "Jatka", "refresh": "Päivitä", - "create": "Luo", "delete": "Poista", - "confirm": "Vahvista", "created": "Luotu", - "expires": "Vanhenee", "name": "Nimi", - "dashboard": "Hallintapaneeli", "backToDashboard": "← Hallintapaneeli", "copied": "Kopioitu", "copyToClipboard": "Kopioi", "verifying": "Vahvistetaan", "saving": "Tallennetaan", "creating": "Luodaan", - "updating": "Päivitetään", "sending": "Lähetetään", - "authenticating": "Todennetaan", "checking": "Tarkistetaan", - "redirecting": "Ohjataan", "signIn": "Kirjaudu sisään", "verify": "Vahvista", - "remove": "Poista", "revoke": "Peruuta", "resendCode": "Lähetä koodi", - "startOver": "Aloita alusta", - "tryAgain": "Yritä uudelleen", - "password": "Salasana", - "email": "Sähköposti", - "emailAddress": "Sähköpostiosoite", - "handle": "Käsittely", - "did": "DID", - "verificationCode": "Vahvistuskoodi", - "inviteCode": "Kutsukoodi", - "newPassword": "Uusi salasana", - "confirmPassword": "Vahvista salasana", - "enterSixDigitCode": "Syötä 6-numeroinen koodi", - "passwordHint": "Vähintään 8 merkkiä", - "enterPassword": "Syötä salasanasi", - "emailPlaceholder": "sinä@esimerkki.com", - "verified": "Vahvistettu", - "disabled": "Poistettu käytöstä", - "available": "Saatavilla", - "deactivated": "Deaktivoitu", - "unverified": "Vahvistamaton", "backToLogin": "Takaisin kirjautumiseen", - "backToSettings": "Takaisin asetuksiin", - "alreadyHaveAccount": "Onko sinulla jo tili?", - "createAccount": "Luo tili", - "passwordsMismatch": "Salasanat eivät täsmää", - "passwordTooShort": "Salasanan on oltava vähintään 8 merkkiä" + "backToSettings": "Takaisin asetuksiin" }, "login": { "title": "Kirjaudu sisään", @@ -75,18 +43,10 @@ "subtitle": "Syötä yhteystietoosi lähetetty koodi", "codeLabel": "Koodi", "codePlaceholder": "6-numeroinen koodi", - "verifyButton": "Vahvista", "resent": "Koodi lähetetty" }, "register": { "title": "Luo tili", - "subtitle": "Luo uusi tili tälle PDS:lle", - "subtitleKeyChoice": "Määritä did:web-identiteettisi", - "subtitleInitialDidDoc": "Lataa DID-dokumenttisi", - "subtitleVerify": "Vahvista {channel}", - "subtitleUpdatedDidDoc": "Päivitä DID-dokumenttisi", - "subtitleActivating": "Aktivoidaan", - "subtitleComplete": "Tili luotu", "redirecting": "Ohjataan", "migrateTitle": "Onko sinulla jo tili?", "migrateDescription": "Siirrä olemassa oleva tilisi", @@ -115,8 +75,6 @@ "didWebWarning2Detail": "Toisin kuin did:plc, did:web ei sisällä rotaatioavaimia. Jos tämä PDS menee pysyvästi offline-tilaan, identiteettiäsi ei voida palauttaa.", "didWebWarning3": "Sitoudumme sinuun:", "didWebWarning3Detail": "Jos siirryt pois, jatkamme minimaalisen DID-dokumentin tarjoamista, joka osoittaa uuteen PDS:ääsi. Identiteettisi pysyy toiminnassa.", - "didWebWarning4": "Suositus:", - "didWebWarning4Detail": "Valitse did:plc, ellei sinulla ole erityistä syytä suosia did:web:iä.", "externalDid": "Sinun did:web", "externalDidPlaceholder": "did:web:verkkotunnuksesi.fi", "externalDidHint": "Verkkotunnuksesi on tarjottava kelvollinen DID-dokumentti osoitteessa /.well-known/did.json, joka osoittaa tähän PDS:ään", @@ -125,7 +83,6 @@ "email": "Sähköposti", "emailAddress": "Sähköpostiosoite", "emailPlaceholder": "sinä@esimerkki.fi", - "emailInUseWarning": "Tämä sähköposti on jo yhdistetty toiseen tiliin. Voit silti käyttää sitä, mutta tilin palauttamiseen saatat joutua käyttämään käsittelynimeäsi.", "discord": "Discord", "discordId": "Discord-käyttäjätunnus", "discordIdPlaceholder": "Discord-käyttäjätunnuksesi", @@ -144,13 +101,11 @@ "inviteCode": "Kutsukoodi", "inviteCodePlaceholder": "Syötä kutsukoodisi", "inviteCodeRequired": "vaaditaan", - "createButton": "Luo tili", "alreadyHaveAccount": "Onko sinulla jo tili?", "signIn": "Kirjaudu sisään", "passkeyAccount": "Pääsyavain", "passwordAccount": "Salasana", "ssoAccount": "SSO", - "ssoSubtitle": "Luo tili ulkoisen palveluntarjoajan kautta", "noSsoProviders": "Tälle palvelimelle ei ole määritetty SSO-palveluntarjoajia.", "continueWith": "Jatka palvelulla {provider}", "validation": { @@ -170,12 +125,13 @@ }, "dashboard": { "title": "Hallintapaneeli", + "accountManager": "Tilinhallinta", + "navDelegationAudit": "Delegointiloki", "switchAccount": "Vaihda tiliä", "addAnotherAccount": "Lisää toinen tili", "signOut": "Kirjaudu ulos @{handle}", "deactivatedTitle": "Tili poistettu käytöstä", "deactivatedMessage": "Tilisi on tällä hetkellä poistettu käytöstä. Tämä tapahtuu yleensä tilin siirron aikana. Jotkut toiminnot voivat olla rajoitettuja, kunnes tilisi aktivoidaan uudelleen.", - "accountOverview": "Tilin yleiskatsaus", "handle": "Käyttäjänimi", "did": "DID", "primaryContact": "Ensisijainen yhteystieto", @@ -184,38 +140,20 @@ "verified": "Vahvistettu", "unverified": "Vahvistamaton", "navAppPasswords": "Sovellusten salasanat", - "navAppPasswordsDesc": "Hallitse kolmannen osapuolen sovellusten salasanoja", "navSessions": "Aktiiviset istunnot", - "navSessionsDesc": "Näytä ja hallitse kirjautumisistuntoja", "navInviteCodes": "Kutsukoodit", - "navInviteCodesDesc": "Näytä ja luo kutsukoodeja", - "navSettings": "Tilin asetukset", - "navSettingsDesc": "Sähköposti, salasana, käyttäjänimi ja muuta", + "navSettings": "Yleinen", "navSecurity": "Turvallisuus", - "navSecurityDesc": "Kaksivaiheinen tunnistautuminen", "navComms": "Viestintäasetukset", - "navCommsDesc": "Discord-, Telegram-, Signal-kanavat", "navRepo": "Tietovarastoselaaja", - "navRepoDesc": "Selaa ja hallitse raakoja AT Protocol -tietueita", "navDelegation": "Delegointi", - "navDelegationDesc": "Hallitse tilin ohjaajia ja delegoituja tilejä", "navAdmin": "Ylläpitopaneeli", - "navAdminDesc": "Palvelintilastot ja ylläpitotoiminnot", "navDidDocument": "DID-dokumentti", - "navDidDocumentDesc": "Hallitse DID-dokumenttiasi ulkoisia siirtoja varten", - "navDidDocumentDescActive": "Muokkaa DID-dokumentin asetuksia", - "navBackup": "Lataa varmuuskopio", - "navBackupDesc": "Lataa tietovarastosi CAR-tiedostona", - "downloadingBackup": "Ladataan...", - "backupFailed": "Varmuuskopion lataus epäonnistui", "migrated": "Siirretty", "migratedTitle": "Tili siirretty", - "migratedMessage": "Tilisi on siirretty palvelimelle {pds}. DID-dokumenttisi isännöidään edelleen täällä, ja voit päivittää sen tulevia siirtoja varten.", - "navMigrateAgain": "Siirrä uudelleen", - "navMigrateAgainDesc": "Siirrä toiseen PDS:ään ja päivitä DID-dokumenttisi" + "migratedMessage": "Tilisi on siirretty palvelimelle {pds}. DID-dokumenttisi isännöidään edelleen täällä, ja voit päivittää sen tulevia siirtoja varten." }, "didEditor": { - "title": "DID-dokumentin muokkain", "preview": "Nykyinen DID-dokumentti", "verificationMethods": "Vahvistusmenetelmät", "verificationMethodsDesc": "Allekirjoitusavaimet, jotka voivat toimia DID:si puolesta. Kun siirryt uuteen PDS:ään, lisää niiden allekirjoitusavain tähän.", @@ -241,14 +179,11 @@ "saveFailed": "DID-dokumentin tallennus epäonnistui", "loadFailed": "DID-dokumentin lataus epäonnistui", "invalidMultibase": "Julkisen avaimen on oltava kelvollinen multibase-merkkijono, joka alkaa 'z':llä", - "invalidHandle": "Kahvan on oltava at://-URI (esim. at://kahva.esimerkki.com)", "helpTitle": "Mikä tämä on?", "helpText": "Kun siirryt toiseen PDS:ään, se luo uudet allekirjoitusavaimet. Päivitä DID-dokumenttisi tässä osoittamaan uusiin avaimiin ja sijaintiin. Tämä mahdollistaa monivaiheiset siirrot (PDS 1 → PDS 2 → PDS 3)." }, "settings": { - "title": "Tilin asetukset", "language": "Kieli", - "languageDescription": "Valitse haluamasi kieli", "changeEmail": "Vaihda sähköposti", "currentEmail": "Nykyinen: {email}", "newEmail": "Uusi sähköposti", @@ -266,7 +201,6 @@ "currentHandle": "Nykyinen: @{handle}", "pdsHandle": "PDS-käyttäjänimi", "customDomain": "Oma verkkotunnus", - "customDomainDescription": "Käytä omaa verkkotunnustasi käyttäjänimenä. Sinun on vahvistettava verkkotunnuksen omistajuus ensin.", "setupInstructions": "Asennusohjeet", "setupMethodsIntro": "Valitse jokin näistä vahvistusmenetelmistä:", "dnsMethod": "Vaihtoehto 1: DNS TXT -tietue (Suositellaan)", @@ -280,33 +214,16 @@ "newHandle": "Uusi käyttäjänimi", "newHandlePlaceholder": "käyttäjänimesi", "changeHandleButton": "Vaihda käyttäjänimi", - "changePassword": "Vaihda salasana", - "currentPassword": "Nykyinen salasana", - "currentPasswordPlaceholder": "Syötä nykyinen salasana", - "newPassword": "Uusi salasana", - "newPasswordPlaceholder": "Vähintään 8 merkkiä", - "confirmNewPassword": "Vahvista uusi salasana", - "confirmNewPasswordPlaceholder": "Vahvista uusi salasana", - "changePasswordButton": "Vaihda salasana", - "changing": "Vaihdetaan...", - "setPassword": "Aseta salasana", - "setPasswordDescription": "Tilisi on tällä hetkellä vain pääsyavain-tili. Voit lisätä salasanan ottaaksesi käyttöön perinteisen kirjautumisen pääsyavainten rinnalla.", - "setPasswordButton": "Aseta salasana", - "setting": "Asetetaan...", "exportData": "Vie tiedot", - "exportDataDescription": "Lataa koko tietovarastosi CAR-tiedostona (Content Addressable Archive). Tämä sisältää kaikki julkaisusi, tykkäyksesi, seuraamisesi ja muut tiedot.", "downloadRepo": "Lataa tietovarasto", "downloadBlobs": "Lataa media", "exporting": "Viedään...", "backups": { "title": "Varmuuskopiot", - "description": "Tietovarastosi varmuuskopioidaan automaattisesti päivittäin. Voit myös luoda manuaalisia varmuuskopioita tai palauttaa aiemmasta varmuuskopiosta.", - "enableAutomatic": "Ota automaattiset varmuuskopiot käyttöön", "enabled": "Automaattiset varmuuskopiot käytössä", "disabled": "Automaattiset varmuuskopiot pois käytöstä", "toggleFailed": "Varmuuskopioasetuksen päivitys epäonnistui", "noBackups": "Varmuuskopioita ei ole vielä saatavilla.", - "blocks": "lohkoa", "download": "Lataa", "delete": "Poista", "createNow": "Luo varmuuskopio nyt", @@ -316,13 +233,13 @@ "deleted": "Varmuuskopio poistettu", "deleteFailed": "Varmuuskopion poisto epäonnistui", "restoreTitle": "Palauta varmuuskopiosta", - "restoreDescription": "Lataa CAR-tiedosto palauttaaksesi tietovarastosi. Tämä korvaa nykyiset tietosi.", - "selectFile": "Valitse CAR-tiedosto", "selectedFile": "Valittu tiedosto", "restore": "Palauta", "restoring": "Palautetaan...", "restored": "Tietovarasto palautettu onnistuneesti", - "restoreFailed": "Tietovaraston palautus epäonnistui" + "restoreFailed": "Tietovaraston palautus epäonnistui", + "autoBackup": "Automaattiset varmuuskopiot", + "restoreHint": "Lataa CAR-tiedosto palauttaaksesi tietovaraston" }, "deleteAccount": "Poista tili", "deleteWarning": "Tämä toiminto on peruuttamaton. Kaikki tietosi poistetaan pysyvästi.", @@ -334,21 +251,11 @@ "permanentlyDelete": "Poista tili pysyvästi", "deleting": "Poistetaan...", "messages": { - "emailCodeSent": "Vahvistuskoodi lähetetty ilmoituskanavallesi", "emailCodeSentToCurrent": "Vahvistuskoodi lähetetty nykyiseen sähköpostiosoitteeseesi", "emailUpdated": "Sähköposti päivitetty", "emailUpdateFailed": "Sähköpostin päivitys epäonnistui", "handleUpdated": "Käyttäjänimi päivitetty", "handleUpdateFailed": "Käyttäjänimen päivitys epäonnistui", - "passwordChanged": "Salasana vaihdettu", - "passwordChangeFailed": "Salasanan vaihto epäonnistui", - "passwordSet": "Salasana asetettu onnistuneesti", - "passwordSetFailed": "Salasanan asettaminen epäonnistui", - "passwordsMismatch": "Salasanat eivät täsmää", - "passwordsDoNotMatch": "Salasanat eivät täsmää", - "passwordLength": "Salasanan on oltava vähintään 8 merkkiä", - "passwordTooShort": "Salasanan on oltava vähintään 8 merkkiä", - "deletionCodeSent": "Poistovahvistus lähetetty sähköpostiisi", "deletionConfirmationSent": "Poistovahvistus lähetetty sähköpostiisi", "deletionRequestFailed": "Tilin poistopyyntö epäonnistui", "deleteConfirmation": "Oletko täysin varma, että haluat poistaa tilisi? Tätä ei voi perua.", @@ -356,35 +263,33 @@ "repoExported": "Tietovarasto viety", "blobsExported": "Mediatiedostot viety", "noBlobsToExport": "Ei vietäviä mediatiedostoja", - "exportFailed": "Vienti epäonnistui", - "confirmDelete": "Oletko täysin varma, että haluat poistaa tilisi? Tätä ei voi perua." + "exportFailed": "Vienti epäonnistui" } }, "appPasswords": { - "title": "Sovellusten salasanat", - "description": "Sovellusten salasanat mahdollistavat kirjautumisen kolmannen osapuolen sovelluksiin antamatta niille pääsalasanaasi. Jokainen sovellusen salasana voidaan perua erikseen.", - "createNew": "Luo uusi sovelluksen salasana", - "appNamePlaceholder": "Sovelluksen nimi (esim. Graysky, Skeets)", "created": "Sovelluksen salasana luotu", "createdMessage": "Kopioi tämä salasana nyt. Et voi nähdä sitä enää myöhemmin.", - "yourPasswords": "Sovellustesi salasanat", "noPasswords": "Ei vielä sovellusten salasanoja", - "revoke": "Peruuta", - "revoking": "Peruutetaan...", - "revokeConfirm": "Peruuta sovelluksen salasana \"{name}\"? Sovellukset, jotka käyttävät tätä salasanaa, eivät enää pääse tilillesi.", "saveWarningTitle": "Tärkeää: Tallenna tämä sovelluksen salasana!", "saveWarningMessage": "Tämä salasana tarvitaan kirjautumiseen sovelluksiin, jotka eivät tue pääsyavaimia tai OAuthia. Näet sen vain kerran.", "acknowledgeLabel": "Olen tallentanut sovelluksen salasanani turvalliseen paikkaan", "permissions": "Käyttöoikeudet", "scopeFull": "Täydet oikeudet", "scopeReadOnly": "Vain luku", - "scopePostOnly": "Vain julkaisut", + "scopePostOnly": "Vain julkaisu", "scopeCustom": "Mukautettu", - "byController": "Hallinnoijan luoma" + "byController": "Hallinnoijan luoma", + "create": "Luo", + "name": "Nimi", + "namePlaceholder": "Sovelluksen nimi (esim. Graysky)", + "deleteConfirm": "Peruuta sovelluksen salasana \"{name}\"?", + "deleted": "Sovelluksen salasana peruutettu", + "loadFailed": "Sovellusten salasanojen lataus epäonnistui", + "createFailed": "Sovelluksen salasanan luonti epäonnistui", + "deleteFailed": "Sovelluksen salasanan peruutus epäonnistui", + "saveWarning": "Tallenna tämä salasana nyt - et näe sitä enää" }, "sessions": { - "title": "Aktiiviset istunnot", - "loadingSessions": "Ladataan istuntoja...", "noSessions": "Aktiivisia istuntoja ei löytynyt.", "current": "Nykyinen", "oauth": "OAuth", @@ -404,52 +309,43 @@ "daysAgo": "{count} päivää sitten", "hoursAgo": "{count} tuntia sitten", "minutesAgo": "{count} minuuttia sitten", - "justNow": "Juuri nyt" + "justNow": "Juuri nyt", + "sessionRevoked": "Istunto peruutettu", + "allSessionsRevoked": "Kaikki muut istunnot peruutettu" }, "inviteCodes": { - "title": "Kutsukoodit", - "description": "Kutsukoodit mahdollistavat ystävien kutsumisen. Jokainen koodi voidaan käyttää kerran.", "createNew": "Luo uusi kutsukoodi", - "uses": "Käyttökerrat", - "usesPlaceholder": "Käyttökertojen määrä (1-100)", "yourCodes": "Kutsukoodisi", "noCodes": "Ei vielä kutsukoodeja", "available": "Saatavilla", "used": "Käyttänyt @{handle}", "spent": "Käytetty", "disabled": "Poistettu käytöstä", - "usedBy": "Käyttänyt", - "disableConfirm": "Poista tämä kutsukoodi käytöstä? Sitä ei voi enää käyttää.", "created": "Kutsukoodi luotu", "copy": "Kopioi", - "createdOn": "Luotu {date}" + "createdOn": "Luotu {date}", + "loadFailed": "Kutsukoodien lataus epäonnistui", + "createFailed": "Kutsukoodin luonti epäonnistui" }, "security": { - "title": "Turvallisuus", "passkeys": "Pääsyavaimet", - "passkeysDescription": "Pääsyavaimet tarjoavat turvallisen, salasanattoman tunnistautumisen käyttäen laitteesi sisäänrakennettua turvallisuutta (sormenjälki, kasvot tai PIN).", "addPasskey": "Lisää pääsyavain", "adding": "Lisätään...", "noPasskeys": "Ei rekisteröityjä pääsyavaimia", "passkeyName": "Pääsyavaimen nimi", "passkeyNamePlaceholder": "esim. MacBook Pro, iPhone", - "register": "Rekisteröi", - "registering": "Rekisteröidään...", "rename": "Nimeä uudelleen", - "renaming": "Nimetään uudelleen...", "deletePasskey": "Poista", "deletePasskeyConfirm": "Poista pääsyavain \"{name}\"? Et voi enää käyttää sitä kirjautumiseen.", "totp": "Todentajasovellus (TOTP)", - "totpDescription": "Käytä todentajasovellusta kuten Google Authenticator, Authy tai 1Password kaksivaiheiseen tunnistautumiseen.", "totpEnabled": "TOTP on käytössä", "totpDisabled": "TOTP ei ole käytössä", "enableTotp": "Ota TOTP käyttöön", "disableTotp": "Poista TOTP käytöstä", "disabling": "Poistetaan käytöstä...", - "totpSetup": "Määritä todentajasovellus", "totpSetupInstructions": "Skannaa tämä QR-koodi todentajasovelluksellasi ja syötä sitten 6-numeroinen koodi vahvistaaksesi.", - "totpCode": "Vahvistuskoodi", - "totpCodePlaceholder": "Syötä 6-numeroinen koodi", + "totpCode": "TOTP-koodi", + "totpCodePlaceholder": "6 numeroa", "verifyAndEnable": "Vahvista ja ota käyttöön", "backupCodes": "Varakoodit", "backupCodesDescription": "Käytä näitä koodeja kirjautuaksesi sisään, jos menetät pääsyn todentajasovellukseesi. Jokainen koodi voidaan käyttää vain kerran.", @@ -463,15 +359,6 @@ "enableLegacyLogin": "Ota vanhentunut kirjautuminen käyttöön", "disableLegacyLogin": "Poista vanhentunut kirjautuminen käytöstä", "legacyLoginWarning": "Varoitus: Vanhentuneen kirjautumisen käyttöönotto ohittaa MFA:n suorissa salasanakirjautumisissa. Ota käyttöön vain jos sovellusyhteensopivuus sitä vaatii.", - "totpPasswordWarning": "Kun TOTP on käytössä, salasanan vaihtaminen Bluesky-sovelluksesta (tai muista vanhentuneista sovelluksista) estetään. Salasanan vaihtamiseen on kaksi vaihtoehtoa:", - "totpPasswordOption1Label": "Vaihda se täällä:", - "totpPasswordOption1Text": "Käytä tämän sivuston", - "totpPasswordOption1Link": "Asetukset-sivua", - "totpPasswordOption1Suffix": "jossa voit vahvistaa todentajasovelluksellasi.", - "totpPasswordOption2Label": "Vahvista istuntosi ensin:", - "totpPasswordOption2Text": "Käytä", - "totpPasswordOption2Link": "uudelleentodennusvaihtoehtoa", - "totpPasswordOption2Suffix": "vahvistaaksesi Bluesky-istuntosi TOTP:lla, sitten salasanan vaihto toimii väliaikaisesti.", "legacyAppsTitle": "Mitä ovat vanhentuneet sovellukset?", "legacyAppsDescription": "Jotkin sovellukset (kuten virallinen Bluesky-sovellus) käyttävät vanhentunutta todennusta, joka vaatii vain salasanasi. Kun sinulla on MFA käytössä, nämä sovellukset ohittavat toisen tekijäsi. Vanhentuneen kirjautumisen poistaminen käytöstä pakottaa kaikki sovellukset käyttämään OAuthia, joka soveltaa MFA:ta oikein.", "password": "Salasana", @@ -479,13 +366,7 @@ "noPassword": "Ei salasanaa asetettuna (vain pääsyavaintili)", "setPassword": "Aseta salasana", "removePassword": "Poista salasana", - "removePasswordConfirm": "Poista salasanasi? Sinun on käytettävä pääsyavaimia kirjautuaksesi sisään.", "removing": "Poistetaan...", - "loading": "Ladataan...", - "loadingPasskeys": "Ladataan pääsyavaimia...", - "cancel": "Peruuta", - "save": "Tallenna", - "back": "Takaisin", "next": "Seuraava: Vahvista koodi", "copyToClipboard": "Kopioi leikepöydälle", "savedMyCodes": "Olen tallentanut koodini", @@ -493,20 +374,10 @@ "unnamedPasskey": "Nimetön pääsyavain", "added": "Lisätty", "lastUsed": "Viimeksi käytetty", - "passwordDescription": "Hallitse tilisi salasanaa. Jos sinulla on pääsyavaimia määritettynä, voit halutessasi poistaa salasanasi täysin salasanattoman kokemuksen saavuttamiseksi.", "disableTotpWarning": "Tämä tekee tilistäsi vähemmän turvallisen.", "removePasswordWarning": "Tämä tekee tilistäsi vain pääsyavaintilin. Voit kirjautua sisään vain rekisteröidyillä pääsyavaimillasi. Jos menetät pääsyn kaikkiin pääsyavaimeesi, voit palauttaa tilisi ilmoituskanavan kautta.", - "beforeProceeding": "Ennen kuin jatkat:", - "beforeProceedingItem1": "Varmista, että sinulla on vähintään yksi luotettava pääsyavain rekisteröitynä", - "beforeProceedingItem2": "Harkitse pääsyavainten rekisteröimistä useille laitteille", - "beforeProceedingItem3": "Varmista, että palautusilmoituskanavasi on ajan tasalla", - "addPasskeyFirst": "Lisää vähintään yksi pääsyavain ennen kuin voit poistaa salasanasi.", - "passkeyOnlyHint": "Kirjaudut sisään vain pääsyavaimilla. Jos menetät pääsyn pääsyavaimeesi, voit palauttaa tilisi käyttämällä \"Kadotitko pääsyavaimen?\" -linkkiä kirjautumissivulla.", - "addPasswordHint": "Haluatko lisätä salasanan? Siirry Asetuksiin määrittääksesi sellaisen.", - "goToSettings": "Siirry asetuksiin", "trustedDevices": "Luotetut laitteet", - "trustedDevicesDescription": "Hallitse laitteita, jotka voivat ohittaa kaksivaiheisen tunnistautumisen kirjautuessaan. Luottamus myönnetään 30 päiväksi ja jatkuu automaattisesti, kun käytät laitetta.", - "manageTrustedDevices": "Hallitse luotettuja laitteita", + "trustedDevicesDescription": "Laitteet, jotka voivat ohittaa kaksivaiheisen tunnistautumisen kirjautuessaan. Luottamus myönnetään 30 päiväksi ja jatkuu automaattisesti, kun käytät laitetta.", "appCompatibility": "Sovellusyhteensopivuus", "enterPassword": "Syötä salasanasi", "sessionExpired": "Istunto vanhentunut. Kirjaudu sisään uudelleen.", @@ -524,13 +395,26 @@ "passkeyCreationCancelled": "Pääsyavaimen luominen peruutettu", "passkeyAddedSuccess": "Pääsyavain lisätty", "passkeyDeleted": "Pääsyavain poistettu", - "passkeyRenamed": "Pääsyavain nimetty uudelleen" + "passkeyRenamed": "Pääsyavain nimetty uudelleen", + "changePassword": "Vaihda salasana", + "currentPassword": "Nykyinen salasana", + "currentPasswordPlaceholder": "Syötä nykyinen salasana", + "newPassword": "Uusi salasana", + "newPasswordPlaceholder": "Syötä uusi salasana", + "confirmPassword": "Vahvista uusi salasana", + "confirmPasswordPlaceholder": "Vahvista uusi salasana", + "passwordsDoNotMatch": "Salasanat eivät täsmää", + "passwordTooShort": "Salasanan on oltava vähintään 8 merkkiä", + "passwordChanged": "Salasana vaihdettu", + "failedToChangePassword": "Salasanan vaihto epäonnistui", + "changing": "Vaihdetaan...", + "setting": "Asetetaan...", + "passwordSet": "Salasana asetettu", + "failedToSetPassword": "Salasanan asetus epäonnistui", + "failedToDisableTotp": "TOTP:n poistaminen käytöstä epäonnistui" }, "comms": { - "title": "Viestintäasetukset", - "description": "Valitse, miten haluat vastaanottaa tärkeitä viestejä kuten salasanan palautuksia, turvallisuushälytyksiä ja tilipäivityksiä.", "preferredChannel": "Ensisijainen kanava", - "preferredChannelDescription": "Valitse ensisijainen tapasi vastaanottaa viestejä. Sinun on määritettävä kanava ennen kuin voit valita sen.", "channelConfiguration": "Kanavan määritys", "emailVia": "Vastaanota viestejä sähköpostitse", "discordVia": "Vastaanota viestejä Discord-yksityisviestinä", @@ -538,10 +422,6 @@ "signalVia": "Vastaanota viestejä Signalissa", "configureToEnable": "Määritä alla ottaaksesi käyttöön", "notConfiguredOnServer": "Ei määritetty tällä palvelimella", - "emailManagedInSettings": "Sähköpostisi hallinnoidaan Tilin asetuksissa", - "discordIdHint": "Discord-käyttäjätunnuksesi (ei käyttäjänimi). Ota Kehittäjätila käyttöön Discordissa kopioidaksesi sen.", - "telegramHint": "Telegram-käyttäjänimesi ilman @-merkkiä", - "signalHint": "Signal-puhelinnumerosi maakoodilla", "primary": "Ensisijainen", "verified": "Vahvistettu", "notVerified": "Vahvistamaton", @@ -552,29 +432,20 @@ "preferencesSaved": "Viestintäasetukset tallennettu", "verifiedSuccess": "{channel} vahvistettu", "messageHistory": "Viestihistoria", - "historyDescription": "Näytä viimeisimmät tilillesi lähetetyt viestit.", - "loadHistory": "Lataa historia", - "hideHistory": "Piilota historia", "noMessages": "Viestejä ei löytynyt.", - "sent": "lähetetty", - "failed": "epäonnistui", "discordInUseWarning": "Tämä Discord-tunnus on jo yhdistetty toiseen tiliin.", "telegramInUseWarning": "Tämä Telegram-käyttäjänimi on jo yhdistetty toiseen tiliin.", - "signalInUseWarning": "Tämä Signal-numero on jo yhdistetty toiseen tiliin." + "signalInUseWarning": "Tämä Signal-numero on jo yhdistetty toiseen tiliin.", + "failedToLoad": "Asetusten lataus epäonnistui", + "failedToSave": "Asetusten tallennus epäonnistui", + "failedToVerify": "Vahvistus epäonnistui", + "failedToLoadHistory": "Viestihistorian lataus epäonnistui" }, "repoExplorer": { - "title": "Tietovarastoselaaja", - "description": "Selaa ja hallitse raakoja AT Protocol -tietueitasi.", "collections": "Kokoelmat", - "noCollections": "Kokoelmia ei löytynyt", - "records": "Tietueet", "noRecords": "Ei tietueita tässä kokoelmassa", - "recordDetails": "Tietueen tiedot", - "rkey": "Tietueavain", "uri": "URI", "cid": "CID", - "value": "Arvo", - "deleteRecord": "Poista tietue", "deleteConfirm": "Poista tietue {rkey}? Tätä ei voi perua.", "unknownError": "Tuntematon virhe tapahtui", "invalidJson": "Virheellinen JSON", @@ -587,7 +458,6 @@ "filterCollections": "Suodata kokoelmia...", "filterRecords": "Suodata tietueita...", "noCollectionsYet": "Ei vielä kokoelmia. Luo ensimmäinen tietueesi aloittaaksesi.", - "loadMore": "Lataa lisää", "recordJson": "Tietueen JSON", "updateRecord": "Päivitä tietue", "collectionNsid": "Kokoelma (NSID)", @@ -599,8 +469,6 @@ "demoBio": "Lyhyt kuvaus itsestäsi." }, "admin": { - "title": "Ylläpitopaneeli", - "loading": "Ladataan...", "serverConfig": "Palvelinasetukset", "serverName": "Palvelimen nimi", "serverNamePlaceholder": "Oma PDS", @@ -623,8 +491,6 @@ "refreshStats": "Päivitä tilastot", "userManagement": "Käyttäjähallinta", "searchPlaceholder": "Hae käyttäjänimellä (valinnainen)", - "searchUsers": "Hae käyttäjiä", - "noUsers": "Käyttäjiä ei löytynyt", "handle": "Käyttäjänimi", "email": "Sähköposti", "status": "Tila", @@ -634,10 +500,8 @@ "loadInviteCodes": "Lataa kutsukoodit", "refresh": "Päivitä", "noInvites": "Kutsukoodeja ei löytynyt", - "code": "Koodi", "available": "Saatavilla", "uses": "Käyttökerrat", - "actions": "Toiminnot", "disable": "Poista käytöstä", "disableInviteConfirm": "Poista kutsukoodi {code} käytöstä?", "active": "Aktiivinen", @@ -654,9 +518,17 @@ "verified": "Vahvistettu", "unverified": "Vahvistamaton", "deactivated": "Poistettu käytöstä", - "colorDefault": "{color} (oletus)", "secondaryLight": "Toissijainen (vaalea tila)", - "secondaryDark": "Toissijainen (tumma tila)" + "secondaryDark": "Toissijainen (tumma tila)", + "failedToLoadStats": "Palvelintilastojen lataus epäonnistui", + "failedToLoadUsers": "Käyttäjien lataus epäonnistui", + "searchToSeeUsers": "Hae nähdäksesi käyttäjät", + "search": "Hae", + "inviteDisabled": "Kutsukoodi poistettu käytöstä", + "invitesEnabled": "Käyttäjäkutsut käytössä", + "invitesDisabled": "Käyttäjäkutsut pois käytöstä", + "userDeleted": "Käyttäjätili poistettu", + "failedToLoadConfig": "Palvelinasetusten lataus epäonnistui" }, "oauth": { "login": { @@ -675,7 +547,6 @@ "passkeyHintNotAvailable": "Ei pääsyavainta", "passwordPlaceholder": "Salasana", "usePasskey": "Käytä pääsyavainta", - "orContinueWith": "tai", "orUseCredentials": "tai" }, "register": { @@ -686,77 +557,31 @@ }, "sso": { "linkedAccounts": "Linkitetyt tilit", - "linkedAccountsDesc": "Ulkoiset tilit, jotka on linkitetty identiteettiisi kertakirjautumista varten.", "noLinkedAccounts": "Ei linkitettyjä tilejä", - "noLinkedAccountsDesc": "Linkitä ulkoinen tili ottaaksesi käyttöön nopean kirjautumisen kyseisellä palveluntarjoajalla.", - "linkAccount": "Linkitä tili", - "unlinkAccount": "Poista linkitys", "unlinkConfirm": "Haluatko varmasti poistaa tämän tilin linkityksen?", "unlinked": "Linkitys poistettu: {provider}", - "lastLoginAt": "Viimeksi käytetty", - "linkedAt": "Linkitetty" + "linkedAt": "Linkitetty", + "unlink": "Poista linkitys", + "linkNewAccount": "Linkitä tili", + "linked": "Linkitetty", + "linkSuccess": "Tili linkitetty", + "linkFailed": "Tilin linkitys epäonnistui", + "unlinkFailed": "Linkityksen poisto epäonnistui" }, "consent": { "title": "Valtuuta sovellus", "appWantsAccess": "{app} haluaa käyttää tiliäsi", - "permissions": "Tämä sovellus voi:", - "readProfile": "Lukea profiilitietosi", - "readPosts": "Lukea julkaisusi ja sisältösi", - "writePosts": "Luoda ja poistaa julkaisuja puolestasi", - "readNotifications": "Lukea ilmoituksesi", - "fullAccess": "Täysi pääsy tiliisi", "authorize": "Valtuuta", "deny": "Estä", "authorizing": "Valtuutetaan...", - "rememberChoice": "Muista tämä valinta", "signingInAs": "Kirjaudutaan käyttäjänä:", "permissionsRequested": "Pyydetyt oikeudet", "required": "Vaaditaan", "rememberChoiceLabel": "Muista valintani tälle sovellukselle", "scopes": { - "atproto": { - "name": "Täysi käyttöoikeus", - "description": "Täysi oikeus lukea, kirjoittaa ja hallita tätä tiliä" - }, "atprotoWithGranular": { "name": "AT Protocol -käyttöoikeus", "description": "AT Protocol -peruslaajuus (oikeudet määräytyvät alla valittujen vaihtoehtojen mukaan)" - }, - "transitionGeneric": { - "name": "Siirtymäkäyttöoikeus", - "description": "Yleinen siirtymälaajuus yhteensopivuutta varten" - }, - "transitionChat": { - "name": "Chat-käyttöoikeus", - "description": "Pääsy Bluesky-chat-ominaisuuksiin" - }, - "transitionEmail": { - "name": "Sähköpostikäyttöoikeus", - "description": "Lue tilisi sähköpostiosoite" - }, - "repoCreate": { - "name": "Luo tietueita", - "description": "Luo uusia tietueita tietovarastoosi" - }, - "repoUpdate": { - "name": "Päivitä tietueita", - "description": "Päivitä olemassa olevia tietueita tietovarastossasi" - }, - "repoDelete": { - "name": "Poista tietueita", - "description": "Poista tietueita tietovarastostasi" - }, - "blobAll": { - "name": "Lataa mediaa", - "description": "Lataa kuvia, videoita ja muita mediatiedostoja" - }, - "repoFull": { - "name": "Täysi tietovarastokäyttö", - "description": "Täysi luku- ja kirjoitusoikeus kaikkiin tietovaraston tietueisiin" - }, - "accountManage": { - "name": "Hallitse tiliä", - "description": "Hallitse tilin asetuksia ja asetuksia" } }, "unexpectedState": { @@ -769,11 +594,6 @@ "title": "Valitse tili", "useAnother": "Käytä toista tiliä" }, - "twoFactor": { - "title": "Vahvistus", - "usePasskey": "Käytä pääsyavainta", - "useTotp": "Käytä todentajaa" - }, "twoFactorCode": { "title": "Vahvistus", "subtitle": "Koodi lähetetty: {channel}", @@ -789,21 +609,14 @@ "totp": { "title": "Todentajakoodi", "codePlaceholder": "6-numeroinen koodi", - "useBackupCode": "Käytä varakoodia", "backupCodePlaceholder": "Varakoodi", "trustDevice": "Luota tähän laitteeseen 30 päivää", "hintBackupCode": "Varakoodi", "hintTotpCode": "Todentajakoodi" }, - "passkey": { - "title": "Pääsyavain", - "waiting": "Odotetaan", - "useTotp": "Käytä todentajaa" - }, "error": { "title": "Valtuutus epäonnistui", - "tryAgain": "Yritä uudelleen", - "backToApp": "Takaisin" + "tryAgain": "Yritä uudelleen" } }, "sso_register": { @@ -829,10 +642,9 @@ "subtitle": "Olemme lähettäneet vahvistuskoodin {channel}. Syötä se alla viimeistelläksesi rekisteröinnin.", "tokenTitle": "Vahvista", "tokenSubtitle": "Syötä vahvistuskoodi ja tunniste, johon se lähetettiin.", - "codePlaceholder": "Paste verification code", + "codePlaceholder": "Liitä vahvistuskoodi", "codeLabel": "Vahvistuskoodi", "codeHelp": "Kopioi koko koodi viestistäsi, ", - "verifyButton": "Vahvista tili", "pleaseWait": "Odota...", "codeResent": "Vahvistuskoodi lähetetty uudelleen!", "codeResentDetail": "Vahvistuskoodi lähetetty! Tarkista saapuneet-kansiosi.", @@ -874,8 +686,6 @@ "sendCode": "Lähetä palautuskoodi", "sending": "Lähetetään...", "codeSent": "Palautuskoodi lähetetty! Tarkista ensisijainen ilmoituskanavasi.", - "multipleAccountsWarning": "Useampi tili käyttää tätä sähköpostia. Palautuskoodi lähetettiin viimeksi luodulle tilille. Käytä käsittelynimeäsi tietylle tilille.", - "enterCode": "Syötä saamasi koodi ja uusi salasanasi.", "code": "Palautuskoodi", "codePlaceholder": "Syötä palautuskoodi", "newPassword": "Uusi salasana", @@ -931,32 +741,8 @@ "sending": "Lähetetään..." }, "registerPasskey": { - "title": "Luo pääsyavaintili", - "subtitleKeyChoice": "Määritä did:web-identiteettisi", - "subtitleInitialDidDoc": "Lataa DID-dokumenttisi", - "subtitleCreating": "Luodaan tiliä", - "subtitlePasskey": "Rekisteröi pääsyavaimesi", - "subtitleAppPassword": "Tallenna sovellussalasanasi", - "subtitleVerify": "Vahvista {channel}", - "subtitleUpdatedDidDoc": "Päivitä DID-dokumenttisi", - "subtitleActivating": "Aktivoidaan", - "subtitleComplete": "Tili luotu", - "handle": "Käyttäjänimi", - "handlePlaceholder": "nimesi", - "handleHint": "Täydellinen käyttäjänimesi on: @{handle}", - "contactMethod": "Yhteysmenetelmä", - "verificationMethod": "Vahvistusmenetelmä", - "email": "Sähköpostiosoite", - "emailPlaceholder": "sinä@esimerkki.fi", - "inviteCode": "Kutsukoodi", - "inviteCodePlaceholder": "Syötä kutsukoodisi", "externalDid": "Sinun did:web", "externalDidPlaceholder": "did:web:verkkotunnuksesi.fi", - "createButton": "Luo tili", - "alreadyHaveAccount": "Onko sinulla jo tili?", - "signIn": "Kirjaudu sisään", - "wantPassword": "Haluatko käyttää salasanaa?", - "createPasswordAccount": "Luo salasanatili", "errors": { "handleRequired": "Käyttäjänimi vaaditaan", "handleNoDots": "Käyttäjänimi ei voi sisältää pisteitä. Voit määrittää oman verkkotunnuksen tilin luomisen jälkeen.", @@ -971,7 +757,6 @@ "externalDidFormat": "Ulkoisen DID:n on alettava did:web:", "discordRequired": "Discord-tunnus vaaditaan Discord-vahvistukseen" }, - "creatingPasskey": "Luodaan", "identityType": "Identiteettityyppi", "identityTypeHint": "Valitse, miten hajautettua identiteettiäsi hallitaan.", "passkeyNamePlaceholder": "esim. MacBook Touch ID", @@ -994,29 +779,17 @@ "didWebWarning4": "Suositus:", "didWebWarning4Detail": "Valitse did:plc, ellei sinulla ole erityistä syytä suosia did:web.", "externalDidHint": "Sinun on tarjottava DID-dokumentti osoitteessa", - "continue": "Jatka", - "back": "Takaisin", - "loading": "Ladataan...", - "redirecting": "Ohjataan hallintapaneeliin...", - "handleDotWarning": "Mukautetut verkkotunnuskahvat voidaan määrittää tilin luomisen jälkeen.", - "wantTraditional": "Haluatko perinteisen salasanan?", - "registerWithPassword": "Rekisteröidy salasanalla", - "activatingAccount": "Activating", - "creatingAccount": "Creating account", - "passkeyDescription": "Register a passkey for this account", - "passkeyName": "Passkey Name", - "setupPasskey": "Create Passkey" + "activatingAccount": "Aktivoidaan", + "creatingAccount": "Luodaan tiliä", + "passkeyDescription": "Rekisteröi pääsyavain tälle tilille", + "passkeyName": "Pääsyavaimen nimi", + "setupPasskey": "Luo pääsyavain" }, "trustedDevices": { - "title": "Luotetut laitteet", - "backToSecurity": "← Turvallisuusasetukset", - "description": "Luotetut laitteet voivat ohittaa kaksivaiheisen tunnistautumisen kirjautuessaan. Luottamus myönnetään 30 päiväksi ja jatkuu automaattisesti, kun käytät laitetta.", - "failedToLoad": "Luotettujen laitteiden lataaminen epäonnistui", "noDevices": "Ei vielä luotettuja laitteita.", "noDevicesHint": "Kun kirjaudut sisään kaksivaiheisen tunnistautumisen ollessa käytössä, voit valita luottaa laitteeseen 30 päivää.", "lastSeen": "Viimeksi nähty:", "trustedSince": "Luotettu alkaen:", - "trustExpires": "Luottamus vanhenee:", "expired": "Vanhentunut", "tomorrow": "Huomenna", "inDays": "{days} päivän kuluttua", @@ -1025,7 +798,6 @@ "deviceRevoked": "Laitteen luottamus peruutettu", "deviceRenamed": "Laite nimetty uudelleen", "deviceNamePlaceholder": "Laitteen nimi", - "browser": "Selain:", "unknownDevice": "Tuntematon laite" }, "reauth": { @@ -1034,37 +806,11 @@ "totp": "TOTP", "passkey": "Pääsyavain", "authenticatorCode": "Todentajan koodi", - "usePassword": "Salasana", "usePasskey": "Pääsyavain", - "useTotp": "Todentaja", - "passwordPlaceholder": "Syötä salasanasi", - "totpPlaceholder": "6-numeroinen koodi", "authenticating": "Todennetaan", "cancel": "Peruuta" }, - "verifyChannel": { - "title": "Vahvista kanava", - "subtitle": "Syötä ilmoituskanavallesi lähetetty vahvistuskoodi.", - "signInRequired": "Kirjautuminen vaaditaan", - "signInRequiredDesc": "Sinun on kirjauduttava sisään vahvistaaksesi kanavan.", - "signIn": "Kirjaudu sisään", - "verifying": "Vahvistetaan...", - "pleaseWait": "Odota, vahvistamme kanavaasi.", - "successTitle": "Vahvistettu!", - "successDesc": "{channel} on vahvistettu onnistuneesti.", - "backToSettings": "Takaisin asetuksiin", - "channelLabel": "Kanava", - "selectChannel": "Valitse kanava...", - "identifierLabel": "Tunniste", - "identifierPlaceholder": "Sähköposti, Discord ID jne.", - "identifierHelp": "Vahvistettava sähköpostiosoite, Discord ID, Telegram-käyttäjänimi tai Signal-numero.", - "codeLabel": "Vahvistuskoodi", - "codeHelp": "Kopioi koko koodi viestistäsi, .", - "verifyButton": "Vahvista" - }, "delegation": { - "title": "Tilin delegointi", - "loading": "Ladataan...", "controllers": "Hallinnoijat", "controllersDesc": "Tilit, jotka voivat toimia puolestasi", "noControllers": "Tilillesi ei ole myönnetty hallinnoijia.", @@ -1087,8 +833,6 @@ "addControllerConfirm": "Ymmärrän, etten voi enää kirjautua suoraan", "controllerAdded": "Hallinnoija lisätty", "controllerRemoved": "Hallinnoija poistettu", - "failedToAddController": "Hallinnoijan lisääminen epäonnistui", - "failedToRemoveController": "Hallinnoijan poistaminen epäonnistui", "controlledAccounts": "Hallinnoidut tilit", "controlledAccountsDesc": "Tilit, joiden puolesta voit toimia", "noControlledAccounts": "Sinulla ei ole pääsyä delegoituihin tileihin.", @@ -1101,40 +845,34 @@ "createAccount": "Luo tili", "createDelegatedAccountButton": "+ Luo delegoitu tili", "accountCreated": "Delegoitu tili luotu: {handle}", - "failedToCreateAccount": "Delegoidun tilin luominen epäonnistui", "auditLog": "Tapahtumaloki", "auditLogDesc": "Näytä kaikki delegointitoiminta", "viewAuditLog": "Näytä tapahtumaloki", "scopeOwner": "Omistaja", "scopeViewer": "Katsoja", "scopeCustom": "Mukautettu", - "backToControllers": "Takaisin hallinnoijiin", - "auditLogTitle": "Delegoinnin tapahtumaloki", - "noActivity": "Delegointitoimintaa ei ole tallennettu.", "actor": "Toimija", - "controller": "Hallinnoija", - "account": "Tili", "details": "Tiedot", "previous": "Edellinen", "next": "Seuraava", - "showing": "Näytetään {start} - {end} / {total}", "refresh": "Päivitä", - "failedToLoadAuditLog": "Tapahtumalokin lataaminen epäonnistui", "actionGrantCreated": "Oikeus luotu", "actionGrantRevoked": "Oikeus peruttu", "actionScopesModified": "Oikeuksia muokattu", "actionTokenIssued": "Token myönnetty", "actionRepoWrite": "Tietovaraston kirjoitus", "actionBlobUpload": "Tiedoston lataus", - "actionAccountAction": "Tilitoiminto" + "actionAccountAction": "Tilitoiminto", + "noAuditEntries": "Ei lokimerkintöjä", + "target": "Kohde", + "pageInfo": "{start} - {end} / {total}", + "failedToLoadAudit": "Lokin lataus epäonnistui" }, "actAs": { "noAccountSpecified": "Tilin DID:tä ei määritetty", - "failedToVerify": "Delegointioikeuden tarkistus epäonnistui", "noAccess": "Sinulla ei ole pääsyä tähän tiliin", "failedToInitiate": "OAuth-kirjautumisen aloitus epäonnistui", "invalidResponse": "Virheellinen OAuth-vastaus", - "failedError": "Toiminto epäonnistui: {error}", "preparing": "Valmistellaan tilin vaihtoa...", "title": "Toimi käyttäjänä", "backToControllers": "Takaisin hallinnoijiin" @@ -1185,8 +923,6 @@ "migration": { "title": "Tilin siirto", "subtitle": "Siirrä AT Protocol -identiteettisi palvelimien välillä", - "navTitle": "Siirto", - "navDesc": "Siirrä tilisi toiseen tai toisesta PDS:stä", "migrateHere": "Siirrä tänne", "migrateHereDesc": "Siirrä olemassa oleva AT Protocol -tilisi tähän PDS:ään toiselta palvelimelta.", "bringDid": "Tuo DID ja identiteettisi", @@ -1252,6 +988,7 @@ "checkingAvailability": "Tarkistetaan saatavuutta...", "handleAvailable": "Käyttäjätunnus on saatavilla!", "handleTaken": "Käyttäjätunnus on jo varattu", + "handleTooShort": "Käyttäjätunnuksen on oltava vähintään 3 merkkiä", "handleHint": "Voit myös käyttää omaa verkkotunnustasi syöttämällä täydellisen käyttäjätunnuksen (esim. maija.omadomain.fi)", "email": "Sähköpostiosoite", "authMethod": "Tunnistautumistapa", @@ -1276,7 +1013,6 @@ "authentication": "Tunnistautuminen", "authPasskey": "Pääsyavain (salasanaton)", "authPassword": "Salasana", - "inviteCode": "Kutsukoodi", "warning": "Kun klikkaat \"Aloita siirto\", tietovarastosi ja datasi alkavat siirtyä. Tätä prosessia ei voi helposti peruuttaa.", "startMigration": "Aloita siirto", "starting": "Aloitetaan..." @@ -1313,9 +1049,7 @@ "hint": "Syötä koodi alle tai klikkaa sähköpostissa olevaa linkkiä jatkaaksesi automaattisesti.", "tokenLabel": "Vahvistuskoodi", "tokenPlaceholder": "Syötä sähköpostista saatu koodi", - "resend": "Lähetä koodi uudelleen", - "verify": "Vahvista sähköposti", - "verifying": "Vahvistetaan..." + "resend": "Lähetä koodi uudelleen" }, "plcToken": { "title": "Vahvista siirto", @@ -1423,7 +1157,6 @@ "desc": "Tarkista offline-palautuksen tiedot.", "carFile": "CAR-tiedosto", "rotationKey": "Rotaatioavain", - "warning": "Kun aloitat palautuksen, identiteettisi päivitetään osoittamaan tähän PDS:ään. Tätä ei voi helposti perua.", "plcWarningTitle": "Ei paluuta", "plcWarning": "Kun aloitat, DID-dokumenttisi päivitetään osoittamaan tähän PDS:ään. Jos jokin menee pieleen, voit käyttää rotaatioavaintasi palautumiseen, mutta sinun tulisi suorittaa siirto loppuun välttääksesi rikkinäisen identiteettitilan." }, @@ -1431,9 +1164,7 @@ "title": "Palautetaan tiliä", "desc": "Odota, tiliäsi palautetaan...", "creating": "Luodaan tili", - "importing": "Tuodaan tietovarastoa", - "plcSigning": "Päivitetään identiteettiä", - "activating": "Aktivoidaan tili" + "importing": "Tuodaan tietovarastoa" }, "success": { "desc": "Tilisi on palautettu onnistuneesti tähän PDS:ään." @@ -1443,28 +1174,8 @@ "desc": "Yritetään palauttaa kuvia ja mediaa vanhasta PDS:stäsi...", "migrating": "Siirretään blob-tiedostoja", "failedTitle": "Joitain blob-tiedostoja ei voitu siirtää", - "failedDesc": "{count} blob-tiedostoa ei voitu hakea vanhasta PDS:stäsi. Tämä voi tapahtua, jos palvelin ei ole tavoitettavissa tai tiedostot on poistettu.", - "sourceUnreachableTitle": "Lähde-PDS ei tavoitettavissa", - "sourceUnreachable": "Ei voitu yhdistää vanhaan PDS:ääsi mediatiedostojen hakemiseksi. Tämä on yleistä siirrettäessä suljetulta palvelimelta. Julkaisusi toimivat, mutta joitain kuvia saattaa puuttua." + "failedDesc": "{count} blob-tiedostoa ei voitu hakea vanhasta PDS:stäsi. Tämä voi tapahtua, jos palvelin ei ole tavoitettavissa tai tiedostot on poistettu." } - }, - "progress": { - "repoExported": "Tietovarasto viety", - "repoImported": "Tietovarasto tuotu", - "blobsMigrated": "{count} blob-tiedostoa siirretty", - "prefsMigrated": "Asetukset siirretty", - "plcSigned": "Identiteetti päivitetty", - "activated": "Tili aktivoitu", - "deactivated": "Vanha tili deaktivoitu" - }, - "errors": { - "connectionFailed": "Yhteys PDS:ään epäonnistui", - "invalidCredentials": "Virheelliset tunnukset", - "twoFactorRequired": "Kaksivaiheinen tunnistautuminen vaaditaan", - "accountExists": "Tili on jo olemassa kohde-PDS:ssä", - "plcFailed": "PLC-toiminto epäonnistui", - "blobFailed": "Blob-tiedoston siirto epäonnistui: {cid}", - "networkError": "Verkkovirhe. Yritä uudelleen." } } } diff --git a/frontend/src/locales/ja.json b/frontend/src/locales/ja.json index b3cfe52..b7aafeb 100644 --- a/frontend/src/locales/ja.json +++ b/frontend/src/locales/ja.json @@ -8,55 +8,23 @@ "done": "完了", "continue": "続行", "refresh": "更新", - "create": "作成", "delete": "削除", - "confirm": "確認", "created": "作成日時", - "expires": "有効期限", "name": "名前", - "dashboard": "ダッシュボード", "backToDashboard": "← ダッシュボード", "copied": "コピー完了", "copyToClipboard": "コピー", "verifying": "確認中", "saving": "保存中", "creating": "作成中", - "updating": "更新中", "sending": "送信中", - "authenticating": "認証中", "checking": "確認中", - "redirecting": "リダイレクト中", "signIn": "サインイン", "verify": "確認", - "remove": "削除", "revoke": "取り消し", "resendCode": "再送信", - "startOver": "やり直す", - "tryAgain": "再試行", - "password": "パスワード", - "email": "メール", - "emailAddress": "メールアドレス", - "handle": "ハンドル", - "did": "DID", - "verificationCode": "確認コード", - "inviteCode": "招待コード", - "newPassword": "新しいパスワード", - "confirmPassword": "パスワードを確認", - "enterSixDigitCode": "6桁のコードを入力", - "passwordHint": "8文字以上", - "enterPassword": "パスワードを入力", - "emailPlaceholder": "you@example.com", - "verified": "確認済み", - "disabled": "無効", - "available": "利用可能", - "deactivated": "非アクティブ", - "unverified": "未確認", "backToLogin": "ログインに戻る", - "backToSettings": "設定に戻る", - "alreadyHaveAccount": "すでにアカウントをお持ちですか?", - "createAccount": "アカウントを作成", - "passwordsMismatch": "パスワードが一致しません", - "passwordTooShort": "パスワードは8文字以上必要です" + "backToSettings": "設定に戻る" }, "login": { "title": "サインイン", @@ -75,18 +43,10 @@ "subtitle": "連絡先に送信されたコードを入力", "codeLabel": "コード", "codePlaceholder": "6桁のコード", - "verifyButton": "確認", "resent": "コード送信済み" }, "register": { "title": "アカウント作成", - "subtitle": "この PDS で新規アカウントを作成", - "subtitleKeyChoice": "did:web アイデンティティを設定", - "subtitleInitialDidDoc": "DID ドキュメントをアップロード", - "subtitleVerify": "{channel}を確認", - "subtitleUpdatedDidDoc": "DID ドキュメントを更新", - "subtitleActivating": "有効化中", - "subtitleComplete": "アカウント作成完了", "redirecting": "リダイレクト中", "migrateTitle": "すでにアカウントをお持ちですか?", "migrateDescription": "既存のアカウントを移行", @@ -115,8 +75,6 @@ "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 ドキュメントを提供する必要があります", @@ -125,7 +83,6 @@ "email": "メール", "emailAddress": "メールアドレス", "emailPlaceholder": "you@example.com", - "emailInUseWarning": "このメールアドレスは既に別のアカウントに関連付けられています。引き続き使用できますが、アカウント回復にはハンドルが必要になる場合があります。", "discord": "Discord", "discordId": "Discord ユーザー ID", "discordIdPlaceholder": "Discord ユーザー ID", @@ -144,13 +101,11 @@ "inviteCode": "招待コード", "inviteCodePlaceholder": "招待コードを入力", "inviteCodeRequired": "必須", - "createButton": "アカウントを作成", "alreadyHaveAccount": "すでにアカウントをお持ちですか?", "signIn": "サインイン", "passkeyAccount": "パスキー", "passwordAccount": "パスワード", "ssoAccount": "SSO", - "ssoSubtitle": "外部プロバイダーを使用してアカウントを作成", "noSsoProviders": "このサーバーにはSSOプロバイダーが設定されていません。", "continueWith": "{provider}で続行", "validation": { @@ -170,12 +125,13 @@ }, "dashboard": { "title": "ダッシュボード", + "accountManager": "アカウント管理", + "navDelegationAudit": "委任監査", "switchAccount": "アカウント切替", "addAnotherAccount": "別のアカウントを追加", "signOut": "@{handle} からサインアウト", "deactivatedTitle": "アカウント無効化", "deactivatedMessage": "アカウントは現在無効化されています。これは通常、アカウント移行中に発生します。アカウントが再有効化されるまで、一部の機能が制限される場合があります。", - "accountOverview": "アカウント概要", "handle": "ハンドル", "did": "DID", "primaryContact": "主要連絡先", @@ -184,38 +140,20 @@ "verified": "認証済み", "unverified": "未認証", "navAppPasswords": "アプリパスワード", - "navAppPasswordsDesc": "サードパーティアプリのパスワードを管理", "navSessions": "アクティブセッション", - "navSessionsDesc": "ログインセッションを表示・管理", "navInviteCodes": "招待コード", - "navInviteCodesDesc": "招待コードを表示・作成", - "navSettings": "アカウント設定", - "navSettingsDesc": "メール、パスワード、ハンドルなど", + "navSettings": "一般", "navSecurity": "セキュリティ", - "navSecurityDesc": "二要素認証", "navComms": "連絡設定", - "navCommsDesc": "Discord、Telegram、Signal チャンネル", "navRepo": "リポジトリエクスプローラー", - "navRepoDesc": "AT Protocol レコードを閲覧・管理", "navDelegation": "委任", - "navDelegationDesc": "アカウントコントローラーと委任アカウントを管理", "navAdmin": "管理パネル", - "navAdminDesc": "サーバー統計と管理操作", "navDidDocument": "DID ドキュメント", - "navDidDocumentDesc": "DID ドキュメントとキーを管理", - "navDidDocumentDescActive": "DID ドキュメント設定を編集", - "navBackup": "バックアップをダウンロード", - "navBackupDesc": "リポジトリを CAR ファイルとしてダウンロード", - "downloadingBackup": "ダウンロード中...", - "backupFailed": "バックアップのダウンロードに失敗しました", "migrated": "移行済み", "migratedTitle": "アカウント移行済み", - "migratedMessage": "アカウントは {pds} に移行されました。DID ドキュメントは引き続きここでホストされています。", - "navMigrateAgain": "再移行", - "navMigrateAgainDesc": "別の PDS に移行して DID ドキュメントを更新" + "migratedMessage": "アカウントは {pds} に移行されました。DID ドキュメントは引き続きここでホストされています。" }, "didEditor": { - "title": "DID ドキュメントエディター", "preview": "現在の DID ドキュメント", "verificationMethods": "検証方法(署名キー)", "verificationMethodsDesc": "DIDの代わりに動作できる署名キー。新しいPDSに移行する際は、そのPDSの署名キーをここに追加してください。", @@ -241,14 +179,11 @@ "saveFailed": "DIDドキュメントの保存に失敗しました", "loadFailed": "DIDドキュメントの読み込みに失敗しました", "invalidMultibase": "公開キーは'z'で始まる有効なmultibase文字列である必要があります", - "invalidHandle": "ハンドルはat:// URIである必要があります(例:at://handle.example.com)", "helpTitle": "これは何ですか?", "helpText": "別の PDS に移行すると、その PDS が新しい署名キーを生成します。ここで DID ドキュメントを更新して、新しいキーと場所を指すようにしてください。" }, "settings": { - "title": "アカウント設定", "language": "言語", - "languageDescription": "お好みの言語を選択", "changeEmail": "メール変更", "currentEmail": "現在: {email}", "newEmail": "新しいメール", @@ -266,7 +201,6 @@ "currentHandle": "現在: @{handle}", "pdsHandle": "PDS ハンドル", "customDomain": "カスタムドメイン", - "customDomainDescription": "独自のドメインをハンドルとして使用します。まずドメインの所有権を確認する必要があります。", "setupInstructions": "設定手順", "setupMethodsIntro": "以下の確認方法のいずれかを選択してください:", "dnsMethod": "方法 1: DNS TXT レコード(推奨)", @@ -280,33 +214,16 @@ "newHandle": "新しいハンドル", "newHandlePlaceholder": "yourhandle", "changeHandleButton": "ハンドルを変更", - "changePassword": "パスワード変更", - "currentPassword": "現在のパスワード", - "currentPasswordPlaceholder": "現在のパスワードを入力", - "newPassword": "新しいパスワード", - "newPasswordPlaceholder": "8文字以上", - "confirmNewPassword": "新しいパスワードの確認", - "confirmNewPasswordPlaceholder": "新しいパスワードを再入力", - "changePasswordButton": "パスワードを変更", - "changing": "変更中...", - "setPassword": "パスワードを設定", - "setPasswordDescription": "現在、あなたのアカウントはパスキーのみです。パスワードを追加すると、パスキーと併せて従来のログインも使用できます。", - "setPasswordButton": "パスワードを設定", - "setting": "設定中...", "exportData": "データエクスポート", - "exportDataDescription": "リポジトリ全体を CAR(Content Addressable Archive)ファイルとしてダウンロードします。投稿、いいね、フォローなどすべてのデータが含まれます。", "downloadRepo": "リポジトリをダウンロード", "downloadBlobs": "メディアをダウンロード", "exporting": "エクスポート中...", "backups": { "title": "バックアップ", - "description": "リポジトリは毎日自動的にバックアップされます。手動でバックアップを作成したり、以前のバックアップから復元することもできます。", - "enableAutomatic": "自動バックアップを有効にする", "enabled": "自動バックアップが有効です", "disabled": "自動バックアップが無効です", "toggleFailed": "バックアップ設定の更新に失敗しました", "noBackups": "バックアップはまだありません。", - "blocks": "ブロック", "download": "ダウンロード", "delete": "削除", "createNow": "今すぐバックアップを作成", @@ -316,13 +233,13 @@ "deleted": "バックアップが削除されました", "deleteFailed": "バックアップの削除に失敗しました", "restoreTitle": "バックアップから復元", - "restoreDescription": "CARファイルをアップロードしてリポジトリを復元します。現在のデータは上書きされます。", - "selectFile": "CARファイルを選択", "selectedFile": "選択されたファイル", "restore": "復元", "restoring": "復元中...", "restored": "リポジトリが正常に復元されました", - "restoreFailed": "リポジトリの復元に失敗しました" + "restoreFailed": "リポジトリの復元に失敗しました", + "autoBackup": "自動バックアップ", + "restoreHint": "CARファイルをアップロードしてリポジトリを復元" }, "deleteAccount": "アカウント削除", "deleteWarning": "この操作は取り消せません。すべてのデータが完全に削除されます。", @@ -334,21 +251,11 @@ "permanentlyDelete": "アカウントを完全に削除", "deleting": "削除中...", "messages": { - "emailCodeSent": "通知チャンネルに確認コードを送信しました", "emailCodeSentToCurrent": "現在のメールアドレスに確認コードを送信しました", "emailUpdated": "メールを更新しました", "emailUpdateFailed": "メールの更新に失敗しました", "handleUpdated": "ハンドルを更新しました", "handleUpdateFailed": "ハンドルの更新に失敗しました", - "passwordChanged": "パスワードを変更しました", - "passwordChangeFailed": "パスワードの変更に失敗しました", - "passwordSet": "パスワードを設定しました", - "passwordSetFailed": "パスワードの設定に失敗しました", - "passwordsMismatch": "パスワードが一致しません", - "passwordsDoNotMatch": "パスワードが一致しません", - "passwordLength": "パスワードは8文字以上である必要があります", - "passwordTooShort": "パスワードは8文字以上である必要があります", - "deletionCodeSent": "削除確認をメールに送信しました", "deletionConfirmationSent": "削除確認をメールに送信しました", "deletionRequestFailed": "アカウント削除リクエストに失敗しました", "deleteConfirmation": "本当にアカウントを削除しますか?この操作は取り消せません。", @@ -356,22 +263,13 @@ "repoExported": "リポジトリをエクスポートしました", "blobsExported": "メディアファイルをエクスポートしました", "noBlobsToExport": "エクスポートするメディアファイルがありません", - "exportFailed": "エクスポートに失敗しました", - "confirmDelete": "本当にアカウントを削除しますか?この操作は取り消せません。" + "exportFailed": "エクスポートに失敗しました" } }, "appPasswords": { - "title": "アプリパスワード", - "description": "アプリパスワードを使用すると、メインパスワードを提供せずにサードパーティアプリにサインインできます。各アプリパスワードは個別に取り消すことができます。", - "createNew": "新しいアプリパスワードを作成", - "appNamePlaceholder": "アプリ名(例: Graysky、Skeets)", "created": "アプリパスワードを作成しました", "createdMessage": "このパスワードを今すぐコピーしてください。再度表示することはできません。", - "yourPasswords": "アプリパスワード一覧", "noPasswords": "アプリパスワードはまだありません", - "revoke": "取り消す", - "revoking": "取り消し中...", - "revokeConfirm": "アプリパスワード「{name}」を取り消しますか?このパスワードを使用しているアプリはアカウントにアクセスできなくなります。", "saveWarningTitle": "重要: このアプリパスワードを保存してください!", "saveWarningMessage": "このパスワードはパスキーや OAuth をサポートしていないアプリにサインインするために必要です。一度しか表示されません。", "acknowledgeLabel": "アプリパスワードを安全な場所に保存しました", @@ -380,11 +278,18 @@ "scopeReadOnly": "読み取り専用", "scopePostOnly": "投稿のみ", "scopeCustom": "カスタム", - "byController": "管理者作成" + "byController": "管理者作成", + "create": "作成", + "name": "名前", + "namePlaceholder": "アプリ名(例:Graysky)", + "deleteConfirm": "アプリパスワード「{name}」を取り消しますか?", + "deleted": "アプリパスワードを取り消しました", + "loadFailed": "アプリパスワードの読み込みに失敗しました", + "createFailed": "アプリパスワードの作成に失敗しました", + "deleteFailed": "アプリパスワードの取り消しに失敗しました", + "saveWarning": "このパスワードを今すぐ保存してください - 再表示できません" }, "sessions": { - "title": "アクティブセッション", - "loadingSessions": "セッションを読み込み中...", "noSessions": "アクティブなセッションが見つかりません。", "current": "現在", "oauth": "OAuth", @@ -404,52 +309,43 @@ "daysAgo": "{count}日前", "hoursAgo": "{count}時間前", "minutesAgo": "{count}分前", - "justNow": "たった今" + "justNow": "たった今", + "sessionRevoked": "セッションを取り消しました", + "allSessionsRevoked": "他のすべてのセッションを取り消しました" }, "inviteCodes": { - "title": "招待コード", - "description": "招待コードで友人をこの PDS に招待できます。各コードは1回のみ使用可能です。", "createNew": "新しい招待コードを作成", - "uses": "使用回数", - "usesPlaceholder": "使用回数(1-100)", "yourCodes": "招待コード一覧", "noCodes": "招待コードはまだありません", "available": "利用可能", "used": "@{handle} が使用済み", "spent": "使用済み", "disabled": "無効", - "usedBy": "使用者", - "disableConfirm": "この招待コードを無効にしますか?使用できなくなります。", "created": "招待コードを作成しました", "copy": "コピー", - "createdOn": "{date} に作成" + "createdOn": "{date} に作成", + "loadFailed": "招待コードの読み込みに失敗しました", + "createFailed": "招待コードの作成に失敗しました" }, "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桁のコードを入力", + "totpCode": "TOTPコード", + "totpCodePlaceholder": "6桁", "verifyAndEnable": "確認して有効化", "backupCodes": "バックアップコード", "backupCodesDescription": "認証アプリにアクセスできなくなった場合、これらのコードを使用してサインインします。各コードは1回のみ使用可能です。", @@ -463,15 +359,6 @@ "enableLegacyLogin": "レガシーログインを有効にする", "disableLegacyLogin": "レガシーログインを無効にする", "legacyLoginWarning": "警告: レガシーログインを有効にすると、直接パスワードログインの MFA がバイパスされます。アプリの互換性が必要な場合にのみ有効にしてください。", - "totpPasswordWarning": "TOTP が有効な場合、Bluesky アプリ(または他のレガシーアプリ)からパスワードを変更することはできません。パスワードを変更するには、2つの方法があります:", - "totpPasswordOption1Label": "ここで変更する:", - "totpPasswordOption1Text": "このウェブサイトの", - "totpPasswordOption1Link": "設定ページ", - "totpPasswordOption1Suffix": "を使用して、認証アプリで確認できます。", - "totpPasswordOption2Label": "まずセッションを確認する:", - "totpPasswordOption2Text": "", - "totpPasswordOption2Link": "再認証オプション", - "totpPasswordOption2Suffix": "を使用して Bluesky セッションを TOTP で確認すると、一時的にパスワード変更が可能になります。", "legacyAppsTitle": "レガシーアプリとは?", "legacyAppsDescription": "一部のアプリ(公式 Bluesky アプリなど)は、パスワードのみを必要とする古い認証を使用します。MFA を有効にしている場合、これらのアプリは二要素認証をバイパスします。レガシーログインを無効にすると、すべてのアプリが OAuth を使用するよう強制され、MFA が適切に適用されます。", "password": "パスワード", @@ -479,13 +366,7 @@ "noPassword": "パスワードは設定されていません(パスキーのみのアカウント)", "setPassword": "パスワードを設定", "removePassword": "パスワードを削除", - "removePasswordConfirm": "パスワードを削除しますか?サインインにパスキーが必要になります。", "removing": "削除中...", - "loading": "読み込み中...", - "loadingPasskeys": "パスキーを読み込み中...", - "cancel": "キャンセル", - "save": "保存", - "back": "戻る", "next": "次へ: コードを確認", "copyToClipboard": "クリップボードにコピー", "savedMyCodes": "コードを保存しました", @@ -493,20 +374,10 @@ "unnamedPasskey": "名前のないパスキー", "added": "追加日", "lastUsed": "最終使用日", - "passwordDescription": "アカウントパスワードを管理します。パスキーを設定している場合、完全にパスワードレスな体験のためにパスワードを削除することもできます。", "disableTotpWarning": "これによりアカウントのセキュリティが低下します。", "removePasswordWarning": "これによりアカウントはパスキーのみになります。登録済みのパスキーでのみサインインできます。すべてのパスキーにアクセスできなくなった場合、通知チャンネルを使用してアカウントを復旧できます。", - "beforeProceeding": "続行する前に:", - "beforeProceedingItem1": "少なくとも1つの信頼できるパスキーが登録されていることを確認", - "beforeProceedingItem2": "複数のデバイスにパスキーを登録することを検討", - "beforeProceedingItem3": "復旧用の通知チャンネルが最新であることを確認", - "addPasskeyFirst": "パスワードを削除する前に、少なくとも1つのパスキーを追加してください。", - "passkeyOnlyHint": "パスキーのみでサインインしています。パスキーにアクセスできなくなった場合、ログインページの「パスキーを紛失しましたか?」リンクからアカウントを復旧できます。", - "addPasswordHint": "パスワードを追加しますか?設定で追加できます。", - "goToSettings": "設定へ移動", "trustedDevices": "信頼済みデバイス", - "trustedDevicesDescription": "サインイン時に二要素認証をスキップできるデバイスを管理します。信頼は30日間有効で、デバイスを使用すると自動的に延長されます。", - "manageTrustedDevices": "信頼済みデバイスを管理", + "trustedDevicesDescription": "サインイン時に二要素認証をスキップできるデバイス。信頼は30日間有効で、デバイスを使用すると自動的に延長されます。", "appCompatibility": "アプリ互換性", "enterPassword": "パスワードを入力", "sessionExpired": "セッションが期限切れです。再度ログインしてください。", @@ -524,13 +395,26 @@ "passkeyCreationCancelled": "パスキーの作成がキャンセルされました", "passkeyAddedSuccess": "パスキーが追加されました", "passkeyDeleted": "パスキーが削除されました", - "passkeyRenamed": "パスキーの名前が変更されました" + "passkeyRenamed": "パスキーの名前が変更されました", + "changePassword": "パスワードを変更", + "currentPassword": "現在のパスワード", + "currentPasswordPlaceholder": "現在のパスワードを入力", + "newPassword": "新しいパスワード", + "newPasswordPlaceholder": "新しいパスワードを入力", + "confirmPassword": "新しいパスワード(確認)", + "confirmPasswordPlaceholder": "新しいパスワードを再入力", + "passwordsDoNotMatch": "パスワードが一致しません", + "passwordTooShort": "パスワードは8文字以上である必要があります", + "passwordChanged": "パスワードを変更しました", + "failedToChangePassword": "パスワードの変更に失敗しました", + "changing": "変更中...", + "setting": "設定中...", + "passwordSet": "パスワードを設定しました", + "failedToSetPassword": "パスワードの設定に失敗しました", + "failedToDisableTotp": "TOTPの無効化に失敗しました" }, "comms": { - "title": "連絡設定", - "description": "パスワードリセット、セキュリティアラート、アカウント更新などの重要なメッセージの受信方法を選択してください。", "preferredChannel": "優先チャンネル", - "preferredChannelDescription": "メッセージの優先受信方法を選択してください。選択する前にチャンネルを設定する必要があります。", "channelConfiguration": "チャンネル設定", "emailVia": "メールでメッセージを受信", "discordVia": "Discord DM でメッセージを受信", @@ -538,10 +422,6 @@ "signalVia": "Signal でメッセージを受信", "configureToEnable": "有効にするには下記で設定", "notConfiguredOnServer": "このサーバーでは設定されていません", - "emailManagedInSettings": "メールはアカウント設定で管理されています", - "discordIdHint": "Discord ユーザー ID(ユーザー名ではありません)。Discord で開発者モードを有効にしてコピーしてください。", - "telegramHint": "@ 記号なしの Telegram ユーザー名", - "signalHint": "国番号付きの Signal 電話番号", "primary": "優先", "verified": "確認済み", "notVerified": "未確認", @@ -552,29 +432,20 @@ "preferencesSaved": "連絡設定を保存しました", "verifiedSuccess": "{channel} を確認しました", "messageHistory": "メッセージ履歴", - "historyDescription": "アカウントに送信された最近のメッセージを表示します。", - "loadHistory": "履歴を読み込む", - "hideHistory": "履歴を隠す", "noMessages": "メッセージが見つかりません。", - "sent": "送信済み", - "failed": "失敗", "discordInUseWarning": "この Discord ID は既に別のアカウントに関連付けられています。", "telegramInUseWarning": "この Telegram ユーザー名は既に別のアカウントに関連付けられています。", - "signalInUseWarning": "この Signal 番号は既に別のアカウントに関連付けられています。" + "signalInUseWarning": "この Signal 番号は既に別のアカウントに関連付けられています。", + "failedToLoad": "設定の読み込みに失敗しました", + "failedToSave": "設定の保存に失敗しました", + "failedToVerify": "確認に失敗しました", + "failedToLoadHistory": "メッセージ履歴の読み込みに失敗しました" }, "repoExplorer": { - "title": "リポジトリエクスプローラー", - "description": "AT Protocol レコードを閲覧・管理します。", "collections": "コレクション", - "noCollections": "コレクションが見つかりません", - "records": "レコード", "noRecords": "このコレクションにレコードはありません", - "recordDetails": "レコード詳細", - "rkey": "レコードキー", "uri": "URI", "cid": "CID", - "value": "値", - "deleteRecord": "レコードを削除", "deleteConfirm": "レコード {rkey} を削除しますか?この操作は取り消せません。", "unknownError": "不明なエラーが発生しました", "invalidJson": "無効な JSON", @@ -587,7 +458,6 @@ "filterCollections": "コレクションを検索...", "filterRecords": "レコードを検索...", "noCollectionsYet": "コレクションがまだありません。最初のレコードを作成して開始しましょう。", - "loadMore": "さらに読み込む", "recordJson": "レコード JSON", "updateRecord": "レコードを更新", "collectionNsid": "コレクション (NSID)", @@ -599,8 +469,6 @@ "demoBio": "自己紹介を書いてください。" }, "admin": { - "title": "管理パネル", - "loading": "読み込み中...", "serverConfig": "サーバー設定", "serverName": "サーバー名", "serverNamePlaceholder": "マイ PDS", @@ -623,8 +491,6 @@ "refreshStats": "統計を更新", "userManagement": "ユーザー管理", "searchPlaceholder": "ハンドルで検索(任意)", - "searchUsers": "ユーザーを検索", - "noUsers": "ユーザーが見つかりません", "handle": "ハンドル", "email": "メール", "status": "ステータス", @@ -634,10 +500,8 @@ "loadInviteCodes": "招待コードを読み込む", "refresh": "更新", "noInvites": "招待コードが見つかりません", - "code": "コード", "available": "利用可能", "uses": "使用回数", - "actions": "アクション", "disable": "無効化", "disableInviteConfirm": "招待コード {code} を無効にしますか?", "active": "アクティブ", @@ -654,9 +518,17 @@ "verified": "確認済み", "unverified": "未確認", "deactivated": "無効化", - "colorDefault": "{color}(デフォルト)", "secondaryLight": "セカンダリ(ライトモード)", - "secondaryDark": "セカンダリ(ダークモード)" + "secondaryDark": "セカンダリ(ダークモード)", + "failedToLoadStats": "サーバー統計の読み込みに失敗しました", + "failedToLoadUsers": "ユーザーの読み込みに失敗しました", + "searchToSeeUsers": "検索してユーザーを表示", + "search": "検索", + "inviteDisabled": "招待コードを無効にしました", + "invitesEnabled": "ユーザー招待を有効にしました", + "invitesDisabled": "ユーザー招待を無効にしました", + "userDeleted": "ユーザーアカウントを削除しました", + "failedToLoadConfig": "サーバー設定の読み込みに失敗しました" }, "oauth": { "login": { @@ -675,7 +547,6 @@ "passkeyHintNotAvailable": "パスキーなし", "passwordPlaceholder": "パスワード", "usePasskey": "パスキーを使用", - "orContinueWith": "または", "orUseCredentials": "または" }, "register": { @@ -686,77 +557,31 @@ }, "sso": { "linkedAccounts": "連携アカウント", - "linkedAccountsDesc": "シングルサインオン用に連携された外部アカウント。", "noLinkedAccounts": "連携アカウントなし", - "noLinkedAccountsDesc": "外部アカウントを連携して、そのプロバイダーでのクイックサインインを有効にします。", - "linkAccount": "アカウントを連携", - "unlinkAccount": "連携解除", "unlinkConfirm": "このアカウントの連携を解除しますか?", "unlinked": "{provider} の連携を解除しました", - "lastLoginAt": "最終使用", - "linkedAt": "連携日時" + "linkedAt": "連携日時", + "unlink": "連携解除", + "linkNewAccount": "アカウント連携", + "linked": "連携済み", + "linkSuccess": "アカウントを連携しました", + "linkFailed": "アカウントの連携に失敗しました", + "unlinkFailed": "連携解除に失敗しました" }, "consent": { "title": "アプリを承認", "appWantsAccess": "{app} があなたのアカウントにアクセスしようとしています", - "permissions": "このアプリは以下のことができるようになります:", - "readProfile": "プロフィール情報を読み取る", - "readPosts": "投稿とコンテンツを読み取る", - "writePosts": "あなたに代わって投稿を作成・削除する", - "readNotifications": "通知を読み取る", - "fullAccess": "アカウントへのフルアクセス", "authorize": "承認", "deny": "拒否", "authorizing": "承認中...", - "rememberChoice": "この選択を記憶", "signingInAs": "サインイン中のアカウント:", "permissionsRequested": "リクエストされた権限", "required": "必須", "rememberChoiceLabel": "このアプリに対する選択を記憶する", "scopes": { - "atproto": { - "name": "フルアクセス", - "description": "このアカウントの読み取り、書き込み、管理へのフルアクセス" - }, "atprotoWithGranular": { "name": "AT Protocol アクセス", "description": "AT Protocol 基本スコープ(権限は以下で選択したオプションによって決まります)" - }, - "transitionGeneric": { - "name": "移行アクセス", - "description": "互換性のための汎用移行スコープ" - }, - "transitionChat": { - "name": "チャットアクセス", - "description": "Blueskyチャット機能へのアクセス" - }, - "transitionEmail": { - "name": "メールアクセス", - "description": "アカウントのメールアドレスを読み取る" - }, - "repoCreate": { - "name": "レコード作成", - "description": "リポジトリに新しいレコードを作成" - }, - "repoUpdate": { - "name": "レコード更新", - "description": "リポジトリの既存レコードを更新" - }, - "repoDelete": { - "name": "レコード削除", - "description": "リポジトリからレコードを削除" - }, - "blobAll": { - "name": "メディアアップロード", - "description": "画像、動画、その他のメディアファイルをアップロード" - }, - "repoFull": { - "name": "リポジトリフルアクセス", - "description": "すべてのリポジトリレコードへのフル読み書きアクセス" - }, - "accountManage": { - "name": "アカウント管理", - "description": "アカウント設定と設定を管理" } }, "unexpectedState": { @@ -769,11 +594,6 @@ "title": "アカウントを選択", "useAnother": "別のアカウントを使用" }, - "twoFactor": { - "title": "確認", - "usePasskey": "パスキーを使用", - "useTotp": "認証アプリを使用" - }, "twoFactorCode": { "title": "確認", "subtitle": "{channel} にコード送信済み", @@ -789,21 +609,14 @@ "totp": { "title": "認証コード", "codePlaceholder": "6桁のコード", - "useBackupCode": "バックアップコードを使用", "backupCodePlaceholder": "バックアップコード", "trustDevice": "このデバイスを30日間信頼", "hintBackupCode": "バックアップコード", "hintTotpCode": "認証コード" }, - "passkey": { - "title": "パスキー", - "waiting": "待機中", - "useTotp": "認証アプリを使用" - }, "error": { "title": "承認失敗", - "tryAgain": "再試行", - "backToApp": "戻る" + "tryAgain": "再試行" } }, "sso_register": { @@ -829,10 +642,9 @@ "subtitle": "{channel} に確認コードを送信しました。以下に入力して登録を完了してください。", "tokenTitle": "確認", "tokenSubtitle": "確認コードと送信先の識別子を入力してください。", - "codePlaceholder": "Paste verification code", + "codePlaceholder": "認証コードを貼り付け", "codeLabel": "確認コード", "codeHelp": "完全なコードをメッセージからコピーしてください", - "verifyButton": "アカウントを確認", "pleaseWait": "お待ちください...", "codeResent": "確認コードを再送信しました!", "codeResentDetail": "確認コードを送信しました!受信トレイを確認してください。", @@ -874,8 +686,6 @@ "sendCode": "リセットコードを送信", "sending": "送信中...", "codeSent": "パスワードリセットコードを送信しました!優先通知チャンネルを確認してください。", - "multipleAccountsWarning": "複数のアカウントがこのメールを共有しています。リセットコードは最後に作成されたアカウントに送信されました。特定のアカウントにはハンドルを使用してください。", - "enterCode": "受け取ったコードと新しいパスワードを入力してください。", "code": "リセットコード", "codePlaceholder": "リセットコードを入力", "newPassword": "新しいパスワード", @@ -931,32 +741,8 @@ "sending": "送信中..." }, "registerPasskey": { - "title": "パスキーアカウントを作成", - "subtitleKeyChoice": "did:web アイデンティティを設定", - "subtitleInitialDidDoc": "DID ドキュメントをアップロード", - "subtitleCreating": "アカウント作成中", - "subtitlePasskey": "パスキーを登録", - "subtitleAppPassword": "アプリパスワードを保存", - "subtitleVerify": "{channel}を確認", - "subtitleUpdatedDidDoc": "DID ドキュメントを更新", - "subtitleActivating": "有効化中", - "subtitleComplete": "アカウント作成完了", - "handle": "ハンドル", - "handlePlaceholder": "あなたの名前", - "handleHint": "完全なハンドル: @{handle}", - "contactMethod": "連絡方法", - "verificationMethod": "確認方法", - "email": "メールアドレス", - "emailPlaceholder": "you@example.com", - "inviteCode": "招待コード", - "inviteCodePlaceholder": "招待コードを入力", "externalDid": "あなたの did:web", "externalDidPlaceholder": "did:web:yourdomain.com", - "createButton": "アカウントを作成", - "alreadyHaveAccount": "すでにアカウントをお持ちですか?", - "signIn": "サインイン", - "wantPassword": "パスワードを使用しますか?", - "createPasswordAccount": "パスワードアカウントを作成", "errors": { "handleRequired": "ハンドルは必須です", "handleNoDots": "ハンドルにドットは使用できません。アカウント作成後にカスタムドメインを設定できます。", @@ -971,7 +757,6 @@ "externalDidFormat": "外部DIDはdid:web:で始まる必要があります", "discordRequired": "Discord認証にはDiscord IDが必要です" }, - "creatingPasskey": "作成中", "identityType": "アイデンティティタイプ", "identityTypeHint": "分散型アイデンティティの管理方法を選択してください。", "passkeyNamePlaceholder": "例:MacBook Touch ID", @@ -994,29 +779,17 @@ "didWebWarning4": "推奨事項:", "didWebWarning4Detail": "did:webを好む特別な理由がない限り、did:plcを選択してください。", "externalDidHint": "以下の場所でDIDドキュメントを提供する必要があります", - "continue": "続行", - "back": "戻る", - "loading": "読み込み中...", - "redirecting": "ダッシュボードに移動中...", - "handleDotWarning": "カスタムドメインハンドルはアカウント作成後に設定できます。", - "wantTraditional": "従来のパスワードを使用しますか?", - "registerWithPassword": "パスワードで登録", - "activatingAccount": "Activating", - "creatingAccount": "Creating account", - "passkeyDescription": "Register a passkey for this account", - "passkeyName": "Passkey Name", - "setupPasskey": "Create Passkey" + "activatingAccount": "有効化中", + "creatingAccount": "アカウント作成中", + "passkeyDescription": "このアカウントにパスキーを登録", + "passkeyName": "パスキー名", + "setupPasskey": "パスキーを作成" }, "trustedDevices": { - "title": "信頼済みデバイス", - "backToSecurity": "← セキュリティ設定", - "description": "信頼済みデバイスはログイン時に二要素認証をスキップできます。信頼は30日間有効で、デバイスを使用すると自動的に延長されます。", - "failedToLoad": "信頼済みデバイスの読み込みに失敗しました", "noDevices": "信頼済みデバイスはまだありません。", "noDevicesHint": "二要素認証を有効にしてログインする際に、デバイスを30日間信頼することを選択できます。", "lastSeen": "最終使用:", "trustedSince": "信頼開始:", - "trustExpires": "信頼期限:", "expired": "期限切れ", "tomorrow": "明日", "inDays": "あと{days}日", @@ -1025,7 +798,6 @@ "deviceRevoked": "デバイスの信頼を取り消しました", "deviceRenamed": "デバイス名を変更しました", "deviceNamePlaceholder": "デバイス名", - "browser": "ブラウザ:", "unknownDevice": "不明なデバイス" }, "reauth": { @@ -1034,36 +806,11 @@ "totp": "TOTP", "passkey": "パスキー", "authenticatorCode": "認証コード", - "usePassword": "パスワード", "usePasskey": "パスキー", - "useTotp": "認証アプリ", - "passwordPlaceholder": "パスワードを入力", - "totpPlaceholder": "6桁のコード", "authenticating": "認証中", "cancel": "キャンセル" }, - "verifyChannel": { - "title": "チャンネル認証", - "subtitle": "通知チャンネルに送信された認証コードを入力してください。", - "signInRequired": "ログインが必要です", - "signInRequiredDesc": "チャンネルを認証するにはログインが必要です。", - "signIn": "ログイン", - "verifying": "認証中...", - "pleaseWait": "チャンネルを認証しています。しばらくお待ちください。", - "successTitle": "認証完了!", - "successDesc": "{channel} が正常に認証されました。", - "backToSettings": "設定に戻る", - "channelLabel": "チャンネル", - "selectChannel": "チャンネルを選択...", - "identifierLabel": "識別子", - "identifierPlaceholder": "メール、Discord ID など", - "identifierHelp": "認証するメールアドレス、Discord ID、Telegram ユーザー名、または Signal 番号。", - "codeLabel": "認証コード", - "codeHelp": "メッセージからハイフンを含む完全なコードをコピーしてください。", - "verifyButton": "認証" - }, "delegation": { - "title": "アカウント委任", "controllers": "コントローラー", "controlledAccounts": "管理アカウント", "noControllers": "コントローラーはまだいません", @@ -1076,13 +823,7 @@ "scopeCustom": "カスタム", "actAs": "として行動", "auditLog": "監査ログ", - "auditLogTitle": "委任監査ログ", - "backToControllers": "← コントローラーに戻る", - "loading": "読み込み中...", - "noActivity": "アクティビティはまだありません", "actor": "アクター", - "controller": "コントローラー", - "account": "アカウント", "details": "詳細", "actionGrantCreated": "許可作成", "actionGrantRevoked": "許可取り消し", @@ -1093,9 +834,7 @@ "actionAccountAction": "アカウントアクション", "previous": "前へ", "next": "次へ", - "showing": "{start}~{end} / {total}件", "refresh": "更新", - "failedToLoadAuditLog": "監査ログの読み込みに失敗しました", "adding": "追加中...", "accessLevel": "アクセスレベル", "addControllerButton": "+ コントローラーを追加", @@ -1117,25 +856,24 @@ "createDelegatedAccount": "委任アカウントを作成", "createDelegatedAccountButton": "+ 委任アカウントを作成", "emailOptional": "メール(任意)", - "failedToAddController": "コントローラーの追加に失敗しました", - "failedToCreateAccount": "委任アカウントの作成に失敗しました", - "failedToRemoveController": "コントローラーの削除に失敗しました", "granted": "許可日", "inactive": "非アクティブ", "remove": "削除", "removeConfirm": "このコントローラーを削除しますか?", "viewAuditLog": "監査ログを表示", "yourAccessLevel": "あなたのアクセスレベル", - "accountCreated": "委任アカウントを作成しました: {handle}" + "accountCreated": "委任アカウントを作成しました: {handle}", + "noAuditEntries": "監査エントリなし", + "target": "対象", + "pageInfo": "{start} - {end} / {total}", + "failedToLoadAudit": "監査ログの読み込みに失敗しました" }, "actAs": { "title": "として行動", "noAccountSpecified": "アカウントDIDが指定されていません", - "failedToVerify": "アカウントへのアクセスを確認できませんでした", "noAccess": "このアカウントへのアクセス権がありません", "failedToInitiate": "認証の開始に失敗しました", "invalidResponse": "サーバーからの応答が無効です", - "failedError": "失敗しました: {error}", "preparing": "委任アカウントへのログインを準備中...", "backToControllers": "コントローラーに戻る" }, @@ -1185,8 +923,6 @@ "migration": { "title": "アカウント移行", "subtitle": "AT Protocolアイデンティティをサーバー間で移動", - "navTitle": "移行", - "navDesc": "別のPDSへ、または別のPDSからアカウントを移動", "migrateHere": "ここに移行", "migrateHereDesc": "既存のAT ProtocolアカウントをこのPDSに移動します。", "bringDid": "DIDとアイデンティティを持ち込む", @@ -1252,6 +988,7 @@ "checkingAvailability": "利用可能か確認中...", "handleAvailable": "ハンドルは利用可能です!", "handleTaken": "このハンドルは既に使用されています", + "handleTooShort": "ハンドルは3文字以上必要です", "handleHint": "フルハンドル(例:alice.mydomain.com)を入力して独自ドメインを使用することもできます", "email": "メールアドレス", "authMethod": "認証方法", @@ -1276,7 +1013,6 @@ "authentication": "認証", "authPasskey": "パスキー(パスワードレス)", "authPassword": "パスワード", - "inviteCode": "招待コード", "warning": "「移行を開始」をクリックすると、リポジトリとデータの転送が始まります。このプロセスは簡単に元に戻すことができません。", "startMigration": "移行を開始", "starting": "開始中..." @@ -1313,9 +1049,7 @@ "hint": "下記にコードを入力するか、メール内のリンクをクリックして自動的に続行できます。", "tokenLabel": "確認コード", "tokenPlaceholder": "メールに記載されたコードを入力", - "resend": "コードを再送信", - "verify": "メールを確認", - "verifying": "確認中..." + "resend": "コードを再送信" }, "plcToken": { "title": "移行を確認", @@ -1423,7 +1157,6 @@ "desc": "オフライン復元の詳細を確認してください。", "carFile": "CARファイル", "rotationKey": "ローテーションキー", - "warning": "復元を開始すると、アイデンティティがこのPDSを指すように更新されます。これは簡単に元に戻すことができません。", "plcWarningTitle": "引き返せないポイント", "plcWarning": "開始すると、DIDドキュメントがこのPDSを指すように更新されます。問題が発生した場合はローテーションキーを使用して回復できますが、壊れたアイデンティティ状態を避けるために移行を完了する必要があります。" }, @@ -1431,9 +1164,7 @@ "title": "アカウントを復元中", "desc": "アカウントを復元しています...", "creating": "アカウントを作成中", - "importing": "リポジトリをインポート中", - "plcSigning": "アイデンティティを更新中", - "activating": "アカウントをアクティベート中" + "importing": "リポジトリをインポート中" }, "success": { "desc": "アカウントはこのPDSに正常に復元されました。" @@ -1443,28 +1174,8 @@ "desc": "古いPDSから画像とメディアの復元を試みています...", "migrating": "Blobを移行中", "failedTitle": "一部のBlobを移行できませんでした", - "failedDesc": "{count}個のBlobを古いPDSから取得できませんでした。サーバーに接続できないか、ファイルが削除された可能性があります。", - "sourceUnreachableTitle": "ソースPDSに接続できません", - "sourceUnreachable": "古いPDSに接続してメディアファイルを取得できませんでした。シャットダウンしたサーバーからの移行ではよくあることです。投稿は機能しますが、一部の画像が欠落する可能性があります。" + "failedDesc": "{count}個のBlobを古いPDSから取得できませんでした。サーバーに接続できないか、ファイルが削除された可能性があります。" } - }, - "progress": { - "repoExported": "リポジトリをエクスポートしました", - "repoImported": "リポジトリをインポートしました", - "blobsMigrated": "{count}個のblobを移行しました", - "prefsMigrated": "設定を移行しました", - "plcSigned": "アイデンティティを更新しました", - "activated": "アカウントを有効化しました", - "deactivated": "古いアカウントを無効化しました" - }, - "errors": { - "connectionFailed": "PDSに接続できませんでした", - "invalidCredentials": "認証情報が無効です", - "twoFactorRequired": "2要素認証が必要です", - "accountExists": "移行先PDSにアカウントが既に存在します", - "plcFailed": "PLC操作に失敗しました", - "blobFailed": "blobの移行に失敗しました: {cid}", - "networkError": "ネットワークエラー。再試行してください。" } } } diff --git a/frontend/src/locales/ko.json b/frontend/src/locales/ko.json index db1c5d2..48e5d78 100644 --- a/frontend/src/locales/ko.json +++ b/frontend/src/locales/ko.json @@ -8,55 +8,23 @@ "done": "완료", "continue": "계속", "refresh": "새로고침", - "create": "생성", "delete": "삭제", - "confirm": "확인", "created": "생성일", - "expires": "만료일", "name": "이름", - "dashboard": "대시보드", "backToDashboard": "← 대시보드", "copied": "복사됨", "copyToClipboard": "복사", "verifying": "확인 중", "saving": "저장 중", "creating": "생성 중", - "updating": "업데이트 중", "sending": "전송 중", - "authenticating": "인증 중", "checking": "확인 중", - "redirecting": "리디렉션 중", "signIn": "로그인", "verify": "확인", - "remove": "삭제", "revoke": "취소", "resendCode": "재전송", - "startOver": "처음부터 다시", - "tryAgain": "재시도", - "password": "비밀번호", - "email": "이메일", - "emailAddress": "이메일 주소", - "handle": "핸들", - "did": "DID", - "verificationCode": "인증 코드", - "inviteCode": "초대 코드", - "newPassword": "새 비밀번호", - "confirmPassword": "비밀번호 확인", - "enterSixDigitCode": "6자리 코드 입력", - "passwordHint": "8자 이상", - "enterPassword": "비밀번호를 입력하세요", - "emailPlaceholder": "you@example.com", - "verified": "인증됨", - "disabled": "비활성화됨", - "available": "사용 가능", - "deactivated": "비활성화됨", - "unverified": "미인증", "backToLogin": "로그인으로 돌아가기", - "backToSettings": "설정으로 돌아가기", - "alreadyHaveAccount": "이미 계정이 있으신가요?", - "createAccount": "계정 만들기", - "passwordsMismatch": "비밀번호가 일치하지 않습니다", - "passwordTooShort": "비밀번호는 8자 이상이어야 합니다" + "backToSettings": "설정으로 돌아가기" }, "login": { "title": "로그인", @@ -75,18 +43,10 @@ "subtitle": "연락처로 전송된 코드를 입력하세요", "codeLabel": "코드", "codePlaceholder": "6자리 코드", - "verifyButton": "인증", "resent": "코드 전송됨" }, "register": { "title": "계정 만들기", - "subtitle": "이 PDS에 새 계정을 만듭니다", - "subtitleKeyChoice": "did:web 신원 설정", - "subtitleInitialDidDoc": "DID 문서 업로드", - "subtitleVerify": "{channel} 인증", - "subtitleUpdatedDidDoc": "DID 문서 업데이트", - "subtitleActivating": "활성화 중", - "subtitleComplete": "계정 생성됨", "redirecting": "리디렉션 중", "migrateTitle": "이미 계정이 있으신가요?", "migrateDescription": "기존 계정 마이그레이션", @@ -115,8 +75,6 @@ "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 문서를 제공해야 합니다", @@ -125,7 +83,6 @@ "email": "이메일", "emailAddress": "이메일 주소", "emailPlaceholder": "you@example.com", - "emailInUseWarning": "이 이메일은 이미 다른 계정과 연결되어 있습니다. 계속 사용할 수 있지만, 계정 복구 시 핸들이 필요할 수 있습니다.", "discord": "Discord", "discordId": "Discord 사용자 ID", "discordIdPlaceholder": "Discord 사용자 ID", @@ -144,13 +101,11 @@ "inviteCode": "초대 코드", "inviteCodePlaceholder": "초대 코드 입력", "inviteCodeRequired": "필수", - "createButton": "계정 만들기", "alreadyHaveAccount": "이미 계정이 있으신가요?", "signIn": "로그인", "passkeyAccount": "패스키", "passwordAccount": "비밀번호", "ssoAccount": "SSO", - "ssoSubtitle": "외부 제공자를 사용하여 계정 만들기", "noSsoProviders": "이 서버에 SSO 제공자가 설정되어 있지 않습니다.", "continueWith": "{provider}로 계속", "validation": { @@ -170,12 +125,13 @@ }, "dashboard": { "title": "대시보드", + "accountManager": "계정 관리", + "navDelegationAudit": "위임 감사", "switchAccount": "계정 전환", "addAnotherAccount": "다른 계정 추가", "signOut": "@{handle} 로그아웃", "deactivatedTitle": "계정 비활성화됨", "deactivatedMessage": "계정이 현재 비활성화되어 있습니다. 이는 일반적으로 계정 마이그레이션 중에 발생합니다. 계정이 다시 활성화될 때까지 일부 기능이 제한될 수 있습니다.", - "accountOverview": "계정 개요", "handle": "핸들", "did": "DID", "primaryContact": "주요 연락처", @@ -184,38 +140,20 @@ "verified": "인증됨", "unverified": "미인증", "navAppPasswords": "앱 비밀번호", - "navAppPasswordsDesc": "타사 앱의 비밀번호 관리", "navSessions": "활성 세션", - "navSessionsDesc": "로그인 세션 보기 및 관리", "navInviteCodes": "초대 코드", - "navInviteCodesDesc": "초대 코드 보기 및 생성", - "navSettings": "계정 설정", - "navSettingsDesc": "이메일, 비밀번호, 핸들 등", + "navSettings": "일반", "navSecurity": "보안", - "navSecurityDesc": "2단계 인증", "navComms": "통신 설정", - "navCommsDesc": "Discord, Telegram, Signal 채널", "navRepo": "저장소 탐색기", - "navRepoDesc": "AT Protocol 레코드 탐색 및 관리", "navDelegation": "위임", - "navDelegationDesc": "계정 컨트롤러 및 위임된 계정 관리", "navAdmin": "관리 패널", - "navAdminDesc": "서버 통계 및 관리 작업", "navDidDocument": "DID 문서", - "navDidDocumentDesc": "DID 문서 및 키 관리", - "navDidDocumentDescActive": "DID 문서 설정 편집", - "navBackup": "백업 다운로드", - "navBackupDesc": "저장소를 CAR 파일로 다운로드", - "downloadingBackup": "다운로드 중...", - "backupFailed": "백업 다운로드 실패", "migrated": "마이그레이션됨", "migratedTitle": "계정 마이그레이션됨", - "migratedMessage": "계정이 {pds}로 마이그레이션되었습니다. DID 문서는 여전히 여기에서 호스팅됩니다.", - "navMigrateAgain": "다시 마이그레이션", - "navMigrateAgainDesc": "다른 PDS로 이동하고 DID 문서 업데이트" + "migratedMessage": "계정이 {pds}로 마이그레이션되었습니다. DID 문서는 여전히 여기에서 호스팅됩니다." }, "didEditor": { - "title": "DID 문서 편집기", "preview": "현재 DID 문서", "verificationMethods": "검증 방법 (서명 키)", "verificationMethodsDesc": "DID를 대신하여 동작할 수 있는 서명 키입니다. 새 PDS로 마이그레이션할 때 해당 서명 키를 여기에 추가하세요.", @@ -241,14 +179,11 @@ "saveFailed": "DID 문서 저장에 실패했습니다", "loadFailed": "DID 문서 로드에 실패했습니다", "invalidMultibase": "공개 키는 'z'로 시작하는 유효한 multibase 문자열이어야 합니다", - "invalidHandle": "핸들은 at:// URI여야 합니다 (예: at://handle.example.com)", "helpTitle": "이것은 무엇인가요?", "helpText": "다른 PDS로 마이그레이션하면 해당 PDS가 새 서명 키를 생성합니다. 여기에서 DID 문서를 업데이트하여 새 키와 위치를 가리키도록 하세요." }, "settings": { - "title": "계정 설정", "language": "언어", - "languageDescription": "선호하는 언어를 선택하세요", "changeEmail": "이메일 변경", "currentEmail": "현재: {email}", "newEmail": "새 이메일", @@ -266,7 +201,6 @@ "currentHandle": "현재: @{handle}", "pdsHandle": "PDS 핸들", "customDomain": "사용자 정의 도메인", - "customDomainDescription": "자체 도메인을 핸들로 사용합니다. 먼저 도메인 소유권을 확인해야 합니다.", "setupInstructions": "설정 지침", "setupMethodsIntro": "다음 인증 방법 중 하나를 선택하세요:", "dnsMethod": "방법 1: DNS TXT 레코드 (권장)", @@ -280,33 +214,16 @@ "newHandle": "새 핸들", "newHandlePlaceholder": "yourhandle", "changeHandleButton": "핸들 변경", - "changePassword": "비밀번호 변경", - "currentPassword": "현재 비밀번호", - "currentPasswordPlaceholder": "현재 비밀번호 입력", - "newPassword": "새 비밀번호", - "newPasswordPlaceholder": "8자 이상", - "confirmNewPassword": "새 비밀번호 확인", - "confirmNewPasswordPlaceholder": "새 비밀번호 재입력", - "changePasswordButton": "비밀번호 변경", - "changing": "변경 중...", - "setPassword": "비밀번호 설정", - "setPasswordDescription": "현재 계정은 패스키 전용입니다. 비밀번호를 추가하면 패스키와 함께 기존 로그인 방식도 사용할 수 있습니다.", - "setPasswordButton": "비밀번호 설정", - "setting": "설정 중...", "exportData": "데이터 내보내기", - "exportDataDescription": "전체 저장소를 CAR (Content Addressable Archive) 파일로 다운로드합니다. 모든 게시물, 좋아요, 팔로우 및 기타 데이터가 포함됩니다.", "downloadRepo": "저장소 다운로드", "downloadBlobs": "미디어 다운로드", "exporting": "내보내기 중...", "backups": { "title": "백업", - "description": "자동 백업을 관리하고 계정 데이터를 복원하세요. 백업에는 모든 기록과 blob이 포함됩니다.", - "enableAutomatic": "자동 백업", "enabled": "활성화됨", "disabled": "비활성화됨", "toggleFailed": "백업 설정 변경 실패", "noBackups": "아직 백업이 없습니다", - "blocks": "블록", "download": "다운로드", "delete": "삭제", "createNow": "지금 백업 생성", @@ -316,13 +233,13 @@ "deleted": "백업이 삭제되었습니다", "deleteFailed": "백업 삭제 실패", "restoreTitle": "백업에서 복원", - "restoreDescription": "이전에 내보낸 CAR 파일에서 계정 데이터를 복원합니다. 이렇게 하면 현재 저장소가 업로드한 백업으로 교체됩니다.", - "selectFile": "CAR 파일 선택", "selectedFile": "선택된 파일", "restore": "백업 복원", "restoring": "복원 중...", "restored": "백업이 성공적으로 복원되었습니다", - "restoreFailed": "백업 복원 실패" + "restoreFailed": "백업 복원 실패", + "autoBackup": "자동 백업", + "restoreHint": "CAR 파일을 업로드하여 저장소 복원" }, "deleteAccount": "계정 삭제", "deleteWarning": "이 작업은 되돌릴 수 없습니다. 모든 데이터가 영구적으로 삭제됩니다.", @@ -334,21 +251,11 @@ "permanentlyDelete": "계정 영구 삭제", "deleting": "삭제 중...", "messages": { - "emailCodeSent": "알림 채널로 인증 코드를 보냈습니다", "emailCodeSentToCurrent": "현재 이메일 주소로 인증 코드를 보냈습니다", "emailUpdated": "이메일이 업데이트되었습니다", "emailUpdateFailed": "이메일 업데이트에 실패했습니다", "handleUpdated": "핸들이 업데이트되었습니다", "handleUpdateFailed": "핸들 업데이트에 실패했습니다", - "passwordChanged": "비밀번호가 변경되었습니다", - "passwordChangeFailed": "비밀번호 변경에 실패했습니다", - "passwordSet": "비밀번호가 설정되었습니다", - "passwordSetFailed": "비밀번호 설정에 실패했습니다", - "passwordsMismatch": "비밀번호가 일치하지 않습니다", - "passwordsDoNotMatch": "비밀번호가 일치하지 않습니다", - "passwordLength": "비밀번호는 8자 이상이어야 합니다", - "passwordTooShort": "비밀번호는 8자 이상이어야 합니다", - "deletionCodeSent": "이메일로 삭제 확인을 보냈습니다", "deletionConfirmationSent": "이메일로 삭제 확인을 보냈습니다", "deletionRequestFailed": "계정 삭제 요청에 실패했습니다", "deleteConfirmation": "정말로 계정을 삭제하시겠습니까? 이 작업은 되돌릴 수 없습니다.", @@ -356,35 +263,33 @@ "repoExported": "저장소를 내보냈습니다", "blobsExported": "미디어 파일을 내보냈습니다", "noBlobsToExport": "내보낼 미디어 파일이 없습니다", - "exportFailed": "내보내기에 실패했습니다", - "confirmDelete": "정말로 계정을 삭제하시겠습니까? 이 작업은 되돌릴 수 없습니다." + "exportFailed": "내보내기에 실패했습니다" } }, "appPasswords": { - "title": "앱 비밀번호", - "description": "앱 비밀번호를 사용하면 기본 비밀번호를 제공하지 않고 타사 앱에 로그인할 수 있습니다. 각 앱 비밀번호는 개별적으로 취소할 수 있습니다.", - "createNew": "새 앱 비밀번호 만들기", - "appNamePlaceholder": "앱 이름 (예: Graysky, Skeets)", "created": "앱 비밀번호가 생성되었습니다", "createdMessage": "지금 이 비밀번호를 복사하세요. 다시 볼 수 없습니다.", - "yourPasswords": "앱 비밀번호 목록", "noPasswords": "앱 비밀번호가 아직 없습니다", - "revoke": "취소", - "revoking": "취소 중...", - "revokeConfirm": "앱 비밀번호 \"{name}\"을(를) 취소하시겠습니까? 이 비밀번호를 사용하는 앱은 더 이상 계정에 액세스할 수 없습니다.", "saveWarningTitle": "중요: 이 앱 비밀번호를 저장하세요!", "saveWarningMessage": "이 비밀번호는 패스키 또는 OAuth를 지원하지 않는 앱에 로그인하는 데 필요합니다. 한 번만 볼 수 있습니다.", "acknowledgeLabel": "앱 비밀번호를 안전한 곳에 저장했습니다", "permissions": "권한", "scopeFull": "전체 권한", "scopeReadOnly": "읽기 전용", - "scopePostOnly": "게시만 가능", + "scopePostOnly": "게시만", "scopeCustom": "사용자 지정", - "byController": "컨트롤러 생성" + "byController": "컨트롤러 생성", + "create": "생성", + "name": "이름", + "namePlaceholder": "앱 이름 (예: Graysky)", + "deleteConfirm": "앱 비밀번호 \"{name}\"을(를) 취소하시겠습니까?", + "deleted": "앱 비밀번호가 취소되었습니다", + "loadFailed": "앱 비밀번호 로딩 실패", + "createFailed": "앱 비밀번호 생성 실패", + "deleteFailed": "앱 비밀번호 취소 실패", + "saveWarning": "지금 이 비밀번호를 저장하세요 - 다시 볼 수 없습니다" }, "sessions": { - "title": "활성 세션", - "loadingSessions": "세션 로딩 중...", "noSessions": "활성 세션이 없습니다.", "current": "현재", "oauth": "OAuth", @@ -404,52 +309,43 @@ "daysAgo": "{count}일 전", "hoursAgo": "{count}시간 전", "minutesAgo": "{count}분 전", - "justNow": "방금" + "justNow": "방금", + "sessionRevoked": "세션이 취소되었습니다", + "allSessionsRevoked": "다른 모든 세션이 취소되었습니다" }, "inviteCodes": { - "title": "초대 코드", - "description": "초대 코드로 친구를 이 PDS에 초대할 수 있습니다. 각 코드는 한 번만 사용할 수 있습니다.", "createNew": "새 초대 코드 만들기", - "uses": "사용 횟수", - "usesPlaceholder": "사용 횟수 (1-100)", "yourCodes": "초대 코드 목록", "noCodes": "초대 코드가 아직 없습니다", "available": "사용 가능", "used": "@{handle}이(가) 사용함", "spent": "소진됨", "disabled": "비활성화됨", - "usedBy": "사용자", - "disableConfirm": "이 초대 코드를 비활성화하시겠습니까? 더 이상 사용할 수 없습니다.", "created": "초대 코드가 생성되었습니다", "copy": "복사", - "createdOn": "{date}에 생성됨" + "createdOn": "{date}에 생성됨", + "loadFailed": "초대 코드 로딩 실패", + "createFailed": "초대 코드 생성 실패" }, "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자리 코드 입력", + "totpCode": "TOTP 코드", + "totpCodePlaceholder": "6자리", "verifyAndEnable": "확인 후 활성화", "backupCodes": "백업 코드", "backupCodesDescription": "인증 앱에 액세스할 수 없는 경우 이 코드를 사용하여 로그인합니다. 각 코드는 한 번만 사용할 수 있습니다.", @@ -463,15 +359,6 @@ "enableLegacyLogin": "레거시 로그인 활성화", "disableLegacyLogin": "레거시 로그인 비활성화", "legacyLoginWarning": "경고: 레거시 로그인을 활성화하면 직접 비밀번호 로그인에 대한 MFA가 우회됩니다. 앱 호환성이 필요한 경우에만 활성화하세요.", - "totpPasswordWarning": "TOTP가 활성화되면 Bluesky 앱(또는 기타 레거시 앱)에서 비밀번호를 변경할 수 없습니다. 비밀번호를 변경하려면 두 가지 방법이 있습니다:", - "totpPasswordOption1Label": "여기에서 변경:", - "totpPasswordOption1Text": "이 웹사이트의", - "totpPasswordOption1Link": "설정 페이지", - "totpPasswordOption1Suffix": "에서 인증 앱으로 확인할 수 있습니다.", - "totpPasswordOption2Label": "먼저 세션 확인:", - "totpPasswordOption2Text": "", - "totpPasswordOption2Link": "재인증 옵션", - "totpPasswordOption2Suffix": "을 사용하여 TOTP로 Bluesky 세션을 확인하면 일시적으로 비밀번호 변경이 가능합니다.", "legacyAppsTitle": "레거시 앱이란?", "legacyAppsDescription": "일부 앱(공식 Bluesky 앱 등)은 비밀번호만 필요한 이전 인증을 사용합니다. MFA가 활성화되어 있으면 이러한 앱은 두 번째 인증 요소를 우회합니다. 레거시 로그인을 비활성화하면 모든 앱이 OAuth를 사용하도록 강제되어 MFA가 적절히 적용됩니다.", "password": "비밀번호", @@ -479,13 +366,7 @@ "noPassword": "비밀번호가 설정되지 않음 (패스키 전용 계정)", "setPassword": "비밀번호 설정", "removePassword": "비밀번호 제거", - "removePasswordConfirm": "비밀번호를 제거하시겠습니까? 로그인에 패스키가 필요합니다.", "removing": "제거 중...", - "loading": "로딩 중...", - "loadingPasskeys": "패스키 로딩 중...", - "cancel": "취소", - "save": "저장", - "back": "뒤로", "next": "다음: 코드 확인", "copyToClipboard": "클립보드에 복사", "savedMyCodes": "코드를 저장했습니다", @@ -493,20 +374,10 @@ "unnamedPasskey": "이름 없는 패스키", "added": "추가됨", "lastUsed": "마지막 사용", - "passwordDescription": "계정 비밀번호를 관리합니다. 패스키를 설정한 경우 완전한 비밀번호 없는 경험을 위해 비밀번호를 제거할 수 있습니다.", "disableTotpWarning": "이렇게 하면 계정 보안이 약해집니다.", "removePasswordWarning": "이렇게 하면 계정이 패스키 전용이 됩니다. 등록된 패스키로만 로그인할 수 있습니다. 모든 패스키에 액세스할 수 없게 되면 알림 채널을 사용하여 계정을 복구할 수 있습니다.", - "beforeProceeding": "계속하기 전에:", - "beforeProceedingItem1": "최소 하나의 신뢰할 수 있는 패스키가 등록되어 있는지 확인", - "beforeProceedingItem2": "여러 기기에 패스키 등록을 고려", - "beforeProceedingItem3": "복구 알림 채널이 최신인지 확인", - "addPasskeyFirst": "비밀번호를 제거하려면 먼저 최소 하나의 패스키를 추가하세요.", - "passkeyOnlyHint": "패스키로만 로그인합니다. 패스키에 액세스할 수 없게 되면 로그인 페이지의 '패스키를 분실하셨나요?' 링크를 사용하여 계정을 복구할 수 있습니다.", - "addPasswordHint": "비밀번호를 추가하시겠습니까? 설정에서 설정하세요.", - "goToSettings": "설정으로 이동", "trustedDevices": "신뢰할 수 있는 기기", - "trustedDevicesDescription": "로그인 시 2단계 인증을 건너뛸 수 있는 기기를 관리합니다. 신뢰는 30일간 유효하며 기기를 사용하면 자동으로 연장됩니다.", - "manageTrustedDevices": "신뢰할 수 있는 기기 관리", + "trustedDevicesDescription": "로그인 시 2단계 인증을 건너뛸 수 있는 기기. 신뢰는 30일간 유효하며 기기를 사용하면 자동으로 연장됩니다.", "appCompatibility": "앱 호환성", "enterPassword": "비밀번호를 입력하세요", "sessionExpired": "세션이 만료되었습니다. 다시 로그인하세요.", @@ -524,13 +395,26 @@ "passkeyCreationCancelled": "패스키 생성이 취소되었습니다", "passkeyAddedSuccess": "패스키가 추가되었습니다", "passkeyDeleted": "패스키가 삭제되었습니다", - "passkeyRenamed": "패스키 이름이 변경되었습니다" + "passkeyRenamed": "패스키 이름이 변경되었습니다", + "changePassword": "비밀번호 변경", + "currentPassword": "현재 비밀번호", + "currentPasswordPlaceholder": "현재 비밀번호 입력", + "newPassword": "새 비밀번호", + "newPasswordPlaceholder": "새 비밀번호 입력", + "confirmPassword": "새 비밀번호 확인", + "confirmPasswordPlaceholder": "새 비밀번호 재입력", + "passwordsDoNotMatch": "비밀번호가 일치하지 않습니다", + "passwordTooShort": "비밀번호는 8자 이상이어야 합니다", + "passwordChanged": "비밀번호가 변경되었습니다", + "failedToChangePassword": "비밀번호 변경 실패", + "changing": "변경 중...", + "setting": "설정 중...", + "passwordSet": "비밀번호가 설정되었습니다", + "failedToSetPassword": "비밀번호 설정 실패", + "failedToDisableTotp": "TOTP 비활성화 실패" }, "comms": { - "title": "통신 설정", - "description": "비밀번호 재설정, 보안 알림, 계정 업데이트 등 중요한 메시지를 받는 방법을 선택하세요.", "preferredChannel": "선호 채널", - "preferredChannelDescription": "메시지 수신 방법을 선택하세요. 선택하기 전에 채널을 설정해야 합니다.", "channelConfiguration": "채널 설정", "emailVia": "이메일로 메시지 받기", "discordVia": "Discord DM으로 메시지 받기", @@ -538,10 +422,6 @@ "signalVia": "Signal로 메시지 받기", "configureToEnable": "활성화하려면 아래에서 설정", "notConfiguredOnServer": "이 서버에서 설정되지 않음", - "emailManagedInSettings": "이메일은 계정 설정에서 관리됩니다", - "discordIdHint": "Discord 사용자 ID (사용자 이름 아님). Discord에서 개발자 모드를 활성화하여 복사하세요.", - "telegramHint": "@ 기호 없이 Telegram 사용자 이름", - "signalHint": "국가 코드가 포함된 Signal 전화번호", "primary": "기본", "verified": "인증됨", "notVerified": "미인증", @@ -552,29 +432,20 @@ "preferencesSaved": "통신 설정이 저장되었습니다", "verifiedSuccess": "{channel} 인증 완료", "messageHistory": "메시지 기록", - "historyDescription": "계정에 전송된 최근 메시지를 확인합니다.", - "loadHistory": "기록 불러오기", - "hideHistory": "기록 숨기기", "noMessages": "메시지가 없습니다.", - "sent": "전송됨", - "failed": "실패", "discordInUseWarning": "이 Discord ID는 이미 다른 계정과 연결되어 있습니다.", "telegramInUseWarning": "이 Telegram 사용자 이름은 이미 다른 계정과 연결되어 있습니다.", - "signalInUseWarning": "이 Signal 번호는 이미 다른 계정과 연결되어 있습니다." + "signalInUseWarning": "이 Signal 번호는 이미 다른 계정과 연결되어 있습니다.", + "failedToLoad": "설정 로딩 실패", + "failedToSave": "설정 저장 실패", + "failedToVerify": "인증 실패", + "failedToLoadHistory": "메시지 기록 로딩 실패" }, "repoExplorer": { - "title": "저장소 탐색기", - "description": "AT Protocol 레코드를 탐색하고 관리합니다.", "collections": "컬렉션", - "noCollections": "컬렉션을 찾을 수 없습니다", - "records": "레코드", "noRecords": "이 컬렉션에 레코드가 없습니다", - "recordDetails": "레코드 세부 정보", - "rkey": "레코드 키", "uri": "URI", "cid": "CID", - "value": "값", - "deleteRecord": "레코드 삭제", "deleteConfirm": "레코드 {rkey}을(를) 삭제하시겠습니까? 이 작업은 되돌릴 수 없습니다.", "unknownError": "알 수 없는 오류가 발생했습니다", "invalidJson": "잘못된 JSON", @@ -587,7 +458,6 @@ "filterCollections": "컬렉션 검색...", "filterRecords": "레코드 검색...", "noCollectionsYet": "컬렉션이 아직 없습니다. 첫 번째 레코드를 만들어 시작하세요.", - "loadMore": "더 불러오기", "recordJson": "레코드 JSON", "updateRecord": "레코드 업데이트", "collectionNsid": "컬렉션 (NSID)", @@ -599,8 +469,6 @@ "demoBio": "간단한 자기소개를 작성하세요." }, "admin": { - "title": "관리 패널", - "loading": "로딩 중...", "serverConfig": "서버 설정", "serverName": "서버 이름", "serverNamePlaceholder": "내 PDS", @@ -623,8 +491,6 @@ "refreshStats": "통계 새로고침", "userManagement": "사용자 관리", "searchPlaceholder": "핸들로 검색 (선택사항)", - "searchUsers": "사용자 검색", - "noUsers": "사용자를 찾을 수 없습니다", "handle": "핸들", "email": "이메일", "status": "상태", @@ -634,10 +500,8 @@ "loadInviteCodes": "초대 코드 불러오기", "refresh": "새로고침", "noInvites": "초대 코드가 없습니다", - "code": "코드", "available": "사용 가능", "uses": "사용 횟수", - "actions": "작업", "disable": "비활성화", "disableInviteConfirm": "초대 코드 {code}을(를) 비활성화하시겠습니까?", "active": "활성", @@ -654,9 +518,17 @@ "verified": "인증됨", "unverified": "미인증", "deactivated": "비활성화됨", - "colorDefault": "{color} (기본값)", "secondaryLight": "보조 (라이트 모드)", - "secondaryDark": "보조 (다크 모드)" + "secondaryDark": "보조 (다크 모드)", + "failedToLoadStats": "서버 통계 로딩 실패", + "failedToLoadUsers": "사용자 로딩 실패", + "searchToSeeUsers": "검색하여 사용자 보기", + "search": "검색", + "inviteDisabled": "초대 코드가 비활성화되었습니다", + "invitesEnabled": "사용자 초대가 활성화되었습니다", + "invitesDisabled": "사용자 초대가 비활성화되었습니다", + "userDeleted": "사용자 계정이 삭제되었습니다", + "failedToLoadConfig": "서버 설정 로딩 실패" }, "oauth": { "login": { @@ -675,7 +547,6 @@ "passkeyHintNotAvailable": "패스키 없음", "passwordPlaceholder": "비밀번호", "usePasskey": "패스키 사용", - "orContinueWith": "또는", "orUseCredentials": "또는" }, "register": { @@ -686,77 +557,31 @@ }, "sso": { "linkedAccounts": "연결된 계정", - "linkedAccountsDesc": "싱글 사인온을 위해 연결된 외부 계정입니다.", "noLinkedAccounts": "연결된 계정 없음", - "noLinkedAccountsDesc": "외부 계정을 연결하여 해당 제공자로 빠르게 로그인하세요.", - "linkAccount": "계정 연결", - "unlinkAccount": "연결 해제", "unlinkConfirm": "이 계정의 연결을 해제하시겠습니까?", "unlinked": "{provider} 연결 해제됨", - "lastLoginAt": "마지막 사용", - "linkedAt": "연결됨" + "linkedAt": "연결됨", + "unlink": "연결 해제", + "linkNewAccount": "계정 연결", + "linked": "연결됨", + "linkSuccess": "계정이 연결되었습니다", + "linkFailed": "계정 연결 실패", + "unlinkFailed": "연결 해제 실패" }, "consent": { "title": "앱 승인", "appWantsAccess": "{app}이(가) 계정에 액세스하려고 합니다", - "permissions": "이 앱은 다음을 수행할 수 있습니다:", - "readProfile": "프로필 정보 읽기", - "readPosts": "게시물 및 콘텐츠 읽기", - "writePosts": "대신 게시물 작성 및 삭제", - "readNotifications": "알림 읽기", - "fullAccess": "계정에 대한 전체 액세스", "authorize": "승인", "deny": "거부", "authorizing": "승인 중...", - "rememberChoice": "이 선택 기억", "signingInAs": "로그인 계정:", "permissionsRequested": "요청된 권한", "required": "필수", "rememberChoiceLabel": "이 앱에 대한 선택 기억하기", "scopes": { - "atproto": { - "name": "전체 액세스", - "description": "이 계정을 읽고, 쓰고, 관리하는 전체 액세스" - }, "atprotoWithGranular": { "name": "AT Protocol 액세스", "description": "AT Protocol 기본 범위 (권한은 아래 선택한 옵션에 의해 결정됨)" - }, - "transitionGeneric": { - "name": "전환 액세스", - "description": "호환성을 위한 일반 전환 범위" - }, - "transitionChat": { - "name": "채팅 액세스", - "description": "Bluesky 채팅 기능 액세스" - }, - "transitionEmail": { - "name": "이메일 액세스", - "description": "계정 이메일 주소 읽기" - }, - "repoCreate": { - "name": "레코드 생성", - "description": "저장소에 새 레코드 생성" - }, - "repoUpdate": { - "name": "레코드 업데이트", - "description": "저장소의 기존 레코드 업데이트" - }, - "repoDelete": { - "name": "레코드 삭제", - "description": "저장소에서 레코드 삭제" - }, - "blobAll": { - "name": "미디어 업로드", - "description": "이미지, 비디오 및 기타 미디어 파일 업로드" - }, - "repoFull": { - "name": "전체 저장소 액세스", - "description": "모든 저장소 레코드에 대한 전체 읽기 및 쓰기 액세스" - }, - "accountManage": { - "name": "계정 관리", - "description": "계정 설정 및 환경설정 관리" } }, "unexpectedState": { @@ -769,11 +594,6 @@ "title": "계정 선택", "useAnother": "다른 계정 사용" }, - "twoFactor": { - "title": "인증", - "usePasskey": "패스키 사용", - "useTotp": "인증 앱 사용" - }, "twoFactorCode": { "title": "인증", "subtitle": "{channel}(으)로 코드 전송됨", @@ -789,21 +609,14 @@ "totp": { "title": "인증 코드", "codePlaceholder": "6자리 코드", - "useBackupCode": "백업 코드 사용", "backupCodePlaceholder": "백업 코드", "trustDevice": "이 기기를 30일간 신뢰", "hintBackupCode": "백업 코드", "hintTotpCode": "인증 코드" }, - "passkey": { - "title": "패스키", - "waiting": "대기 중", - "useTotp": "인증 앱 사용" - }, "error": { "title": "승인 실패", - "tryAgain": "다시 시도", - "backToApp": "돌아가기" + "tryAgain": "다시 시도" } }, "sso_register": { @@ -829,10 +642,9 @@ "subtitle": "{channel}(으)로 인증 코드를 보냈습니다. 아래에 입력하여 등록을 완료하세요.", "tokenTitle": "인증", "tokenSubtitle": "인증 코드와 전송된 식별자를 입력하세요.", - "codePlaceholder": "Paste verification code", + "codePlaceholder": "인증 코드 붙여넣기", "codeLabel": "인증 코드", "codeHelp": "메시지에서 하이픈을 를 복사하세요", - "verifyButton": "계정 인증", "pleaseWait": "잠시 기다려 주세요...", "codeResent": "인증 코드를 다시 보냈습니다!", "codeResentDetail": "인증 코드가 전송되었습니다! 받은 편지함을 확인하세요.", @@ -874,7 +686,6 @@ "sendCode": "재설정 코드 보내기", "sending": "전송 중...", "codeSent": "비밀번호 재설정 코드를 보냈습니다! 선호하는 알림 채널을 확인하세요.", - "enterCode": "받은 코드와 새 비밀번호를 입력하세요.", "code": "재설정 코드", "codePlaceholder": "재설정 코드 입력", "newPassword": "새 비밀번호", @@ -886,8 +697,7 @@ "success": "비밀번호가 재설정되었습니다!", "requestNewCode": "새 코드 요청", "passwordsMismatch": "비밀번호가 일치하지 않습니다", - "passwordLength": "비밀번호는 8자 이상이어야 합니다", - "multipleAccountsWarning": "여러 계정에서 이 이메일을 공유하고 있습니다. 재설정 코드는 가장 최근에 생성된 계정으로 전송되었습니다. 특정 계정을 복구하려면 핸들을 사용하세요." + "passwordLength": "비밀번호는 8자 이상이어야 합니다" }, "recoverPasskey": { "title": "계정 복구", @@ -931,32 +741,8 @@ "sending": "전송 중..." }, "registerPasskey": { - "title": "패스키 계정 만들기", - "subtitleKeyChoice": "did:web 아이덴티티 설정", - "subtitleInitialDidDoc": "DID 문서 업로드", - "subtitleCreating": "계정 생성 중", - "subtitlePasskey": "패스키 등록", - "subtitleAppPassword": "앱 비밀번호 저장", - "subtitleVerify": "{channel} 인증", - "subtitleUpdatedDidDoc": "DID 문서 업데이트", - "subtitleActivating": "활성화 중", - "subtitleComplete": "계정 생성됨", - "handle": "핸들", - "handlePlaceholder": "사용자 이름", - "handleHint": "전체 핸들: @{handle}", - "contactMethod": "연락 방법", - "verificationMethod": "인증 방법", - "email": "이메일 주소", - "emailPlaceholder": "you@example.com", - "inviteCode": "초대 코드", - "inviteCodePlaceholder": "초대 코드 입력", "externalDid": "귀하의 did:web", "externalDidPlaceholder": "did:web:yourdomain.com", - "createButton": "계정 만들기", - "alreadyHaveAccount": "이미 계정이 있으신가요?", - "signIn": "로그인", - "wantPassword": "비밀번호를 사용하시겠습니까?", - "createPasswordAccount": "비밀번호 계정 만들기", "errors": { "handleRequired": "핸들은 필수입니다", "handleNoDots": "핸들에 점을 포함할 수 없습니다. 계정 생성 후 사용자 정의 도메인을 설정할 수 있습니다.", @@ -971,7 +757,6 @@ "externalDidFormat": "외부 DID는 did:web:으로 시작해야 합니다", "discordRequired": "Discord 인증에는 Discord ID가 필요합니다" }, - "creatingPasskey": "생성 중", "identityType": "아이덴티티 유형", "identityTypeHint": "분산 아이덴티티 관리 방법을 선택하세요.", "passkeyNamePlaceholder": "예: MacBook Touch ID", @@ -994,29 +779,17 @@ "didWebWarning4": "권장 사항:", "didWebWarning4Detail": "did:web을 선호할 특별한 이유가 없다면 did:plc를 선택하세요.", "externalDidHint": "다음 위치에서 DID 문서를 제공해야 합니다", - "continue": "계속", - "back": "뒤로", - "loading": "로딩 중...", - "redirecting": "대시보드로 이동 중...", - "handleDotWarning": "사용자 정의 도메인 핸들은 계정 생성 후 설정할 수 있습니다.", - "wantTraditional": "기존 비밀번호를 원하시나요?", - "registerWithPassword": "비밀번호로 가입", - "activatingAccount": "Activating", - "creatingAccount": "Creating account", - "passkeyDescription": "Register a passkey for this account", - "passkeyName": "Passkey Name", - "setupPasskey": "Create Passkey" + "activatingAccount": "활성화 중", + "creatingAccount": "계정 생성 중", + "passkeyDescription": "이 계정에 패스키 등록", + "passkeyName": "패스키 이름", + "setupPasskey": "패스키 생성" }, "trustedDevices": { - "title": "신뢰할 수 있는 기기", - "backToSecurity": "← 보안 설정", - "description": "신뢰할 수 있는 기기는 로그인 시 2단계 인증을 건너뛸 수 있습니다. 신뢰는 30일간 유효하며 기기를 사용할 때 자동으로 연장됩니다.", - "failedToLoad": "신뢰할 수 있는 기기를 불러오지 못했습니다", "noDevices": "신뢰할 수 있는 기기가 아직 없습니다.", "noDevicesHint": "2단계 인증이 활성화된 상태로 로그인할 때 기기를 30일간 신뢰하도록 선택할 수 있습니다.", "lastSeen": "마지막 접속:", "trustedSince": "신뢰 시작:", - "trustExpires": "신뢰 만료:", "expired": "만료됨", "tomorrow": "내일", "inDays": "{days}일 후", @@ -1025,7 +798,6 @@ "deviceRevoked": "기기 신뢰가 취소되었습니다", "deviceRenamed": "기기 이름이 변경되었습니다", "deviceNamePlaceholder": "기기 이름", - "browser": "브라우저:", "unknownDevice": "알 수 없는 기기" }, "reauth": { @@ -1034,36 +806,11 @@ "totp": "TOTP", "passkey": "패스키", "authenticatorCode": "인증 코드", - "usePassword": "비밀번호", "usePasskey": "패스키", - "useTotp": "인증 앱", - "passwordPlaceholder": "비밀번호 입력", - "totpPlaceholder": "6자리 코드", "authenticating": "인증 중", "cancel": "취소" }, - "verifyChannel": { - "title": "채널 인증", - "subtitle": "알림 채널로 전송된 인증 코드를 입력하세요.", - "signInRequired": "로그인 필요", - "signInRequiredDesc": "채널을 인증하려면 로그인해야 합니다.", - "signIn": "로그인", - "verifying": "인증 중...", - "pleaseWait": "채널을 인증하는 중입니다. 잠시 기다려 주세요.", - "successTitle": "인증 완료!", - "successDesc": "{channel}이(가) 성공적으로 인증되었습니다.", - "backToSettings": "설정으로 돌아가기", - "channelLabel": "채널", - "selectChannel": "채널 선택...", - "identifierLabel": "식별자", - "identifierPlaceholder": "이메일, Discord ID 등", - "identifierHelp": "인증할 이메일 주소, Discord ID, Telegram 사용자 이름 또는 Signal 번호.", - "codeLabel": "인증 코드", - "codeHelp": "메시지에서 하이픈을 를 복사하세요.", - "verifyButton": "인증" - }, "delegation": { - "title": "계정 위임", "controllers": "컨트롤러", "controlledAccounts": "관리 계정", "noControllers": "아직 컨트롤러가 없습니다", @@ -1076,13 +823,7 @@ "scopeCustom": "사용자 정의", "actAs": "로 활동", "auditLog": "감사 로그", - "auditLogTitle": "위임 감사 로그", - "backToControllers": "← 컨트롤러로 돌아가기", - "loading": "로딩 중...", - "noActivity": "아직 활동이 없습니다", "actor": "액터", - "controller": "컨트롤러", - "account": "계정", "details": "세부정보", "actionGrantCreated": "권한 생성", "actionGrantRevoked": "권한 취소", @@ -1093,9 +834,7 @@ "actionAccountAction": "계정 작업", "previous": "이전", "next": "다음", - "showing": "{start}~{end} / {total}개", "refresh": "새로고침", - "failedToLoadAuditLog": "감사 로그를 불러오지 못했습니다", "adding": "추가 중...", "accessLevel": "액세스 수준", "addControllerButton": "+ 컨트롤러 추가", @@ -1117,25 +856,24 @@ "createDelegatedAccount": "위임 계정 생성", "createDelegatedAccountButton": "+ 위임 계정 생성", "emailOptional": "이메일 (선택사항)", - "failedToAddController": "컨트롤러 추가에 실패했습니다", - "failedToCreateAccount": "위임 계정 생성에 실패했습니다", - "failedToRemoveController": "컨트롤러 제거에 실패했습니다", "granted": "허용일", "inactive": "비활성", "remove": "제거", "removeConfirm": "이 컨트롤러를 제거하시겠습니까?", "viewAuditLog": "감사 로그 보기", "yourAccessLevel": "귀하의 액세스 수준", - "accountCreated": "위임 계정이 생성되었습니다: {handle}" + "accountCreated": "위임 계정이 생성되었습니다: {handle}", + "noAuditEntries": "감사 항목 없음", + "target": "대상", + "pageInfo": "{start} - {end} / {total}", + "failedToLoadAudit": "감사 로그 로딩 실패" }, "actAs": { "title": "로 활동", "noAccountSpecified": "계정 DID가 지정되지 않았습니다", - "failedToVerify": "계정 액세스를 확인하지 못했습니다", "noAccess": "이 계정에 대한 액세스 권한이 없습니다", "failedToInitiate": "인증 시작에 실패했습니다", "invalidResponse": "서버에서 잘못된 응답을 받았습니다", - "failedError": "실패: {error}", "preparing": "위임 계정 로그인 준비 중...", "backToControllers": "컨트롤러로 돌아가기" }, @@ -1185,8 +923,6 @@ "migration": { "title": "계정 마이그레이션", "subtitle": "AT Protocol 아이덴티티를 서버 간에 이동", - "navTitle": "마이그레이션", - "navDesc": "다른 PDS로 또는 다른 PDS에서 계정 이동", "migrateHere": "여기로 마이그레이션", "migrateHereDesc": "기존 AT Protocol 계정을 다른 서버에서 이 PDS로 이동합니다.", "bringDid": "DID와 아이덴티티 가져오기", @@ -1252,6 +988,7 @@ "checkingAvailability": "사용 가능 여부 확인 중...", "handleAvailable": "핸들을 사용할 수 있습니다!", "handleTaken": "핸들이 이미 사용 중입니다", + "handleTooShort": "핸들은 최소 3자 이상이어야 합니다", "handleHint": "전체 핸들(예: alice.mydomain.com)을 입력하여 자체 도메인을 사용할 수도 있습니다", "email": "이메일 주소", "authMethod": "인증 방법", @@ -1276,7 +1013,6 @@ "authentication": "인증", "authPasskey": "패스키 (비밀번호 없음)", "authPassword": "비밀번호", - "inviteCode": "초대 코드", "warning": "\"마이그레이션 시작\"을 클릭하면 저장소와 데이터 전송이 시작됩니다. 이 과정은 쉽게 되돌릴 수 없습니다.", "startMigration": "마이그레이션 시작", "starting": "시작 중..." @@ -1313,9 +1049,7 @@ "hint": "아래에 코드를 입력하거나, 이메일의 링크를 클릭하여 자동으로 계속할 수 있습니다.", "tokenLabel": "인증 코드", "tokenPlaceholder": "이메일에서 받은 코드 입력", - "resend": "코드 재전송", - "verify": "이메일 인증", - "verifying": "인증 중..." + "resend": "코드 재전송" }, "plcToken": { "title": "마이그레이션 확인", @@ -1423,7 +1157,6 @@ "desc": "오프라인 복원 세부 정보를 확인하세요.", "carFile": "CAR 파일", "rotationKey": "회전 키", - "warning": "복원을 시작하면 아이덴티티가 이 PDS를 가리키도록 업데이트됩니다. 이것은 쉽게 되돌릴 수 없습니다.", "plcWarningTitle": "되돌릴 수 없는 지점", "plcWarning": "시작하면 DID 문서가 이 PDS를 가리키도록 업데이트됩니다. 문제가 발생하면 회전 키를 사용하여 복구할 수 있지만, 손상된 아이덴티티 상태를 피하려면 마이그레이션을 완료해야 합니다." }, @@ -1431,9 +1164,7 @@ "title": "계정 복원 중", "desc": "계정을 복원하는 중입니다...", "creating": "계정 생성 중", - "importing": "저장소 가져오는 중", - "plcSigning": "아이덴티티 업데이트 중", - "activating": "계정 활성화 중" + "importing": "저장소 가져오는 중" }, "success": { "desc": "계정이 이 PDS에 성공적으로 복원되었습니다." @@ -1443,28 +1174,8 @@ "desc": "이전 PDS에서 이미지와 미디어를 복구하는 중...", "migrating": "Blob 마이그레이션 중", "failedTitle": "일부 Blob을 마이그레이션할 수 없음", - "failedDesc": "{count}개의 Blob을 이전 PDS에서 가져올 수 없습니다. 서버에 연결할 수 없거나 파일이 삭제되었을 수 있습니다.", - "sourceUnreachableTitle": "원본 PDS에 연결할 수 없음", - "sourceUnreachable": "이전 PDS에 연결하여 미디어 파일을 가져올 수 없습니다. 종료된 서버에서 마이그레이션할 때 흔히 발생합니다. 게시물은 작동하지만 일부 이미지가 누락될 수 있습니다." + "failedDesc": "{count}개의 Blob을 이전 PDS에서 가져올 수 없습니다. 서버에 연결할 수 없거나 파일이 삭제되었을 수 있습니다." } - }, - "progress": { - "repoExported": "저장소 내보내기 완료", - "repoImported": "저장소 가져오기 완료", - "blobsMigrated": "{count}개 blob 마이그레이션됨", - "prefsMigrated": "환경설정 마이그레이션됨", - "plcSigned": "아이덴티티 업데이트됨", - "activated": "계정 활성화됨", - "deactivated": "이전 계정 비활성화됨" - }, - "errors": { - "connectionFailed": "PDS에 연결할 수 없습니다", - "invalidCredentials": "잘못된 인증 정보", - "twoFactorRequired": "2단계 인증이 필요합니다", - "accountExists": "대상 PDS에 계정이 이미 존재합니다", - "plcFailed": "PLC 작업 실패", - "blobFailed": "blob 마이그레이션 실패: {cid}", - "networkError": "네트워크 오류. 다시 시도하세요." } } } diff --git a/frontend/src/locales/sv.json b/frontend/src/locales/sv.json index 61a3dbc..16642ad 100644 --- a/frontend/src/locales/sv.json +++ b/frontend/src/locales/sv.json @@ -8,55 +8,23 @@ "done": "Klar", "continue": "Fortsätt", "refresh": "Uppdatera", - "create": "Skapa", "delete": "Radera", - "confirm": "Bekräfta", "created": "Skapad", - "expires": "Upphör", "name": "Namn", - "dashboard": "Kontrollpanel", "backToDashboard": "← Kontrollpanel", "copied": "Kopierat", "copyToClipboard": "Kopiera", "verifying": "Verifierar", "saving": "Sparar", "creating": "Skapar", - "updating": "Uppdaterar", "sending": "Skickar", - "authenticating": "Autentiserar", "checking": "Kontrollerar", - "redirecting": "Omdirigerar", "signIn": "Logga in", "verify": "Verifiera", - "remove": "Ta bort", "revoke": "Återkalla", "resendCode": "Skicka kod", - "startOver": "Börja om", - "tryAgain": "Försök igen", - "password": "Lösenord", - "email": "E-post", - "emailAddress": "E-postadress", - "handle": "Användarnamn", - "did": "DID", - "verificationCode": "Verifieringskod", - "inviteCode": "Inbjudningskod", - "newPassword": "Nytt lösenord", - "confirmPassword": "Bekräfta lösenord", - "enterSixDigitCode": "Ange 6-siffrig kod", - "passwordHint": "Minst 8 tecken", - "enterPassword": "Ange ditt lösenord", - "emailPlaceholder": "du@exempel.se", - "verified": "Verifierad", - "disabled": "Inaktiverad", - "available": "Tillgänglig", - "deactivated": "Avaktiverad", - "unverified": "Overifierad", "backToLogin": "Tillbaka till inloggning", - "backToSettings": "Tillbaka till inställningar", - "alreadyHaveAccount": "Har du redan ett konto?", - "createAccount": "Skapa konto", - "passwordsMismatch": "Lösenorden matchar inte", - "passwordTooShort": "Lösenordet måste vara minst 8 tecken" + "backToSettings": "Tillbaka till inställningar" }, "login": { "title": "Logga in", @@ -75,18 +43,10 @@ "subtitle": "Ange koden som skickades till din kontaktmetod", "codeLabel": "Kod", "codePlaceholder": "6-siffrig kod", - "verifyButton": "Verifiera", "resent": "Kod skickad" }, "register": { "title": "Skapa konto", - "subtitle": "Skapa ett nytt konto på denna PDS", - "subtitleKeyChoice": "Konfigurera din did:web-identitet", - "subtitleInitialDidDoc": "Ladda upp ditt DID-dokument", - "subtitleVerify": "Verifiera din {channel}", - "subtitleUpdatedDidDoc": "Uppdatera ditt DID-dokument", - "subtitleActivating": "Aktiverar", - "subtitleComplete": "Konto skapat", "redirecting": "Omdirigerar", "migrateTitle": "Har du redan ett konto?", "migrateDescription": "Migrera istället för att skapa nytt", @@ -115,8 +75,6 @@ "didWebWarning2Detail": "Till skillnad från did:plc har did:web inga rotationsnycklar. Om denna PDS går offline permanent kan din identitet inte återställas.", "didWebWarning3": "Vi förbinder oss till dig:", "didWebWarning3Detail": "Om du flyttar härifrån kommer vi att fortsätta tillhandahålla ett minimalt DID-dokument som pekar på din nya PDS. Din identitet förblir funktionell.", - "didWebWarning4": "Rekommendation:", - "didWebWarning4Detail": "Välj did:plc om du inte har en specifik anledning att föredra did:web.", "externalDid": "Din did:web", "externalDidPlaceholder": "did:web:dindomän.se", "externalDidHint": "Din domän måste tillhandahålla ett giltigt DID-dokument på /.well-known/did.json som pekar på denna PDS", @@ -143,14 +101,11 @@ "inviteCode": "Inbjudningskod", "inviteCodePlaceholder": "Ange din inbjudningskod", "inviteCodeRequired": "krävs", - "createButton": "Skapa konto", "alreadyHaveAccount": "Har du redan ett konto?", "signIn": "Logga in", - "emailInUseWarning": "Denna e-post är redan kopplad till ett annat konto. Du kan fortfarande använda den, men för kontoåterställning kan du behöva använda ditt användarnamn istället.", "passkeyAccount": "Nyckel", "passwordAccount": "Lösenord", "ssoAccount": "SSO", - "ssoSubtitle": "Skapa ett konto med en extern leverantör", "noSsoProviders": "Inga SSO-leverantörer är konfigurerade på denna server.", "continueWith": "Fortsätt med {provider}", "validation": { @@ -170,12 +125,13 @@ }, "dashboard": { "title": "Kontrollpanel", + "accountManager": "Kontohantering", + "navDelegationAudit": "Delegeringsgranskning", "switchAccount": "Byt konto", "addAnotherAccount": "Lägg till ett annat konto", "signOut": "Logga ut @{handle}", "deactivatedTitle": "Konto inaktiverat", "deactivatedMessage": "Ditt konto är för närvarande inaktiverat. Detta sker vanligtvis under kontoflyttning. Vissa funktioner kan vara begränsade tills ditt konto återaktiveras.", - "accountOverview": "Kontoöversikt", "handle": "Användarnamn", "did": "DID", "primaryContact": "Primär kontakt", @@ -184,38 +140,20 @@ "verified": "Verifierad", "unverified": "Ej verifierad", "navAppPasswords": "Applösenord", - "navAppPasswordsDesc": "Hantera lösenord för tredjepartsappar", "navSessions": "Aktiva sessioner", - "navSessionsDesc": "Visa och hantera dina inloggningssessioner", "navInviteCodes": "Inbjudningskoder", - "navInviteCodesDesc": "Visa och skapa inbjudningskoder", - "navSettings": "Kontoinställningar", - "navSettingsDesc": "E-post, lösenord, användarnamn och mer", + "navSettings": "Allmänt", "navSecurity": "Säkerhet", - "navSecurityDesc": "Tvåfaktorsautentisering", "navComms": "Kommunikationsinställningar", - "navCommsDesc": "Discord, Telegram, Signal-kanaler", "navRepo": "Dataförvarsutforskare", - "navRepoDesc": "Bläddra och hantera råa AT Protocol-poster", "navDelegation": "Delegering", - "navDelegationDesc": "Hantera kontokontrollanter och delegerade konton", "navAdmin": "Adminpanel", - "navAdminDesc": "Serverstatistik och administratörsoperationer", "navDidDocument": "DID-dokument", - "navDidDocumentDesc": "Hantera ditt DID-dokument och nycklar", - "navDidDocumentDescActive": "Redigera dina DID-dokumentinställningar", - "navBackup": "Ladda ner säkerhetskopia", - "navBackupDesc": "Ladda ner ditt dataförvar som en CAR-fil", - "downloadingBackup": "Laddar ner...", - "backupFailed": "Kunde inte ladda ner säkerhetskopia", "migrated": "Flyttad", "migratedTitle": "Konto flyttat", - "migratedMessage": "Ditt konto har flyttats till {pds}. Ditt DID-dokument finns fortfarande här.", - "navMigrateAgain": "Flytta igen", - "navMigrateAgainDesc": "Flytta till en annan PDS och uppdatera ditt DID-dokument" + "migratedMessage": "Ditt konto har flyttats till {pds}. Ditt DID-dokument finns fortfarande här." }, "didEditor": { - "title": "DID-dokumentredigerare", "preview": "Nuvarande DID-dokument", "verificationMethods": "Verifieringsmetoder (signeringsnycklar)", "verificationMethodsDesc": "Signeringsnycklar som kan agera å din DIDs vägnar. När du migrerar till en ny PDS, lägg till deras signeringsnyckel här.", @@ -241,14 +179,11 @@ "saveFailed": "Kunde inte spara DID-dokument", "loadFailed": "Kunde inte ladda DID-dokument", "invalidMultibase": "Publik nyckel måste vara en giltig multibase-sträng som börjar med 'z'", - "invalidHandle": "Användarnamn måste vara en at:// URI (t.ex. at://handle.example.com)", "helpTitle": "Vad är detta?", "helpText": "När du flyttar till en annan PDS genererar den PDS nya signeringsnycklar. Uppdatera ditt DID-dokument här så att det pekar på dina nya nycklar och plats." }, "settings": { - "title": "Kontoinställningar", "language": "Språk", - "languageDescription": "Välj ditt föredragna språk", "changeEmail": "Ändra e-post", "currentEmail": "Nuvarande: {email}", "newEmail": "Ny e-post", @@ -266,7 +201,6 @@ "currentHandle": "Nuvarande: @{handle}", "pdsHandle": "PDS-användarnamn", "customDomain": "Egen domän", - "customDomainDescription": "Använd din egen domän som användarnamn. Du måste verifiera domänägande först.", "setupInstructions": "Installationsanvisningar", "setupMethodsIntro": "Välj en av dessa verifieringsmetoder:", "dnsMethod": "Alternativ 1: DNS TXT-post (Rekommenderas)", @@ -280,33 +214,16 @@ "newHandle": "Nytt användarnamn", "newHandlePlaceholder": "dittanvändarnamn", "changeHandleButton": "Ändra användarnamn", - "changePassword": "Ändra lösenord", - "currentPassword": "Nuvarande lösenord", - "currentPasswordPlaceholder": "Ange nuvarande lösenord", - "newPassword": "Nytt lösenord", - "newPasswordPlaceholder": "Minst 8 tecken", - "confirmNewPassword": "Bekräfta nytt lösenord", - "confirmNewPasswordPlaceholder": "Bekräfta nytt lösenord", - "changePasswordButton": "Ändra lösenord", - "changing": "Ändrar...", - "setPassword": "Ange lösenord", - "setPasswordDescription": "Ditt konto är för närvarande endast passnycklar. Du kan lägga till ett lösenord för att aktivera traditionell inloggning tillsammans med dina passnycklar.", - "setPasswordButton": "Ange lösenord", - "setting": "Anger...", "exportData": "Exportera data", - "exportDataDescription": "Ladda ner hela ditt arkiv som en CAR-fil (Content Addressable Archive). Detta inkluderar alla dina inlägg, gillanden, följningar och annan data.", "downloadRepo": "Ladda ner arkiv", "downloadBlobs": "Ladda ner media", "exporting": "Exporterar...", "backups": { "title": "Säkerhetskopior", - "description": "Hantera automatiska säkerhetskopior och återställ din kontodata. Säkerhetskopior inkluderar alla poster och blobbar.", - "enableAutomatic": "Automatiska säkerhetskopior", "enabled": "Aktiverad", "disabled": "Inaktiverad", "toggleFailed": "Kunde inte ändra säkerhetskopieringsinställning", "noBackups": "Inga säkerhetskopior ännu", - "blocks": "block", "download": "Ladda ner", "delete": "Radera", "createNow": "Skapa säkerhetskopia nu", @@ -316,13 +233,13 @@ "deleted": "Säkerhetskopia raderad", "deleteFailed": "Kunde inte radera säkerhetskopia", "restoreTitle": "Återställ från säkerhetskopia", - "restoreDescription": "Återställ din kontodata från en tidigare exporterad CAR-fil. Detta ersätter ditt nuvarande dataförvar med den uppladdade säkerhetskopian.", - "selectFile": "Välj CAR-fil", "selectedFile": "Vald fil", "restore": "Återställ säkerhetskopia", "restoring": "Återställer...", "restored": "Säkerhetskopia återställd", - "restoreFailed": "Kunde inte återställa säkerhetskopia" + "restoreFailed": "Kunde inte återställa säkerhetskopia", + "autoBackup": "Automatiska säkerhetskopior", + "restoreHint": "Ladda upp en CAR-fil för att återställa ditt arkiv" }, "deleteAccount": "Radera konto", "deleteWarning": "Denna åtgärd är oåterkallelig. All din data kommer att raderas permanent.", @@ -334,21 +251,11 @@ "permanentlyDelete": "Radera konto permanent", "deleting": "Raderar...", "messages": { - "emailCodeSent": "Verifieringskod skickad till din meddelandekanal", "emailCodeSentToCurrent": "Verifieringskod skickad till din nuvarande e-postadress", "emailUpdated": "E-post uppdaterad", "emailUpdateFailed": "Kunde inte uppdatera e-post", "handleUpdated": "Användarnamn uppdaterat", "handleUpdateFailed": "Kunde inte uppdatera användarnamn", - "passwordChanged": "Lösenord ändrat", - "passwordChangeFailed": "Kunde inte ändra lösenord", - "passwordSet": "Lösenord har angetts", - "passwordSetFailed": "Kunde inte ange lösenord", - "passwordsMismatch": "Lösenorden matchar inte", - "passwordsDoNotMatch": "Lösenorden matchar inte", - "passwordLength": "Lösenordet måste vara minst 8 tecken", - "passwordTooShort": "Lösenordet måste vara minst 8 tecken", - "deletionCodeSent": "Bekräftelse för radering skickad till din e-post", "deletionConfirmationSent": "Bekräftelse för radering skickad till din e-post", "deletionRequestFailed": "Kunde inte begära kontoradering", "deleteConfirmation": "Är du helt säker på att du vill radera ditt konto? Detta kan inte ångras.", @@ -356,35 +263,33 @@ "repoExported": "Arkiv exporterat", "blobsExported": "Mediafiler exporterade", "noBlobsToExport": "Inga mediafiler att exportera", - "exportFailed": "Export misslyckades", - "confirmDelete": "Är du helt säker på att du vill radera ditt konto? Detta kan inte ångras." + "exportFailed": "Export misslyckades" } }, "appPasswords": { - "title": "Applösenord", - "description": "Applösenord låter dig logga in i tredjepartsappar utan att ge dem ditt huvudlösenord. Varje applösenord kan återkallas individuellt.", - "createNew": "Skapa nytt applösenord", - "appNamePlaceholder": "Appnamn (t.ex. Graysky, Skeets)", "created": "Applösenord skapat", "createdMessage": "Kopiera detta lösenord nu. Du kommer inte att kunna se det igen.", - "yourPasswords": "Dina applösenord", "noPasswords": "Inga applösenord ännu", - "revoke": "Återkalla", - "revoking": "Återkallar...", - "revokeConfirm": "Återkalla applösenord \"{name}\"? Appar som använder detta lösenord kommer inte längre att kunna komma åt ditt konto.", "saveWarningTitle": "Viktigt: Spara detta applösenord!", "saveWarningMessage": "Detta lösenord krävs för att logga in i appar som inte stöder passkeys eller OAuth. Du ser det bara en gång.", "acknowledgeLabel": "Jag har sparat mitt applösenord på en säker plats", "permissions": "Behörigheter", "scopeFull": "Full åtkomst", "scopeReadOnly": "Endast läsning", - "scopePostOnly": "Endast publicering", + "scopePostOnly": "Endast inlägg", "scopeCustom": "Anpassad", - "byController": "Av controller" + "byController": "Av controller", + "create": "Skapa", + "name": "Namn", + "namePlaceholder": "Appnamn (t.ex. Graysky)", + "deleteConfirm": "Återkalla applösenord \"{name}\"?", + "deleted": "Applösenord återkallat", + "loadFailed": "Kunde inte ladda applösenord", + "createFailed": "Kunde inte skapa applösenord", + "deleteFailed": "Kunde inte återkalla applösenord", + "saveWarning": "Spara detta lösenord nu - du kommer inte se det igen" }, "sessions": { - "title": "Aktiva sessioner", - "loadingSessions": "Laddar sessioner...", "noSessions": "Inga aktiva sessioner hittades.", "current": "Nuvarande", "oauth": "OAuth", @@ -404,52 +309,43 @@ "daysAgo": "{count} dagar sedan", "hoursAgo": "{count} timmar sedan", "minutesAgo": "{count} minuter sedan", - "justNow": "Just nu" + "justNow": "Just nu", + "sessionRevoked": "Session återkallad", + "allSessionsRevoked": "Alla andra sessioner återkallade" }, "inviteCodes": { - "title": "Inbjudningskoder", - "description": "Inbjudningskoder låter dig bjuda in vänner. Varje kod kan användas en gång.", "createNew": "Skapa ny inbjudningskod", - "uses": "Användningar", - "usesPlaceholder": "Antal användningar (1-100)", "yourCodes": "Dina inbjudningskoder", "noCodes": "Inga inbjudningskoder ännu", "available": "Tillgänglig", "used": "Använd av @{handle}", "disabled": "Inaktiverad", - "usedBy": "Använd av", - "disableConfirm": "Inaktivera denna inbjudningskod? Den kan inte längre användas.", "created": "Inbjudningskod skapad", "copy": "Kopiera", "createdOn": "Skapad {date}", - "spent": "Förbrukad" + "spent": "Förbrukad", + "loadFailed": "Kunde inte ladda inbjudningskoder", + "createFailed": "Kunde inte skapa inbjudningskod" }, "security": { - "title": "Säkerhet", "passkeys": "Nycklar", - "passkeysDescription": "Nycklar ger säker, lösenordsfri autentisering med din enhets inbyggda säkerhet (fingeravtryck, ansikte eller PIN).", "addPasskey": "Lägg till nyckel", "adding": "Lägger till...", "noPasskeys": "Inga nycklar registrerade", "passkeyName": "Nyckelnamn", "passkeyNamePlaceholder": "t.ex. MacBook Pro, iPhone", - "register": "Registrera", - "registering": "Registrerar...", "rename": "Byt namn", - "renaming": "Byter namn...", "deletePasskey": "Radera", "deletePasskeyConfirm": "Radera nyckel \"{name}\"? Du kommer inte att kunna använda den för att logga in längre.", "totp": "Autentiseringsapp (TOTP)", - "totpDescription": "Använd en autentiseringsapp som Google Authenticator, Authy eller 1Password för tvåfaktorsautentisering.", "totpEnabled": "TOTP är aktiverat", "totpDisabled": "TOTP är inte aktiverat", "enableTotp": "Aktivera TOTP", "disableTotp": "Inaktivera TOTP", "disabling": "Inaktiverar...", - "totpSetup": "Konfigurera autentiseringsapp", "totpSetupInstructions": "Skanna denna QR-kod med din autentiseringsapp och ange sedan den 6-siffriga koden för att verifiera.", - "totpCode": "Verifieringskod", - "totpCodePlaceholder": "Ange 6-siffrig kod", + "totpCode": "TOTP-kod", + "totpCodePlaceholder": "6 siffror", "verifyAndEnable": "Verifiera och aktivera", "backupCodes": "Reservkoder", "backupCodesDescription": "Använd dessa koder för att logga in om du förlorar tillgång till din autentiseringsapp. Varje kod kan endast användas en gång.", @@ -463,15 +359,6 @@ "enableLegacyLogin": "Aktivera föråldrad inloggning", "disableLegacyLogin": "Inaktivera föråldrad inloggning", "legacyLoginWarning": "Varning: Att aktivera föråldrad inloggning kringgår MFA för direkta lösenordsinloggningar. Aktivera endast om det behövs för appkompatibilitet.", - "totpPasswordWarning": "Med TOTP aktiverat blockeras lösenordsändringar från Bluesky-appen (eller andra föråldrade appar). För att ändra ditt lösenord har du två alternativ:", - "totpPasswordOption1Label": "Ändra det här:", - "totpPasswordOption1Text": "Använd denna webbplats", - "totpPasswordOption1Link": "Inställningssida", - "totpPasswordOption1Suffix": "där du kan verifiera med din autentiseringsapp.", - "totpPasswordOption2Label": "Verifiera din session först:", - "totpPasswordOption2Text": "Använd", - "totpPasswordOption2Link": "återautentiseringsalternativet", - "totpPasswordOption2Suffix": "för att verifiera din Bluesky-session med TOTP, sedan fungerar lösenordsändringar tillfälligt.", "legacyAppsTitle": "Vad är föråldrade appar?", "legacyAppsDescription": "Vissa appar (som den officiella Bluesky-appen) använder föråldrad autentisering som endast kräver ditt lösenord. När du har MFA aktiverat kringgår dessa appar din andra faktor. Att inaktivera föråldrad inloggning tvingar alla appar att använda OAuth, som korrekt tillämpar MFA.", "password": "Lösenord", @@ -479,13 +366,7 @@ "noPassword": "Inget lösenord inställt (endast nyckelkonto)", "setPassword": "Ställ in lösenord", "removePassword": "Ta bort lösenord", - "removePasswordConfirm": "Ta bort ditt lösenord? Du måste använda nycklar för att logga in.", "removing": "Tar bort...", - "loading": "Laddar...", - "loadingPasskeys": "Laddar nycklar...", - "cancel": "Avbryt", - "save": "Spara", - "back": "Tillbaka", "next": "Nästa: Verifiera kod", "copyToClipboard": "Kopiera till urklipp", "savedMyCodes": "Jag har sparat mina koder", @@ -493,20 +374,10 @@ "unnamedPasskey": "Namnlös nyckel", "added": "Tillagd", "lastUsed": "Senast använd", - "passwordDescription": "Hantera ditt kontolösenord. Om du har nycklar konfigurerade kan du valfritt ta bort ditt lösenord för en helt lösenordsfri upplevelse.", "disableTotpWarning": "Detta gör ditt konto mindre säkert.", "removePasswordWarning": "Detta gör ditt konto till endast nyckelkonto. Du kan endast logga in med dina registrerade nycklar. Om du förlorar tillgång till alla dina nycklar kan du återställa ditt konto via din meddelandekanal.", - "beforeProceeding": "Innan du fortsätter:", - "beforeProceedingItem1": "Se till att du har minst en pålitlig nyckel registrerad", - "beforeProceedingItem2": "Överväg att registrera nycklar på flera enheter", - "beforeProceedingItem3": "Se till att din meddelandekanal för återställning är uppdaterad", - "addPasskeyFirst": "Lägg till minst en nyckel innan du kan ta bort ditt lösenord.", - "passkeyOnlyHint": "Du loggar in med endast nycklar. Om du förlorar tillgång till dina nycklar kan du återställa ditt konto med länken \"Tappat bort nyckeln?\" på inloggningssidan.", - "addPasswordHint": "Vill du lägga till ett lösenord? Gå till Inställningar för att ställa in ett.", - "goToSettings": "Gå till inställningar", "trustedDevices": "Betrodda enheter", - "trustedDevicesDescription": "Hantera enheter som kan hoppa över tvåfaktorsautentisering vid inloggning. Förtroende beviljas i 30 dagar och förlängs automatiskt när du använder enheten.", - "manageTrustedDevices": "Hantera betrodda enheter", + "trustedDevicesDescription": "Enheter som kan hoppa över tvåfaktorsautentisering vid inloggning. Förtroende beviljas i 30 dagar och förlängs automatiskt när du använder enheten.", "appCompatibility": "Appkompatibilitet", "enterPassword": "Ange ditt lösenord", "sessionExpired": "Sessionen har gått ut. Logga in igen.", @@ -524,13 +395,26 @@ "passkeyCreationCancelled": "Nyckelskapande avbröts", "passkeyAddedSuccess": "Nyckel tillagd", "passkeyDeleted": "Nyckel raderad", - "passkeyRenamed": "Nyckel omdöpt" + "passkeyRenamed": "Nyckel omdöpt", + "changePassword": "Ändra lösenord", + "currentPassword": "Nuvarande lösenord", + "currentPasswordPlaceholder": "Ange nuvarande lösenord", + "newPassword": "Nytt lösenord", + "newPasswordPlaceholder": "Ange nytt lösenord", + "confirmPassword": "Bekräfta nytt lösenord", + "confirmPasswordPlaceholder": "Bekräfta nytt lösenord", + "passwordsDoNotMatch": "Lösenorden matchar inte", + "passwordTooShort": "Lösenordet måste vara minst 8 tecken", + "passwordChanged": "Lösenordet har ändrats", + "failedToChangePassword": "Kunde inte ändra lösenord", + "changing": "Ändrar...", + "setting": "Ställer in...", + "passwordSet": "Lösenordet har ställts in", + "failedToSetPassword": "Kunde inte ställa in lösenord", + "failedToDisableTotp": "Kunde inte inaktivera TOTP" }, "comms": { - "title": "Kommunikationsinställningar", - "description": "Välj hur du vill ta emot viktiga meddelanden som lösenordsåterställningar, säkerhetsvarningar och kontouppdateringar.", "preferredChannel": "Föredragen kanal", - "preferredChannelDescription": "Välj ditt föredragna sätt att ta emot meddelanden. Du måste konfigurera en kanal innan du kan välja den.", "channelConfiguration": "Kanalkonfiguration", "emailVia": "Ta emot meddelanden via e-post", "discordVia": "Ta emot meddelanden via Discord DM", @@ -538,10 +422,6 @@ "signalVia": "Ta emot meddelanden via Signal", "configureToEnable": "Konfigurera nedan för att aktivera", "notConfiguredOnServer": "Inte konfigurerat på denna server", - "emailManagedInSettings": "Din e-post hanteras i Kontoinställningar", - "discordIdHint": "Ditt Discord användar-ID (inte användarnamn). Aktivera Utvecklarläge i Discord för att kopiera det.", - "telegramHint": "Ditt Telegram-användarnamn utan @-symbolen", - "signalHint": "Ditt Signal-telefonnummer med landskod", "primary": "Primär", "verified": "Verifierad", "notVerified": "Ej verifierad", @@ -552,29 +432,20 @@ "preferencesSaved": "Kommunikationsinställningar sparade", "verifiedSuccess": "{channel} verifierad", "messageHistory": "Meddelandehistorik", - "historyDescription": "Visa senaste meddelanden skickade till ditt konto.", - "loadHistory": "Ladda historik", - "hideHistory": "Dölj historik", "noMessages": "Inga meddelanden hittades.", - "sent": "skickad", - "failed": "misslyckades", "discordInUseWarning": "Detta Discord-ID är redan kopplat till ett annat konto.", "telegramInUseWarning": "Detta Telegram-användarnamn är redan kopplat till ett annat konto.", - "signalInUseWarning": "Detta Signal-nummer är redan kopplat till ett annat konto." + "signalInUseWarning": "Detta Signal-nummer är redan kopplat till ett annat konto.", + "failedToLoad": "Kunde inte ladda inställningar", + "failedToSave": "Kunde inte spara inställningar", + "failedToVerify": "Verifiering misslyckades", + "failedToLoadHistory": "Kunde inte ladda meddelandehistorik" }, "repoExplorer": { - "title": "Dataförvarsutforskare", - "description": "Bläddra och hantera dina råa AT Protocol-poster.", "collections": "Samlingar", - "noCollections": "Inga samlingar hittades", - "records": "Poster", "noRecords": "Inga poster i denna samling", - "recordDetails": "Postdetaljer", - "rkey": "Postnyckel", "uri": "URI", "cid": "CID", - "value": "Värde", - "deleteRecord": "Radera post", "deleteConfirm": "Radera post {rkey}? Detta kan inte ångras.", "unknownError": "Ett okänt fel uppstod", "invalidJson": "Ogiltig JSON", @@ -587,7 +458,6 @@ "filterCollections": "Filtrera samlingar...", "filterRecords": "Filtrera poster...", "noCollectionsYet": "Inga samlingar ännu. Skapa din första post för att komma igång.", - "loadMore": "Ladda fler", "recordJson": "Post-JSON", "updateRecord": "Uppdatera post", "collectionNsid": "Samling (NSID)", @@ -599,8 +469,6 @@ "demoBio": "En kort presentation om dig själv." }, "admin": { - "title": "Adminpanel", - "loading": "Laddar...", "serverConfig": "Serverkonfiguration", "serverName": "Servernamn", "serverNamePlaceholder": "Min PDS", @@ -623,8 +491,6 @@ "refreshStats": "Uppdatera statistik", "userManagement": "Användarhantering", "searchPlaceholder": "Sök på användarnamn (valfritt)", - "searchUsers": "Sök användare", - "noUsers": "Inga användare hittades", "handle": "Användarnamn", "email": "E-post", "status": "Status", @@ -634,10 +500,8 @@ "loadInviteCodes": "Ladda inbjudningskoder", "refresh": "Uppdatera", "noInvites": "Inga inbjudningskoder hittades", - "code": "Kod", "available": "Tillgänglig", "uses": "Användningar", - "actions": "Åtgärder", "disable": "Inaktivera", "disableInviteConfirm": "Inaktivera inbjudningskod {code}?", "active": "Aktiv", @@ -654,9 +518,17 @@ "verified": "Verifierad", "unverified": "Ej verifierad", "deactivated": "Inaktiverad", - "colorDefault": "{color} (standard)", "secondaryLight": "Sekundär (Ljust läge)", - "secondaryDark": "Sekundär (Mörkt läge)" + "secondaryDark": "Sekundär (Mörkt läge)", + "failedToLoadStats": "Kunde inte ladda serverstatistik", + "failedToLoadUsers": "Kunde inte ladda användare", + "searchToSeeUsers": "Sök för att visa användare", + "search": "Sök", + "inviteDisabled": "Inbjudningskod inaktiverad", + "invitesEnabled": "Användarinbjudningar aktiverade", + "invitesDisabled": "Användarinbjudningar inaktiverade", + "userDeleted": "Användarkonto raderat", + "failedToLoadConfig": "Kunde inte ladda serverkonfiguration" }, "oauth": { "login": { @@ -675,7 +547,6 @@ "passkeyHintNotAvailable": "Ingen nyckel registrerad", "passwordPlaceholder": "Lösenord", "usePasskey": "Använd nyckel", - "orContinueWith": "eller", "orUseCredentials": "eller" }, "register": { @@ -686,77 +557,31 @@ }, "sso": { "linkedAccounts": "Länkade konton", - "linkedAccountsDesc": "Externa konton länkade till din identitet för enkel inloggning.", "noLinkedAccounts": "Inga länkade konton", - "noLinkedAccountsDesc": "Länka ett externt konto för att aktivera snabb inloggning med den leverantören.", - "linkAccount": "Länka konto", - "unlinkAccount": "Ta bort länk", "unlinkConfirm": "Är du säker på att du vill ta bort länken till detta konto?", "unlinked": "Länk till {provider} borttagen", - "lastLoginAt": "Senast använd", - "linkedAt": "Länkad" + "linkedAt": "Länkad", + "unlink": "Ta bort länk", + "linkNewAccount": "Länka konto", + "linked": "Länkad", + "linkSuccess": "Konto länkat", + "linkFailed": "Kunde inte länka konto", + "unlinkFailed": "Kunde inte ta bort länk" }, "consent": { "title": "Auktorisera applikation", "appWantsAccess": "{app} vill ha tillgång till ditt konto", - "permissions": "Denna applikation kommer att kunna:", - "readProfile": "Läsa din profilinformation", - "readPosts": "Läsa dina inlägg och innehåll", - "writePosts": "Skapa och radera inlägg för din räkning", - "readNotifications": "Läsa dina aviseringar", - "fullAccess": "Full tillgång till ditt konto", "authorize": "Auktorisera", "deny": "Neka", "authorizing": "Auktoriserar...", - "rememberChoice": "Kom ihåg detta val", "signingInAs": "Loggar in som:", "permissionsRequested": "Begärda behörigheter", "required": "Krävs", "rememberChoiceLabel": "Kom ihåg mitt val för denna applikation", "scopes": { - "atproto": { - "name": "Full åtkomst", - "description": "Full åtkomst för att läsa, skriva och hantera detta konto" - }, "atprotoWithGranular": { "name": "AT Protocol-åtkomst", "description": "AT Protocol basomfattning (behörigheter bestäms av valda alternativ nedan)" - }, - "transitionGeneric": { - "name": "Övergångsåtkomst", - "description": "Generisk övergångsomfattning för kompatibilitet" - }, - "transitionChat": { - "name": "Chattåtkomst", - "description": "Åtkomst till Bluesky-chattfunktioner" - }, - "transitionEmail": { - "name": "E-poståtkomst", - "description": "Läs din kontots e-postadress" - }, - "repoCreate": { - "name": "Skapa poster", - "description": "Skapa nya poster i ditt arkiv" - }, - "repoUpdate": { - "name": "Uppdatera poster", - "description": "Uppdatera befintliga poster i ditt arkiv" - }, - "repoDelete": { - "name": "Ta bort poster", - "description": "Ta bort poster från ditt arkiv" - }, - "blobAll": { - "name": "Ladda upp media", - "description": "Ladda upp bilder, videor och andra mediefiler" - }, - "repoFull": { - "name": "Full arkivåtkomst", - "description": "Full läs- och skrivåtkomst till alla arkivposter" - }, - "accountManage": { - "name": "Hantera konto", - "description": "Hantera kontoinställningar och preferenser" } }, "unexpectedState": { @@ -769,11 +594,6 @@ "title": "Välj konto", "useAnother": "Använd ett annat konto" }, - "twoFactor": { - "title": "Verifiering", - "usePasskey": "Använd nyckel", - "useTotp": "Använd autentiserare" - }, "twoFactorCode": { "title": "Verifiering", "subtitle": "Kod skickad till {channel}", @@ -789,21 +609,14 @@ "totp": { "title": "Autentiseringskod", "codePlaceholder": "6-siffrig kod", - "useBackupCode": "Använd reservkod", "backupCodePlaceholder": "Reservkod", "trustDevice": "Lita på denna enhet i 30 dagar", "hintBackupCode": "Reservkod", "hintTotpCode": "Autentiseringskod" }, - "passkey": { - "title": "Nyckel", - "waiting": "Väntar", - "useTotp": "Använd autentiserare" - }, "error": { "title": "Auktorisering misslyckades", - "tryAgain": "Försök igen", - "backToApp": "Tillbaka" + "tryAgain": "Försök igen" } }, "sso_register": { @@ -829,10 +642,9 @@ "subtitle": "Vi har skickat en verifieringskod till din {channel}. Ange den nedan för att slutföra registreringen.", "tokenTitle": "Verifiera", "tokenSubtitle": "Ange verifieringskoden och identifieraren den skickades till.", - "codePlaceholder": "Paste verification code", + "codePlaceholder": "Klistra in verifieringskod", "codeLabel": "Verifieringskod", "codeHelp": "Kopiera hela koden från ditt meddelande, ", - "verifyButton": "Verifiera konto", "pleaseWait": "Vänta...", "codeResent": "Verifieringskod skickad igen!", "codeResentDetail": "Verifieringskod skickad! Kontrollera din inkorg.", @@ -874,7 +686,6 @@ "sendCode": "Skicka återställningskod", "sending": "Skickar...", "codeSent": "Återställningskod skickad! Kontrollera din föredragna meddelandekanal.", - "enterCode": "Ange koden du fick och ditt nya lösenord.", "code": "Återställningskod", "codePlaceholder": "Ange återställningskod", "newPassword": "Nytt lösenord", @@ -886,8 +697,7 @@ "success": "Lösenord återställt!", "requestNewCode": "Begär ny kod", "passwordsMismatch": "Lösenorden matchar inte", - "passwordLength": "Lösenordet måste vara minst 8 tecken", - "multipleAccountsWarning": "Flera konton delar denna e-post. Återställningskoden skickades till det senast skapade kontot. Använd ditt användarnamn istället för ett specifikt konto." + "passwordLength": "Lösenordet måste vara minst 8 tecken" }, "recoverPasskey": { "title": "Återställ ditt konto", @@ -931,32 +741,8 @@ "sending": "Skickar..." }, "registerPasskey": { - "title": "Skapa nyckelkonto", - "subtitleKeyChoice": "Konfigurera din did:web-identitet", - "subtitleInitialDidDoc": "Ladda upp ditt DID-dokument", - "subtitleCreating": "Skapar konto", - "subtitlePasskey": "Registrera din nyckel", - "subtitleAppPassword": "Spara ditt applösenord", - "subtitleVerify": "Verifiera din {channel}", - "subtitleUpdatedDidDoc": "Uppdatera ditt DID-dokument", - "subtitleActivating": "Aktiverar", - "subtitleComplete": "Konto skapat", - "handle": "Användarnamn", - "handlePlaceholder": "dittnamn", - "handleHint": "Ditt fullständiga användarnamn blir: @{handle}", - "contactMethod": "Kontaktmetod", - "verificationMethod": "Verifieringsmetod", - "email": "E-postadress", - "emailPlaceholder": "du@exempel.se", - "inviteCode": "Inbjudningskod", - "inviteCodePlaceholder": "Ange din inbjudningskod", "externalDid": "Din did:web", "externalDidPlaceholder": "did:web:dindomän.se", - "createButton": "Skapa konto", - "alreadyHaveAccount": "Har du redan ett konto?", - "signIn": "Logga in", - "wantPassword": "Vill du använda ett lösenord?", - "createPasswordAccount": "Skapa ett lösenordskonto", "errors": { "handleRequired": "Användarnamn krävs", "handleNoDots": "Användarnamn kan inte innehålla punkter. Du kan konfigurera ett eget domännamn efter att kontot skapats.", @@ -971,7 +757,6 @@ "externalDidFormat": "Extern DID måste börja med did:web:", "discordRequired": "Discord-ID krävs för Discord-verifiering" }, - "creatingPasskey": "Skapar", "identityType": "Identitetstyp", "identityTypeHint": "Välj hur din decentraliserade identitet ska hanteras.", "passkeyNamePlaceholder": "t.ex. MacBook Touch ID", @@ -994,29 +779,17 @@ "didWebWarning4": "Rekommendation:", "didWebWarning4Detail": "Välj did:plc om du inte har en specifik anledning att föredra did:web.", "externalDidHint": "Du behöver servera ett DID-dokument på", - "continue": "Fortsätt", - "back": "Tillbaka", - "loading": "Laddar...", - "redirecting": "Omdirigerar till instrumentpanelen...", - "handleDotWarning": "Egna domännamn kan konfigureras efter att kontot skapats.", - "wantTraditional": "Vill du ha ett traditionellt lösenord?", - "registerWithPassword": "Registrera med lösenord", - "activatingAccount": "Activating", - "creatingAccount": "Creating account", - "passkeyDescription": "Register a passkey for this account", - "passkeyName": "Passkey Name", - "setupPasskey": "Create Passkey" + "activatingAccount": "Aktiverar", + "creatingAccount": "Skapar konto", + "passkeyDescription": "Registrera en nyckel för detta konto", + "passkeyName": "Nyckelnamn", + "setupPasskey": "Skapa nyckel" }, "trustedDevices": { - "title": "Betrodda enheter", - "backToSecurity": "← Säkerhetsinställningar", - "description": "Betrodda enheter kan hoppa över tvåfaktorsautentisering vid inloggning. Förtroende beviljas i 30 dagar och förlängs automatiskt när du använder enheten.", - "failedToLoad": "Kunde inte ladda betrodda enheter", "noDevices": "Inga betrodda enheter ännu.", "noDevicesHint": "När du loggar in med tvåfaktorsautentisering aktiverat kan du välja att lita på enheten i 30 dagar.", "lastSeen": "Senast sedd:", "trustedSince": "Betrodd sedan:", - "trustExpires": "Förtroende upphör:", "expired": "Upphört", "tomorrow": "I morgon", "inDays": "Om {days} dagar", @@ -1025,7 +798,6 @@ "deviceRevoked": "Enhetsförtroende återkallat", "deviceRenamed": "Enhet omdöpt", "deviceNamePlaceholder": "Enhetsnamn", - "browser": "Webbläsare:", "unknownDevice": "Okänd enhet" }, "reauth": { @@ -1034,36 +806,11 @@ "totp": "TOTP", "passkey": "Nyckel", "authenticatorCode": "Autentiseringskod", - "usePassword": "Lösenord", "usePasskey": "Nyckel", - "useTotp": "Autentiserare", - "passwordPlaceholder": "Ange ditt lösenord", - "totpPlaceholder": "6-siffrig kod", "authenticating": "Autentiserar", "cancel": "Avbryt" }, - "verifyChannel": { - "title": "Verifiera kanal", - "subtitle": "Ange verifieringskoden som skickades till din meddelandekanal.", - "signInRequired": "Inloggning krävs", - "signInRequiredDesc": "Du måste vara inloggad för att verifiera en kanal.", - "signIn": "Logga in", - "verifying": "Verifierar...", - "pleaseWait": "Vänta medan vi verifierar din kanal.", - "successTitle": "Verifierad!", - "successDesc": "Din {channel} har verifierats.", - "backToSettings": "Tillbaka till inställningar", - "channelLabel": "Kanal", - "selectChannel": "Välj kanal...", - "identifierLabel": "Identifierare", - "identifierPlaceholder": "E-post, Discord ID, etc.", - "identifierHelp": "E-postadressen, Discord ID, Telegram-användarnamn eller Signal-nummer som verifieras.", - "codeLabel": "Verifieringskod", - "codeHelp": "Kopiera hela koden från ditt meddelande, .", - "verifyButton": "Verifiera" - }, "delegation": { - "title": "Kontodelegering", "controllers": "Kontrollanter", "controlledAccounts": "Kontrollerade konton", "noControllers": "Inga kontrollanter ännu", @@ -1076,13 +823,7 @@ "scopeCustom": "Anpassad", "actAs": "Agera som", "auditLog": "Granskningslogg", - "auditLogTitle": "Delegerings-granskningslogg", - "backToControllers": "← Tillbaka till kontrollanter", - "loading": "Laddar...", - "noActivity": "Ingen aktivitet ännu", "actor": "Aktör", - "controller": "Kontrollant", - "account": "Konto", "details": "Detaljer", "actionGrantCreated": "Behörighet skapad", "actionGrantRevoked": "Behörighet återkallad", @@ -1093,9 +834,7 @@ "actionAccountAction": "Kontoåtgärd", "previous": "Föregående", "next": "Nästa", - "showing": "{start}–{end} av {total}", "refresh": "Uppdatera", - "failedToLoadAuditLog": "Kunde inte ladda granskningsloggen", "adding": "Lägger till...", "accessLevel": "Åtkomstnivå", "addControllerButton": "+ Lägg till kontrollant", @@ -1117,25 +856,24 @@ "createDelegatedAccount": "Skapa delegerat konto", "createDelegatedAccountButton": "+ Skapa delegerat konto", "emailOptional": "E-post (valfritt)", - "failedToAddController": "Kunde inte lägga till kontrollant", - "failedToCreateAccount": "Kunde inte skapa delegerat konto", - "failedToRemoveController": "Kunde inte ta bort kontrollant", "granted": "Beviljad", "inactive": "Inaktiv", "remove": "Ta bort", "removeConfirm": "Vill du ta bort denna kontrollant?", "viewAuditLog": "Visa granskningslogg", "yourAccessLevel": "Din åtkomstnivå", - "accountCreated": "Skapade delegerat konto: {handle}" + "accountCreated": "Skapade delegerat konto: {handle}", + "noAuditEntries": "Inga granskningsposter", + "target": "Mål", + "pageInfo": "{start} - {end} av {total}", + "failedToLoadAudit": "Kunde inte ladda granskningslogg" }, "actAs": { "title": "Agera som", "noAccountSpecified": "Inget konto-DID angivet", - "failedToVerify": "Kunde inte verifiera kontoåtkomst", "noAccess": "Du har inte åtkomst till detta konto", "failedToInitiate": "Kunde inte initiera autentisering", "invalidResponse": "Ogiltigt svar från servern", - "failedError": "Misslyckades: {error}", "preparing": "Förbereder inloggning till delegerat konto...", "backToControllers": "Tillbaka till kontrollanter" }, @@ -1185,8 +923,6 @@ "migration": { "title": "Kontoflyttning", "subtitle": "Flytta din AT Protocol-identitet mellan servrar", - "navTitle": "Flytta", - "navDesc": "Flytta ditt konto till eller från en annan PDS", "migrateHere": "Flytta hit", "migrateHereDesc": "Flytta ditt befintliga AT Protocol-konto till denna PDS från en annan server.", "bringDid": "Ta med din DID och identitet", @@ -1252,6 +988,7 @@ "checkingAvailability": "Kontrollerar tillgänglighet...", "handleAvailable": "Användarnamnet är tillgängligt!", "handleTaken": "Användarnamnet är redan taget", + "handleTooShort": "Användarnamnet måste vara minst 3 tecken", "handleHint": "Du kan också använda din egen domän genom att ange det fullständiga användarnamnet (t.ex. alice.mindomän.se)", "email": "E-postadress", "authMethod": "Autentiseringsmetod", @@ -1276,7 +1013,6 @@ "authentication": "Autentisering", "authPasskey": "Passkey (lösenordslös)", "authPassword": "Lösenord", - "inviteCode": "Inbjudningskod", "warning": "När du klickar på \"Starta flytt\" börjar ditt arkiv och data överföras. Denna process kan inte enkelt ångras.", "startMigration": "Starta flytt", "starting": "Startar..." @@ -1313,9 +1049,7 @@ "hint": "Ange koden nedan eller klicka på länken i e-postmeddelandet för att fortsätta automatiskt.", "tokenLabel": "Verifieringskod", "tokenPlaceholder": "Ange kod från e-post", - "resend": "Skicka kod igen", - "verify": "Verifiera e-post", - "verifying": "Verifierar..." + "resend": "Skicka kod igen" }, "plcToken": { "title": "Verifiera flytt", @@ -1423,7 +1157,6 @@ "desc": "Granska dina offline-återställningsuppgifter.", "carFile": "CAR-fil", "rotationKey": "Rotationsnyckel", - "warning": "När du startar återställningen kommer din identitet att uppdateras för att peka på denna PDS. Detta kan inte enkelt ångras.", "plcWarningTitle": "Ingen återvändo", "plcWarning": "När du startar kommer ditt DID-dokument att uppdateras för att peka på denna PDS. Om något går fel kan du använda din rotationsnyckel för att återställa, men du bör slutföra flytten för att undvika ett trasigt identitetstillstånd." }, @@ -1431,9 +1164,7 @@ "title": "Återställer konto", "desc": "Vänta medan ditt konto återställs...", "creating": "Skapar konto", - "importing": "Importerar arkiv", - "plcSigning": "Uppdaterar identitet", - "activating": "Aktiverar konto" + "importing": "Importerar arkiv" }, "success": { "desc": "Ditt konto har framgångsrikt återställts till denna PDS." @@ -1443,28 +1174,8 @@ "desc": "Försöker återställa bilder och media från din gamla PDS...", "migrating": "Flyttar blobbar", "failedTitle": "Vissa blobbar kunde inte flyttas", - "failedDesc": "{count} blobbar kunde inte hämtas från din gamla PDS. Detta kan hända om servern är otillgänglig eller om filerna raderades.", - "sourceUnreachableTitle": "Käll-PDS otillgänglig", - "sourceUnreachable": "Kunde inte ansluta till din gamla PDS för att hämta mediafiler. Detta är vanligt vid flytt från en nedstängd server. Dina inlägg kommer att fungera, men vissa bilder kan saknas." + "failedDesc": "{count} blobbar kunde inte hämtas från din gamla PDS. Detta kan hända om servern är otillgänglig eller om filerna raderades." } - }, - "progress": { - "repoExported": "Arkiv exporterat", - "repoImported": "Arkiv importerat", - "blobsMigrated": "{count} blobbar flyttade", - "prefsMigrated": "Inställningar flyttade", - "plcSigned": "Identitet uppdaterad", - "activated": "Konto aktiverat", - "deactivated": "Gammalt konto inaktiverat" - }, - "errors": { - "connectionFailed": "Kunde inte ansluta till PDS", - "invalidCredentials": "Ogiltiga uppgifter", - "twoFactorRequired": "Tvåfaktorautentisering krävs", - "accountExists": "Konto finns redan på mål-PDS", - "plcFailed": "PLC-operation misslyckades", - "blobFailed": "Kunde inte flytta blob: {cid}", - "networkError": "Nätverksfel. Försök igen." } } } diff --git a/frontend/src/locales/zh.json b/frontend/src/locales/zh.json index 72511d0..e8d0b78 100644 --- a/frontend/src/locales/zh.json +++ b/frontend/src/locales/zh.json @@ -8,55 +8,23 @@ "done": "完成", "continue": "继续", "refresh": "刷新", - "create": "创建", "delete": "删除", - "confirm": "确认", "created": "创建时间", - "expires": "过期时间", "name": "名称", - "dashboard": "控制台", "backToDashboard": "← 返回控制台", "copied": "已复制", "copyToClipboard": "复制", "verifying": "验证中", "saving": "保存中", "creating": "创建中", - "updating": "更新中", "sending": "发送中", - "authenticating": "认证中", "checking": "检查中", - "redirecting": "跳转中", "signIn": "登录", "verify": "验证", - "remove": "移除", "revoke": "撤销", "resendCode": "重新发送", - "startOver": "重新开始", - "tryAgain": "重试", - "password": "密码", - "email": "邮箱", - "emailAddress": "邮箱地址", - "handle": "用户名", - "did": "DID", - "verificationCode": "验证码", - "inviteCode": "邀请码", - "newPassword": "新密码", - "confirmPassword": "确认密码", - "enterSixDigitCode": "输入6位验证码", - "passwordHint": "至少8个字符", - "enterPassword": "请输入密码", - "emailPlaceholder": "you@example.com", - "verified": "已验证", - "disabled": "已禁用", - "available": "可用", - "deactivated": "已停用", - "unverified": "未验证", "backToLogin": "返回登录", - "backToSettings": "返回设置", - "alreadyHaveAccount": "已有账户?", - "createAccount": "立即注册", - "passwordsMismatch": "密码不匹配", - "passwordTooShort": "密码至少需要8个字符" + "backToSettings": "返回设置" }, "login": { "title": "登录", @@ -75,18 +43,10 @@ "subtitle": "请输入发送到您联系方式的验证码", "codeLabel": "验证码", "codePlaceholder": "6位验证码", - "verifyButton": "验证", "resent": "验证码已发送" }, "register": { "title": "创建账户", - "subtitle": "在此 PDS 上创建新账户", - "subtitleKeyChoice": "设置 did:web 身份", - "subtitleInitialDidDoc": "上传 DID 文档", - "subtitleVerify": "验证您的{channel}", - "subtitleUpdatedDidDoc": "更新 DID 文档", - "subtitleActivating": "激活中", - "subtitleComplete": "账户已创建", "redirecting": "跳转中", "migrateTitle": "已有账户?", "migrateDescription": "迁移现有账户", @@ -115,8 +75,6 @@ "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 文档", @@ -143,14 +101,11 @@ "inviteCode": "邀请码", "inviteCodePlaceholder": "输入您的邀请码", "inviteCodeRequired": "必填", - "createButton": "创建账户", "alreadyHaveAccount": "已有账户?", "signIn": "立即登录", - "emailInUseWarning": "此邮箱已与其他账户关联。您仍可使用,但账户恢复时可能需要使用用户名。", "passkeyAccount": "通行密钥", "passwordAccount": "密码", "ssoAccount": "SSO", - "ssoSubtitle": "使用外部提供商创建账户", "noSsoProviders": "此服务器未配置SSO提供商。", "continueWith": "使用{provider}继续", "validation": { @@ -170,12 +125,13 @@ }, "dashboard": { "title": "控制台", + "accountManager": "账户管理", + "navDelegationAudit": "委托审计", "switchAccount": "切换账户", "addAnotherAccount": "添加其他账户", "signOut": "退出 @{handle}", "deactivatedTitle": "账户已停用", "deactivatedMessage": "您的账户目前已停用。这通常发生在账户迁移期间。在账户重新激活之前,部分功能可能受限。", - "accountOverview": "账户概览", "handle": "用户名", "did": "DID", "primaryContact": "主要联系方式", @@ -184,38 +140,20 @@ "verified": "已验证", "unverified": "未验证", "navAppPasswords": "应用专用密码", - "navAppPasswordsDesc": "管理第三方应用的专用密码", "navSessions": "登录会话", - "navSessionsDesc": "查看和管理您的登录会话", "navInviteCodes": "邀请码", - "navInviteCodesDesc": "查看和创建邀请码", - "navSettings": "账户设置", - "navSettingsDesc": "邮箱、密码、用户名等", + "navSettings": "通用", "navSecurity": "安全设置", - "navSecurityDesc": "双重身份验证", "navComms": "通讯偏好", - "navCommsDesc": "Discord、Telegram、Signal 渠道设置", "navRepo": "数据浏览器", - "navRepoDesc": "浏览和管理原始 AT Protocol 记录", "navDelegation": "账户委托", - "navDelegationDesc": "管理控制者和委托账户", "navAdmin": "管理后台", - "navAdminDesc": "服务器统计和管理操作", "navDidDocument": "DID 文档", - "navDidDocumentDesc": "管理您的 DID 文档和密钥", - "navDidDocumentDescActive": "编辑您的 DID 文档设置", - "navBackup": "下载备份", - "navBackupDesc": "将您的存储库下载为 CAR 文件", - "downloadingBackup": "下载中...", - "backupFailed": "下载备份失败", "migrated": "已迁移", "migratedTitle": "账户已迁移", - "migratedMessage": "您的账户已迁移到 {pds}。您的 DID 文档仍在此处托管。", - "navMigrateAgain": "再次迁移", - "navMigrateAgainDesc": "迁移到另一个 PDS 并更新您的 DID 文档" + "migratedMessage": "您的账户已迁移到 {pds}。您的 DID 文档仍在此处托管。" }, "didEditor": { - "title": "DID 文档编辑器", "preview": "当前 DID 文档", "verificationMethods": "验证方法(签名密钥)", "verificationMethodsDesc": "可以代表您的 DID 进行操作的签名密钥。迁移到新 PDS 时,请在此添加其签名密钥。", @@ -241,14 +179,11 @@ "saveFailed": "保存 DID 文档失败", "loadFailed": "加载 DID 文档失败", "invalidMultibase": "公钥必须是以 'z' 开头的有效 multibase 字符串", - "invalidHandle": "用户名必须是 at:// URI(例如:at://handle.example.com)", "helpTitle": "这是什么?", "helpText": "当您迁移到另一个 PDS 时,该 PDS 会生成新的签名密钥。在此处更新您的 DID 文档,使其指向您的新密钥和位置。" }, "settings": { - "title": "账户设置", "language": "语言", - "languageDescription": "选择您的首选语言", "changeEmail": "更改邮箱", "currentEmail": "当前:{email}", "newEmail": "新邮箱", @@ -266,7 +201,6 @@ "currentHandle": "当前:@{handle}", "pdsHandle": "PDS 用户名", "customDomain": "自定义域名", - "customDomainDescription": "使用您自己的域名作为用户名。需要先验证域名所有权。", "setupInstructions": "设置说明", "setupMethodsIntro": "选择以下验证方式之一:", "dnsMethod": "方式一:DNS TXT 记录(推荐)", @@ -280,33 +214,16 @@ "newHandle": "新用户名", "newHandlePlaceholder": "yourhandle", "changeHandleButton": "更改用户名", - "changePassword": "更改密码", - "currentPassword": "当前密码", - "currentPasswordPlaceholder": "输入当前密码", - "newPassword": "新密码", - "newPasswordPlaceholder": "至少8位字符", - "confirmNewPassword": "确认新密码", - "confirmNewPasswordPlaceholder": "再次输入新密码", - "changePasswordButton": "更改密码", - "changing": "更改中...", - "setPassword": "设置密码", - "setPasswordDescription": "您的账户当前仅使用通行密钥。您可以添加密码以启用传统登录方式与通行密钥并用。", - "setPasswordButton": "设置密码", - "setting": "设置中...", "exportData": "导出数据", - "exportDataDescription": "将您的所有数据下载为 CAR 文件。包括您的所有帖子、点赞、关注等数据。", "downloadRepo": "下载数据", "downloadBlobs": "下载媒体文件", "exporting": "导出中...", "backups": { "title": "备份", - "description": "管理自动备份并恢复账户数据。备份包括所有记录和文件。", - "enableAutomatic": "自动备份", "enabled": "已启用", "disabled": "已禁用", "toggleFailed": "更改备份设置失败", "noBackups": "暂无备份", - "blocks": "块", "download": "下载", "delete": "删除", "createNow": "立即创建备份", @@ -316,13 +233,13 @@ "deleted": "备份已删除", "deleteFailed": "删除备份失败", "restoreTitle": "从备份恢复", - "restoreDescription": "从之前导出的 CAR 文件恢复账户数据。这将用上传的备份替换当前的存储库。", - "selectFile": "选择 CAR 文件", "selectedFile": "已选文件", "restore": "恢复备份", "restoring": "恢复中...", "restored": "备份恢复成功", - "restoreFailed": "备份恢复失败" + "restoreFailed": "备份恢复失败", + "autoBackup": "自动备份", + "restoreHint": "上传 CAR 文件以恢复您的仓库" }, "deleteAccount": "删除账户", "deleteWarning": "此操作不可逆。您的所有数据将被永久删除。", @@ -334,21 +251,11 @@ "permanentlyDelete": "永久删除账户", "deleting": "删除中...", "messages": { - "emailCodeSent": "验证码已发送到您的通知渠道", "emailCodeSentToCurrent": "验证码已发送到您当前的邮箱地址", "emailUpdated": "邮箱更新成功", "emailUpdateFailed": "邮箱更新失败", "handleUpdated": "用户名更新成功", "handleUpdateFailed": "用户名更新失败", - "passwordChanged": "密码更改成功", - "passwordChangeFailed": "密码更改失败", - "passwordSet": "密码设置成功", - "passwordSetFailed": "密码设置失败", - "passwordsMismatch": "两次输入的密码不一致", - "passwordsDoNotMatch": "两次输入的密码不一致", - "passwordLength": "密码至少需要8位字符", - "passwordTooShort": "密码至少需要8位字符", - "deletionCodeSent": "删除确认码已发送到您的邮箱", "deletionConfirmationSent": "删除确认码已发送到您的邮箱", "deletionRequestFailed": "账户删除请求失败", "deleteConfirmation": "您确定要删除账户吗?此操作无法撤销。", @@ -356,22 +263,13 @@ "repoExported": "数据导出成功", "blobsExported": "媒体文件导出成功", "noBlobsToExport": "没有可导出的媒体文件", - "exportFailed": "导出失败", - "confirmDelete": "您确定要删除账户吗?此操作无法撤销。" + "exportFailed": "导出失败" } }, "appPasswords": { - "title": "应用专用密码", - "description": "应用专用密码可让您登录第三方应用而无需提供主密码。每个密码都可以单独撤销。", - "createNew": "创建新密码", - "appNamePlaceholder": "应用名称(如 Graysky、Skeets)", "created": "应用专用密码已创建", "createdMessage": "请立即复制此密码,您将无法再次查看。", - "yourPasswords": "您的应用专用密码", "noPasswords": "暂无应用专用密码", - "revoke": "撤销", - "revoking": "撤销中...", - "revokeConfirm": "撤销「{name}」的密码?使用此密码的应用将无法再访问您的账户。", "saveWarningTitle": "重要:请保存此应用专用密码!", "saveWarningMessage": "此密码用于登录不支持通行密钥或 OAuth 的应用。您只能看到一次。", "acknowledgeLabel": "我已将应用专用密码保存在安全的地方", @@ -380,11 +278,18 @@ "scopeReadOnly": "只读", "scopePostOnly": "仅发帖", "scopeCustom": "自定义", - "byController": "由控制者创建" + "byController": "由控制者创建", + "create": "创建", + "name": "名称", + "namePlaceholder": "应用名称(如 Graysky)", + "deleteConfirm": "撤销应用专用密码「{name}」?", + "deleted": "应用专用密码已撤销", + "loadFailed": "加载应用专用密码失败", + "createFailed": "创建应用专用密码失败", + "deleteFailed": "撤销应用专用密码失败", + "saveWarning": "请立即保存此密码 - 您将无法再次查看" }, "sessions": { - "title": "登录会话", - "loadingSessions": "加载会话中...", "noSessions": "没有活跃的登录会话", "current": "当前", "oauth": "OAuth", @@ -404,52 +309,43 @@ "daysAgo": "{count} 天前", "hoursAgo": "{count} 小时前", "minutesAgo": "{count} 分钟前", - "justNow": "刚刚" + "justNow": "刚刚", + "sessionRevoked": "会话已撤销", + "allSessionsRevoked": "所有其他会话已撤销" }, "inviteCodes": { - "title": "邀请码", - "description": "邀请码可让您邀请朋友加入。每个邀请码只能使用一次。", "createNew": "创建新邀请码", - "uses": "使用次数", - "usesPlaceholder": "使用次数(1-100)", "yourCodes": "您的邀请码", "noCodes": "暂无邀请码", "available": "可用", "used": "已被 @{handle} 使用", "disabled": "已禁用", - "usedBy": "使用者", - "disableConfirm": "禁用此邀请码?它将无法再被使用。", "created": "邀请码已创建", "copy": "复制", "createdOn": "创建于 {date}", - "spent": "已使用" + "spent": "已使用", + "loadFailed": "加载邀请码失败", + "createFailed": "创建邀请码失败" }, "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位验证码", + "totpCode": "TOTP码", + "totpCodePlaceholder": "6位数字", "verifyAndEnable": "验证并启用", "backupCodes": "备用验证码", "backupCodesDescription": "如果无法使用身份验证器,可以使用这些备用码登录。每个验证码只能使用一次。", @@ -463,15 +359,6 @@ "enableLegacyLogin": "启用传统登录", "disableLegacyLogin": "禁用传统登录", "legacyLoginWarning": "警告:启用传统登录会绕过双重身份验证。仅在需要兼容旧版应用时启用。", - "totpPasswordWarning": "启用 TOTP 后,将无法从 Bluesky 应用(或其他旧版应用)更改密码。要更改密码,您有两个选择:", - "totpPasswordOption1Label": "在这里更改:", - "totpPasswordOption1Text": "使用本网站的", - "totpPasswordOption1Link": "设置页面", - "totpPasswordOption1Suffix": ",您可以使用身份验证器应用进行验证。", - "totpPasswordOption2Label": "先验证您的会话:", - "totpPasswordOption2Text": "使用", - "totpPasswordOption2Link": "重新验证选项", - "totpPasswordOption2Suffix": "用 TOTP 验证您的 Bluesky 会话,然后密码更改将暂时有效。", "legacyAppsTitle": "什么是旧版应用?", "legacyAppsDescription": "某些应用(如官方 Bluesky 应用)使用仅需密码的旧版身份验证。启用双重验证后,这些应用会绕过您的第二重验证。禁用传统登录会强制所有应用使用 OAuth,从而正确执行双重验证。", "password": "密码", @@ -479,13 +366,7 @@ "noPassword": "未设置密码(仅通行密钥账户)", "setPassword": "设置密码", "removePassword": "移除密码", - "removePasswordConfirm": "移除密码后需要使用通行密钥登录,确定继续?", "removing": "移除中...", - "loading": "加载中...", - "loadingPasskeys": "加载通行密钥中...", - "cancel": "取消", - "save": "保存", - "back": "返回", "next": "下一步:验证代码", "copyToClipboard": "复制到剪贴板", "savedMyCodes": "我已保存备用码", @@ -493,20 +374,10 @@ "unnamedPasskey": "未命名的通行密钥", "added": "添加于", "lastUsed": "上次使用", - "passwordDescription": "管理您的账户密码。如果您已设置通行密钥,可以选择移除密码以获得完全无密码的体验。", "disableTotpWarning": "这将降低您的账户安全性。", "removePasswordWarning": "这将使您的账户变为仅通行密钥模式。您只能使用已注册的通行密钥登录。如果您丢失了所有通行密钥,可以通过通知渠道恢复账户。", - "beforeProceeding": "继续之前:", - "beforeProceedingItem1": "确保您至少注册了一个可靠的通行密钥", - "beforeProceedingItem2": "考虑在多个设备上注册通行密钥", - "beforeProceedingItem3": "确保您的恢复通知渠道是最新的", - "addPasskeyFirst": "请先添加至少一个通行密钥才能移除密码。", - "passkeyOnlyHint": "您使用通行密钥登录。如果您丢失了通行密钥,可以使用登录页面上的「丢失通行密钥?」链接恢复账户。", - "addPasswordHint": "想要添加密码?前往设置进行设置。", - "goToSettings": "前往设置", "trustedDevices": "受信任设备", - "trustedDevicesDescription": "管理可以跳过双重身份验证的设备。信任有效期为30天,使用设备时自动延长。", - "manageTrustedDevices": "管理受信任设备", + "trustedDevicesDescription": "可以跳过双重身份验证的设备。信任有效期为30天,使用设备时自动延长。", "appCompatibility": "应用兼容性", "enterPassword": "输入您的密码", "sessionExpired": "会话已过期,请重新登录。", @@ -524,13 +395,26 @@ "passkeyCreationCancelled": "通行密钥创建已取消", "passkeyAddedSuccess": "通行密钥添加成功", "passkeyDeleted": "通行密钥已删除", - "passkeyRenamed": "通行密钥已重命名" + "passkeyRenamed": "通行密钥已重命名", + "changePassword": "修改密码", + "currentPassword": "当前密码", + "currentPasswordPlaceholder": "输入当前密码", + "newPassword": "新密码", + "newPasswordPlaceholder": "输入新密码", + "confirmPassword": "确认新密码", + "confirmPasswordPlaceholder": "再次输入新密码", + "passwordsDoNotMatch": "两次输入的密码不一致", + "passwordTooShort": "密码至少需要8位字符", + "passwordChanged": "密码修改成功", + "failedToChangePassword": "密码修改失败", + "changing": "修改中...", + "setting": "设置中...", + "passwordSet": "密码设置成功", + "failedToSetPassword": "密码设置失败", + "failedToDisableTotp": "禁用 TOTP 失败" }, "comms": { - "title": "通讯偏好", - "description": "选择您希望如何接收重要消息,如密码重置、安全提醒和账户更新。", "preferredChannel": "首选渠道", - "preferredChannelDescription": "选择您首选的消息接收方式。必须先配置好渠道才能选择。", "channelConfiguration": "渠道配置", "emailVia": "通过邮件接收消息", "discordVia": "通过 Discord 私信接收消息", @@ -538,10 +422,6 @@ "signalVia": "通过 Signal 接收消息", "configureToEnable": "请先在下方配置", "notConfiguredOnServer": "此服务器未配置", - "emailManagedInSettings": "邮箱在账户设置中管理", - "discordIdHint": "您的 Discord 数字用户 ID(非用户名)。在 Discord 中开启开发者模式即可复制。", - "telegramHint": "您的 Telegram 用户名,不含 @ 符号", - "signalHint": "您的 Signal 电话号码,需包含国家代码", "primary": "主要", "verified": "已验证", "notVerified": "未验证", @@ -552,29 +432,20 @@ "preferencesSaved": "通讯偏好已保存", "verifiedSuccess": "{channel} 验证成功", "messageHistory": "消息历史", - "historyDescription": "查看发送到您账户的最近消息。", - "loadHistory": "加载历史", - "hideHistory": "隐藏历史", "noMessages": "暂无消息记录", - "sent": "已发送", - "failed": "发送失败", "discordInUseWarning": "此 Discord ID 已与另一个账户关联。", "telegramInUseWarning": "此 Telegram 用户名已与另一个账户关联。", - "signalInUseWarning": "此 Signal 号码已与另一个账户关联。" + "signalInUseWarning": "此 Signal 号码已与另一个账户关联。", + "failedToLoad": "加载偏好设置失败", + "failedToSave": "保存偏好设置失败", + "failedToVerify": "验证失败", + "failedToLoadHistory": "加载消息历史失败" }, "repoExplorer": { - "title": "数据浏览器", - "description": "浏览和管理您的原始 AT Protocol 记录。", "collections": "集合", - "noCollections": "暂无集合", - "records": "记录", "noRecords": "此集合中暂无记录", - "recordDetails": "记录详情", - "rkey": "记录键", "uri": "URI", "cid": "CID", - "value": "值", - "deleteRecord": "删除记录", "deleteConfirm": "删除记录 {rkey}?此操作无法撤销。", "unknownError": "发生未知错误", "invalidJson": "无效的 JSON", @@ -587,7 +458,6 @@ "filterCollections": "筛选集合...", "filterRecords": "筛选记录...", "noCollectionsYet": "暂无集合。创建您的第一条记录开始使用。", - "loadMore": "加载更多", "recordJson": "记录 JSON", "updateRecord": "更新记录", "collectionNsid": "集合 (NSID)", @@ -599,8 +469,6 @@ "demoBio": "写一段简短的自我介绍。" }, "admin": { - "title": "管理后台", - "loading": "加载中...", "serverConfig": "服务器配置", "serverName": "服务器名称", "serverNamePlaceholder": "我的 PDS", @@ -612,7 +480,6 @@ "themeColors": "主题颜色", "themeColorsHint": "留空使用默认颜色。", "primaryLight": "主色(浅色模式)", - "colorDefault": "{color}(默认)", "primaryDark": "主色(深色模式)", "secondaryLight": "副色(浅色模式)", "secondaryDark": "副色(深色模式)", @@ -626,8 +493,6 @@ "refreshStats": "刷新统计", "userManagement": "用户管理", "searchPlaceholder": "按用户名搜索(可选)", - "searchUsers": "搜索用户", - "noUsers": "未找到用户", "handle": "用户名", "email": "邮箱", "status": "状态", @@ -637,10 +502,8 @@ "loadInviteCodes": "加载邀请码", "refresh": "刷新", "noInvites": "暂无邀请码", - "code": "邀请码", "available": "可用", "uses": "使用次数", - "actions": "操作", "disable": "禁用", "disableInviteConfirm": "禁用邀请码 {code}?", "active": "活跃", @@ -656,7 +519,16 @@ "deleteConfirm": "删除账户 @{handle}?此操作无法撤销。", "verified": "已验证", "unverified": "未验证", - "deactivated": "已停用" + "deactivated": "已停用", + "failedToLoadStats": "加载服务器统计失败", + "failedToLoadUsers": "加载用户失败", + "searchToSeeUsers": "搜索以查看用户", + "search": "搜索", + "inviteDisabled": "邀请码已禁用", + "invitesEnabled": "用户邀请已启用", + "invitesDisabled": "用户邀请已禁用", + "userDeleted": "用户账户已删除", + "failedToLoadConfig": "加载服务器配置失败" }, "oauth": { "login": { @@ -675,7 +547,6 @@ "passkeyHintNotAvailable": "未注册通行密钥", "passwordPlaceholder": "密码", "usePasskey": "使用通行密钥", - "orContinueWith": "或", "orUseCredentials": "或" }, "register": { @@ -686,77 +557,31 @@ }, "sso": { "linkedAccounts": "已关联账户", - "linkedAccountsDesc": "已关联到您身份的外部账户,用于单点登录。", "noLinkedAccounts": "暂无关联账户", - "noLinkedAccountsDesc": "关联外部账户以启用该服务商的快速登录。", - "linkAccount": "关联账户", - "unlinkAccount": "取消关联", "unlinkConfirm": "确定要取消关联此账户吗?", "unlinked": "已取消关联 {provider}", - "lastLoginAt": "上次使用", - "linkedAt": "关联时间" + "linkedAt": "关联时间", + "unlink": "取消关联", + "linkNewAccount": "关联账户", + "linked": "已关联", + "linkSuccess": "账户关联成功", + "linkFailed": "账户关联失败", + "unlinkFailed": "取消关联失败" }, "consent": { "title": "授权应用", "appWantsAccess": "{app} 想要访问您的账户", - "permissions": "此应用将能够:", - "readProfile": "读取您的个人资料", - "readPosts": "读取您的帖子和内容", - "writePosts": "代表您发布和删除帖子", - "readNotifications": "读取您的通知", - "fullAccess": "完全访问您的账户", "authorize": "授权", "deny": "拒绝", "authorizing": "授权中...", - "rememberChoice": "记住此选择", "signingInAs": "登录账户:", "permissionsRequested": "请求的权限", "required": "必需", "rememberChoiceLabel": "记住对此应用的授权选择", "scopes": { - "atproto": { - "name": "完全访问", - "description": "完全访问权限以读取、写入和管理此账户" - }, "atprotoWithGranular": { "name": "AT Protocol 访问", "description": "AT Protocol 基础范围(权限由下方选择的选项决定)" - }, - "transitionGeneric": { - "name": "过渡访问", - "description": "用于兼容性的通用过渡范围" - }, - "transitionChat": { - "name": "聊天访问", - "description": "访问 Bluesky 聊天功能" - }, - "transitionEmail": { - "name": "邮箱访问", - "description": "读取您的账户邮箱地址" - }, - "repoCreate": { - "name": "创建记录", - "description": "在您的仓库中创建新记录" - }, - "repoUpdate": { - "name": "更新记录", - "description": "更新您仓库中的现有记录" - }, - "repoDelete": { - "name": "删除记录", - "description": "从您的仓库中删除记录" - }, - "blobAll": { - "name": "上传媒体", - "description": "上传图片、视频和其他媒体文件" - }, - "repoFull": { - "name": "完全仓库访问", - "description": "对所有仓库记录的完全读写访问权限" - }, - "accountManage": { - "name": "管理账户", - "description": "管理账户设置和偏好" } }, "unexpectedState": { @@ -769,11 +594,6 @@ "title": "选择账户", "useAnother": "使用其他账户" }, - "twoFactor": { - "title": "验证", - "usePasskey": "使用通行密钥", - "useTotp": "使用验证器" - }, "twoFactorCode": { "title": "验证", "subtitle": "验证码已发送到 {channel}", @@ -789,21 +609,14 @@ "totp": { "title": "验证器验证码", "codePlaceholder": "6位验证码", - "useBackupCode": "使用备用码", "backupCodePlaceholder": "备用码", "trustDevice": "信任此设备30天", "hintBackupCode": "备用码", "hintTotpCode": "验证器验证码" }, - "passkey": { - "title": "通行密钥", - "waiting": "等待中", - "useTotp": "使用验证器" - }, "error": { "title": "授权失败", - "tryAgain": "重试", - "backToApp": "返回" + "tryAgain": "重试" } }, "sso_register": { @@ -829,10 +642,9 @@ "subtitle": "我们已将验证码发送到您的{channel}。请在下方输入以完成注册。", "tokenSubtitle": "输入验证码和接收验证码的标识符。", "tokenTitle": "验证", - "codePlaceholder": "Paste verification code", + "codePlaceholder": "粘贴验证码", "codeLabel": "验证码", "codeHelp": "复制消息中的完整验证码,", - "verifyButton": "验证账户", "pleaseWait": "请稍候...", "codeResent": "验证码已重新发送!", "codeResentDetail": "验证码已发送!请查收。", @@ -874,7 +686,6 @@ "sendCode": "发送重置验证码", "sending": "发送中...", "codeSent": "重置验证码已发送!请检查您的首选通知渠道。", - "enterCode": "输入您收到的验证码和新密码。", "code": "重置验证码", "codePlaceholder": "输入重置验证码", "newPassword": "新密码", @@ -886,8 +697,7 @@ "success": "密码重置成功!", "requestNewCode": "重新获取验证码", "passwordsMismatch": "两次输入的密码不一致", - "passwordLength": "密码至少需要8位字符", - "multipleAccountsWarning": "多个账户共享此邮箱。重置验证码已发送至最新创建的账户。如需恢复特定账户,请使用用户名。" + "passwordLength": "密码至少需要8位字符" }, "recoverPasskey": { "title": "恢复账户", @@ -931,35 +741,6 @@ "sending": "发送中..." }, "registerPasskey": { - "title": "创建通行密钥账户", - "subtitleKeyChoice": "设置 did:web 身份", - "subtitleInitialDidDoc": "上传 DID 文档", - "subtitleCreating": "创建账户", - "subtitlePasskey": "注册通行密钥", - "subtitleAppPassword": "保存应用专用密码", - "subtitleVerify": "验证{channel}", - "subtitleUpdatedDidDoc": "更新 DID 文档", - "subtitleActivating": "激活中", - "subtitleComplete": "账户已创建", - "handle": "用户名", - "handlePlaceholder": "您的用户名", - "handleHint": "您的完整用户名将是:@{handle}", - "handleDotWarning": "可以在创建账户后设置自定义域名。", - "email": "邮箱地址", - "emailPlaceholder": "you@example.com", - "inviteCode": "邀请码", - "inviteCodePlaceholder": "输入您的邀请码", - "createButton": "创建账户", - "continue": "继续", - "back": "返回", - "alreadyHaveAccount": "已有账户?", - "signIn": "立即登录", - "wantPassword": "想使用密码?", - "createPasswordAccount": "创建密码账户", - "wantTraditional": "想使用传统密码?", - "registerWithPassword": "使用密码注册", - "contactMethod": "联系方式", - "verificationMethod": "验证方式", "identityType": "身份类型", "identityTypeHint": "选择如何管理您的去中心化身份。", "didPlcRecommended": "did:plc(推荐)", @@ -980,13 +761,10 @@ "externalDid": "您的 did:web", "externalDidPlaceholder": "did:web:yourdomain.com", "externalDidHint": "您需要在以下地址提供 DID 文档", - "passkeyName": "Passkey Name", + "passkeyName": "通行密钥名称", "passkeyNamePlaceholder": "MacBook Touch ID", "passkeyNameHint": "可选标识", "createPasskey": "创建通行密钥", - "creatingPasskey": "正在创建通行密钥...", - "redirecting": "正在跳转到控制台...", - "loading": "加载中...", "errors": { "handleRequired": "请输入用户名", "handleNoDots": "用户名不能包含点号。您可以在创建账户后设置自定义域名。", @@ -1002,21 +780,16 @@ "passkeyFailed": "通行密钥注册失败" }, "didWebWarning1Detail": "您的身份将是 {did}。", - "activatingAccount": "Activating", - "creatingAccount": "Creating account", - "passkeyDescription": "Register a passkey for this account", - "setupPasskey": "Create Passkey" + "activatingAccount": "激活中", + "creatingAccount": "创建账户中", + "passkeyDescription": "为此账户注册通行密钥", + "setupPasskey": "创建通行密钥" }, "trustedDevices": { - "title": "受信任设备", - "backToSecurity": "← 安全设置", - "description": "受信任设备可以跳过双重身份验证。信任有效期为30天,使用设备时自动延长。", - "failedToLoad": "加载受信任设备失败", "noDevices": "暂无受信任设备", "noDevicesHint": "开启双重身份验证后登录时,可以选择信任设备30天。", "lastSeen": "最后使用:", "trustedSince": "信任时间:", - "trustExpires": "信任过期:", "expired": "已过期", "tomorrow": "明天", "inDays": "{days}天后", @@ -1025,7 +798,6 @@ "deviceRevoked": "设备信任已撤销", "deviceRenamed": "设备已重命名", "deviceNamePlaceholder": "设备名称", - "browser": "浏览器:", "unknownDevice": "未知设备" }, "reauth": { @@ -1034,36 +806,11 @@ "totp": "TOTP", "passkey": "通行密钥", "authenticatorCode": "验证码", - "usePassword": "密码", "usePasskey": "通行密钥", - "useTotp": "身份验证器", - "passwordPlaceholder": "输入您的密码", - "totpPlaceholder": "6位验证码", "authenticating": "验证中", "cancel": "取消" }, - "verifyChannel": { - "title": "验证通道", - "subtitle": "输入发送到您通知通道的验证码。", - "signInRequired": "需要登录", - "signInRequiredDesc": "您必须登录才能验证通道。", - "signIn": "登录", - "verifying": "验证中...", - "pleaseWait": "请稍候,正在验证您的通道。", - "successTitle": "验证成功!", - "successDesc": "您的 {channel} 已成功验证。", - "backToSettings": "返回设置", - "channelLabel": "通道", - "selectChannel": "选择通道...", - "identifierLabel": "标识符", - "identifierPlaceholder": "邮箱、Discord ID 等", - "identifierHelp": "正在验证的邮箱地址、Discord ID、Telegram 用户名或 Signal 号码。", - "codeLabel": "验证码", - "codeHelp": "复制消息中的完整验证码,。", - "verifyButton": "验证" - }, "delegation": { - "title": "账户委托", "controllers": "控制者", "controlledAccounts": "受控账户", "noControllers": "暂无控制者", @@ -1076,13 +823,7 @@ "scopeCustom": "自定义", "actAs": "代理操作", "auditLog": "审计日志", - "auditLogTitle": "委托审计日志", - "backToControllers": "← 返回控制者", - "loading": "加载中...", - "noActivity": "暂无活动", "actor": "执行者", - "controller": "控制者", - "account": "账户", "accountCreated": "已创建委托账户:{handle}", "details": "详情", "actionGrantCreated": "授权创建", @@ -1094,9 +835,7 @@ "actionAccountAction": "账户操作", "previous": "上一页", "next": "下一页", - "showing": "{start}–{end} / 共{total}条", "refresh": "刷新", - "failedToLoadAuditLog": "加载审计日志失败", "adding": "添加中...", "accessLevel": "访问级别", "addControllerButton": "+ 添加控制者", @@ -1118,24 +857,23 @@ "createDelegatedAccount": "创建委托账户", "createDelegatedAccountButton": "+ 创建委托账户", "emailOptional": "邮箱(可选)", - "failedToAddController": "添加控制者失败", - "failedToCreateAccount": "创建委托账户失败", - "failedToRemoveController": "移除控制者失败", "granted": "授权日期", "inactive": "未激活", "remove": "移除", "removeConfirm": "确定要移除此控制者吗?", "viewAuditLog": "查看审计日志", - "yourAccessLevel": "您的访问级别" + "yourAccessLevel": "您的访问级别", + "noAuditEntries": "无审计记录", + "target": "目标", + "pageInfo": "{start} - {end} / {total}", + "failedToLoadAudit": "加载审计日志失败" }, "actAs": { "title": "代理操作", "noAccountSpecified": "未指定账户 DID", - "failedToVerify": "无法验证账户访问权限", "noAccess": "您没有此账户的访问权限", "failedToInitiate": "无法启动认证", "invalidResponse": "服务器返回无效响应", - "failedError": "失败: {error}", "preparing": "正在准备登录委托账户...", "backToControllers": "返回控制者" }, @@ -1185,8 +923,6 @@ "migration": { "title": "账户迁移", "subtitle": "在服务器之间移动您的AT Protocol身份", - "navTitle": "迁移", - "navDesc": "将您的账户移至其他PDS或从其他PDS移入", "migrateHere": "迁移到此处", "migrateHereDesc": "将您现有的AT Protocol账户从其他服务器移至此PDS。", "bringDid": "携带您的DID和身份", @@ -1252,6 +988,7 @@ "checkingAvailability": "检查可用性...", "handleAvailable": "用户名可用!", "handleTaken": "用户名已被占用", + "handleTooShort": "用户名至少需要3个字符", "handleHint": "您也可以输入完整的用户名(如alice.mydomain.com)来使用您自己的域名", "email": "邮箱地址", "authMethod": "身份验证方式", @@ -1276,7 +1013,6 @@ "authentication": "身份验证", "authPasskey": "通行密钥(无密码)", "authPassword": "密码", - "inviteCode": "邀请码", "warning": "点击「开始迁移」后,您的存储库和数据将开始转移。此过程无法轻易撤销。", "startMigration": "开始迁移", "starting": "启动中..." @@ -1313,9 +1049,7 @@ "hint": "在下方输入验证码,或点击邮件中的链接自动继续。", "tokenLabel": "验证码", "tokenPlaceholder": "输入邮件中的验证码", - "resend": "重新发送", - "verify": "验证邮箱", - "verifying": "验证中..." + "resend": "重新发送" }, "plcToken": { "title": "验证迁移", @@ -1423,7 +1157,6 @@ "desc": "检查您的离线恢复详情。", "carFile": "CAR 文件", "rotationKey": "轮换密钥", - "warning": "开始恢复后,您的身份将更新为指向此 PDS。此操作无法轻易撤销。", "plcWarningTitle": "不可逆转点", "plcWarning": "一旦开始,您的 DID 文档将更新为指向此 PDS。如果出现问题,您可以使用轮换密钥恢复,但您应该完成迁移以避免身份状态损坏。" }, @@ -1431,9 +1164,7 @@ "title": "恢复账户", "desc": "请稍候,正在恢复您的账户...", "creating": "创建账户", - "importing": "导入存储库", - "plcSigning": "更新身份", - "activating": "激活账户" + "importing": "导入存储库" }, "success": { "desc": "您的账户已成功恢复到此 PDS。" @@ -1443,28 +1174,8 @@ "desc": "正在尝试从您的旧 PDS 恢复图片和媒体...", "migrating": "正在迁移 blob", "failedTitle": "部分 blob 无法迁移", - "failedDesc": "{count} 个 blob 无法从您的旧 PDS 获取。这可能是因为服务器无法访问或文件已被删除。", - "sourceUnreachableTitle": "源 PDS 无法访问", - "sourceUnreachable": "无法连接到您的旧 PDS 来获取媒体文件。从已关闭的服务器迁移时这很常见。您的帖子将正常工作,但部分图片可能会丢失。" + "failedDesc": "{count} 个 blob 无法从您的旧 PDS 获取。这可能是因为服务器无法访问或文件已被删除。" } - }, - "progress": { - "repoExported": "存储库已导出", - "repoImported": "存储库已导入", - "blobsMigrated": "已迁移{count}个blob", - "prefsMigrated": "偏好设置已迁移", - "plcSigned": "身份已更新", - "activated": "账户已激活", - "deactivated": "旧账户已停用" - }, - "errors": { - "connectionFailed": "无法连接到PDS", - "invalidCredentials": "凭据无效", - "twoFactorRequired": "需要双因素认证", - "accountExists": "目标PDS上已存在账户", - "plcFailed": "PLC操作失败", - "blobFailed": "blob迁移失败:{cid}", - "networkError": "网络错误,请重试。" } } } diff --git a/frontend/src/routes/ActAs.svelte b/frontend/src/routes/ActAs.svelte index 4aa3bcc..a594930 100644 --- a/frontend/src/routes/ActAs.svelte +++ b/frontend/src/routes/ActAs.svelte @@ -75,7 +75,7 @@ return } - const authUrl = `${window.location.origin}/delegation/auth-token` + const authUrl = `${window.location.origin}/oauth/delegation/auth-token` const body = JSON.stringify({ request_uri: parData.request_uri, delegated_did: did diff --git a/frontend/src/routes/Admin.svelte b/frontend/src/routes/Admin.svelte deleted file mode 100644 index 841f88c..0000000 --- a/frontend/src/routes/Admin.svelte +++ /dev/null @@ -1,1155 +0,0 @@ - -{#if session?.isAdmin} -
-
- {$_('common.backToDashboard')} -

{$_('admin.title')}

-
- {#if loading} -

{$_('admin.loading')}

- {:else} -
-

{$_('admin.serverConfig')}

- -
- - - {$_('admin.serverNameHelp')} -
- -
- -
- {#if logoPreview} -
- {$_('admin.logoPreview')} - -
- {:else} - - {/if} -
- {$_('admin.logoHelp')} -
- -

{$_('admin.themeColors')}

-

{$_('admin.themeColorsHint')}

- -
-
- -
- - -
-
-
- -
- - -
-
-
- -
- - -
-
-
- -
- - -
-
-
- - - -
- {#if stats} -
-

{$_('admin.serverStats')}

-
-
-
{formatNumber(stats.userCount)}
-
{$_('admin.users')}
-
-
-
{formatNumber(stats.repoCount)}
-
{$_('admin.repos')}
-
-
-
{formatNumber(stats.recordCount)}
-
{$_('admin.records')}
-
-
-
{formatBytes(stats.blobStorageBytes)}
-
{$_('admin.blobStorage')}
-
-
- -
- {/if} -
-

{$_('admin.userManagement')}

-
- - -
- {#if showUsers} -
- {#if users.length === 0} -

{$_('admin.noUsers')}

- {:else} - - - - - - - - - - - {#each users as user} - selectUser(user.did)}> - - - - - - {/each} - -
{$_('admin.handle')}{$_('admin.email')}{$_('admin.status')}{$_('admin.created')}
@{user.handle} - {#if user.deactivatedAt} - {$_('admin.deactivated')} - {:else if user.emailConfirmedAt} - {$_('admin.verified')} - {:else} - {$_('admin.unverified')} - {/if} - {formatDate(user.indexedAt)}
- {#if usersCursor} - - {/if} - {/if} -
- {/if} -
-
-

{$_('admin.inviteCodes')}

-
- -
- {#if showInvites} -
- {#if invites.length === 0} -

{$_('admin.noInvites')}

- {:else} - - - - - - - - - - - - - {#each invites as invite} - - - - - - - - - {/each} - -
{$_('admin.code')}{$_('admin.available')}{$_('admin.uses')}{$_('admin.status')}{$_('admin.created')}{$_('admin.actions')}
{invite.code}{invite.available}{invite.uses.length} - {#if invite.disabled} - {$_('admin.disabled')} - {:else if invite.available === 0} - {$_('admin.exhausted')} - {:else} - {$_('admin.active')} - {/if} - {formatDate(invite.createdAt)} - {#if !invite.disabled} - - {:else} - - - {/if} -
- {#if invitesCursor} - - {/if} - {/if} -
- {/if} -
- {/if} -
- {#if selectedUser} - - {/if} -{:else if authLoading} -
{$_('admin.loading')}
-{/if} - diff --git a/frontend/src/routes/AppPasswords.svelte b/frontend/src/routes/AppPasswords.svelte deleted file mode 100644 index 93a1875..0000000 --- a/frontend/src/routes/AppPasswords.svelte +++ /dev/null @@ -1,472 +0,0 @@ - -
-
- {$_('common.backToDashboard')} -

{$_('appPasswords.title')}

-
-

- {$_('appPasswords.description')} -

- {#if createdPassword} -
-
- {$_('appPasswords.saveWarningTitle')} -

{$_('appPasswords.saveWarningMessage')}

-
-
-
{$_('common.name')}: {createdPassword.name}
- {createdPassword.password} - -
- - -
- {/if} -
-

{$_('appPasswords.createNew')}

-
- -
- {$_('appPasswords.permissions')}: -
- {#each SCOPE_PRESETS as preset} - - {/each} -
-
- -
-
-
-

{$_('appPasswords.yourPasswords')}

- {#if loading} -
    - {#each Array(2) as _} -
  • - {/each} -
- {:else if passwords.length === 0} -

{$_('appPasswords.noPasswords')}

- {:else} -
    - {#each passwords as pw} -
  • -
    - {pw.name} - - {getScopeLabel(pw.scopes)} - {#if pw.createdByController} - {$_('appPasswords.byController')} - {/if} - {$_('common.created')} {formatDate(pw.createdAt)} - -
    - -
  • - {/each} -
- {/if} -
-
- diff --git a/frontend/src/routes/Comms.svelte b/frontend/src/routes/Comms.svelte deleted file mode 100644 index 7bd1e61..0000000 --- a/frontend/src/routes/Comms.svelte +++ /dev/null @@ -1,814 +0,0 @@ - -
-
- {$_('common.backToDashboard')} -

{$_('comms.title')}

-

{$_('comms.description')}

-
- - {#if loading} -
-
-
-
- {:else} - - {/if} -
- diff --git a/frontend/src/routes/Controllers.svelte b/frontend/src/routes/Controllers.svelte deleted file mode 100644 index fbdafc8..0000000 --- a/frontend/src/routes/Controllers.svelte +++ /dev/null @@ -1,699 +0,0 @@ - - - - {#snippet children({ session, client })} -
-
- {$_('common.backToDashboard')} -

{$_('delegation.title')}

-
- - {#if loading} -
- {#each Array(2) as _} -
- {/each} -
- {:else} -
-
-

{$_('delegation.controllers')}

-

{$_('delegation.controllersDesc')}

-
- - {#if controllers.length === 0} -

{$_('delegation.noControllers')}

- {:else} -
- {#each controllers as controller} -
-
-
- @{controller.handle} - {getScopeLabel(controller.grantedScopes)} - {#if !controller.isActive} - {$_('delegation.inactive')} - {/if} -
-
-
- {$_('delegation.did')} - {controller.did} -
-
- {$_('delegation.granted')} - {formatDateTime(controller.grantedAt)} -
-
-
-
- -
-
- {/each} -
- {/if} - - {#if !canAddControllers} -
-

{$_('delegation.cannotAddControllers')}

-
- {:else if showAddController} -
-

{$_('delegation.addController')}

- -
-
- - - - - - {$_('delegation.addControllerWarningTitle')} -
-

{$_('delegation.addControllerWarningText')}

-
    -
  • {$_('delegation.addControllerWarningBullet1')}
  • -
  • {$_('delegation.addControllerWarningBullet2')}
  • -
  • {$_('delegation.addControllerWarningBullet3')}
  • -
-
- -
- - -
-
- - -
- -
- - -
-
- {:else} - - {/if} -
- -
-
-

{$_('delegation.controlledAccounts')}

-

{$_('delegation.controlledAccountsDesc')}

-
- - {#if controlledAccounts.length === 0} -

{$_('delegation.noControlledAccounts')}

- {:else} -
- {#each controlledAccounts as account} -
-
-
- @{account.handle} - {getScopeLabel(account.grantedScopes)} -
-
-
- {$_('delegation.did')} - {account.did} -
-
- {$_('delegation.granted')} - {formatDateTime(account.grantedAt)} -
-
-
- -
- {/each} -
- {/if} - - {#if !canControlAccounts} -
-

{$_('delegation.cannotControlAccounts')}

-
- {:else if showCreateDelegated} -
-

{$_('delegation.createDelegatedAccount')}

-
- - -
-
- - -
-
- - -
-
- - -
-
- {:else} - - {/if} -
- -
-
-

{$_('delegation.auditLog')}

-

{$_('delegation.auditLogDesc')}

-
- {$_('delegation.viewAuditLog')} -
- {/if} -
- {/snippet} -
- - diff --git a/frontend/src/routes/Dashboard.svelte b/frontend/src/routes/Dashboard.svelte index 5843085..3e93edf 100644 --- a/frontend/src/routes/Dashboard.svelte +++ b/frontend/src/routes/Dashboard.svelte @@ -5,14 +5,28 @@ switchAccount, type SavedAccount, } from '../lib/auth.svelte' - import { navigate, routes, getFullUrl } from '../lib/router.svelte' + import { navigate, routes, getCurrentPath } from '../lib/router.svelte' import { _ } from '../lib/i18n' import { api } from '../lib/api' import { isOk } from '../lib/types/result' import { unsafeAsDid, type Did } from '../lib/types/branded' import type { Session } from '../lib/types/api' - import { isMigrated, isDeactivated, getSessionEmail, isEmailVerified } from '../lib/types/api' import { onMount } from 'svelte' + import { getServerConfigState } from '../lib/serverConfig.svelte' + + import SettingsContent from '../components/dashboard/SettingsContent.svelte' + import SecurityContent from '../components/dashboard/SecurityContent.svelte' + import SessionsContent from '../components/dashboard/SessionsContent.svelte' + import AppPasswordsContent from '../components/dashboard/AppPasswordsContent.svelte' + import CommsContent from '../components/dashboard/CommsContent.svelte' + import RepoContent from '../components/dashboard/RepoContent.svelte' + import ControllersContent from '../components/dashboard/ControllersContent.svelte' + import InviteCodesContent from '../components/dashboard/InviteCodesContent.svelte' + import DidDocumentContent from '../components/dashboard/DidDocumentContent.svelte' + import AdminContent from '../components/dashboard/AdminContent.svelte' + import DelegationAuditContent from '../components/dashboard/DelegationAuditContent.svelte' + + type Section = 'settings' | 'security' | 'sessions' | 'app-passwords' | 'comms' | 'repo' | 'controllers' | 'delegation-audit' | 'invite-codes' | 'did-document' | 'admin' const auth = $derived(getAuthState()) let dropdownOpen = $state(false) @@ -34,10 +48,40 @@ const session = $derived(getSession()) const savedAccounts = $derived(getSavedAccounts()) const loading = $derived(isLoading()) - const isDidWeb = $derived(session?.did?.startsWith('did:web:') ?? false) + const isPdsHostedDidWeb = $derived.by(() => { + if (!session?.did?.startsWith('did:web:')) return false + const didParts = session.did.split(':') + if (didParts.length < 3) return false + const didDomain = didParts[2] + const hostname = globalThis.location?.hostname + if (!hostname) return false + return didDomain === hostname || didDomain.endsWith(`.${hostname}`) + }) const otherAccounts = $derived(savedAccounts.filter(a => a.did !== session?.did)) + let isMobile = $state(true) + const serverConfig = $derived(getServerConfigState()) + + const currentPath = $derived(getCurrentPath()) + const currentSection = $derived.by
(() => { + const path = currentPath.split('?')[0] + const sectionMap: Record = { + '/settings': 'settings', + '/security': 'security', + '/sessions': 'sessions', + '/app-passwords': 'app-passwords', + '/comms': 'comms', + '/repo': 'repo', + '/controllers': 'controllers', + '/delegation-audit': 'delegation-audit', + '/invite-codes': 'invite-codes', + '/did-document': 'did-document', + '/admin': 'admin', + } + return sectionMap[path] ?? null + }) onMount(async () => { + isMobile = window.matchMedia('(max-width: 768px)').matches try { const serverInfo = await api.describeServer() inviteCodesEnabled = serverInfo.inviteCodeRequired @@ -52,6 +96,12 @@ } }) + $effect(() => { + if (session && currentSection === null && !isMobile) { + navigate('/settings' as typeof routes.dashboard) + } + }) + async function handleLogout() { await logout() navigate(routes.login) @@ -88,235 +138,258 @@ } } }) + + const sectionRoutes: Record = { + 'settings': '/settings', + 'security': '/security', + 'sessions': '/sessions', + 'app-passwords': '/app-passwords', + 'comms': '/comms', + 'repo': '/repo', + 'controllers': '/controllers', + 'delegation-audit': '/delegation-audit', + 'invite-codes': '/invite-codes', + 'did-document': '/did-document', + 'admin': '/admin', + } + + function selectSection(section: Section) { + navigate(sectionRoutes[section] as typeof routes.dashboard) + } + + function goBack() { + navigate(routes.dashboard) + } + + interface NavItem { + id: Section + label: string + show: boolean + highlight?: 'admin' | 'migrated' | 'did-web' + } + + const navItems = $derived([ + { id: 'settings', label: $_('dashboard.navSettings'), show: session?.accountKind !== 'migrated' }, + { id: 'security', label: $_('dashboard.navSecurity'), show: true }, + { id: 'sessions', label: $_('dashboard.navSessions'), show: true }, + { id: 'app-passwords', label: $_('dashboard.navAppPasswords'), show: session?.accountKind !== 'migrated' }, + { id: 'comms', label: $_('dashboard.navComms'), show: session?.accountKind !== 'migrated' }, + { id: 'repo', label: $_('dashboard.navRepo'), show: session?.accountKind !== 'migrated' }, + { id: 'controllers', label: $_('dashboard.navDelegation'), show: session?.accountKind !== 'migrated' }, + { id: 'delegation-audit', label: $_('dashboard.navDelegationAudit'), show: session?.accountKind !== 'migrated' }, + { id: 'invite-codes', label: $_('dashboard.navInviteCodes'), show: inviteCodesEnabled && (session?.isAdmin ?? false) && session?.accountKind !== 'migrated' }, + { id: 'did-document', label: $_('dashboard.navDidDocument'), show: isPdsHostedDidWeb || session?.accountKind === 'migrated', highlight: session?.accountKind === 'migrated' ? 'migrated' : 'did-web' }, + { id: 'admin', label: $_('dashboard.navAdmin'), show: session?.isAdmin ?? false, highlight: 'admin' }, + ]) + + const visibleNavItems = $derived(navItems.filter(item => item.show)) + + function getSectionTitle(section: Section): string { + const item = navItems.find(i => i.id === section) + return item?.label ?? '' + } {#if session} -
-
-

{$_('dashboard.title')}

-
- {#if session.accountKind === 'migrated'} -
- {$_('dashboard.migratedTitle')} -

{$_('dashboard.migratedMessage', { values: { pds: session.migratedToPds || 'another PDS' } })}

-
- {:else if session.accountKind === 'deactivated'} -
- {$_('dashboard.deactivatedTitle')} -

{$_('dashboard.deactivatedMessage')}

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

{currentSection ? getSectionTitle(currentSection) : ''}

+
+ +
+ {#if currentSection === 'settings'} + + {:else if currentSection === 'security'} + + {:else if currentSection === 'sessions'} + + {:else if currentSection === 'app-passwords'} + + {:else if currentSection === 'comms'} + + {:else if currentSection === 'repo'} + + {:else if currentSection === 'controllers'} + + {:else if currentSection === 'delegation-audit'} + + {:else if currentSection === 'invite-codes'} + + {:else if currentSection === 'did-document'} + + {:else if currentSection === 'admin'} + + {/if} +
+
{:else if loading} -
-
- +
+ +
+
+
{/if} diff --git a/frontend/src/routes/DelegationAudit.svelte b/frontend/src/routes/DelegationAudit.svelte deleted file mode 100644 index 694c00c..0000000 --- a/frontend/src/routes/DelegationAudit.svelte +++ /dev/null @@ -1,311 +0,0 @@ - - - - {#snippet children({ session, client })} -
-
- {$_('delegation.backToControllers')} -

{$_('delegation.auditLogTitle')}

-
- - {#if loading} -
- {#each Array(3) as _} -
- {/each} -
- {:else} - {#if entries.length === 0} -

{$_('delegation.noActivity')}

- {:else} -
- {#each entries as entry} -
-
- {formatActionType(entry.actionType)} - {formatDateTime(entry.createdAt)} -
-
-
- {$_('delegation.actor')} - {truncateDid(entry.actorDid)} -
- {#if entry.controllerDid} -
- {$_('delegation.controller')} - {truncateDid(entry.controllerDid)} -
- {/if} -
- {$_('delegation.account')} - {truncateDid(entry.delegatedDid)} -
- {#if entry.actionDetails} -
- {$_('delegation.details')} - {formatActionDetails(entry.actionDetails)} -
- {/if} -
-
- {/each} -
- - - {/if} - -
- -
- {/if} -
- {/snippet} -
- - diff --git a/frontend/src/routes/DidDocumentEditor.svelte b/frontend/src/routes/DidDocumentEditor.svelte deleted file mode 100644 index f56c04d..0000000 --- a/frontend/src/routes/DidDocumentEditor.svelte +++ /dev/null @@ -1,478 +0,0 @@ - - -
-
- {$_('common.backToDashboard')} -

{$_('didEditor.title')}

-
- - {#if loading} -
-
-
-
-
-
- {:else} -
-

{$_('didEditor.helpTitle')}

-

{$_('didEditor.helpText')}

-
- -
-

{$_('didEditor.preview')}

-
{JSON.stringify(didDocument, null, 2)}
-
- -
-

{$_('didEditor.verificationMethods')}

-

{$_('didEditor.verificationMethodsDesc')}

- - {#if verificationMethods.length > 0} -
    - {#each verificationMethods as method, index} -
  • -
    - {method.id} - {method.type} - {method.publicKeyMultibase} -
    - -
  • - {/each} -
- {:else} -

{$_('didEditor.noKeys')}

- {/if} - -
-

{$_('didEditor.addKey')}

-
-
- - -
-
- - -
- -
-
-
- -
-

{$_('didEditor.alsoKnownAs')}

-

{$_('didEditor.alsoKnownAsDesc')}

- - {#if alsoKnownAs.length > 0} -
    - {#each alsoKnownAs as handle, index} -
  • - {handle} - -
  • - {/each} -
- {:else} -

{$_('didEditor.noHandles')}

- {/if} - -
-
-
- - -
- -
-
-
- -
-

{$_('didEditor.serviceEndpoint')}

-

{$_('didEditor.serviceEndpointDesc')}

-
- - -
-
- -
- -
- {/if} -
- - diff --git a/frontend/src/routes/Security.svelte b/frontend/src/routes/Security.svelte deleted file mode 100644 index 016bf21..0000000 --- a/frontend/src/routes/Security.svelte +++ /dev/null @@ -1,1512 +0,0 @@ - - - - {#snippet children({ session, client })} -
-
- {$_('common.backToDashboard')} -

{$_('security.title')}

-
- - {#if loading} -
- {#each Array(4) as _} -
- {/each} -
- {:else} -
-
-

{$_('security.totp')}

-

- {$_('security.totpDescription')} -

- - {#if totpSetup.step === 'idle'} - {#if totpEnabled} -
- {$_('security.totpEnabled')} -
- - {#if !showDisableForm && !showRegenForm} -
- - -
- {/if} - - {#if showRegenForm} -
-

{$_('security.regenerateBackupCodes')}

-

{$_('security.regenerateConfirm')}

-
- - -
-
- - -
-
- - -
-
- {/if} - - {#if showDisableForm} -
-

{$_('security.disableTotp')}

-

{$_('security.disableTotpWarning')}

-
- - -
-
- - -
-
- - -
-
- {/if} - {:else} -
- {$_('security.totpDisabled')} -
- - {/if} - {:else if totpSetup.step === 'qr'} - {@const qrData = totpSetup as TotpQr} -
-

{$_('security.totpSetup')}

-

{$_('security.totpSetupInstructions')}

-
- TOTP QR Code -
-
- {$_('security.cantScan')} - {qrData.totpUri.split('secret=')[1]?.split('&')[0] || ''} -
- -
- {:else if totpSetup.step === 'verify'} - {@const verifyData = totpSetup} -
-

{$_('security.totpSetup')}

-

{$_('security.totpCodePlaceholder')}

-
-
- -
-
- - -
-
-
- {:else if totpSetup.step === 'backup'} -
-

{$_('security.backupCodes')}

-

- {$_('security.backupCodesDescription')} -

-
- {#each totpSetup.backupCodes as code} - {code} - {/each} -
-
- - -
-
- {/if} -
- -
-

{$_('security.passkeys')}

-

- {$_('security.passkeysDescription')} -

- - {#if !passkeysLoading} - {#if passkeys.length > 0} -
- {#each passkeys as passkey} -
- {#if editingPasskeyId === passkey.id} -
- -
- - -
-
- {:else} -
- {passkey.friendlyName || $_('security.unnamedPasskey')} - - {$_('security.added')} {formatDate(passkey.createdAt)} - {#if passkey.lastUsed} - · {$_('security.lastUsed')} {formatDate(passkey.lastUsed)} - {/if} - -
-
- - {#if hasPassword || passkeys.length > 1} - - {/if} -
- {/if} -
- {/each} -
- {:else} -
- {$_('security.noPasskeys')} -
- {/if} - -
-
- - -
- -
- {/if} -
- -
-

{$_('security.password')}

-

- {$_('security.passwordDescription')} -

- - {#if !passwordLoading && hasPassword} -
- {$_('security.passwordStatus')} -
- - {#if passkeys.length > 0} - {#if !showRemovePasswordForm} - - {:else} -
-

{$_('security.removePassword')}

-

- {$_('security.removePasswordWarning')} -

-
- {$_('security.beforeProceeding')} -
    -
  • {$_('security.beforeProceedingItem1')}
  • -
  • {$_('security.beforeProceedingItem2')}
  • -
  • {$_('security.beforeProceedingItem3')}
  • -
-
-
- - -
-
- {/if} - {:else} -

{$_('security.addPasskeyFirst')}

- {/if} - {:else} -
- {$_('security.noPassword')} -
-

- {$_('security.passkeyOnlyHint')} -

-

- {$_('security.addPasswordHint')} -

- - {$_('security.goToSettings')} - - {/if} -
- -
-

{$_('security.trustedDevices')}

-

- {$_('security.trustedDevicesDescription')} -

- - {$_('security.manageTrustedDevices')} → - -
- - {#if ssoProviders.length > 0} -
-

{$_('oauth.sso.linkedAccounts')}

-

- {$_('oauth.sso.linkedAccountsDesc')} -

- - {#if !linkedAccountsLoading} - {#if linkedAccounts.length > 0} -
- {#each linkedAccounts as account} - - {/each} -
- {:else} -
- {$_('oauth.sso.noLinkedAccounts')} -
-

{$_('oauth.sso.noLinkedAccountsDesc')}

- {/if} - - {#if ssoProviders.some(p => !linkedAccounts.some(a => a.provider === p.provider))} - - {/if} - {:else} -
{$_('common.loading')}
- {/if} -
- {/if} -
- - {#if hasMfa} -
-

{$_('security.appCompatibility')}

-

- {$_('security.legacyLoginDescription')} -

- - {#if !legacyLoginLoading} -
-
- {$_('security.legacyLogin')} - - {#if allowLegacyLogin} - {$_('security.legacyLoginOn')} - {:else} - {$_('security.legacyLoginOff')} - {/if} - -
- -
- - {#if totpEnabled} -
- {$_('security.legacyLoginWarning')} -

{$_('security.totpPasswordWarning')}

-
    -
  1. {$_('security.totpPasswordOption1Label')} {$_('security.totpPasswordOption1Text')} {$_('security.totpPasswordOption1Link')} {$_('security.totpPasswordOption1Suffix')}
  2. -
  3. {$_('security.totpPasswordOption2Label')} {$_('security.totpPasswordOption2Text')} {$_('security.totpPasswordOption2Link')} {$_('security.totpPasswordOption2Suffix')}
  4. -
-
- {/if} - -
- {$_('security.legacyAppsTitle')} -

{$_('security.legacyAppsDescription')}

-
- {/if} -
- {/if} - {/if} -
- - - {/snippet} -
- - diff --git a/frontend/src/routes/TrustedDevices.svelte b/frontend/src/routes/TrustedDevices.svelte deleted file mode 100644 index 62cae8c..0000000 --- a/frontend/src/routes/TrustedDevices.svelte +++ /dev/null @@ -1,327 +0,0 @@ - - -
-
- {$_('trustedDevices.backToSecurity')} -

{$_('trustedDevices.title')}

-
- -
-

- {$_('trustedDevices.description')} -

-
- - {#if loading} -
- {#each Array(2) as _} -
- {/each} -
- {:else if devices.length === 0} -
-

{$_('trustedDevices.noDevices')}

-

{$_('trustedDevices.noDevicesHint')}

-
- {:else} -
- {#each devices as device} -
-
- {#if editingDeviceId === device.id} - -
- - -
- {:else} -

{device.friendlyName || parseUserAgent(device.userAgent)}

- - {/if} -
- -
- {#if device.userAgent && !device.friendlyName} -

{$_('trustedDevices.browser')} {device.userAgent}

- {:else if device.userAgent} -

{$_('trustedDevices.browser')} {parseUserAgent(device.userAgent)}

- {/if} -

- {$_('trustedDevices.lastSeen')} {formatDate(device.lastSeenAt)} -

- {#if device.trustedAt} -

- {$_('trustedDevices.trustedSince')} {formatDate(device.trustedAt)} -

- {/if} - {#if device.trustedUntil} - {@const daysRemaining = getDaysRemaining(device.trustedUntil)} -

- {$_('trustedDevices.trustExpires')} - {#if daysRemaining <= 0} - {$_('trustedDevices.expired')} - {:else if daysRemaining === 1} - {$_('trustedDevices.tomorrow')} - {:else} - {$_('trustedDevices.inDays', { values: { days: daysRemaining } })} - {/if} -

- {/if} -
- -
- -
-
- {/each} -
- {/if} -
- - diff --git a/frontend/src/tests/AppPasswords.test.ts b/frontend/src/tests/AppPasswords.test.ts deleted file mode 100644 index 6d577db..0000000 --- a/frontend/src/tests/AppPasswords.test.ts +++ /dev/null @@ -1,404 +0,0 @@ -import { beforeEach, describe, expect, it, vi } from "vitest"; -import { fireEvent, render, screen, waitFor } from "@testing-library/svelte"; -import AppPasswords from "../routes/AppPasswords.svelte"; -import { - clearMocks, - errorResponse, - getErrorToasts, - jsonResponse, - mockData, - mockEndpoint, - setupAuthenticatedUser, - setupFetchMock, - setupIndexedDBMock, - setupUnauthenticatedUser, -} from "./mocks.ts"; -import { unsafeAsISODateString } from "../lib/types/branded.ts"; -describe("AppPasswords", () => { - beforeEach(() => { - clearMocks(); - setupFetchMock(); - setupIndexedDBMock(); - globalThis.confirm = vi.fn(() => true); - }); - describe("authentication guard", () => { - it("redirects to login when not authenticated", async () => { - setupUnauthenticatedUser(); - render(AppPasswords); - await waitFor(() => { - expect(globalThis.location.pathname).toBe("/app/login"); - }); - }); - }); - describe("page structure", () => { - beforeEach(() => { - setupAuthenticatedUser(); - mockEndpoint( - "com.atproto.server.listAppPasswords", - () => jsonResponse({ passwords: [] }), - ); - }); - it("displays all page elements", async () => { - render(AppPasswords); - await waitFor(() => { - expect( - screen.getByRole("heading", { name: /app passwords/i, level: 1 }), - ).toBeInTheDocument(); - expect(screen.getByRole("link", { name: /dashboard/i })) - .toHaveAttribute("href", "/app/dashboard"); - expect(screen.getByText(/third-party apps/i)).toBeInTheDocument(); - }); - }); - }); - describe("loading state", () => { - beforeEach(() => { - setupAuthenticatedUser(); - }); - it("shows loading skeleton while fetching passwords", () => { - mockEndpoint( - "com.atproto.server.listAppPasswords", - () => - new Promise((resolve) => - setTimeout(() => resolve(jsonResponse({ passwords: [] })), 100) - ), - ); - const { container } = render(AppPasswords); - expect(container.querySelectorAll(".skeleton-item").length) - .toBeGreaterThan(0); - }); - }); - describe("empty state", () => { - beforeEach(() => { - setupAuthenticatedUser(); - mockEndpoint( - "com.atproto.server.listAppPasswords", - () => jsonResponse({ passwords: [] }), - ); - }); - it("shows empty message when no passwords exist", async () => { - render(AppPasswords); - await waitFor(() => { - expect(screen.getByText(/no app passwords yet/i)).toBeInTheDocument(); - }); - }); - }); - describe("password list", () => { - const testPasswords = [ - mockData.appPassword({ - name: "Graysky", - createdAt: unsafeAsISODateString("2024-01-15T10:00:00Z"), - }), - mockData.appPassword({ - name: "Skeets", - createdAt: unsafeAsISODateString("2024-02-20T15:30:00Z"), - }), - ]; - beforeEach(() => { - setupAuthenticatedUser(); - mockEndpoint( - "com.atproto.server.listAppPasswords", - () => jsonResponse({ passwords: testPasswords }), - ); - }); - it("displays all app passwords with dates and revoke buttons", async () => { - render(AppPasswords); - await waitFor(() => { - expect(screen.getByText("Graysky")).toBeInTheDocument(); - expect(screen.getByText("Skeets")).toBeInTheDocument(); - expect(screen.getByText(/created.*2024-01-15/i)).toBeInTheDocument(); - expect(screen.getByText(/created.*2024-02-20/i)).toBeInTheDocument(); - expect(screen.getAllByRole("button", { name: /revoke/i })).toHaveLength( - 2, - ); - }); - }); - }); - describe("create app password", () => { - beforeEach(() => { - setupAuthenticatedUser(); - mockEndpoint( - "com.atproto.server.listAppPasswords", - () => jsonResponse({ passwords: [] }), - ); - }); - it("displays create form with input and button", async () => { - render(AppPasswords); - await waitFor(() => { - expect(screen.getByPlaceholderText(/app name/i)).toBeInTheDocument(); - expect(screen.getByRole("button", { name: /create/i })) - .toBeInTheDocument(); - }); - }); - it("disables create button when input is empty", async () => { - render(AppPasswords); - await waitFor(() => { - expect(screen.getByRole("button", { name: /create/i })).toBeDisabled(); - }); - }); - it("enables create button when input has value", async () => { - render(AppPasswords); - await waitFor(() => { - expect(screen.getByPlaceholderText(/app name/i)).toBeInTheDocument(); - }); - await fireEvent.input(screen.getByPlaceholderText(/app name/i), { - target: { value: "My New App" }, - }); - expect(screen.getByRole("button", { name: /create/i })).not - .toBeDisabled(); - }); - it("calls createAppPassword with correct name", async () => { - let capturedName: string | null = null; - mockEndpoint("com.atproto.server.createAppPassword", (_url, options) => { - const body = JSON.parse((options?.body as string) || "{}"); - capturedName = body.name; - return jsonResponse({ - name: body.name, - password: "xxxx-xxxx-xxxx-xxxx", - createdAt: new Date().toISOString(), - }); - }); - render(AppPasswords); - await waitFor(() => { - expect(screen.getByPlaceholderText(/app name/i)).toBeInTheDocument(); - }); - await fireEvent.input(screen.getByPlaceholderText(/app name/i), { - target: { value: "Graysky" }, - }); - await fireEvent.click(screen.getByRole("button", { name: /create/i })); - await waitFor(() => { - expect(capturedName).toBe("Graysky"); - }); - }); - it("shows loading state while creating", async () => { - mockEndpoint("com.atproto.server.createAppPassword", async () => { - await new Promise((resolve) => setTimeout(resolve, 100)); - return jsonResponse({ - name: "Test", - password: "xxxx-xxxx-xxxx-xxxx", - createdAt: new Date().toISOString(), - }); - }); - render(AppPasswords); - await waitFor(() => { - expect(screen.getByPlaceholderText(/app name/i)).toBeInTheDocument(); - }); - await fireEvent.input(screen.getByPlaceholderText(/app name/i), { - target: { value: "Test" }, - }); - await fireEvent.click(screen.getByRole("button", { name: /create/i })); - expect(screen.getByRole("button", { name: /creating/i })) - .toBeInTheDocument(); - expect(screen.getByRole("button", { name: /creating/i })).toBeDisabled(); - }); - it("displays created password in success box and clears input", async () => { - mockEndpoint("com.atproto.server.createAppPassword", () => - jsonResponse({ - name: "MyApp", - password: "abcd-efgh-ijkl-mnop", - createdAt: new Date().toISOString(), - })); - render(AppPasswords); - await waitFor(() => { - expect(screen.getByPlaceholderText(/app name/i)).toBeInTheDocument(); - }); - const input = screen.getByPlaceholderText( - /app name/i, - ) as HTMLInputElement; - await fireEvent.input(input, { target: { value: "MyApp" } }); - await fireEvent.click(screen.getByRole("button", { name: /create/i })); - await waitFor(() => { - expect(screen.getByText(/save this app password/i)).toBeInTheDocument(); - expect(screen.getByText("abcd-efgh-ijkl-mnop")).toBeInTheDocument(); - expect(screen.getByText("MyApp")).toBeInTheDocument(); - expect(input.value).toBe(""); - }); - }); - it("dismisses created password box when clicking Done", async () => { - mockEndpoint("com.atproto.server.createAppPassword", () => - jsonResponse({ - name: "Test", - password: "xxxx-xxxx-xxxx-xxxx", - createdAt: new Date().toISOString(), - })); - render(AppPasswords); - await waitFor(() => { - expect(screen.getByPlaceholderText(/app name/i)).toBeInTheDocument(); - }); - await fireEvent.input(screen.getByPlaceholderText(/app name/i), { - target: { value: "Test" }, - }); - await fireEvent.click(screen.getByRole("button", { name: /create/i })); - await waitFor(() => { - expect(screen.getByText(/save this app password/i)).toBeInTheDocument(); - }); - await fireEvent.click( - screen.getByLabelText(/i have saved my app password/i), - ); - await fireEvent.click(screen.getByRole("button", { name: /done/i })); - await waitFor(() => { - expect(screen.queryByText(/save this app password/i)).not - .toBeInTheDocument(); - }); - }); - it("shows error toast when creation fails", async () => { - mockEndpoint( - "com.atproto.server.createAppPassword", - () => errorResponse("InvalidRequest", "Name already exists", 400), - ); - render(AppPasswords); - await waitFor(() => { - expect(screen.getByPlaceholderText(/app name/i)).toBeInTheDocument(); - }); - await fireEvent.input(screen.getByPlaceholderText(/app name/i), { - target: { value: "Duplicate" }, - }); - await fireEvent.click(screen.getByRole("button", { name: /create/i })); - await waitFor(() => { - const errors = getErrorToasts(); - expect(errors.some((e) => /name already exists/i.test(e))).toBe(true); - }); - }); - }); - describe("revoke app password", () => { - const testPassword = mockData.appPassword({ name: "TestApp" }); - beforeEach(() => { - setupAuthenticatedUser(); - }); - it("shows confirmation dialog before revoking", async () => { - const confirmSpy = vi.fn(() => false); - globalThis.confirm = confirmSpy; - mockEndpoint( - "com.atproto.server.listAppPasswords", - () => jsonResponse({ passwords: [testPassword] }), - ); - render(AppPasswords); - await waitFor(() => { - expect(screen.getByText("TestApp")).toBeInTheDocument(); - }); - await fireEvent.click(screen.getByRole("button", { name: /revoke/i })); - expect(confirmSpy).toHaveBeenCalledWith( - expect.stringContaining("TestApp"), - ); - }); - it("does not revoke when confirmation is cancelled", async () => { - globalThis.confirm = vi.fn(() => false); - let revokeCalled = false; - mockEndpoint( - "com.atproto.server.listAppPasswords", - () => jsonResponse({ passwords: [testPassword] }), - ); - mockEndpoint("com.atproto.server.revokeAppPassword", () => { - revokeCalled = true; - return jsonResponse({}); - }); - render(AppPasswords); - await waitFor(() => { - expect(screen.getByText("TestApp")).toBeInTheDocument(); - }); - await fireEvent.click(screen.getByRole("button", { name: /revoke/i })); - expect(revokeCalled).toBe(false); - }); - it("calls revokeAppPassword with correct name", async () => { - globalThis.confirm = vi.fn(() => true); - let capturedName: string | null = null; - mockEndpoint( - "com.atproto.server.listAppPasswords", - () => jsonResponse({ passwords: [testPassword] }), - ); - mockEndpoint("com.atproto.server.revokeAppPassword", (_url, options) => { - const body = JSON.parse((options?.body as string) || "{}"); - capturedName = body.name; - return jsonResponse({}); - }); - render(AppPasswords); - await waitFor(() => { - expect(screen.getByText("TestApp")).toBeInTheDocument(); - }); - await fireEvent.click(screen.getByRole("button", { name: /revoke/i })); - await waitFor(() => { - expect(capturedName).toBe("TestApp"); - }); - }); - it("shows loading state while revoking", async () => { - globalThis.confirm = vi.fn(() => true); - mockEndpoint( - "com.atproto.server.listAppPasswords", - () => jsonResponse({ passwords: [testPassword] }), - ); - mockEndpoint("com.atproto.server.revokeAppPassword", async () => { - await new Promise((resolve) => setTimeout(resolve, 100)); - return jsonResponse({}); - }); - render(AppPasswords); - await waitFor(() => { - expect(screen.getByText("TestApp")).toBeInTheDocument(); - }); - await fireEvent.click(screen.getByRole("button", { name: /revoke/i })); - expect(screen.getByRole("button", { name: /revoking/i })) - .toBeInTheDocument(); - expect(screen.getByRole("button", { name: /revoking/i })).toBeDisabled(); - }); - it("reloads password list after successful revocation", async () => { - globalThis.confirm = vi.fn(() => true); - let listCallCount = 0; - mockEndpoint("com.atproto.server.listAppPasswords", () => { - listCallCount++; - if (listCallCount === 1) { - return jsonResponse({ passwords: [testPassword] }); - } - return jsonResponse({ passwords: [] }); - }); - mockEndpoint( - "com.atproto.server.revokeAppPassword", - () => jsonResponse({}), - ); - render(AppPasswords); - await waitFor(() => { - expect(screen.getByText("TestApp")).toBeInTheDocument(); - }); - await fireEvent.click(screen.getByRole("button", { name: /revoke/i })); - await waitFor(() => { - expect(screen.queryByText("TestApp")).not.toBeInTheDocument(); - expect(screen.getByText(/no app passwords yet/i)).toBeInTheDocument(); - }); - }); - it("shows error toast when revocation fails", async () => { - globalThis.confirm = vi.fn(() => true); - mockEndpoint( - "com.atproto.server.listAppPasswords", - () => jsonResponse({ passwords: [testPassword] }), - ); - mockEndpoint( - "com.atproto.server.revokeAppPassword", - () => errorResponse("InternalError", "Server error", 500), - ); - render(AppPasswords); - await waitFor(() => { - expect(screen.getByText("TestApp")).toBeInTheDocument(); - }); - await fireEvent.click(screen.getByRole("button", { name: /revoke/i })); - await waitFor(() => { - const errors = getErrorToasts(); - expect(errors.some((e) => /server error/i.test(e))).toBe(true); - }); - }); - }); - describe("error handling", () => { - beforeEach(() => { - setupAuthenticatedUser(); - }); - it("shows error toast when loading passwords fails", async () => { - mockEndpoint( - "com.atproto.server.listAppPasswords", - () => errorResponse("InternalError", "Database connection failed", 500), - ); - render(AppPasswords); - await waitFor(() => { - const errors = getErrorToasts(); - expect(errors.some((e) => /database connection failed/i.test(e))).toBe( - true, - ); - }); - }); - }); -}); diff --git a/frontend/src/tests/Comms.test.ts b/frontend/src/tests/Comms.test.ts deleted file mode 100644 index 54bff88..0000000 --- a/frontend/src/tests/Comms.test.ts +++ /dev/null @@ -1,508 +0,0 @@ -import { beforeEach, describe, expect, it } from "vitest"; -import { fireEvent, render, screen, waitFor } from "@testing-library/svelte"; -import Comms from "../routes/Comms.svelte"; -import { - clearMocks, - errorResponse, - getErrorToasts, - getToasts, - jsonResponse, - mockData, - mockEndpoint, - setupAuthenticatedUser, - setupDefaultMocks, - setupUnauthenticatedUser, -} from "./mocks.ts"; -describe("Comms", () => { - beforeEach(() => { - clearMocks(); - setupDefaultMocks(); - }); - describe("authentication guard", () => { - it("redirects to login when not authenticated", async () => { - setupUnauthenticatedUser(); - render(Comms); - await waitFor(() => { - expect(globalThis.location.pathname).toBe("/app/login"); - }); - }); - }); - describe("page structure", () => { - beforeEach(() => { - setupAuthenticatedUser(); - mockEndpoint( - "_account.getNotificationPrefs", - () => jsonResponse(mockData.notificationPrefs()), - ); - mockEndpoint( - "com.atproto.server.describeServer", - () => jsonResponse(mockData.describeServer()), - ); - mockEndpoint( - "_account.getNotificationHistory", - () => jsonResponse({ notifications: [] }), - ); - }); - it("displays all page elements and sections", async () => { - render(Comms); - await waitFor(() => { - expect( - screen.getByRole("heading", { - name: /communication preferences|notification preferences/i, - level: 1, - }), - ).toBeInTheDocument(); - expect(screen.getByRole("link", { name: /dashboard/i })) - .toHaveAttribute("href", "/app/dashboard"); - expect(screen.getByRole("heading", { name: /preferred channel/i })) - .toBeInTheDocument(); - expect(screen.getByRole("heading", { name: /channel configuration/i })) - .toBeInTheDocument(); - }); - }); - }); - describe("loading state", () => { - beforeEach(() => { - setupAuthenticatedUser(); - mockEndpoint( - "com.atproto.server.describeServer", - () => jsonResponse(mockData.describeServer()), - ); - mockEndpoint( - "_account.getNotificationHistory", - () => jsonResponse({ notifications: [] }), - ); - }); - it("shows loading skeleton while fetching preferences", () => { - mockEndpoint( - "_account.getNotificationPrefs", - () => - new Promise((resolve) => - setTimeout( - () => resolve(jsonResponse(mockData.notificationPrefs())), - 100, - ) - ), - ); - const { container } = render(Comms); - expect(container.querySelectorAll(".skeleton-section").length) - .toBeGreaterThan(0); - }); - }); - describe("channel options", () => { - beforeEach(() => { - setupAuthenticatedUser(); - mockEndpoint( - "com.atproto.server.describeServer", - () => jsonResponse(mockData.describeServer()), - ); - mockEndpoint( - "_account.getNotificationHistory", - () => jsonResponse({ notifications: [] }), - ); - }); - it("displays all four channel options", async () => { - mockEndpoint( - "_account.getNotificationPrefs", - () => jsonResponse(mockData.notificationPrefs()), - ); - render(Comms); - await waitFor(() => { - expect(screen.getByRole("radio", { name: /email/i })) - .toBeInTheDocument(); - expect(screen.getByRole("radio", { name: /discord/i })) - .toBeInTheDocument(); - expect(screen.getByRole("radio", { name: /telegram/i })) - .toBeInTheDocument(); - expect(screen.getByRole("radio", { name: /signal/i })) - .toBeInTheDocument(); - }); - }); - it("email channel is always selectable", async () => { - mockEndpoint( - "_account.getNotificationPrefs", - () => jsonResponse(mockData.notificationPrefs()), - ); - render(Comms); - await waitFor(() => { - const emailRadio = screen.getByRole("radio", { name: /email/i }); - expect(emailRadio).not.toBeDisabled(); - }); - }); - it("discord channel is disabled when not configured", async () => { - mockEndpoint( - "_account.getNotificationPrefs", - () => jsonResponse(mockData.notificationPrefs({ discordId: null })), - ); - render(Comms); - await waitFor(() => { - const discordRadio = screen.getByRole("radio", { name: /discord/i }); - expect(discordRadio).toBeDisabled(); - }); - }); - it("discord channel is enabled when configured", async () => { - mockEndpoint( - "_account.getNotificationPrefs", - () => - jsonResponse(mockData.notificationPrefs({ discordId: "123456789" })), - ); - render(Comms); - await waitFor(() => { - const discordRadio = screen.getByRole("radio", { name: /discord/i }); - expect(discordRadio).not.toBeDisabled(); - }); - }); - it("shows hint for disabled channels", async () => { - mockEndpoint( - "_account.getNotificationPrefs", - () => jsonResponse(mockData.notificationPrefs()), - ); - render(Comms); - await waitFor(() => { - expect(screen.getAllByText(/configure.*to enable/i).length) - .toBeGreaterThan(0); - }); - }); - it("selects current preferred channel", async () => { - mockEndpoint( - "_account.getNotificationPrefs", - () => - jsonResponse( - mockData.notificationPrefs({ preferredChannel: "email" }), - ), - ); - render(Comms); - await waitFor(() => { - const emailRadio = screen.getByRole("radio", { - name: /email/i, - }) as HTMLInputElement; - expect(emailRadio.checked).toBe(true); - }); - }); - }); - describe("channel configuration", () => { - beforeEach(() => { - setupAuthenticatedUser(); - mockEndpoint( - "com.atproto.server.describeServer", - () => jsonResponse(mockData.describeServer()), - ); - mockEndpoint( - "_account.getNotificationHistory", - () => jsonResponse({ notifications: [] }), - ); - }); - it("displays email as readonly with current value", async () => { - mockEndpoint( - "_account.getNotificationPrefs", - () => jsonResponse(mockData.notificationPrefs()), - ); - render(Comms); - await waitFor(() => { - const emailInput = screen.getByLabelText( - /^email$/i, - ) as HTMLInputElement; - expect(emailInput).toBeDisabled(); - expect(emailInput.value).toBe("test@example.com"); - }); - }); - it("displays all channel inputs with current values", async () => { - mockEndpoint( - "_account.getNotificationPrefs", - () => - jsonResponse(mockData.notificationPrefs({ - discordId: "123456789", - telegramUsername: "testuser", - signalNumber: "+1234567890", - })), - ); - render(Comms); - await waitFor(() => { - expect( - (screen.getByLabelText(/discord.*id/i) as HTMLInputElement).value, - ).toBe("123456789"); - expect( - (screen.getByLabelText(/telegram.*username/i) as HTMLInputElement) - .value, - ).toBe("testuser"); - expect( - (screen.getByLabelText(/signal.*number/i) as HTMLInputElement) - .value, - ).toBe("+1234567890"); - }); - }); - }); - describe("verification status badges", () => { - beforeEach(() => { - setupAuthenticatedUser(); - mockEndpoint( - "com.atproto.server.describeServer", - () => jsonResponse(mockData.describeServer()), - ); - mockEndpoint( - "_account.getNotificationHistory", - () => jsonResponse({ notifications: [] }), - ); - }); - it("shows Primary badge for email", async () => { - mockEndpoint( - "_account.getNotificationPrefs", - () => jsonResponse(mockData.notificationPrefs()), - ); - render(Comms); - await waitFor(() => { - expect(screen.getByText("Primary")).toBeInTheDocument(); - }); - }); - it("shows Verified badge for verified discord", async () => { - mockEndpoint( - "_account.getNotificationPrefs", - () => - jsonResponse(mockData.notificationPrefs({ - discordId: "123456789", - discordVerified: true, - })), - ); - render(Comms); - await waitFor(() => { - const verifiedBadges = screen.getAllByText("Verified"); - expect(verifiedBadges.length).toBeGreaterThan(0); - }); - }); - it("shows Not verified badge for unverified discord", async () => { - mockEndpoint( - "_account.getNotificationPrefs", - () => - jsonResponse(mockData.notificationPrefs({ - discordId: "123456789", - discordVerified: false, - })), - ); - render(Comms); - await waitFor(() => { - expect(screen.getByText("Not verified")).toBeInTheDocument(); - }); - }); - it("does not show badge when channel not configured", async () => { - mockEndpoint( - "_account.getNotificationPrefs", - () => jsonResponse(mockData.notificationPrefs()), - ); - render(Comms); - await waitFor(() => { - expect(screen.getByText("Primary")).toBeInTheDocument(); - expect(screen.queryByText("Not verified")).not.toBeInTheDocument(); - }); - }); - }); - describe("save preferences", () => { - beforeEach(() => { - setupAuthenticatedUser(); - mockEndpoint( - "com.atproto.server.describeServer", - () => jsonResponse(mockData.describeServer()), - ); - mockEndpoint( - "_account.getNotificationHistory", - () => jsonResponse({ notifications: [] }), - ); - }); - it("calls updateNotificationPrefs with correct data", async () => { - let capturedBody: Record | null = null; - mockEndpoint( - "_account.getNotificationPrefs", - () => jsonResponse(mockData.notificationPrefs()), - ); - mockEndpoint( - "_account.updateNotificationPrefs", - (_url, options) => { - capturedBody = JSON.parse((options?.body as string) || "{}"); - return jsonResponse({ success: true }); - }, - ); - render(Comms); - await waitFor(() => { - expect(screen.getByLabelText(/discord.*id/i)).toBeInTheDocument(); - }); - await fireEvent.input(screen.getByLabelText(/discord.*id/i), { - target: { value: "999888777" }, - }); - await fireEvent.click( - screen.getByRole("button", { name: /save preferences/i }), - ); - await waitFor(() => { - expect(capturedBody).not.toBeNull(); - expect(capturedBody?.discordId).toBe("999888777"); - expect(capturedBody?.preferredChannel).toBe("email"); - }); - }); - it("shows loading state while saving", async () => { - mockEndpoint( - "_account.getNotificationPrefs", - () => jsonResponse(mockData.notificationPrefs()), - ); - mockEndpoint("_account.updateNotificationPrefs", async () => { - await new Promise((resolve) => setTimeout(resolve, 100)); - return jsonResponse({ success: true }); - }); - render(Comms); - await waitFor(() => { - expect(screen.getByRole("button", { name: /save preferences/i })) - .toBeInTheDocument(); - }); - await fireEvent.click( - screen.getByRole("button", { name: /save preferences/i }), - ); - expect(screen.getByRole("button", { name: /saving/i })) - .toBeInTheDocument(); - expect(screen.getByRole("button", { name: /saving/i })).toBeDisabled(); - }); - it("shows success toast after saving", async () => { - mockEndpoint( - "_account.getNotificationPrefs", - () => jsonResponse(mockData.notificationPrefs()), - ); - mockEndpoint( - "_account.updateNotificationPrefs", - () => jsonResponse({ success: true }), - ); - render(Comms); - await waitFor(() => { - expect(screen.getByRole("button", { name: /save preferences/i })) - .toBeInTheDocument(); - }); - await fireEvent.click( - screen.getByRole("button", { name: /save preferences/i }), - ); - await waitFor(() => { - const toasts = getToasts(); - expect( - toasts.some((t) => t.type === "success" && /saved/i.test(t.message)), - ).toBe(true); - }); - }); - it("shows error toast when save fails", async () => { - mockEndpoint( - "_account.getNotificationPrefs", - () => jsonResponse(mockData.notificationPrefs()), - ); - mockEndpoint( - "_account.updateNotificationPrefs", - () => - errorResponse("InvalidRequest", "Invalid channel configuration", 400), - ); - render(Comms); - await waitFor(() => { - expect(screen.getByRole("button", { name: /save preferences/i })) - .toBeInTheDocument(); - }); - await fireEvent.click( - screen.getByRole("button", { name: /save preferences/i }), - ); - await waitFor(() => { - const errors = getErrorToasts(); - expect(errors.some((e) => /invalid channel configuration/i.test(e))) - .toBe(true); - }); - }); - it("reloads preferences after successful save", async () => { - let loadCount = 0; - mockEndpoint("_account.getNotificationPrefs", () => { - loadCount++; - return jsonResponse(mockData.notificationPrefs()); - }); - mockEndpoint( - "_account.updateNotificationPrefs", - () => jsonResponse({ success: true }), - ); - render(Comms); - await waitFor(() => { - expect(screen.getByRole("button", { name: /save preferences/i })) - .toBeInTheDocument(); - }); - const initialLoadCount = loadCount; - await fireEvent.click( - screen.getByRole("button", { name: /save preferences/i }), - ); - await waitFor(() => { - expect(loadCount).toBeGreaterThan(initialLoadCount); - }); - }); - }); - describe("channel selection interaction", () => { - beforeEach(() => { - setupAuthenticatedUser(); - mockEndpoint( - "com.atproto.server.describeServer", - () => jsonResponse(mockData.describeServer()), - ); - mockEndpoint( - "_account.getNotificationHistory", - () => jsonResponse({ notifications: [] }), - ); - }); - it("enables discord channel after entering discord ID", async () => { - mockEndpoint( - "_account.getNotificationPrefs", - () => jsonResponse(mockData.notificationPrefs()), - ); - render(Comms); - await waitFor(() => { - expect(screen.getByRole("radio", { name: /discord/i })).toBeDisabled(); - }); - await fireEvent.input(screen.getByLabelText(/discord.*id/i), { - target: { value: "123456789" }, - }); - await waitFor(() => { - expect(screen.getByRole("radio", { name: /discord/i })).not - .toBeDisabled(); - }); - }); - it("allows selecting a configured channel", async () => { - mockEndpoint( - "_account.getNotificationPrefs", - () => - jsonResponse(mockData.notificationPrefs({ - discordId: "123456789", - discordVerified: true, - })), - ); - render(Comms); - await waitFor(() => { - expect(screen.getByRole("radio", { name: /discord/i })).not - .toBeDisabled(); - }); - await fireEvent.click(screen.getByRole("radio", { name: /discord/i })); - const discordRadio = screen.getByRole("radio", { - name: /discord/i, - }) as HTMLInputElement; - expect(discordRadio.checked).toBe(true); - }); - }); - describe("error handling", () => { - beforeEach(() => { - setupAuthenticatedUser(); - mockEndpoint( - "com.atproto.server.describeServer", - () => jsonResponse(mockData.describeServer()), - ); - mockEndpoint( - "_account.getNotificationHistory", - () => jsonResponse({ notifications: [] }), - ); - }); - it("shows error toast when loading preferences fails", async () => { - mockEndpoint( - "_account.getNotificationPrefs", - () => errorResponse("InternalError", "Database connection failed", 500), - ); - render(Comms); - await waitFor(() => { - const errors = getErrorToasts(); - expect(errors.some((e) => /database connection failed/i.test(e))).toBe( - true, - ); - }); - }); - }); -}); diff --git a/frontend/src/tests/Dashboard.test.ts b/frontend/src/tests/Dashboard.test.ts index 9bc79f6..4fdc54a 100644 --- a/frontend/src/tests/Dashboard.test.ts +++ b/frontend/src/tests/Dashboard.test.ts @@ -26,8 +26,8 @@ describe("Dashboard", () => { }); it("shows loading state while checking auth", () => { const { container } = render(Dashboard); - expect(container.querySelector(".skeleton-section")).toBeInTheDocument(); - expect(container.querySelectorAll(".skeleton-card").length) + expect(container.querySelector(".skeleton-header")).toBeInTheDocument(); + expect(container.querySelectorAll(".skeleton-nav-item").length) .toBeGreaterThan(0); }); }); @@ -35,18 +35,13 @@ describe("Dashboard", () => { beforeEach(() => { setupAuthenticatedUser(); }); - it("displays user account info and page structure", async () => { + it("displays user account info and sidebar structure", async () => { render(Dashboard); await waitFor(() => { - expect(screen.getByRole("heading", { name: /dashboard/i })) - .toBeInTheDocument(); - expect(screen.getByRole("heading", { name: /account overview/i })) - .toBeInTheDocument(); expect(screen.getAllByText(/@testuser\.test\.tranquil\.dev/).length) .toBeGreaterThan(0); expect(screen.getByText(/did:web:test\.tranquil\.dev:u:testuser/)) .toBeInTheDocument(); - expect(screen.getByText("test@example.com")).toBeInTheDocument(); expect(screen.getByText("Verified")).toBeInTheDocument(); expect(screen.getByText("Verified")).toHaveClass("badge", "success"); }); @@ -59,23 +54,21 @@ describe("Dashboard", () => { expect(screen.getByText("Unverified")).toHaveClass("badge", "warning"); }); }); - it("displays all navigation cards", async () => { + it("displays sidebar navigation items", async () => { render(Dashboard); await waitFor(() => { - const navCards = [ - { name: /app passwords/i, href: "/app/app-passwords" }, - { name: /account settings/i, href: "/app/settings" }, - { name: /communication preferences/i, href: "/app/comms" }, - { name: /repository explorer/i, href: "/app/repo" }, + const navItems = [ + /general/i, + /security/i, + /sessions/i, + /app passwords/i, ]; - for (const { name, href } of navCards) { - const card = screen.getByRole("link", { name }); - expect(card).toBeInTheDocument(); - expect(card).toHaveAttribute("href", href); + for (const name of navItems) { + expect(screen.getByRole("button", { name })).toBeInTheDocument(); } }); }); - it("displays invite codes card when invites are required and user is admin", async () => { + it("displays invite codes nav when invites are required and user is admin", async () => { setupAuthenticatedUser({ isAdmin: true }); mockEndpoint( "com.atproto.server.describeServer", @@ -84,9 +77,8 @@ describe("Dashboard", () => { ); render(Dashboard); await waitFor(() => { - const inviteCard = screen.getByRole("link", { name: /invite codes/i }); - expect(inviteCard).toBeInTheDocument(); - expect(inviteCard).toHaveAttribute("href", "/app/invite-codes"); + expect(screen.getByRole("button", { name: /invite codes/i })) + .toBeInTheDocument(); }); }); }); diff --git a/frontend/src/tests/Login.test.ts b/frontend/src/tests/Login.test.ts index 3200033..6d08934 100644 --- a/frontend/src/tests/Login.test.ts +++ b/frontend/src/tests/Login.test.ts @@ -52,7 +52,7 @@ describe("Login", () => { it("shows create account link", async () => { render(Login); await waitFor(() => { - expect(screen.getByText(/don't have an account/i)).toBeInTheDocument(); + expect(screen.getByText(/no account\?/i)).toBeInTheDocument(); expect(screen.getByRole("link", { name: /create/i })).toHaveAttribute( "href", "/app/register", diff --git a/frontend/src/tests/Settings.test.ts b/frontend/src/tests/Settings.test.ts deleted file mode 100644 index 9dba382..0000000 --- a/frontend/src/tests/Settings.test.ts +++ /dev/null @@ -1,579 +0,0 @@ -import { beforeEach, describe, expect, it, vi } from "vitest"; -import { fireEvent, render, screen, waitFor } from "@testing-library/svelte"; -import Settings from "../routes/Settings.svelte"; -import { - clearMocks, - errorResponse, - getErrorToasts, - getToasts, - jsonResponse, - mockData, - mockEndpoint, - setupAuthenticatedUser, - setupDefaultMocks, - setupUnauthenticatedUser, -} from "./mocks.ts"; -describe("Settings", () => { - beforeEach(() => { - clearMocks(); - setupDefaultMocks(); - globalThis.confirm = vi.fn(() => true); - }); - describe("authentication guard", () => { - it("redirects to login when not authenticated", async () => { - setupUnauthenticatedUser(); - render(Settings); - await waitFor(() => { - expect(globalThis.location.pathname).toBe("/app/login"); - }); - }); - }); - describe("page structure", () => { - beforeEach(() => { - setupAuthenticatedUser(); - }); - it("displays all page elements and sections", async () => { - render(Settings); - await waitFor(() => { - expect( - screen.getByRole("heading", { name: /account settings/i, level: 1 }), - ).toBeInTheDocument(); - expect(screen.getByRole("link", { name: /dashboard/i })) - .toHaveAttribute("href", "/app/dashboard"); - expect(screen.getByRole("heading", { name: /change email/i })) - .toBeInTheDocument(); - expect(screen.getByRole("heading", { name: /change handle/i })) - .toBeInTheDocument(); - expect(screen.getByRole("heading", { name: /delete account/i })) - .toBeInTheDocument(); - }); - }); - }); - describe("email change", () => { - beforeEach(() => { - setupAuthenticatedUser(); - }); - it("displays current email and change button", async () => { - render(Settings); - await waitFor(() => { - expect(screen.getByText(/current.*test@example.com/i)) - .toBeInTheDocument(); - expect(screen.getByRole("button", { name: /change email/i })) - .toBeInTheDocument(); - }); - }); - it("calls requestEmailUpdate when clicking change email button", async () => { - let requestCalled = false; - mockEndpoint("com.atproto.server.requestEmailUpdate", () => { - requestCalled = true; - return jsonResponse({ tokenRequired: true }); - }); - mockEndpoint( - "_account.checkEmailUpdateStatus", - () => jsonResponse({ pending: false, authorized: false }), - ); - render(Settings); - await waitFor(() => { - expect(screen.getByLabelText(/new email/i)).toBeInTheDocument(); - }); - await fireEvent.input(screen.getByLabelText(/new email/i), { - target: { value: "newemail@example.com" }, - }); - await fireEvent.click( - screen.getByRole("button", { name: /change email/i }), - ); - await waitFor(() => { - expect(requestCalled).toBe(true); - }); - }); - it("shows verification code and new email inputs when token is required", async () => { - mockEndpoint( - "com.atproto.server.requestEmailUpdate", - () => jsonResponse({ tokenRequired: true }), - ); - mockEndpoint( - "_account.checkEmailUpdateStatus", - () => jsonResponse({ pending: false, authorized: false }), - ); - render(Settings); - await waitFor(() => { - expect(screen.getByLabelText(/new email/i)).toBeInTheDocument(); - }); - await fireEvent.input(screen.getByLabelText(/new email/i), { - target: { value: "newemail@example.com" }, - }); - await fireEvent.click( - screen.getByRole("button", { name: /change email/i }), - ); - await waitFor(() => { - expect(screen.getByLabelText(/verification code/i)).toBeInTheDocument(); - expect(screen.getByLabelText(/new email/i)).toBeInTheDocument(); - expect(screen.getByRole("button", { name: /confirm email change/i })) - .toBeInTheDocument(); - }); - }); - it("calls updateEmail with token when confirming", async () => { - let updateCalled = false; - let capturedBody: Record | null = null; - mockEndpoint( - "com.atproto.server.requestEmailUpdate", - () => jsonResponse({ tokenRequired: true }), - ); - mockEndpoint( - "_account.checkEmailUpdateStatus", - () => jsonResponse({ pending: false, authorized: false }), - ); - mockEndpoint("com.atproto.server.updateEmail", (_url, options) => { - updateCalled = true; - capturedBody = JSON.parse((options?.body as string) || "{}"); - return jsonResponse({}); - }); - mockEndpoint( - "com.atproto.server.getSession", - () => jsonResponse(mockData.session()), - ); - render(Settings); - await waitFor(() => { - expect(screen.getByLabelText(/new email/i)).toBeInTheDocument(); - }); - await fireEvent.input(screen.getByLabelText(/new email/i), { - target: { value: "newemail@example.com" }, - }); - await fireEvent.click( - screen.getByRole("button", { name: /change email/i }), - ); - await waitFor(() => { - expect(screen.getByLabelText(/verification code/i)).toBeInTheDocument(); - }); - await fireEvent.input(screen.getByLabelText(/verification code/i), { - target: { value: "123456" }, - }); - await fireEvent.click( - screen.getByRole("button", { name: /confirm email change/i }), - ); - await waitFor(() => { - expect(updateCalled).toBe(true); - expect(capturedBody?.email).toBe("newemail@example.com"); - expect(capturedBody?.token).toBe("123456"); - }); - }); - it("shows success toast after email update", async () => { - mockEndpoint( - "com.atproto.server.requestEmailUpdate", - () => jsonResponse({ tokenRequired: true }), - ); - mockEndpoint( - "_account.checkEmailUpdateStatus", - () => jsonResponse({ pending: false, authorized: false }), - ); - mockEndpoint("com.atproto.server.updateEmail", () => jsonResponse({})); - mockEndpoint( - "com.atproto.server.getSession", - () => jsonResponse(mockData.session()), - ); - render(Settings); - await waitFor(() => { - expect(screen.getByLabelText(/new email/i)).toBeInTheDocument(); - }); - await fireEvent.input(screen.getByLabelText(/new email/i), { - target: { value: "new@test.com" }, - }); - await fireEvent.click( - screen.getByRole("button", { name: /change email/i }), - ); - await waitFor(() => { - expect(screen.getByLabelText(/verification code/i)).toBeInTheDocument(); - }); - await fireEvent.input(screen.getByLabelText(/verification code/i), { - target: { value: "123456" }, - }); - await fireEvent.click( - screen.getByRole("button", { name: /confirm email change/i }), - ); - await waitFor(() => { - const toasts = getToasts(); - expect( - toasts.some((t) => - t.type === "success" && /email.*updated/i.test(t.message) - ), - ).toBe(true); - }); - }); - it("shows cancel button to return to initial state", async () => { - mockEndpoint( - "com.atproto.server.requestEmailUpdate", - () => jsonResponse({ tokenRequired: true }), - ); - mockEndpoint( - "_account.checkEmailUpdateStatus", - () => jsonResponse({ pending: false, authorized: false }), - ); - render(Settings); - await waitFor(() => { - expect(screen.getByLabelText(/new email/i)).toBeInTheDocument(); - }); - await fireEvent.input(screen.getByLabelText(/new email/i), { - target: { value: "newemail@example.com" }, - }); - await fireEvent.click( - screen.getByRole("button", { name: /change email/i }), - ); - await waitFor(() => { - expect(screen.getByRole("button", { name: /cancel/i })) - .toBeInTheDocument(); - }); - const emailSection = screen.getByRole("heading", { - name: /change email/i, - }) - .closest("section"); - const cancelButton = emailSection?.querySelector("button.secondary"); - if (cancelButton) { - await fireEvent.click(cancelButton); - } - await waitFor(() => { - expect(screen.queryByLabelText(/verification code/i)).not - .toBeInTheDocument(); - }); - }); - it("shows error toast when request fails", async () => { - mockEndpoint( - "com.atproto.server.requestEmailUpdate", - () => errorResponse("InvalidEmail", "Invalid email format", 400), - ); - render(Settings); - await waitFor(() => { - expect(screen.getByLabelText(/new email/i)).toBeInTheDocument(); - }); - await fireEvent.input(screen.getByLabelText(/new email/i), { - target: { value: "invalid@email.com" }, - }); - const button = screen.getByRole("button", { name: /change email/i }); - await fireEvent.submit(button.closest("form")!); - await waitFor(() => { - const errors = getErrorToasts(); - expect(errors.some((e) => /invalid email format/i.test(e))).toBe(true); - }); - }); - }); - describe("handle change", () => { - beforeEach(() => { - setupAuthenticatedUser(); - mockEndpoint( - "com.atproto.server.describeServer", - () => jsonResponse(mockData.describeServer()), - ); - }); - it("displays current handle", async () => { - render(Settings); - await waitFor(() => { - expect(screen.getByText(/current.*@testuser\.test\.tranquil\.dev/i)) - .toBeInTheDocument(); - }); - }); - it("shows PDS handle and custom domain tabs", async () => { - render(Settings); - await waitFor(() => { - expect(screen.getByRole("button", { name: /pds handle/i })) - .toBeInTheDocument(); - expect(screen.getByRole("button", { name: /custom domain/i })) - .toBeInTheDocument(); - }); - }); - it("allows entering handle and shows domain suffix", async () => { - render(Settings); - await waitFor(() => { - expect(screen.getByLabelText(/new handle/i)).toBeInTheDocument(); - expect(screen.getByText(/\.test\.tranquil\.dev/i)).toBeInTheDocument(); - }); - const input = screen.getByLabelText(/new handle/i) as HTMLInputElement; - await fireEvent.input(input, { - target: { value: "newhandle" }, - }); - expect(input.value).toBe("newhandle"); - expect(screen.getByRole("button", { name: /change handle/i })) - .toBeInTheDocument(); - }); - it("shows success toast after handle change", async () => { - mockEndpoint("com.atproto.identity.updateHandle", () => jsonResponse({})); - mockEndpoint( - "com.atproto.server.getSession", - () => jsonResponse(mockData.session()), - ); - render(Settings); - await waitFor(() => { - expect(screen.getByLabelText(/new handle/i)).toBeInTheDocument(); - expect(screen.getByText(/\.test\.tranquil\.dev/i)).toBeInTheDocument(); - }); - const input = screen.getByLabelText(/new handle/i) as HTMLInputElement; - await fireEvent.input(input, { - target: { value: "newhandle" }, - }); - const button = screen.getByRole("button", { name: /change handle/i }); - await fireEvent.submit(button.closest("form")!); - await waitFor(() => { - const toasts = getToasts(); - expect( - toasts.some((t) => - t.type === "success" && /handle.*updated/i.test(t.message) - ), - ).toBe(true); - }); - }); - it("shows error toast when handle change fails", async () => { - mockEndpoint( - "com.atproto.identity.updateHandle", - () => - errorResponse("HandleNotAvailable", "Handle is already taken", 400), - ); - render(Settings); - await waitFor(() => { - expect(screen.getByLabelText(/new handle/i)).toBeInTheDocument(); - expect(screen.getByText(/\.test\.tranquil\.dev/i)).toBeInTheDocument(); - }); - const input = screen.getByLabelText(/new handle/i) as HTMLInputElement; - await fireEvent.input(input, { - target: { value: "taken" }, - }); - expect(input.value).toBe("taken"); - const button = screen.getByRole("button", { name: /change handle/i }); - await fireEvent.submit(button.closest("form")!); - await waitFor(() => { - const errors = getErrorToasts(); - expect(errors.some((e) => /handle is already taken/i.test(e))).toBe( - true, - ); - }); - }); - }); - describe("account deletion", () => { - beforeEach(() => { - setupAuthenticatedUser(); - mockEndpoint("com.atproto.server.deleteSession", () => jsonResponse({})); - }); - it("displays delete section with warning and request button", async () => { - render(Settings); - await waitFor(() => { - expect(screen.getByText(/this action is irreversible/i)) - .toBeInTheDocument(); - expect( - screen.getByRole("button", { name: /request account deletion/i }), - ).toBeInTheDocument(); - }); - }); - it("calls requestAccountDelete when clicking request", async () => { - let requestCalled = false; - mockEndpoint("com.atproto.server.requestAccountDelete", () => { - requestCalled = true; - return jsonResponse({}); - }); - render(Settings); - await waitFor(() => { - expect( - screen.getByRole("button", { name: /request account deletion/i }), - ).toBeInTheDocument(); - }); - await fireEvent.click( - screen.getByRole("button", { name: /request account deletion/i }), - ); - await waitFor(() => { - expect(requestCalled).toBe(true); - }); - }); - it("shows confirmation form after requesting deletion", async () => { - mockEndpoint( - "com.atproto.server.requestAccountDelete", - () => jsonResponse({}), - ); - render(Settings); - await waitFor(() => { - expect( - screen.getByRole("button", { name: /request account deletion/i }), - ).toBeInTheDocument(); - }); - await fireEvent.click( - screen.getByRole("button", { name: /request account deletion/i }), - ); - await waitFor(() => { - expect(screen.getByLabelText(/confirmation code/i)).toBeInTheDocument(); - expect(screen.getByLabelText(/your password/i)).toBeInTheDocument(); - expect( - screen.getByRole("button", { name: /permanently delete account/i }), - ).toBeInTheDocument(); - }); - }); - it("shows confirmation dialog before final deletion", async () => { - const confirmSpy = vi.fn(() => false); - globalThis.confirm = confirmSpy; - mockEndpoint( - "com.atproto.server.requestAccountDelete", - () => jsonResponse({}), - ); - render(Settings); - await waitFor(() => { - expect( - screen.getByRole("button", { name: /request account deletion/i }), - ).toBeInTheDocument(); - }); - await fireEvent.click( - screen.getByRole("button", { name: /request account deletion/i }), - ); - await waitFor(() => { - expect(screen.getByLabelText(/confirmation code/i)).toBeInTheDocument(); - }); - await fireEvent.input(screen.getByLabelText(/confirmation code/i), { - target: { value: "ABC123" }, - }); - await fireEvent.input(screen.getByLabelText(/your password/i), { - target: { value: "password" }, - }); - await fireEvent.click( - screen.getByRole("button", { name: /permanently delete account/i }), - ); - expect(confirmSpy).toHaveBeenCalledWith( - expect.stringContaining("absolutely sure"), - ); - }); - it("calls deleteAccount with correct parameters", async () => { - globalThis.confirm = vi.fn(() => true); - let capturedBody: Record | null = null; - mockEndpoint( - "com.atproto.server.requestAccountDelete", - () => jsonResponse({}), - ); - mockEndpoint("com.atproto.server.deleteAccount", (_url, options) => { - capturedBody = JSON.parse((options?.body as string) || "{}"); - return jsonResponse({}); - }); - render(Settings); - await waitFor(() => { - expect( - screen.getByRole("button", { name: /request account deletion/i }), - ).toBeInTheDocument(); - }); - await fireEvent.click( - screen.getByRole("button", { name: /request account deletion/i }), - ); - await waitFor(() => { - expect(screen.getByLabelText(/confirmation code/i)).toBeInTheDocument(); - }); - await fireEvent.input(screen.getByLabelText(/confirmation code/i), { - target: { value: "DEL123" }, - }); - await fireEvent.input(screen.getByLabelText(/your password/i), { - target: { value: "mypassword" }, - }); - await fireEvent.click( - screen.getByRole("button", { name: /permanently delete account/i }), - ); - await waitFor(() => { - expect(capturedBody?.token).toBe("DEL123"); - expect(capturedBody?.password).toBe("mypassword"); - expect(capturedBody?.did).toBe("did:web:test.tranquil.dev:u:testuser"); - }); - }); - it("navigates to login after successful deletion", async () => { - globalThis.confirm = vi.fn(() => true); - mockEndpoint( - "com.atproto.server.requestAccountDelete", - () => jsonResponse({}), - ); - mockEndpoint("com.atproto.server.deleteAccount", () => jsonResponse({})); - render(Settings); - await waitFor(() => { - expect( - screen.getByRole("button", { name: /request account deletion/i }), - ).toBeInTheDocument(); - }); - await fireEvent.click( - screen.getByRole("button", { name: /request account deletion/i }), - ); - await waitFor(() => { - expect(screen.getByLabelText(/confirmation code/i)).toBeInTheDocument(); - }); - await fireEvent.input(screen.getByLabelText(/confirmation code/i), { - target: { value: "DEL123" }, - }); - await fireEvent.input(screen.getByLabelText(/your password/i), { - target: { value: "password" }, - }); - await fireEvent.click( - screen.getByRole("button", { name: /permanently delete account/i }), - ); - await waitFor(() => { - expect(globalThis.location.pathname).toBe("/app/login"); - }); - }); - it("shows cancel button to return to request state", async () => { - mockEndpoint( - "com.atproto.server.requestAccountDelete", - () => jsonResponse({}), - ); - render(Settings); - await waitFor(() => { - expect( - screen.getByRole("button", { name: /request account deletion/i }), - ).toBeInTheDocument(); - }); - await fireEvent.click( - screen.getByRole("button", { name: /request account deletion/i }), - ); - await waitFor(() => { - const cancelButtons = screen.getAllByRole("button", { - name: /cancel/i, - }); - expect(cancelButtons.length).toBeGreaterThan(0); - }); - const deleteHeading = screen.getByRole("heading", { - name: /delete account/i, - }); - const deleteSection = deleteHeading.closest("section"); - const cancelButton = deleteSection?.querySelector("button.secondary"); - if (cancelButton) { - await fireEvent.click(cancelButton); - } - await waitFor(() => { - expect( - screen.getByRole("button", { name: /request account deletion/i }), - ).toBeInTheDocument(); - }); - }); - it("shows error toast when deletion fails", async () => { - globalThis.confirm = vi.fn(() => true); - mockEndpoint( - "com.atproto.server.requestAccountDelete", - () => jsonResponse({}), - ); - mockEndpoint( - "com.atproto.server.deleteAccount", - () => errorResponse("InvalidToken", "Invalid confirmation code", 400), - ); - render(Settings); - await waitFor(() => { - expect( - screen.getByRole("button", { name: /request account deletion/i }), - ).toBeInTheDocument(); - }); - await fireEvent.click( - screen.getByRole("button", { name: /request account deletion/i }), - ); - await waitFor(() => { - expect(screen.getByLabelText(/confirmation code/i)).toBeInTheDocument(); - }); - await fireEvent.input(screen.getByLabelText(/confirmation code/i), { - target: { value: "WRONG" }, - }); - await fireEvent.input(screen.getByLabelText(/your password/i), { - target: { value: "password" }, - }); - await fireEvent.click( - screen.getByRole("button", { name: /permanently delete account/i }), - ); - await waitFor(() => { - const errors = getErrorToasts(); - expect(errors.some((e) => /invalid confirmation code/i.test(e))).toBe( - true, - ); - }); - }); - }); -}); diff --git a/migrations/20260122_backup_storage_key_unique.sql b/migrations/20260122_backup_storage_key_unique.sql new file mode 100644 index 0000000..e2d5932 --- /dev/null +++ b/migrations/20260122_backup_storage_key_unique.sql @@ -0,0 +1,8 @@ +DELETE FROM account_backups a +WHERE EXISTS ( + SELECT 1 FROM account_backups b + WHERE a.storage_key = b.storage_key + AND a.created_at < b.created_at +); + +CREATE UNIQUE INDEX idx_account_backups_storage_key ON account_backups(storage_key);