Our Personal Data Server from scratch! tranquil.farm
oauth atproto pds rust postgresql objectstorage fun

fix: account creation bug & frontend style improvements

+1620 -1739
+16 -7
crates/tranquil-db/src/postgres/user.rs
··· 2334 2334 tranquil_db_traits::CreatePasswordAccountResult, 2335 2335 tranquil_db_traits::CreateAccountError, 2336 2336 > { 2337 + tracing::info!(did = %input.did, handle = %input.handle, "create_password_account: starting transaction"); 2337 2338 let mut tx = self.pool.begin().await.map_err(|e: sqlx::Error| { 2339 + tracing::error!( 2340 + "create_password_account: failed to begin transaction: {}", 2341 + e 2342 + ); 2338 2343 tranquil_db_traits::CreateAccountError::Database(e.to_string()) 2339 2344 })?; 2340 2345 ··· 2366 2371 .await; 2367 2372 2368 2373 let user_id = match user_insert { 2369 - Ok((id,)) => id, 2374 + Ok((id,)) => { 2375 + tracing::info!(did = %input.did, user_id = %id, "create_password_account: user row inserted"); 2376 + id 2377 + } 2370 2378 Err(e) => { 2379 + tracing::error!(did = %input.did, error = %e, "create_password_account: user insert failed"); 2371 2380 if let Some(db_err) = e.as_database_error() 2372 2381 && db_err.code().as_deref() == Some("23505") 2373 2382 { ··· 2455 2464 2456 2465 if let Some(birthdate_pref) = &input.birthdate_pref { 2457 2466 let _ = sqlx::query!( 2458 - "INSERT INTO account_preferences (user_id, name, value_json) VALUES ($1, $2, $3) 2459 - ON CONFLICT (user_id, name) DO NOTHING", 2467 + "INSERT INTO account_preferences (user_id, name, value_json) VALUES ($1, $2, $3)", 2460 2468 user_id, 2461 2469 "app.bsky.actor.defs#personalDetailsPref", 2462 2470 birthdate_pref ··· 2465 2473 .await; 2466 2474 } 2467 2475 2476 + tracing::info!(did = %input.did, user_id = %user_id, "create_password_account: committing transaction"); 2468 2477 tx.commit().await.map_err(|e: sqlx::Error| { 2478 + tracing::error!(did = %input.did, user_id = %user_id, error = %e, "create_password_account: commit failed"); 2469 2479 tranquil_db_traits::CreateAccountError::Database(e.to_string()) 2470 2480 })?; 2481 + tracing::info!(did = %input.did, user_id = %user_id, "create_password_account: transaction committed successfully"); 2471 2482 2472 2483 Ok(tranquil_db_traits::CreatePasswordAccountResult { 2473 2484 user_id, ··· 2716 2727 2717 2728 if let Some(birthdate_pref) = &input.birthdate_pref { 2718 2729 let _ = sqlx::query!( 2719 - "INSERT INTO account_preferences (user_id, name, value_json) VALUES ($1, $2, $3) 2720 - ON CONFLICT (user_id, name) DO NOTHING", 2730 + "INSERT INTO account_preferences (user_id, name, value_json) VALUES ($1, $2, $3)", 2721 2731 user_id, 2722 2732 "app.bsky.actor.defs#personalDetailsPref", 2723 2733 birthdate_pref ··· 2865 2875 2866 2876 if let Some(birthdate_pref) = &input.birthdate_pref { 2867 2877 let _ = sqlx::query!( 2868 - "INSERT INTO account_preferences (user_id, name, value_json) VALUES ($1, $2, $3) 2869 - ON CONFLICT (user_id, name) DO NOTHING", 2878 + "INSERT INTO account_preferences (user_id, name, value_json) VALUES ($1, $2, $3)", 2870 2879 user_id, 2871 2880 "app.bsky.actor.defs#personalDetailsPref", 2872 2881 birthdate_pref
+22 -24
crates/tranquil-pds/src/api/proxy.rs
··· 245 245 246 246 let key_bytes = match auth_user.key_bytes { 247 247 Some(kb) => kb, 248 - None => { 249 - match state.user_repo.get_user_info_by_did(&auth_user.did).await { 250 - Ok(Some(info)) => match info.key_bytes { 251 - Some(key_bytes_enc) => { 252 - match crate::config::decrypt_key( 253 - &key_bytes_enc, 254 - info.encryption_version, 255 - ) { 256 - Ok(key) => key, 257 - Err(e) => { 258 - error!(error = ?e, "Failed to decrypt user key for proxy"); 259 - return ApiError::UpstreamFailure.into_response(); 260 - } 248 + None => match state.user_repo.get_user_info_by_did(&auth_user.did).await { 249 + Ok(Some(info)) => match info.key_bytes { 250 + Some(key_bytes_enc) => { 251 + match crate::config::decrypt_key( 252 + &key_bytes_enc, 253 + info.encryption_version, 254 + ) { 255 + Ok(key) => key, 256 + Err(e) => { 257 + error!(error = ?e, "Failed to decrypt user key for proxy"); 258 + return ApiError::UpstreamFailure.into_response(); 261 259 } 262 260 } 263 - None => { 264 - warn!(did = %auth_user.did, "User has no signing key for proxy"); 265 - return ApiError::UpstreamFailure.into_response(); 266 - } 267 - }, 268 - Ok(None) => { 269 - warn!(did = %auth_user.did, "User not found for proxy service auth"); 270 - return ApiError::UpstreamFailure.into_response(); 271 261 } 272 - Err(e) => { 273 - error!(error = ?e, "DB error fetching user key for proxy"); 262 + None => { 263 + warn!(did = %auth_user.did, "User has no signing key for proxy"); 274 264 return ApiError::UpstreamFailure.into_response(); 275 265 } 266 + }, 267 + Ok(None) => { 268 + warn!(did = %auth_user.did, "User not found for proxy service auth"); 269 + return ApiError::UpstreamFailure.into_response(); 276 270 } 277 - } 271 + Err(e) => { 272 + error!(error = ?e, "DB error fetching user key for proxy"); 273 + return ApiError::UpstreamFailure.into_response(); 274 + } 275 + }, 278 276 }; 279 277 280 278 match crate::auth::create_service_token(
+4 -14
crates/tranquil-pds/src/auth/verification_token.rs
··· 296 296 } 297 297 298 298 pub fn format_token_for_display(token: &str) -> String { 299 - token 300 - .replace(['-', ' '], "") 301 - .chars() 302 - .collect::<Vec<_>>() 303 - .chunks(4) 304 - .map(|chunk| chunk.iter().collect::<String>()) 305 - .collect::<Vec<_>>() 306 - .join("-") 299 + token.to_string() 307 300 } 308 301 309 302 pub fn normalize_token_input(input: &str) -> String { 310 - input 311 - .chars() 312 - .filter(|c| c.is_ascii_alphanumeric() || *c == '_' || *c == '=') 313 - .collect() 303 + input.trim().to_string() 314 304 } 315 305 316 306 #[cfg(test)] ··· 410 400 fn test_format_token_for_display() { 411 401 let token = "ABCDEFGHIJKLMNOP"; 412 402 let formatted = format_token_for_display(token); 413 - assert_eq!(formatted, "ABCD-EFGH-IJKL-MNOP"); 403 + assert_eq!(formatted, "ABCDEFGHIJKLMNOP"); 414 404 } 415 405 416 406 #[test] 417 407 fn test_normalize_token_input() { 418 - let input = "ABCD-EFGH IJKL-MNOP"; 408 + let input = " ABCDEFGHIJKLMNOP "; 419 409 let normalized = normalize_token_input(input); 420 410 assert_eq!(normalized, "ABCDEFGHIJKLMNOP"); 421 411 }
+7 -1
crates/tranquil-pds/src/oauth/endpoints/authorize.rs
··· 3277 3277 } 3278 3278 }; 3279 3279 3280 - let password_valid = password_hashes.iter().fold(false, |acc, hash| { 3280 + let mut password_valid = password_hashes.iter().fold(false, |acc, hash| { 3281 3281 acc | bcrypt::verify(&form.app_password, hash).unwrap_or(false) 3282 3282 }); 3283 + 3284 + if !password_valid 3285 + && let Ok(Some(account_hash)) = state.user_repo.get_password_hash_by_did(&did).await 3286 + { 3287 + password_valid = bcrypt::verify(&form.app_password, &account_hash).unwrap_or(false); 3288 + } 3283 3289 3284 3290 if !password_valid { 3285 3291 return (
+2 -2
crates/tranquil-pds/tests/account_notifications.rs
··· 227 227 .skip_while(|line| !line.contains("verification code")) 228 228 .nth(1) 229 229 .map(|line| line.trim().to_string()) 230 - .filter(|line| !line.is_empty() && line.contains('-')) 230 + .filter(|line| !line.is_empty()) 231 231 .unwrap_or_else(|| { 232 232 body_text 233 233 .lines() 234 234 .find(|line| { 235 235 let trimmed = line.trim(); 236 - trimmed.starts_with("MX") && trimmed.contains('-') 236 + trimmed.len() == 11 && trimmed.chars().nth(5) == Some('-') 237 237 }) 238 238 .map(|s| s.trim().to_string()) 239 239 .unwrap_or_default()
+6 -8
crates/tranquil-pds/tests/common/mod.rs
··· 605 605 .and_then(|(i, _)| lines.get(i + 1).map(|s| s.trim().to_string())) 606 606 .or_else(|| { 607 607 body_text 608 - .split_whitespace() 609 - .find(|word| word.contains('-') && word.chars().filter(|c| *c == '-').count() >= 3) 610 - .map(|s| s.to_string()) 608 + .lines() 609 + .find(|line| line.trim().starts_with("MX")) 610 + .map(|s| s.trim().to_string()) 611 611 }) 612 612 .unwrap_or_else(|| body_text.clone()); 613 613 ··· 774 774 .and_then(|(i, _)| lines.get(i + 1).map(|s: &&str| s.trim().to_string())) 775 775 .or_else(|| { 776 776 body_text 777 - .split_whitespace() 778 - .find(|word: &&str| { 779 - word.contains('-') && word.chars().filter(|c| *c == '-').count() >= 3 780 - }) 781 - .map(|s: &str| s.to_string()) 777 + .lines() 778 + .find(|line| line.trim().starts_with("MX")) 779 + .map(|s| s.trim().to_string()) 782 780 }) 783 781 .unwrap_or_else(|| body_text.clone()); 784 782
+7 -4
crates/tranquil-pds/tests/email_update.rs
··· 17 17 .skip_while(|line| !line.contains("verification code")) 18 18 .nth(1) 19 19 .map(|line| line.trim().to_string()) 20 - .filter(|line| !line.is_empty() && line.contains('-')) 20 + .filter(|line| !line.is_empty()) 21 21 .unwrap_or_else(|| { 22 22 body_text 23 23 .lines() 24 - .find(|line| line.trim().starts_with("MX") && line.contains('-')) 24 + .find(|line| { 25 + let trimmed = line.trim(); 26 + trimmed.len() == 11 && trimmed.chars().nth(5) == Some('-') 27 + }) 25 28 .map(|s| s.trim().to_string()) 26 29 .unwrap_or_default() 27 30 }) ··· 271 274 272 275 let code = body_text 273 276 .lines() 274 - .find(|line| line.trim().starts_with("MX") && line.contains('-')) 277 + .find(|line| line.trim().starts_with("MX")) 275 278 .map(|s| s.trim().to_string()) 276 279 .unwrap_or_default(); 277 280 ··· 334 337 335 338 let code = body_text 336 339 .lines() 337 - .find(|line| line.trim().starts_with("MX") && line.contains('-')) 340 + .find(|line| line.trim().starts_with("MX")) 338 341 .map(|s| s.trim().to_string()) 339 342 .unwrap_or_default(); 340 343
+3 -3
crates/tranquil-pds/tests/jwt_security.rs
··· 696 696 .and_then(|(i, _)| lines.get(i + 1).map(|s| s.trim().to_string())) 697 697 .or_else(|| { 698 698 body_text 699 - .split_whitespace() 700 - .find(|word| word.contains('-') && word.chars().filter(|c| *c == '-').count() >= 3) 701 - .map(|s| s.to_string()) 699 + .lines() 700 + .find(|line| line.trim().starts_with("MX")) 701 + .map(|s| s.trim().to_string()) 702 702 }) 703 703 .unwrap_or_else(|| body_text.clone()); 704 704
+167 -218
frontend/public/homepage.html
··· 129 129 -webkit-font-smoothing: antialiased; 130 130 } 131 131 132 - .pattern-container { 132 + .pattern-canvas { 133 133 position: fixed; 134 - top: -32px; 135 - left: -32px; 136 - right: -32px; 137 - bottom: -32px; 138 - pointer-events: none; 139 - z-index: 1; 140 - overflow: hidden; 141 - } 142 - .pattern { 143 - position: absolute; 144 134 top: 0; 145 135 left: 0; 146 - width: calc(100% + 500px); 136 + width: 100%; 147 137 height: 100%; 148 - animation: drift 80s linear infinite; 149 - } 150 - .dot { 151 - position: absolute; 152 - width: 10px; 153 - height: 10px; 154 - background: rgba(0, 0, 0, 0.06); 155 - border-radius: 50%; 156 - transition: transform 0.04s linear; 157 - } 158 - @media (prefers-color-scheme: dark) { 159 - .dot { 160 - background: rgba(255, 255, 255, 0.1); 161 - } 138 + pointer-events: none; 139 + z-index: 1; 162 140 } 163 141 .pattern-fade { 164 142 position: fixed; ··· 174 152 pointer-events: none; 175 153 z-index: 2; 176 154 } 177 - @keyframes drift { 178 - 0% { 179 - transform: translateX(-500px); 180 - } 181 - 100% { 182 - transform: translateX(0); 183 - } 184 - } 185 155 186 156 nav { 187 157 position: fixed; ··· 246 216 padding: 72px 32px 32px; 247 217 } 248 218 .hero { 249 - padding: var(--space-7) 0 var(--space-8); 250 - border-bottom: 1px solid var(--border-color); 251 - margin-bottom: var(--space-8); 219 + padding: var(--space-7) 0 var(--space-6); 220 + margin-bottom: var(--space-5); 252 221 } 253 222 h1 { 254 223 font-size: var(--text-4xl); ··· 294 263 border: 1px solid transparent; 295 264 } 296 265 .btn.primary { 297 - background: var(--secondary); 266 + background: var(--accent); 298 267 color: var(--text-inverse); 299 - border-color: var(--secondary); 268 + border-color: var(--accent); 300 269 } 301 270 .btn.primary:hover { 302 - background: var(--secondary-hover); 303 - border-color: var(--secondary-hover); 271 + background: var(--accent-hover); 272 + border-color: var(--accent-hover); 304 273 } 305 274 .btn.secondary { 306 - background: transparent; 275 + background: var(--bg-primary); 307 276 color: var(--text-primary); 308 277 border-color: var(--border-color); 309 278 } ··· 349 318 margin-bottom: var(--space-5); 350 319 line-height: var(--leading-relaxed); 351 320 } 352 - .features { 353 - display: grid; 354 - grid-template-columns: repeat(2, 1fr); 355 - gap: var(--space-6); 356 - margin: var(--space-6) 0 var(--space-8); 357 - } 358 321 .feature { 359 - padding: var(--space-5); 322 + padding: var(--space-6); 360 323 background: var(--bg-secondary); 361 324 border-radius: var(--radius-xl); 362 325 border: 1px solid var(--border-color); 363 - } 364 - .feature h3 { 365 - font-size: var(--text-base); 366 - font-weight: var(--font-semibold); 367 - color: var(--text-primary); 368 - margin-bottom: var(--space-3); 326 + margin: var(--space-6) 0 var(--space-8); 369 327 } 370 328 .feature p { 371 - font-size: var(--text-sm); 329 + font-size: var(--text-base); 372 330 color: var(--text-secondary); 373 331 margin: 0; 374 332 line-height: var(--leading-relaxed); 375 333 } 376 334 @media (max-width: 700px) { 377 - .features { 378 - grid-template-columns: 1fr; 379 - } 380 335 h1 { 381 336 font-size: var(--text-3xl); 382 337 } ··· 407 362 </style> 408 363 </head> 409 364 <body> 410 - <div class="pattern-container"> 411 - <div class="pattern" id="dotPattern"></div> 412 - </div> 365 + <canvas class="pattern-canvas" id="patternCanvas"></canvas> 413 366 <div class="pattern-fade"></div> 414 367 415 368 <nav> ··· 458 411 <section class="content"> 459 412 <h2>What you get</h2> 460 413 461 - <div class="features"> 462 - <div class="feature"> 463 - <h3>Real security</h3> 464 - <p> 465 - Sign in with passkeys or SSO, add two-factor authentication, set 466 - up backup codes, and mark devices you trust. Your account stays 467 - yours. 468 - </p> 469 - </div> 470 - 471 - <div class="feature"> 472 - <h3>Your own identity</h3> 473 - <p> 474 - Use your own domain as your handle, or get a subdomain on ours. 475 - Either way, your identity moves with you if you ever leave. 476 - </p> 477 - </div> 478 - 479 - <div class="feature"> 480 - <h3>Stay in the loop</h3> 481 - <p> 482 - Get important alerts where you actually see them: email, Discord, 483 - Telegram, or Signal. 484 - </p> 485 - </div> 486 - 487 - <div class="feature"> 488 - <h3>You decide what apps can do</h3> 489 - <p> 490 - When an app asks for access, you'll see exactly what it wants in 491 - plain language. Grant what makes sense, deny what doesn't. 492 - </p> 493 - </div> 494 - 495 - <div class="feature"> 496 - <h3>App passwords with guardrails</h3> 497 - <p> 498 - Create app passwords that can only do specific things: read-only 499 - for feed readers, post-only for bots. Full control over what each 500 - password can access. 501 - </p> 502 - </div> 503 - 504 - <div class="feature"> 505 - <h3>Delegate without sharing passwords</h3> 506 - <p> 507 - Let team members or tools manage your account with specific 508 - permission levels. They authenticate with their own credentials, 509 - you see everything they do in an audit log. 510 - </p> 511 - </div> 512 - 513 - <div class="feature"> 514 - <h3>Automatic backups</h3> 515 - <p> 516 - Your repository is backed up daily to object storage. Download any 517 - backup or restore with one click. You own your data, even if the 518 - worst happens. 519 - </p> 520 - </div> 521 - </div> 522 - 523 - <h2>Everything in one place</h2> 524 - 525 - <p> 526 - Manage your profile, security settings, connected apps, and more from 527 - a clean dashboard. No command line or 3rd party apps required. 528 - </p> 529 - 530 - <h2>Works with everything</h2> 531 - 532 - <p> 533 - Use any ATProto app you already like. Tranquil PDS speaks the same 534 - language as Bluesky's servers, so all your favorite clients and tools 535 - just work. 536 - </p> 537 - 538 - <h2>Ready to try it?</h2> 539 - 540 - <p> 541 - Join this server, or grab the source and run your own. Either way, you 542 - can migrate an existing account over and your followers, posts, and 543 - identity come with you. 544 - </p> 545 - 546 - <div class="actions" id="footerActions"> 547 - <a href="/app/register" class="btn primary" id="footerPrimary" 548 - >Join This Server</a> 549 - <a href="/app/login" class="btn secondary" id="footerLogin">Login</a> 550 - <a 551 - href="https://tangled.org/tranquil.farm/tranquil-pds" 552 - class="btn secondary" 553 - target="_blank" 554 - rel="noopener" 555 - >View Source</a> 414 + <div class="feature"> 415 + <p> 416 + A superset of the reference PDS: passkeys and 2FA (TOTP, backup 417 + codes, trusted devices), SSO login and signup, did:web support 418 + (PDS-hosted subdomains or bring-your-own), multi-channel 419 + notifications (email, Discord, Telegram, Signal) for verification 420 + and alerts, granular OAuth scopes with human-readable descriptions, 421 + app passwords with configurable permissions (read-only, post-only, 422 + or custom scopes), account delegation with permission levels and 423 + audit logging, and a built-in web UI for account management, OAuth 424 + consent, repo browsing, and admin. 425 + </p> 556 426 </div> 557 427 </section> 558 428 ··· 565 435 <script> 566 436 (function checkSession() { 567 437 try { 568 - const stored = localStorage.getItem("tranquil_pds_session"); 438 + var stored = localStorage.getItem("tranquil_pds_session"); 569 439 if (stored) { 570 - const session = JSON.parse(stored); 440 + var session = JSON.parse(stored); 571 441 if (session && session.handle) { 572 - const handle = "@" + session.handle; 573 - const heroPrimary = document.getElementById( 574 - "heroPrimary", 575 - ); 576 - const footerPrimary = document.getElementById( 577 - "footerPrimary", 578 - ); 579 - const heroSecondary = document.getElementById( 442 + var heroPrimary = document.getElementById("heroPrimary"); 443 + var heroLogin = document.getElementById("heroLogin"); 444 + var heroSecondary = document.getElementById( 580 445 "heroSecondary", 581 446 ); 582 447 if (heroPrimary) { 583 448 heroPrimary.href = "/app/dashboard"; 584 - heroPrimary.textContent = handle; 449 + heroPrimary.textContent = "@" + session.handle; 585 450 } 586 - if (footerPrimary) { 587 - footerPrimary.href = "/app/dashboard"; 588 - footerPrimary.textContent = handle; 589 - } 590 - var heroLogin = document.getElementById("heroLogin"); 591 - var footerLogin = document.getElementById("footerLogin"); 592 451 if (heroLogin) heroLogin.classList.add("hidden"); 593 - if (footerLogin) footerLogin.classList.add("hidden"); 594 - if (heroSecondary) { 595 - heroSecondary.classList.add("hidden"); 596 - } 452 + if (heroSecondary) heroSecondary.classList.add("hidden"); 597 453 } 598 454 } 599 455 } catch (e) {} ··· 624 480 setTimeout(cycleWord, 2000); 625 481 626 482 fetch("/xrpc/com.atproto.server.describeServer") 627 - .then((r) => r.json()) 628 - .then((info) => { 629 - if (info.availableUserDomains?.length) { 630 - document.getElementById("hostname").textContent = 631 - info.availableUserDomains[0]; 632 - document.getElementById("hostname").classList.remove( 633 - "placeholder", 634 - ); 483 + .then(function (r) { 484 + return r.json(); 485 + }) 486 + .then(function (info) { 487 + var hostnameEl = document.getElementById("hostname"); 488 + if ( 489 + info.availableUserDomains && 490 + info.availableUserDomains.length 491 + ) { 492 + hostnameEl.textContent = info.availableUserDomains[0]; 493 + } else { 494 + hostnameEl.textContent = "Tranquil PDS"; 635 495 } 496 + hostnameEl.classList.remove("placeholder"); 636 497 if (info.version) { 637 498 document.getElementById("version").textContent = 638 499 info.version; 639 500 } 640 501 }) 641 - .catch(() => {}); 502 + .catch(function () { 503 + var hostnameEl = document.getElementById("hostname"); 504 + hostnameEl.textContent = "Tranquil PDS"; 505 + hostnameEl.classList.remove("placeholder"); 506 + }); 507 + 508 + (function loadServerColors() { 509 + var darkMode = 510 + window.matchMedia("(prefers-color-scheme: dark)").matches; 511 + var root = document.documentElement; 512 + 513 + function applyColors(config) { 514 + if (darkMode) { 515 + if (config.primaryColorDark) { 516 + root.style.setProperty( 517 + "--accent", 518 + config.primaryColorDark, 519 + ); 520 + } 521 + if (config.secondaryColorDark) { 522 + root.style.setProperty( 523 + "--secondary", 524 + config.secondaryColorDark, 525 + ); 526 + } 527 + } else { 528 + if (config.primaryColor) { 529 + root.style.setProperty("--accent", config.primaryColor); 530 + } 531 + if (config.secondaryColor) { 532 + root.style.setProperty( 533 + "--secondary", 534 + config.secondaryColor, 535 + ); 536 + } 537 + } 538 + } 539 + 540 + fetch("/xrpc/_server.getConfig") 541 + .then(function (r) { 542 + return r.json(); 543 + }) 544 + .then(function (config) { 545 + applyColors(config); 546 + window.matchMedia("(prefers-color-scheme: dark)") 547 + .addEventListener("change", function (e) { 548 + darkMode = e.matches; 549 + applyColors(config); 550 + }); 551 + }) 552 + .catch(function () {}); 553 + })(); 642 554 643 555 fetch("/xrpc/com.atproto.sync.listRepos?limit=1000") 644 556 .then((r) => r.json()) ··· 661 573 }) 662 574 .catch(() => {}); 663 575 664 - const pattern = document.getElementById("dotPattern"); 665 - const spacing = 32; 666 - const cols = Math.ceil((window.innerWidth + 600) / spacing); 667 - const rows = Math.ceil((window.innerHeight + 100) / spacing); 668 - const dots = []; 576 + (function initPattern() { 577 + var canvas = document.getElementById("patternCanvas"); 578 + var ctx = canvas.getContext("2d"); 579 + var dpr = window.devicePixelRatio || 1; 580 + var spacing = 32; 581 + var baseRadius = 5; 582 + var maxDist = 120; 583 + var driftSpeed = 500 / 80; 584 + var driftOffset = 0; 585 + var mouseX = -1000; 586 + var mouseY = -1000; 587 + var darkMode = 588 + window.matchMedia("(prefers-color-scheme: dark)").matches; 669 589 670 - for (let y = 0; y < rows; y++) { 671 - for (let x = 0; x < cols; x++) { 672 - const dot = document.createElement("div"); 673 - dot.className = "dot"; 674 - dot.style.left = (x * spacing) + "px"; 675 - dot.style.top = (y * spacing) + "px"; 676 - pattern.appendChild(dot); 677 - dots.push({ el: dot, x: x * spacing, y: y * spacing }); 590 + function resize() { 591 + canvas.width = window.innerWidth * dpr; 592 + canvas.height = window.innerHeight * dpr; 593 + canvas.style.width = window.innerWidth + "px"; 594 + canvas.style.height = window.innerHeight + "px"; 595 + ctx.scale(dpr, dpr); 678 596 } 679 - } 597 + resize(); 598 + window.addEventListener("resize", resize); 680 599 681 - let mouseX = -1000, mouseY = -1000; 682 - document.addEventListener("mousemove", (e) => { 683 - mouseX = e.clientX; 684 - mouseY = e.clientY; 685 - }); 600 + window.matchMedia("(prefers-color-scheme: dark)") 601 + .addEventListener("change", function (e) { 602 + darkMode = e.matches; 603 + }); 686 604 687 - function updateDots() { 688 - const patternRect = pattern.getBoundingClientRect(); 689 - dots.forEach((dot) => { 690 - const dotX = patternRect.left + dot.x + 5; 691 - const dotY = patternRect.top + dot.y + 5; 692 - const dist = Math.hypot(mouseX - dotX, mouseY - dotY); 693 - const maxDist = 120; 694 - const scale = Math.min(1, Math.max(0.1, dist / maxDist)); 695 - dot.el.style.transform = "scale(" + scale + ")"; 605 + document.addEventListener("mousemove", function (e) { 606 + mouseX = e.clientX; 607 + mouseY = e.clientY; 696 608 }); 697 - requestAnimationFrame(updateDots); 698 - } 699 - updateDots(); 609 + 610 + document.addEventListener("mouseleave", function () { 611 + mouseX = -1000; 612 + mouseY = -1000; 613 + }); 614 + 615 + var lastTime = 0; 616 + function draw(time) { 617 + var dt = lastTime ? (time - lastTime) / 1000 : 0; 618 + lastTime = time; 619 + driftOffset = (driftOffset + driftSpeed * dt) % spacing; 620 + 621 + ctx.clearRect(0, 0, canvas.width / dpr, canvas.height / dpr); 622 + 623 + var fillColor = darkMode 624 + ? "rgba(255, 255, 255, 0.1)" 625 + : "rgba(0, 0, 0, 0.06)"; 626 + ctx.fillStyle = fillColor; 627 + 628 + var startX = -spacing + driftOffset; 629 + var startY = -spacing; 630 + var endX = window.innerWidth + spacing; 631 + var endY = window.innerHeight + spacing; 632 + 633 + for (var y = startY; y < endY; y += spacing) { 634 + for (var x = startX; x < endX; x += spacing) { 635 + var dist = Math.hypot(mouseX - x, mouseY - y); 636 + var scale = Math.min(1, Math.max(0.1, dist / maxDist)); 637 + var radius = baseRadius * scale; 638 + 639 + ctx.beginPath(); 640 + ctx.arc(x, y, radius, 0, Math.PI * 2); 641 + ctx.fill(); 642 + } 643 + } 644 + 645 + requestAnimationFrame(draw); 646 + } 647 + requestAnimationFrame(draw); 648 + })(); 700 649 </script> 701 650 </body> 702 651 </html>
+21
frontend/src/components/RandomHandle.svelte
··· 1 + <script lang="ts" module> 2 + const EXAMPLE_HANDLES = [ 3 + "nel.pet", 4 + "lewis.moe", 5 + "llaama.bsky.social", 6 + "debugman.wizardry.systems", 7 + "nonbinary.computer", 8 + "mary.my.id", 9 + "olaren.dev", 10 + ] as const; 11 + 12 + export function getRandomHandle(): string { 13 + return EXAMPLE_HANDLES[Math.floor(Math.random() * EXAMPLE_HANDLES.length)]; 14 + } 15 + </script> 16 + 17 + <script lang="ts"> 18 + let handle = $state(getRandomHandle()); 19 + </script> 20 + 21 + @{handle}
-5
frontend/src/components/ReauthModal.svelte
··· 143 143 <button class="close-btn" onclick={handleClose} aria-label="Close">&times;</button> 144 144 </div> 145 145 146 - <p class="modal-description"> 147 - {$_('reauth.subtitle')} 148 - </p> 149 - 150 146 {#if error} 151 147 <div class="error-message">{error}</div> 152 148 {/if} ··· 221 217 </form> 222 218 {:else if activeMethod === 'passkey'} 223 219 <div class="passkey-auth"> 224 - <p>{$_('reauth.passkeyPrompt')}</p> 225 220 <button onclick={handlePasskeyAuth} disabled={loading}> 226 221 {loading ? $_('reauth.authenticating') : $_('reauth.usePasskey')} 227 222 </button>
+7 -5
frontend/src/lib/api.ts
··· 253 253 export type { DidType, VerificationChannel }; 254 254 255 255 function buildContactState(s: Record<string, unknown>): ContactState { 256 - const preferredChannel = s.preferredChannel as VerificationChannel | undefined; 256 + const preferredChannel = s.preferredChannel as 257 + | VerificationChannel 258 + | undefined; 257 259 const email = s.email ? unsafeAsEmail(s.email as string) : undefined; 258 260 259 261 if (preferredChannel) { ··· 319 321 }; 320 322 } 321 323 322 - function castDelegationController(raw: unknown): DelegationController { 324 + function _castDelegationController(raw: unknown): DelegationController { 323 325 const c = raw as Record<string, unknown>; 324 326 return { 325 327 did: unsafeAsDid(c.did as string), ··· 328 330 }; 329 331 } 330 332 331 - function castDelegationControlledAccount( 333 + function _castDelegationControlledAccount( 332 334 raw: unknown, 333 335 ): DelegationControlledAccount { 334 336 const a = raw as Record<string, unknown>; ··· 339 341 }; 340 342 } 341 343 342 - function castDelegationAuditEntry(raw: unknown): DelegationAuditEntry { 344 + function _castDelegationAuditEntry(raw: unknown): DelegationAuditEntry { 343 345 const e = raw as Record<string, unknown>; 344 346 return { 345 347 id: e.id as string, ··· 351 353 }; 352 354 } 353 355 354 - function castSsoLinkedAccount(raw: unknown): SsoLinkedAccount { 356 + function _castSsoLinkedAccount(raw: unknown): SsoLinkedAccount { 355 357 const a = raw as Record<string, unknown>; 356 358 return { 357 359 id: a.id as string,
+1 -1
frontend/src/lib/auth.svelte.ts
··· 1 - import { api, ApiError, typedApi, castSession } from "./api.ts"; 1 + import { api, ApiError, castSession, typedApi } from "./api.ts"; 2 2 import type { 3 3 CreateAccountParams, 4 4 CreateAccountResult,
+7 -1
frontend/src/lib/authenticated-client.ts
··· 1 - import type { AccessToken, Did, EmailAddress, Handle, ScopeSet } from "./types/branded.ts"; 1 + import type { 2 + AccessToken, 3 + Did, 4 + EmailAddress, 5 + Handle, 6 + ScopeSet, 7 + } from "./types/branded.ts"; 2 8 import type { Session } from "./types/api.ts"; 3 9 import type { 4 10 DelegationAuditEntry,
+3 -1
frontend/src/lib/migration/atproto-client.ts
··· 240 240 error: "Unknown", 241 241 message: res.statusText, 242 242 })); 243 - const retryError = new Error(retryErr.message || retryErr.error || res.statusText) as 243 + const retryError = new Error( 244 + retryErr.message || retryErr.error || res.statusText, 245 + ) as 244 246 & Error 245 247 & { status: number; error: string }; 246 248 retryError.status = res.status;
+30 -10
frontend/src/lib/registration/flow.svelte.ts
··· 34 34 error: string | null; 35 35 submitting: boolean; 36 36 pdsHostname: string; 37 - emailInUse: boolean; 37 + handleAvailable: boolean | null; 38 + checkingHandle: boolean; 38 39 discordInUse: boolean; 39 40 telegramInUse: boolean; 40 41 signalInUse: boolean; ··· 67 68 error: null, 68 69 submitting: false, 69 70 pdsHostname, 70 - emailInUse: false, 71 + handleAvailable: null, 72 + checkingHandle: false, 71 73 discordInUse: false, 72 74 telegramInUse: false, 73 75 signalInUse: false, ··· 105 107 106 108 function setError(err: unknown) { 107 109 if (err instanceof ApiError) { 108 - state.error = err.message || "An error occurred"; 110 + const errorMessages: Record<string, string> = { 111 + HandleNotAvailable: "This handle is already taken", 112 + InvalidHandle: "Invalid handle format", 113 + InvalidInviteCode: "Invalid invite code", 114 + InviteCodeRequired: "An invite code is required", 115 + InvalidEmail: "Invalid email address", 116 + InvalidPassword: "Password must be at least 8 characters", 117 + }; 118 + state.error = errorMessages[err.error] || err.message || 119 + "An error occurred"; 109 120 } else if (err instanceof Error) { 110 121 state.error = err.message || "An error occurred"; 111 122 } else { ··· 113 124 } 114 125 } 115 126 116 - async function checkEmailInUse(email: string): Promise<void> { 117 - if (!email.trim() || !email.includes("@")) { 118 - state.emailInUse = false; 127 + async function checkHandleAvailability(handle: string): Promise<void> { 128 + if (!handle.trim() || handle.length < 3) { 129 + state.handleAvailable = null; 130 + state.checkingHandle = false; 119 131 return; 120 132 } 133 + state.checkingHandle = true; 121 134 try { 122 - const result = await api.checkEmailInUse(email.trim()); 123 - state.emailInUse = result.inUse; 135 + const response = await fetch( 136 + `${getPdsEndpoint()}/oauth/sso/check-handle-available?handle=${ 137 + encodeURIComponent(handle) 138 + }`, 139 + ); 140 + const data = await response.json(); 141 + state.handleAvailable = data.available === true; 124 142 } catch { 125 - state.emailInUse = false; 143 + state.handleAvailable = null; 144 + } finally { 145 + state.checkingHandle = false; 126 146 } 127 147 } 128 148 ··· 522 542 activateAccount, 523 543 finalizeSession, 524 544 goBack, 525 - checkEmailInUse, 545 + checkHandleAvailability, 526 546 checkCommsChannelInUse, 527 547 528 548 setError(msg: string) {
+14 -3
frontend/src/lib/types/totp-state.ts
··· 34 34 } 35 35 36 36 export function verifyState(state: TotpQr): TotpVerify { 37 - return { step: "verify", qrBase64: state.qrBase64, totpUri: state.totpUri } as TotpVerify; 37 + return { 38 + step: "verify", 39 + qrBase64: state.qrBase64, 40 + totpUri: state.totpUri, 41 + } as TotpVerify; 38 42 } 39 43 40 - export function backupState(state: TotpVerify, backupCodes: readonly string[]): TotpBackup { 44 + export function backupState( 45 + state: TotpVerify, 46 + backupCodes: readonly string[], 47 + ): TotpBackup { 41 48 void state; 42 49 return { step: "backup", backupCodes } as TotpBackup; 43 50 } 44 51 45 52 export function goBackToQr(state: TotpVerify): TotpQr { 46 - return { step: "qr", qrBase64: state.qrBase64, totpUri: state.totpUri } as TotpQr; 53 + return { 54 + step: "qr", 55 + qrBase64: state.qrBase64, 56 + totpUri: state.totpUri, 57 + } as TotpQr; 47 58 } 48 59 49 60 export function finish(_state: TotpBackup): TotpIdle {
+185 -233
frontend/src/locales/en.json
··· 1 1 { 2 2 "common": { 3 - "loading": "Loading...", 3 + "loading": "Loading", 4 4 "error": "Error", 5 5 "save": "Save", 6 6 "cancel": "Cancel", ··· 16 16 "name": "Name", 17 17 "dashboard": "Dashboard", 18 18 "backToDashboard": "← Dashboard", 19 - "copied": "Copied!", 20 - "copyToClipboard": "Copy to Clipboard", 21 - 22 - "verifying": "Verifying...", 23 - "saving": "Saving...", 24 - "creating": "Creating...", 25 - "updating": "Updating...", 26 - "sending": "Sending...", 27 - "authenticating": "Authenticating...", 28 - "checking": "Checking...", 29 - "redirecting": "Redirecting...", 30 - 31 - "signIn": "Sign In", 19 + "copied": "Copied", 20 + "copyToClipboard": "Copy", 21 + "verifying": "Verifying", 22 + "saving": "Saving", 23 + "creating": "Creating", 24 + "updating": "Updating", 25 + "sending": "Sending", 26 + "authenticating": "Authenticating", 27 + "checking": "Checking", 28 + "redirecting": "Redirecting", 29 + "signIn": "Sign in", 32 30 "verify": "Verify", 33 31 "remove": "Remove", 34 32 "revoke": "Revoke", 35 - "resendCode": "Resend Code", 36 - "startOver": "Start Over", 37 - "tryAgain": "Try Again", 38 - 33 + "resendCode": "Resend code", 34 + "startOver": "Start over", 35 + "tryAgain": "Retry", 39 36 "password": "Password", 40 37 "email": "Email", 41 38 "emailAddress": "Email Address", ··· 45 42 "inviteCode": "Invite Code", 46 43 "newPassword": "New Password", 47 44 "confirmPassword": "Confirm Password", 48 - 49 45 "enterSixDigitCode": "Enter 6-digit code", 50 46 "passwordHint": "At least 8 characters", 51 47 "enterPassword": "Enter your password", 52 48 "emailPlaceholder": "you@example.com", 53 - 54 49 "verified": "Verified", 55 50 "disabled": "Disabled", 56 51 "available": "Available", 57 52 "deactivated": "Deactivated", 58 53 "unverified": "Unverified", 59 - 60 54 "backToLogin": "Back to Login", 61 55 "backToSettings": "Back to Settings", 62 56 "alreadyHaveAccount": "Already have an account?", 63 57 "createAccount": "Create account", 64 - 65 58 "passwordsMismatch": "Passwords do not match", 66 59 "passwordTooShort": "Password must be at least 8 characters" 67 60 }, 68 61 "login": { 69 62 "title": "Sign In", 70 - "subtitle": "Sign in to manage your PDS account", 71 - "button": "Sign In", 72 - "redirecting": "Redirecting...", 63 + "button": "Sign in", 64 + "redirecting": "Redirecting", 73 65 "chooseAccount": "Choose an account", 74 - "signInToAnother": "Sign in to another account", 75 - "backToSaved": "← Back to saved accounts", 66 + "signInToAnother": "Or sign in to another account", 76 67 "forgotPassword": "Forgot password?", 77 68 "lostPasskey": "Lost passkey?", 78 - "noAccount": "Don't have an account?", 79 - "createAccount": "Create account", 80 - "removeAccount": "Remove from saved accounts", 81 - "infoSavedAccountsTitle": "Saved accounts", 82 - "infoSavedAccountsDesc": "Click an account to sign in instantly. Your session tokens are stored securely in this browser.", 83 - "infoNewAccountTitle": "New account", 84 - "infoNewAccountDesc": "Use the sign-in button to add a different account. Click the × to remove saved accounts from this browser.", 85 - "infoSecureSignInTitle": "Secure sign-in", 86 - "infoSecureSignInDesc": "You'll be redirected to authenticate securely. If you have passkeys or two-factor authentication enabled, you'll be prompted for those too.", 87 - "infoStaySignedInTitle": "Stay signed in", 88 - "infoStaySignedInDesc": "After signing in, your account will be saved to this browser for quick access next time.", 89 - "infoRecoveryTitle": "Account recovery", 90 - "infoRecoveryDesc": "Lost your password or passkey? Use the recovery links below the sign-in button." 69 + "noAccount": "No account?", 70 + "createAccount": "Create one", 71 + "removeAccount": "Remove" 91 72 }, 92 73 "verification": { 93 - "title": "Verify Your Account", 94 - "subtitle": "Your account needs verification. Enter the code sent to your verification method.", 95 - "codeLabel": "Verification Code", 96 - "codePlaceholder": "Enter 6-digit code", 97 - "verifyButton": "Verify Account", 98 - "resent": "Verification code resent!" 74 + "title": "Verify account", 75 + "subtitle": "Enter the code sent to your contact method", 76 + "codeLabel": "Code", 77 + "codePlaceholder": "6-digit code", 78 + "verifyButton": "Verify", 79 + "resent": "Code resent" 99 80 }, 100 81 "register": { 101 82 "title": "Create Account", 102 83 "subtitle": "Create a new account on this PDS", 103 - "subtitleKeyChoice": "Choose how to set up your external did:web identity.", 104 - "subtitleInitialDidDoc": "Upload your DID document to continue.", 105 - "subtitleVerify": "Verify your {channel} to continue.", 106 - "subtitleUpdatedDidDoc": "Update your DID document with the PDS signing key.", 107 - "subtitleActivating": "Activating your account...", 108 - "subtitleComplete": "Your account has been created successfully!", 109 - "redirecting": "Redirecting to dashboard...", 110 - "infoIdentityDesc": "Your identity determines how your account is identified across the ATProto network. Most users should choose the standard option.", 111 - "infoContactDesc": "We'll use this to verify your account and send important notifications about your account security.", 112 - "infoNextTitle": "What happens next?", 113 - "infoNextDesc": "After creating your account, you'll verify your contact method and then you're ready to use any ATProto app with your new identity.", 114 - "migrateTitle": "Already have a Bluesky account?", 115 - "migrateDescription": "You can migrate your existing account to this PDS instead of creating a new one. Your followers, posts, and identity will come with you.", 84 + "subtitleKeyChoice": "Set up your did:web identity", 85 + "subtitleInitialDidDoc": "Upload your DID document", 86 + "subtitleVerify": "Verify your {channel}", 87 + "subtitleUpdatedDidDoc": "Update your DID document", 88 + "subtitleActivating": "Activating", 89 + "subtitleComplete": "Account created", 90 + "redirecting": "Redirecting", 91 + "migrateTitle": "Already have an account?", 92 + "migrateDescription": "Migrate instead of creating a new account", 116 93 "migrateLink": "Migrate your account", 117 94 "handle": "Handle", 118 95 "handlePlaceholder": "yourname", 119 96 "handleHint": "Your full handle will be: @{handle}", 120 - "handleDotWarning": "Custom domain handles can be set up after account creation in Settings.", 97 + "handleTaken": "This handle is already taken", 98 + "handleDotWarning": "Custom domains can be set up in Settings", 121 99 "password": "Password", 122 100 "passwordPlaceholder": "At least 8 characters", 123 101 "confirmPassword": "Confirm Password", 124 102 "confirmPasswordPlaceholder": "Confirm your password", 125 103 "identityType": "Identity Type", 126 - "identityHint": "Choose how your decentralized identity will be managed.", 127 104 "didPlc": "did:plc", 128 - "didPlcRecommended": "(Recommended)", 129 - "didPlcHint": "Portable identity managed by PLC Directory", 105 + "didPlcHint": "Portable identity via PLC Directory", 130 106 "didWeb": "did:web", 131 - "didWebHint": "Identity hosted on this PDS (read warning below)", 132 - "didWebDisabledHint": "Not available on this PDS - use did:plc or bring your own did:web", 107 + "didWebHint": "Identity hosted on this PDS", 108 + "didWebDisabledHint": "Not available on this PDS", 133 109 "didWebBYOD": "did:web (BYOD)", 134 110 "didWebBYODHint": "Bring your own domain", 135 - "didWebWarningTitle": "Important: Understand the trade-offs", 136 - "didWebWarning1": "Permanent tie to this PDS:", 137 - "didWebWarning1Detail": "Your identity will be {did}. Even if you migrate to another PDS later, this server must continue hosting your DID document.", 138 - "didWebWarning2": "No recovery mechanism:", 139 - "didWebWarning2Detail": "Unlike did:plc, did:web has no rotation keys. If this PDS goes offline permanently, your identity cannot be recovered.", 140 - "didWebWarning3": "We commit to you:", 141 - "didWebWarning3Detail": "If you migrate away, we will continue serving a minimal DID document pointing to your new PDS. Your identity will remain functional.", 142 - "didWebWarning4": "Recommendation:", 143 - "didWebWarning4Detail": "Choose did:plc unless you have a specific reason to prefer did:web.", 111 + "didWebWarningTitle": "did:web trade-offs", 112 + "didWebWarning1": "Permanent tie:", 113 + "didWebWarning1Detail": "Your identity will be {did}", 114 + "didWebWarning2": "No recovery:", 115 + "didWebWarning2Detail": "No rotation keys like did:plc", 116 + "didWebWarning3": "Our commitment:", 117 + "didWebWarning3Detail": "We will continue hosting your DID document if you migrate", 118 + "didWebWarning4": "", 119 + "didWebWarning4Detail": "", 144 120 "externalDid": "Your did:web", 145 121 "externalDidPlaceholder": "did:web:yourdomain.com", 146 - "externalDidHint": "Your domain must serve a valid DID document at /.well-known/did.json pointing to this PDS", 122 + "externalDidHint": "Serve DID document at", 147 123 "contactMethod": "Contact Method", 148 - "contactMethodHint": "Choose how you'd like to verify your account and receive notifications. You only need one.", 149 124 "verificationMethod": "Verification Method", 150 125 "email": "Email", 151 126 "emailAddress": "Email Address", 152 127 "emailPlaceholder": "you@example.com", 153 - "emailInUseWarning": "This email is already associated with another account. You can still use it, but for account recovery you may need to use your handle instead.", 128 + "emailInUseWarning": "Email in use by another account", 154 129 "discord": "Discord", 155 130 "discordId": "Discord User ID", 156 - "discordIdPlaceholder": "Your Discord user ID", 157 - "discordIdHint": "Your numeric Discord user ID (enable Developer Mode to find it)", 158 - "discordInUseWarning": "This Discord ID is already associated with another account.", 131 + "discordIdPlaceholder": "123456789012345678", 132 + "discordIdHint": "Enable Developer Mode to find your ID", 133 + "discordInUseWarning": "Discord ID in use by another account", 159 134 "telegram": "Telegram", 160 135 "telegramUsername": "Telegram Username", 161 136 "telegramUsernamePlaceholder": "@yourusername", 162 - "telegramInUseWarning": "This Telegram username is already associated with another account.", 137 + "telegramInUseWarning": "Telegram username in use by another account", 163 138 "signal": "Signal", 164 139 "signalNumber": "Signal Phone Number", 165 140 "signalNumberPlaceholder": "+1234567890", 166 - "signalNumberHint": "Include country code (eg., +1 for US)", 167 - "signalInUseWarning": "This Signal number is already associated with another account.", 141 + "signalNumberHint": "Include country code", 142 + "signalInUseWarning": "Signal number in use by another account", 168 143 "notConfigured": "not configured", 169 144 "inviteCode": "Invite Code", 170 145 "inviteCodePlaceholder": "Enter your invite code", ··· 175 150 "passkeyAccount": "Passkey", 176 151 "passwordAccount": "Password", 177 152 "ssoAccount": "SSO", 178 - "ssoSubtitle": "Create an account using an external provider", 179 - "noSsoProviders": "No SSO providers are configured on this server.", 180 - "ssoHint": "Choose a provider to create your account:", 153 + "ssoSubtitle": "Create account with external provider", 154 + "noSsoProviders": "No SSO providers configured", 181 155 "continueWith": "Continue with {provider}", 182 156 "validation": { 183 157 "handleRequired": "Handle is required", ··· 686 660 }, 687 661 "oauth": { 688 662 "login": { 689 - "title": "Sign In", 690 - "subtitle": "Sign in to continue to the application", 691 - "signingIn": "Signing in...", 692 - "authenticating": "Authenticating...", 693 - "checkingPasskey": "Checking passkey...", 694 - "signInWithPasskey": "Sign in with passkey", 695 - "passkeyNotSetUp": "Passkey not set up", 696 - "orUsePassword": "Or use password", 663 + "title": "Sign in", 664 + "subtitle": "Signing in to", 665 + "signingIn": "Signing in", 666 + "authenticating": "Authenticating", 667 + "checkingPasskey": "Checking", 668 + "signInWithPasskey": "Passkey", 669 + "passkeyNotSetUp": "No passkey", 670 + "orUsePassword": "or", 697 671 "password": "Password", 698 672 "rememberDevice": "Remember this device", 699 - "passkeyHintChecking": "Checking passkey status...", 700 - "passkeyHintAvailable": "Sign in with your passkey", 701 - "passkeyHintNotAvailable": "No passkeys registered for this account", 702 - "passkeyHint": "Use your device's biometrics or security key", 703 - "passwordPlaceholder": "Enter your password", 704 - "usePasskey": "Use Passkey", 705 - "orContinueWith": "Or continue with", 706 - "orUseCredentials": "Or sign in with credentials" 673 + "passkeyHintChecking": "Checking", 674 + "passkeyHintAvailable": "Use passkey", 675 + "passkeyHintNotAvailable": "No passkey registered", 676 + "passwordPlaceholder": "Password", 677 + "usePasskey": "Use passkey", 678 + "orContinueWith": "or", 679 + "orUseCredentials": "or" 707 680 }, 708 681 "sso": { 709 682 "linkedAccounts": "Linked Accounts", ··· 787 760 } 788 761 }, 789 762 "accounts": { 790 - "title": "Choose Account", 791 - "subtitle": "Select an account to continue", 792 - "useAnother": "Use a different account" 763 + "title": "Choose account", 764 + "useAnother": "Use another account" 793 765 }, 794 766 "register": { 795 - "title": "Create Account", 796 - "subtitle": "Create an account to continue to", 797 - "subtitleGeneric": "Create an account to continue", 798 - "haveAccount": "Already have an account? Sign in" 767 + "title": "Create account", 768 + "subtitle": "for", 769 + "subtitleGeneric": "Create an account", 770 + "haveAccount": "Have an account? Sign in" 799 771 }, 800 772 "twoFactor": { 801 - "title": "Two-Factor Authentication", 802 - "subtitle": "Additional verification is required", 803 - "usePasskey": "Use Passkey", 804 - "useTotp": "Use Authenticator App" 773 + "title": "Verification", 774 + "usePasskey": "Use passkey", 775 + "useTotp": "Use authenticator" 805 776 }, 806 777 "twoFactorCode": { 807 - "title": "Two-Factor Authentication", 808 - "subtitle": "A verification code has been sent to your {channel}. Enter the code below to continue.", 809 - "codeLabel": "Verification Code", 810 - "codePlaceholder": "Enter 6-digit code", 778 + "title": "Verification", 779 + "subtitle": "Code sent to {channel}", 780 + "codeLabel": "Code", 781 + "codePlaceholder": "6-digit code", 811 782 "errors": { 812 - "missingRequestUri": "Missing request_uri parameter", 783 + "missingRequestUri": "Missing request URI", 813 784 "verificationFailed": "Verification failed", 814 - "connectionFailed": "Failed to connect to server", 815 - "unexpectedResponse": "Unexpected response from server" 785 + "connectionFailed": "Connection failed", 786 + "unexpectedResponse": "Unexpected response" 816 787 } 817 788 }, 818 789 "totp": { 819 - "title": "Enter Authenticator Code", 820 - "subtitle": "Enter the 6-digit code from your authenticator app", 821 - "codePlaceholder": "Enter 6-digit code", 822 - "useBackupCode": "Use backup code instead", 823 - "backupCodePlaceholder": "Enter backup code", 790 + "title": "Authenticator code", 791 + "codePlaceholder": "6-digit code", 792 + "useBackupCode": "Use backup code", 793 + "backupCodePlaceholder": "Backup code", 824 794 "trustDevice": "Trust this device for 30 days", 825 - "hintBackupCode": "Using backup code", 826 - "hintTotpCode": "Using authenticator code", 827 - "hintDefault": "6 digits for authenticator, 8 characters for backup code" 795 + "hintBackupCode": "Backup code", 796 + "hintTotpCode": "Authenticator code" 828 797 }, 829 798 "passkey": { 830 - "title": "Passkey Verification", 831 - "subtitle": "Use your passkey to verify your identity", 832 - "waiting": "Waiting for passkey...", 833 - "useTotp": "Use authenticator app instead" 799 + "title": "Passkey", 800 + "waiting": "Waiting", 801 + "useTotp": "Use authenticator" 834 802 }, 835 803 "error": { 836 - "title": "Authorization Error", 837 - "genericError": "An error occurred during authorization.", 838 - "tryAgain": "Try Again", 839 - "backToApp": "Back to Application" 804 + "title": "Authorization failed", 805 + "tryAgain": "Retry", 806 + "backToApp": "Back" 840 807 } 841 808 }, 842 809 "sso_register": { ··· 862 829 "subtitle": "We've sent a verification code to your {channel}. Enter it below to complete registration.", 863 830 "tokenSubtitle": "Enter the verification code and the identifier it was sent to.", 864 831 "tokenTitle": "Verify", 865 - "codePlaceholder": "XXXX-XXXX-XXXX-XXXX...", 832 + "codePlaceholder": "Paste verification code", 866 833 "codeLabel": "Verification Code", 867 - "codeHelp": "Copy the entire code from your message, including dashes", 834 + "codeHelp": "Copy the entire code from your message", 868 835 "verifyButton": "Verify Account", 869 836 "pleaseWait": "Please wait...", 870 837 "codeResent": "Verification code resent!", ··· 965 932 }, 966 933 "registerPasskey": { 967 934 "title": "Create Passkey Account", 968 - "subtitle": "Create an ultra-secure account using a passkey instead of a password.", 969 - "subtitleKeyChoice": "Choose how to set up your external did:web identity.", 970 - "subtitleInitialDidDoc": "Upload your DID document to continue.", 971 - "subtitleCreating": "Creating your account...", 972 - "subtitlePasskey": "Register your passkey to secure your account.", 973 - "subtitleAppPassword": "Save your app password for third-party apps.", 974 - "subtitleVerify": "Verify your {channel} to continue.", 975 - "subtitleUpdatedDidDoc": "Update your DID document with the PDS signing key.", 976 - "subtitleActivating": "Activating your account...", 977 - "subtitleComplete": "Your account has been created successfully!", 935 + "subtitleKeyChoice": "Set up your did:web identity", 936 + "subtitleInitialDidDoc": "Upload your DID document", 937 + "subtitleCreating": "Creating account", 938 + "subtitlePasskey": "Register your passkey", 939 + "subtitleAppPassword": "Save your app password", 940 + "subtitleVerify": "Verify your {channel}", 941 + "subtitleUpdatedDidDoc": "Update your DID document", 942 + "subtitleActivating": "Activating", 943 + "subtitleComplete": "Account created", 978 944 "handle": "Handle", 979 945 "handlePlaceholder": "yourname", 980 - "handleHint": "Your full handle will be: @{handle}", 981 - "handleDotWarning": "Custom domain handles can be set up after account creation.", 946 + "handleHint": "Your full handle: @{handle}", 947 + "handleDotWarning": "Custom domains can be set up in Settings", 982 948 "email": "Email Address", 983 949 "emailPlaceholder": "you@example.com", 984 950 "inviteCode": "Invite Code", ··· 988 954 "back": "Back", 989 955 "alreadyHaveAccount": "Already have an account?", 990 956 "signIn": "Sign in", 991 - "wantPassword": "Want to use a password?", 992 - "createPasswordAccount": "Create a password account", 993 - "wantTraditional": "Want a traditional password?", 994 - "registerWithPassword": "Register with password", 957 + "wantPassword": "Use a password instead?", 958 + "createPasswordAccount": "Password account", 959 + "wantTraditional": "Use a password instead?", 960 + "registerWithPassword": "Password account", 995 961 "contactMethod": "Contact Method", 996 - "contactMethodHint": "Choose how you'd like to verify your account and receive notifications.", 997 962 "verificationMethod": "Verification Method", 998 963 "identityType": "Identity Type", 999 - "identityTypeHint": "Choose how your decentralized identity will be managed.", 964 + "identityTypeHint": "How your decentralized identity is managed", 1000 965 "didPlcRecommended": "did:plc (Recommended)", 1001 - "didPlcHint": "Portable identity managed by PLC Directory", 966 + "didPlcHint": "Portable identity via PLC Directory", 1002 967 "didWeb": "did:web", 1003 - "didWebHint": "Identity hosted on this PDS (read warning below)", 1004 - "didWebDisabledHint": "Not available on this PDS - use did:plc or bring your own did:web", 968 + "didWebHint": "Identity hosted on this PDS", 969 + "didWebDisabledHint": "Not available on this PDS", 1005 970 "didWebBYOD": "did:web (BYOD)", 1006 971 "didWebBYODHint": "Bring your own domain", 1007 - "didWebWarningTitle": "Important: Understand the trade-offs", 1008 - "didWebWarning1": "Permanent tie to this PDS:", 1009 - "didWebWarning1Detail": "Your identity will be {did}.", 1010 - "didWebWarning2": "No recovery mechanism:", 1011 - "didWebWarning2Detail": "Unlike did:plc, did:web has no rotation keys.", 1012 - "didWebWarning3": "We commit to you:", 1013 - "didWebWarning3Detail": "If you migrate away, we will continue serving a minimal DID document.", 1014 - "didWebWarning4": "Recommendation:", 1015 - "didWebWarning4Detail": "Choose did:plc unless you have a specific reason to prefer did:web.", 972 + "didWebWarningTitle": "did:web trade-offs", 973 + "didWebWarning1": "Permanent tie:", 974 + "didWebWarning1Detail": "Your identity will be {did}", 975 + "didWebWarning2": "No recovery:", 976 + "didWebWarning2Detail": "No rotation keys like did:plc", 977 + "didWebWarning3": "Our commitment:", 978 + "didWebWarning3Detail": "We will continue hosting your DID document if you migrate", 979 + "didWebWarning4": "", 980 + "didWebWarning4Detail": "", 1016 981 "externalDid": "Your did:web", 1017 982 "externalDidPlaceholder": "did:web:yourdomain.com", 1018 - "externalDidHint": "You'll need to serve a DID document at", 1019 - "whyPasskeyOnly": "Why passkey-only?", 1020 - "whyPasskeyOnlyDesc": "Passkey accounts are more secure than password-based accounts because they:", 1021 - "whyPasskeyBullet1": "Cannot be phished or stolen in data breaches", 1022 - "whyPasskeyBullet2": "Use hardware-backed cryptographic keys", 1023 - "whyPasskeyBullet3": "Require your biometric or device PIN to use", 1024 - "infoWhyPasskey": "Why use a passkey?", 1025 - "infoWhyPasskeyDesc": "Passkeys are cryptographic credentials stored on your device. They cannot be phished, guessed, or stolen in data breaches like passwords can.", 1026 - "infoHowItWorks": "How it works", 1027 - "infoHowItWorksDesc": "When you sign in, your device will prompt you to verify with Face ID, Touch ID, or your device PIN. No password to remember or type.", 1028 - "infoAppAccess": "Using third-party apps", 1029 - "infoAppAccessDesc": "After creating your account, you will receive an app password. Use this to sign in to Bluesky apps and other AT Protocol clients.", 1030 - "passkeyNameLabel": "Passkey Name (optional)", 1031 - "passkeyNamePlaceholder": "eg., MacBook Touch ID", 1032 - "passkeyNameHint": "A friendly name to identify this passkey", 1033 - "passkeyPrompt": "Click the button below to create your passkey. You'll be prompted to use:", 1034 - "passkeyPromptBullet1": "Touch ID or Face ID", 1035 - "passkeyPromptBullet2": "Your device PIN or password", 1036 - "passkeyPromptBullet3": "A security key (if you have one)", 983 + "externalDidHint": "Serve DID document at", 984 + "passkeyName": "Passkey Name", 985 + "passkeyNamePlaceholder": "MacBook Touch ID", 986 + "passkeyNameHint": "Optional identifier", 987 + "setupPasskey": "Create Passkey", 988 + "passkeyDescription": "Register a passkey for this account", 1037 989 "createPasskey": "Create Passkey", 1038 - "creatingPasskey": "Creating Passkey...", 1039 - "redirecting": "Redirecting to dashboard...", 1040 - "loading": "Loading...", 990 + "creatingPasskey": "Creating", 991 + "creatingAccount": "Creating account", 992 + "activatingAccount": "Activating", 993 + "redirecting": "Redirecting", 994 + "loading": "Loading", 1041 995 "errors": { 1042 996 "handleRequired": "Handle is required", 1043 997 "handleNoDots": "Handle cannot contain dots. You can set up a custom domain handle after creating your account.", ··· 1047 1001 "emailRequired": "Email is required for email verification", 1048 1002 "discordRequired": "Discord ID is required for Discord verification", 1049 1003 "telegramRequired": "Telegram username is required for Telegram verification", 1050 - "signalRequired": "Phone number is required for Signal verification", 1051 - "passkeysNotSupported": "Passkeys are not supported in this browser. Please use a different browser or register with a password instead.", 1052 - "passkeyCancelled": "Passkey creation was cancelled", 1004 + "signalRequired": "Phone number required", 1005 + "passkeysNotSupported": "Passkeys not supported in this browser", 1006 + "passkeyCancelled": "Cancelled", 1053 1007 "passkeyFailed": "Passkey registration failed" 1054 1008 } 1055 1009 }, 1056 1010 "trustedDevices": { 1057 1011 "title": "Trusted Devices", 1058 - "backToSecurity": "← Security Settings", 1059 - "description": "Trusted devices can skip two-factor authentication when logging in. Trust is granted for 30 days and automatically extends when you use the device.", 1060 - "failedToLoad": "Failed to load trusted devices", 1061 - "noDevices": "No trusted devices yet.", 1062 - "noDevicesHint": "When you log in with two-factor authentication enabled, you can choose to trust the device for 30 days.", 1012 + "backToSecurity": "← Security", 1013 + "failedToLoad": "Could not load trusted devices", 1014 + "noDevices": "No trusted devices", 1063 1015 "lastSeen": "Last seen:", 1064 1016 "trustedSince": "Trusted since:", 1065 - "trustExpires": "Trust expires:", 1017 + "trustExpires": "Expires:", 1066 1018 "expired": "Expired", 1067 1019 "tomorrow": "Tomorrow", 1068 1020 "inDays": "In {days} days", 1069 - "revoke": "Revoke Trust", 1070 - "revokeConfirm": "Are you sure you want to revoke trust for this device? You will need to enter your 2FA code next time you log in from this device.", 1071 - "deviceRevoked": "Device trust revoked", 1021 + "revoke": "Revoke", 1022 + "revokeConfirm": "Revoke trust for this device?", 1023 + "deviceRevoked": "Trust revoked", 1072 1024 "deviceRenamed": "Device renamed", 1073 1025 "deviceNamePlaceholder": "Device name", 1074 1026 "browser": "Browser:", 1075 - "unknownDevice": "Unknown device" 1027 + "unknownDevice": "Unknown device", 1028 + "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.", 1029 + "noDevicesHint": "When you sign in with two-factor authentication enabled, you can choose to trust the device for 30 days." 1076 1030 }, 1077 1031 "reauth": { 1078 - "title": "Re-authentication Required", 1079 - "subtitle": "Please verify your identity to continue.", 1032 + "title": "Re-authenticate", 1080 1033 "password": "Password", 1081 1034 "totp": "TOTP", 1082 1035 "passkey": "Passkey", 1083 1036 "authenticatorCode": "Authenticator Code", 1084 - "usePassword": "Use Password", 1085 - "usePasskey": "Use Passkey", 1086 - "useTotp": "Use Authenticator", 1037 + "usePassword": "Password", 1038 + "usePasskey": "Passkey", 1039 + "useTotp": "Authenticator", 1087 1040 "passwordPlaceholder": "Enter your password", 1088 - "totpPlaceholder": "Enter 6-digit code", 1089 - "authenticating": "Authenticating...", 1090 - "passkeyPrompt": "Click the button below to authenticate with your passkey.", 1041 + "totpPlaceholder": "6-digit code", 1042 + "authenticating": "Authenticating", 1091 1043 "cancel": "Cancel" 1092 1044 }, 1093 1045 "delegation": { 1094 1046 "title": "Account Delegation", 1095 - "loading": "Loading...", 1047 + "loading": "Loading", 1096 1048 "controllers": "Controllers", 1097 1049 "controllersDesc": "Accounts that can act on your behalf", 1098 - "noControllers": "No controllers have been granted access to your account.", 1050 + "noControllers": "No controllers", 1099 1051 "inactive": "Inactive", 1100 1052 "did": "DID", 1101 1053 "granted": "Granted", ··· 1168 1120 "backToControllers": "Back to Controllers" 1169 1121 }, 1170 1122 "oauthDelegation": { 1171 - "loading": "Loading...", 1123 + "loading": "Loading", 1172 1124 "title": "Delegated Account", 1173 - "isDelegated": "{handle} is a delegated account.", 1174 - "enterControllerHandle": "Sign in with your controller account to access this account.", 1125 + "isDelegated": "{handle} is delegated", 1126 + "enterControllerHandle": "Sign in as controller", 1175 1127 "controllerHandle": "Controller handle", 1176 1128 "handlePlaceholder": "handle.example.com", 1177 - "checking": "Checking...", 1178 - "controllerNotFound": "Account not found or you don't have access to this delegated account", 1129 + "checking": "Checking", 1130 + "controllerNotFound": "Controller not found", 1179 1131 "missingParams": "Missing delegation parameters", 1180 - "missingInfo": "Missing required information", 1181 - "passkeyCancelled": "Passkey authentication cancelled", 1182 - "passkeyFailed": "Passkey authentication failed", 1183 - "failedPasskeyStart": "Failed to start passkey login", 1132 + "missingInfo": "Missing information", 1133 + "passkeyCancelled": "Cancelled", 1134 + "passkeyFailed": "Passkey failed", 1135 + "failedPasskeyStart": "Passkey start failed", 1184 1136 "authFailed": "Authentication failed", 1185 - "unexpectedResponse": "Unexpected response from server", 1186 - "signInAsController": "Sign In as Controller", 1187 - "authenticateAs": "Authenticate as {controller} to act on behalf of {delegated}", 1188 - "useDifferentController": "Use a different controller", 1189 - "signInWithPasskey": "Sign in with Passkey", 1190 - "authenticating": "Authenticating...", 1191 - "usePasskey": "Use Passkey", 1137 + "unexpectedResponse": "Unexpected response", 1138 + "signInAsController": "Controller Sign In", 1139 + "authenticateAs": "Sign in as {controller} for {delegated}", 1140 + "useDifferentController": "Different controller", 1141 + "signInWithPasskey": "Passkey", 1142 + "authenticating": "Authenticating", 1143 + "usePasskey": "Passkey", 1192 1144 "or": "or", 1193 1145 "password": "Password", 1194 1146 "enterPassword": "Enter password", 1195 1147 "rememberDevice": "Remember this device", 1196 - "signingIn": "Signing in...", 1197 - "signIn": "Sign In", 1198 - "goBack": "Go Back", 1199 - "unableToLoad": "Unable to load delegation info" 1148 + "signingIn": "Signing in", 1149 + "signIn": "Sign in", 1150 + "goBack": "Back", 1151 + "unableToLoad": "Could not load delegation info" 1200 1152 }, 1201 1153 "oauthConsent": { 1202 1154 "delegatedAccess": "Delegated Access",
+93 -141
frontend/src/locales/fi.json
··· 1 1 { 2 2 "common": { 3 - "loading": "Ladataan...", 3 + "loading": "Ladataan", 4 4 "error": "Virhe", 5 5 "save": "Tallenna", 6 6 "cancel": "Peruuta", ··· 16 16 "name": "Nimi", 17 17 "dashboard": "Hallintapaneeli", 18 18 "backToDashboard": "← Hallintapaneeli", 19 - "copied": "Kopioitu!", 19 + "copied": "Kopioitu", 20 20 "copyToClipboard": "Kopioi", 21 - 22 - "verifying": "Vahvistetaan...", 23 - "saving": "Tallennetaan...", 24 - "creating": "Luodaan...", 25 - "updating": "Päivitetään...", 26 - "sending": "Lähetetään...", 27 - "authenticating": "Todennetaan...", 28 - "checking": "Tarkistetaan...", 29 - "redirecting": "Ohjataan...", 30 - 21 + "verifying": "Vahvistetaan", 22 + "saving": "Tallennetaan", 23 + "creating": "Luodaan", 24 + "updating": "Päivitetään", 25 + "sending": "Lähetetään", 26 + "authenticating": "Todennetaan", 27 + "checking": "Tarkistetaan", 28 + "redirecting": "Ohjataan", 31 29 "signIn": "Kirjaudu sisään", 32 30 "verify": "Vahvista", 33 31 "remove": "Poista", 34 32 "revoke": "Peruuta", 35 - "resendCode": "Lähetä koodi uudelleen", 33 + "resendCode": "Lähetä koodi", 36 34 "startOver": "Aloita alusta", 37 35 "tryAgain": "Yritä uudelleen", 38 - 39 36 "password": "Salasana", 40 37 "email": "Sähköposti", 41 38 "emailAddress": "Sähköpostiosoite", ··· 45 42 "inviteCode": "Kutsukoodi", 46 43 "newPassword": "Uusi salasana", 47 44 "confirmPassword": "Vahvista salasana", 48 - 49 45 "enterSixDigitCode": "Syötä 6-numeroinen koodi", 50 46 "passwordHint": "Vähintään 8 merkkiä", 51 47 "enterPassword": "Syötä salasanasi", 52 48 "emailPlaceholder": "sinä@esimerkki.com", 53 - 54 49 "verified": "Vahvistettu", 55 50 "disabled": "Poistettu käytöstä", 56 51 "available": "Saatavilla", 57 52 "deactivated": "Deaktivoitu", 58 53 "unverified": "Vahvistamaton", 59 - 60 54 "backToLogin": "Takaisin kirjautumiseen", 61 55 "backToSettings": "Takaisin asetuksiin", 62 56 "alreadyHaveAccount": "Onko sinulla jo tili?", 63 57 "createAccount": "Luo tili", 64 - 65 58 "passwordsMismatch": "Salasanat eivät täsmää", 66 59 "passwordTooShort": "Salasanan on oltava vähintään 8 merkkiä" 67 60 }, 68 61 "login": { 69 62 "title": "Kirjaudu sisään", 70 - "subtitle": "Kirjaudu sisään hallitaksesi PDS-tiliäsi", 71 63 "button": "Kirjaudu sisään", 72 - "redirecting": "Ohjataan...", 64 + "redirecting": "Ohjataan", 73 65 "chooseAccount": "Valitse tili", 74 - "signInToAnother": "Kirjaudu toiselle tilille", 75 - "backToSaved": "← Takaisin tallennettuihin tileihin", 66 + "signInToAnother": "Tai kirjaudu toiselle tilille", 76 67 "forgotPassword": "Unohditko salasanan?", 77 68 "lostPasskey": "Kadotitko pääsyavaimen?", 78 - "noAccount": "Eikö sinulla ole tiliä?", 69 + "noAccount": "Ei tiliä?", 79 70 "createAccount": "Luo tili", 80 - "removeAccount": "Poista tallennetuista tileistä", 81 - "infoSavedAccountsTitle": "Tallennetut tilit", 82 - "infoSavedAccountsDesc": "Napsauta tiliä kirjautuaksesi heti. Istuntotunnuksesi on tallennettu turvallisesti tähän selaimeen.", 83 - "infoNewAccountTitle": "Uusi tili", 84 - "infoNewAccountDesc": "Käytä kirjautumispainiketta lisätäksesi toisen tilin. Napsauta × poistaaksesi tallennettuja tilejä.", 85 - "infoSecureSignInTitle": "Turvallinen kirjautuminen", 86 - "infoSecureSignInDesc": "Sinut ohjataan turvalliseen todennukseen. Jos sinulla on pääsyavaimia tai kaksivaiheinen tunnistautuminen käytössä, sinulta pyydetään myös ne.", 87 - "infoStaySignedInTitle": "Pysy kirjautuneena", 88 - "infoStaySignedInDesc": "Kirjautumisen jälkeen tilisi tallennetaan tähän selaimeen nopeaa pääsyä varten.", 89 - "infoRecoveryTitle": "Tilin palautus", 90 - "infoRecoveryDesc": "Kadotitko salasanasi tai pääsyavaimesi? Käytä palautuslinkkejä kirjautumispainikkeen alla." 71 + "removeAccount": "Poista" 91 72 }, 92 73 "verification": { 93 - "title": "Vahvista tilisi", 94 - "subtitle": "Tilisi vaatii vahvistuksen. Syötä vahvistusmenetelmääsi lähetetty koodi.", 95 - "codeLabel": "Vahvistuskoodi", 96 - "codePlaceholder": "Syötä 6-numeroinen koodi", 97 - "verifyButton": "Vahvista tili", 98 - "resent": "Vahvistuskoodi lähetetty uudelleen!" 74 + "title": "Vahvista tili", 75 + "subtitle": "Syötä yhteystietoosi lähetetty koodi", 76 + "codeLabel": "Koodi", 77 + "codePlaceholder": "6-numeroinen koodi", 78 + "verifyButton": "Vahvista", 79 + "resent": "Koodi lähetetty" 99 80 }, 100 81 "register": { 101 82 "title": "Luo tili", 102 83 "subtitle": "Luo uusi tili tälle PDS:lle", 103 - "subtitleKeyChoice": "Valitse, miten haluat määrittää ulkoisen did:web-identiteettisi.", 104 - "subtitleInitialDidDoc": "Lataa DID-dokumenttisi jatkaaksesi.", 105 - "subtitleVerify": "Vahvista {channel} jatkaaksesi.", 106 - "subtitleUpdatedDidDoc": "Päivitä DID-dokumenttisi PDS-allekirjoitusavaimella.", 107 - "subtitleActivating": "Aktivoidaan tiliäsi...", 108 - "subtitleComplete": "Tilisi on luotu onnistuneesti!", 109 - "redirecting": "Siirrytään kojelaudalle...", 110 - "infoIdentityDesc": "Identiteettisi määrittää, miten tilisi tunnistetaan ATProto-verkossa. Useimpien käyttäjien tulisi valita vakiovaihtoehto.", 111 - "infoContactDesc": "Käytämme tätä tilisi vahvistamiseen ja tärkeiden turvallisuusilmoitusten lähettämiseen.", 112 - "infoNextTitle": "Mitä tapahtuu seuraavaksi?", 113 - "infoNextDesc": "Tilin luomisen jälkeen vahvistat yhteysmenetelmäsi ja olet valmis käyttämään mitä tahansa ATProto-sovellusta uudella identiteetilläsi.", 114 - "migrateTitle": "Onko sinulla jo Bluesky-tili?", 115 - "migrateDescription": "Voit siirtää olemassa olevan tilisi tälle PDS:lle uuden luomisen sijaan. Seuraajasi, julkaisusi ja identiteettisi siirtyvät mukana.", 116 - "migrateLink": "Siirrä PDS Mooverilla", 84 + "subtitleKeyChoice": "Määritä did:web-identiteettisi", 85 + "subtitleInitialDidDoc": "Lataa DID-dokumenttisi", 86 + "subtitleVerify": "Vahvista {channel}", 87 + "subtitleUpdatedDidDoc": "Päivitä DID-dokumenttisi", 88 + "subtitleActivating": "Aktivoidaan", 89 + "subtitleComplete": "Tili luotu", 90 + "redirecting": "Ohjataan", 91 + "migrateTitle": "Onko sinulla jo tili?", 92 + "migrateDescription": "Siirrä olemassa oleva tilisi", 93 + "migrateLink": "Siirrä tilisi", 117 94 "handle": "Käyttäjänimi", 118 95 "handlePlaceholder": "nimesi", 119 96 "handleHint": "Täydellinen käyttäjänimesi on: @{handle}", 97 + "handleTaken": "Tämä käyttäjänimi on jo varattu", 120 98 "handleDotWarning": "Omat verkkotunnukset voidaan määrittää tilin luomisen jälkeen Asetuksissa.", 121 99 "password": "Salasana", 122 100 "passwordPlaceholder": "Vähintään 8 merkkiä", 123 101 "confirmPassword": "Vahvista salasana", 124 102 "confirmPasswordPlaceholder": "Vahvista salasanasi", 125 103 "identityType": "Identiteettityyppi", 126 - "identityHint": "Valitse, miten hajautettu identiteettisi hallinnoidaan.", 127 104 "didPlc": "did:plc", 128 - "didPlcRecommended": "(Suositellaan)", 129 105 "didPlcHint": "Siirrettävä identiteetti, jota hallinnoi PLC Directory", 130 106 "didWeb": "did:web", 131 107 "didWebHint": "Identiteetti isännöidään tällä PDS:llä (lue alla oleva varoitus)", ··· 145 121 "externalDidPlaceholder": "did:web:verkkotunnuksesi.fi", 146 122 "externalDidHint": "Verkkotunnuksesi on tarjottava kelvollinen DID-dokumentti osoitteessa /.well-known/did.json, joka osoittaa tähän PDS:ään", 147 123 "contactMethod": "Yhteysmenetelmä", 148 - "contactMethodHint": "Valitse, miten haluat vahvistaa tilisi ja vastaanottaa ilmoituksia. Tarvitset vain yhden.", 149 124 "verificationMethod": "Vahvistusmenetelmä", 150 125 "email": "Sähköposti", 151 126 "emailAddress": "Sähköpostiosoite", ··· 177 152 "ssoAccount": "SSO", 178 153 "ssoSubtitle": "Luo tili ulkoisen palveluntarjoajan kautta", 179 154 "noSsoProviders": "Tälle palvelimelle ei ole määritetty SSO-palveluntarjoajia.", 180 - "ssoHint": "Valitse palveluntarjoaja tilin luomiseksi:", 181 155 "continueWith": "Jatka palvelulla {provider}", 182 156 "validation": { 183 157 "handleRequired": "Käyttäjänimi vaaditaan", ··· 687 661 "oauth": { 688 662 "login": { 689 663 "title": "Kirjaudu sisään", 690 - "subtitle": "Kirjaudu sisään jatkaaksesi sovellukseen", 691 - "signingIn": "Kirjaudutaan...", 692 - "authenticating": "Todennetaan...", 693 - "checkingPasskey": "Tarkistetaan pääsyavainta...", 694 - "signInWithPasskey": "Kirjaudu pääsyavaimella", 695 - "passkeyNotSetUp": "Pääsyavainta ei ole määritetty", 696 - "orUsePassword": "tai käytä salasanaa", 664 + "subtitle": "Kirjaudutaan", 665 + "signingIn": "Kirjaudutaan", 666 + "authenticating": "Todennetaan", 667 + "checkingPasskey": "Tarkistetaan", 668 + "signInWithPasskey": "Pääsyavain", 669 + "passkeyNotSetUp": "Ei pääsyavainta", 670 + "orUsePassword": "tai", 697 671 "password": "Salasana", 698 672 "rememberDevice": "Muista tämä laite", 699 - "passkeyHintChecking": "Tarkistetaan pääsyavaimen tilaa...", 700 - "passkeyHintAvailable": "Kirjaudu pääsyavaimellasi", 701 - "passkeyHintNotAvailable": "Ei rekisteröityjä pääsyavaimia tälle tilille", 702 - "passkeyHint": "Käytä laitteesi biometriikkaa tai suojausavainta", 703 - "passwordPlaceholder": "Syötä salasanasi", 673 + "passkeyHintChecking": "Tarkistetaan", 674 + "passkeyHintAvailable": "Käytä pääsyavainta", 675 + "passkeyHintNotAvailable": "Ei pääsyavainta", 676 + "passwordPlaceholder": "Salasana", 704 677 "usePasskey": "Käytä pääsyavainta", 705 - "orContinueWith": "Tai jatka käyttäen", 706 - "orUseCredentials": "Tai kirjaudu tunnuksilla" 678 + "orContinueWith": "tai", 679 + "orUseCredentials": "tai" 707 680 }, 708 681 "register": { 709 682 "title": "Luo tili", 710 - "subtitle": "Luo tili jatkaaksesi sovellukseen", 711 - "subtitleGeneric": "Luo tili jatkaaksesi", 712 - "haveAccount": "Onko sinulla jo tili? Kirjaudu sisään" 683 + "subtitle": "sovellukseen", 684 + "subtitleGeneric": "Luo tili", 685 + "haveAccount": "Onko sinulla tili? Kirjaudu" 713 686 }, 714 687 "sso": { 715 688 "linkedAccounts": "Linkitetyt tilit", ··· 794 767 }, 795 768 "accounts": { 796 769 "title": "Valitse tili", 797 - "subtitle": "Valitse tili jatkaaksesi", 798 770 "useAnother": "Käytä toista tiliä" 799 771 }, 800 772 "twoFactor": { 801 - "title": "Kaksivaiheinen tunnistautuminen", 802 - "subtitle": "Lisävahvistus vaaditaan", 773 + "title": "Vahvistus", 803 774 "usePasskey": "Käytä pääsyavainta", 804 - "useTotp": "Käytä todentajasovellusta" 775 + "useTotp": "Käytä todentajaa" 805 776 }, 806 777 "twoFactorCode": { 807 - "title": "Kaksivaiheinen tunnistautuminen", 808 - "subtitle": "Vahvistuskoodi on lähetetty {channel}. Syötä koodi alla jatkaaksesi.", 809 - "codeLabel": "Vahvistuskoodi", 810 - "codePlaceholder": "Syötä 6-numeroinen koodi", 778 + "title": "Vahvistus", 779 + "subtitle": "Koodi lähetetty: {channel}", 780 + "codeLabel": "Koodi", 781 + "codePlaceholder": "6-numeroinen koodi", 811 782 "errors": { 812 - "missingRequestUri": "Puuttuva request_uri-parametri", 783 + "missingRequestUri": "Puuttuva request URI", 813 784 "verificationFailed": "Vahvistus epäonnistui", 814 - "connectionFailed": "Palvelimeen yhdistäminen epäonnistui", 815 - "unexpectedResponse": "Odottamaton vastaus palvelimelta" 785 + "connectionFailed": "Yhteys epäonnistui", 786 + "unexpectedResponse": "Odottamaton vastaus" 816 787 } 817 788 }, 818 789 "totp": { 819 - "title": "Syötä todentajakoodi", 820 - "subtitle": "Syötä 6-numeroinen koodi todentajasovelluksestasi", 821 - "codePlaceholder": "Syötä 6-numeroinen koodi", 822 - "useBackupCode": "Käytä varakoodia sen sijaan", 823 - "backupCodePlaceholder": "Syötä varakoodi", 790 + "title": "Todentajakoodi", 791 + "codePlaceholder": "6-numeroinen koodi", 792 + "useBackupCode": "Käytä varakoodia", 793 + "backupCodePlaceholder": "Varakoodi", 824 794 "trustDevice": "Luota tähän laitteeseen 30 päivää", 825 - "hintBackupCode": "Käytetään varakoodia", 826 - "hintTotpCode": "Käytetään todentajakoodia", 827 - "hintDefault": "6 numeroa todentajalle, 8 merkkiä varakoodille" 795 + "hintBackupCode": "Varakoodi", 796 + "hintTotpCode": "Todentajakoodi" 828 797 }, 829 798 "passkey": { 830 - "title": "Pääsyavaimen vahvistus", 831 - "subtitle": "Käytä pääsyavaintasi vahvistaaksesi henkilöllisyytesi", 832 - "waiting": "Odotetaan pääsyavainta...", 833 - "useTotp": "Käytä todentajasovellusta sen sijaan" 799 + "title": "Pääsyavain", 800 + "waiting": "Odotetaan", 801 + "useTotp": "Käytä todentajaa" 834 802 }, 835 803 "error": { 836 - "title": "Valtuutusvirhe", 837 - "genericError": "Valtuutuksen aikana tapahtui virhe.", 804 + "title": "Valtuutus epäonnistui", 838 805 "tryAgain": "Yritä uudelleen", 839 - "backToApp": "Takaisin sovellukseen" 806 + "backToApp": "Takaisin" 840 807 } 841 808 }, 842 809 "sso_register": { ··· 862 829 "subtitle": "Olemme lähettäneet vahvistuskoodin {channel}. Syötä se alla viimeistelläksesi rekisteröinnin.", 863 830 "tokenTitle": "Vahvista", 864 831 "tokenSubtitle": "Syötä vahvistuskoodi ja tunniste, johon se lähetettiin.", 865 - "codePlaceholder": "XXXX-XXXX-XXXX-XXXX...", 832 + "codePlaceholder": "Paste verification code", 866 833 "codeLabel": "Vahvistuskoodi", 867 - "codeHelp": "Kopioi koko koodi viestistäsi, mukaan lukien väliviivat", 834 + "codeHelp": "Kopioi koko koodi viestistäsi, ", 868 835 "verifyButton": "Vahvista tili", 869 836 "pleaseWait": "Odota...", 870 837 "codeResent": "Vahvistuskoodi lähetetty uudelleen!", ··· 965 932 }, 966 933 "registerPasskey": { 967 934 "title": "Luo pääsyavaintili", 968 - "subtitle": "Luo erittäin turvallinen tili käyttämällä pääsyavainta salasanan sijaan.", 969 - "subtitleKeyChoice": "Valitse, miten haluat määrittää ulkoisen did:web-identiteettisi.", 970 - "subtitleVerify": "Olemme lähettäneet vahvistuskoodin {channel}. Syötä koodi jatkaaksesi.", 971 - "subtitlePasskey": "Luo pääsyavain viimeistelläksesi tilin määrityksen.", 935 + "subtitleKeyChoice": "Määritä did:web-identiteettisi", 936 + "subtitleInitialDidDoc": "Lataa DID-dokumenttisi", 937 + "subtitleCreating": "Luodaan tiliä", 938 + "subtitlePasskey": "Rekisteröi pääsyavaimesi", 939 + "subtitleAppPassword": "Tallenna sovellussalasanasi", 940 + "subtitleVerify": "Vahvista {channel}", 941 + "subtitleUpdatedDidDoc": "Päivitä DID-dokumenttisi", 942 + "subtitleActivating": "Aktivoidaan", 943 + "subtitleComplete": "Tili luotu", 972 944 "handle": "Käyttäjänimi", 973 945 "handlePlaceholder": "nimesi", 974 946 "handleHint": "Täydellinen käyttäjänimesi on: @{handle}", 975 947 "contactMethod": "Yhteysmenetelmä", 976 - "contactMethodHint": "Valitse, miten haluat vahvistaa tilisi ja vastaanottaa ilmoituksia.", 977 948 "verificationMethod": "Vahvistusmenetelmä", 978 949 "email": "Sähköpostiosoite", 979 950 "emailPlaceholder": "sinä@esimerkki.fi", ··· 1000 971 "externalDidFormat": "Ulkoisen DID:n on alettava did:web:", 1001 972 "discordRequired": "Discord-tunnus vaaditaan Discord-vahvistukseen" 1002 973 }, 1003 - "whyPasskeyBullet1": "Ei voi kalastella tai varastaa tietomurroissa", 1004 - "whyPasskeyBullet2": "Käyttää laitteistopohjaisia salausavaimia", 1005 - "whyPasskeyBullet3": "Vaatii biometrisen tunnistuksen tai laitteen PIN-koodin", 1006 - "infoWhyPasskey": "Miksi käyttää pääsyavainta?", 1007 - "infoWhyPasskeyDesc": "Pääsyavaimet ovat laitteellesi tallennettuja salattuja tunnistetietoja. Niitä ei voi kalastella, arvata tai varastaa tietomurroissa kuten salasanoja.", 1008 - "infoHowItWorks": "Miten se toimii", 1009 - "infoHowItWorksDesc": "Kirjautuessasi laitteesi pyytää sinua vahvistamaan Face ID:llä, Touch ID:llä tai laitteen PIN-koodilla. Ei salasanaa muistettavaksi tai kirjoitettavaksi.", 1010 - "infoAppAccess": "Kolmannen osapuolen sovellusten käyttö", 1011 - "infoAppAccessDesc": "Tilin luomisen jälkeen saat sovellussalasanan. Käytä sitä kirjautuaksesi Bluesky-sovelluksiin ja muihin AT Protocol -asiakkaisiin.", 1012 - "whyPasskeyOnly": "Miksi vain pääsyavain?", 1013 - "whyPasskeyOnlyDesc": "Pääsyavaintilit ovat turvallisempia kuin salasanapohjaiset tilit, koska ne:", 1014 - "subtitleInitialDidDoc": "Lataa DID-dokumenttisi jatkaaksesi.", 1015 - "subtitleUpdatedDidDoc": "Päivitä DID-dokumenttisi PDS-allekirjoitusavaimella.", 1016 - "subtitleActivating": "Aktivoidaan tiliäsi...", 1017 - "subtitleComplete": "Tilisi on luotu onnistuneesti!", 1018 - "subtitleCreating": "Luodaan tiliäsi...", 1019 - "subtitleAppPassword": "Tallenna sovellussalasanasi kolmannen osapuolen sovelluksia varten.", 1020 - "creatingPasskey": "Luodaan pääsyavainta...", 1021 - "passkeyPrompt": "Napsauta alla olevaa painiketta luodaksesi pääsyavaimesi. Sinua pyydetään käyttämään:", 1022 - "passkeyPromptBullet1": "Touch ID tai Face ID", 1023 - "passkeyPromptBullet2": "Laitteesi PIN-koodi tai salasana", 1024 - "passkeyPromptBullet3": "Turva-avain (jos sinulla on sellainen)", 974 + "creatingPasskey": "Luodaan", 1025 975 "identityType": "Identiteettityyppi", 1026 976 "identityTypeHint": "Valitse, miten hajautettua identiteettiäsi hallitaan.", 1027 - "passkeyNameLabel": "Pääsyavaimen nimi (valinnainen)", 1028 977 "passkeyNamePlaceholder": "esim. MacBook Touch ID", 1029 978 "passkeyNameHint": "Ystävällinen nimi tämän pääsyavaimen tunnistamiseksi", 1030 979 "createPasskey": "Luo pääsyavain", ··· 1051 1000 "redirecting": "Ohjataan hallintapaneeliin...", 1052 1001 "handleDotWarning": "Mukautetut verkkotunnuskahvat voidaan määrittää tilin luomisen jälkeen.", 1053 1002 "wantTraditional": "Haluatko perinteisen salasanan?", 1054 - "registerWithPassword": "Rekisteröidy salasanalla" 1003 + "registerWithPassword": "Rekisteröidy salasanalla", 1004 + "activatingAccount": "Activating", 1005 + "creatingAccount": "Creating account", 1006 + "passkeyDescription": "Register a passkey for this account", 1007 + "passkeyName": "Passkey Name", 1008 + "setupPasskey": "Create Passkey" 1055 1009 }, 1056 1010 "trustedDevices": { 1057 1011 "title": "Luotetut laitteet", ··· 1075 1029 "unknownDevice": "Tuntematon laite" 1076 1030 }, 1077 1031 "reauth": { 1078 - "title": "Uudelleentodennus vaaditaan", 1079 - "subtitle": "Vahvista henkilöllisyytesi jatkaaksesi.", 1032 + "title": "Todenna uudelleen", 1080 1033 "password": "Salasana", 1081 1034 "totp": "TOTP", 1082 1035 "passkey": "Pääsyavain", 1083 1036 "authenticatorCode": "Todentajan koodi", 1084 - "usePassword": "Käytä salasanaa", 1085 - "usePasskey": "Käytä pääsyavainta", 1086 - "useTotp": "Käytä todentajaa", 1037 + "usePassword": "Salasana", 1038 + "usePasskey": "Pääsyavain", 1039 + "useTotp": "Todentaja", 1087 1040 "passwordPlaceholder": "Syötä salasanasi", 1088 - "totpPlaceholder": "Syötä 6-numeroinen koodi", 1089 - "authenticating": "Todennetaan...", 1090 - "passkeyPrompt": "Klikkaa alla olevaa painiketta todentaaksesi pääsyavaimellasi.", 1041 + "totpPlaceholder": "6-numeroinen koodi", 1042 + "authenticating": "Todennetaan", 1091 1043 "cancel": "Peruuta" 1092 1044 }, 1093 1045 "verifyChannel": { ··· 1107 1059 "identifierPlaceholder": "Sähköposti, Discord ID jne.", 1108 1060 "identifierHelp": "Vahvistettava sähköpostiosoite, Discord ID, Telegram-käyttäjänimi tai Signal-numero.", 1109 1061 "codeLabel": "Vahvistuskoodi", 1110 - "codeHelp": "Kopioi koko koodi viestistäsi, mukaan lukien väliviivat.", 1062 + "codeHelp": "Kopioi koko koodi viestistäsi, .", 1111 1063 "verifyButton": "Vahvista" 1112 1064 }, 1113 1065 "delegation": {
+95 -136
frontend/src/locales/ja.json
··· 1 1 { 2 2 "common": { 3 - "loading": "読み込み中...", 3 + "loading": "読み込み中", 4 4 "error": "エラー", 5 5 "save": "保存", 6 6 "cancel": "キャンセル", ··· 16 16 "name": "名前", 17 17 "dashboard": "ダッシュボード", 18 18 "backToDashboard": "← ダッシュボード", 19 - "copied": "コピーしました!", 20 - "copyToClipboard": "クリップボードにコピー", 21 - "verifying": "確認中...", 22 - "saving": "保存中...", 23 - "creating": "作成中...", 24 - "updating": "更新中...", 25 - "sending": "送信中...", 26 - "authenticating": "認証中...", 27 - "checking": "確認中...", 28 - "redirecting": "リダイレクト中...", 19 + "copied": "コピー完了", 20 + "copyToClipboard": "コピー", 21 + "verifying": "確認中", 22 + "saving": "保存中", 23 + "creating": "作成中", 24 + "updating": "更新中", 25 + "sending": "送信中", 26 + "authenticating": "認証中", 27 + "checking": "確認中", 28 + "redirecting": "リダイレクト中", 29 29 "signIn": "サインイン", 30 30 "verify": "確認", 31 31 "remove": "削除", 32 32 "revoke": "取り消し", 33 - "resendCode": "コードを再送信", 34 - "startOver": "最初からやり直す", 33 + "resendCode": "再送信", 34 + "startOver": "やり直す", 35 35 "tryAgain": "再試行", 36 36 "password": "パスワード", 37 37 "email": "メール", ··· 60 60 }, 61 61 "login": { 62 62 "title": "サインイン", 63 - "subtitle": "PDS アカウントを管理するにはサインインしてください", 64 63 "button": "サインイン", 65 - "redirecting": "リダイレクト中...", 64 + "redirecting": "リダイレクト中", 66 65 "chooseAccount": "アカウントを選択", 67 - "signInToAnother": "別のアカウントでサインイン", 68 - "backToSaved": "← 保存済みアカウントに戻る", 66 + "signInToAnother": "または別のアカウントでサインイン", 69 67 "forgotPassword": "パスワードをお忘れですか?", 70 - "lostPasskey": "パスキーを紛失しましたか?", 71 - "noAccount": "アカウントをお持ちでないですか?", 72 - "createAccount": "アカウントを作成", 73 - "removeAccount": "保存済みアカウントから削除", 74 - "infoSavedAccountsTitle": "保存済みアカウント", 75 - "infoSavedAccountsDesc": "アカウントをクリックすると即座にサインインできます。セッショントークンはこのブラウザに安全に保存されています。", 76 - "infoNewAccountTitle": "新規アカウント", 77 - "infoNewAccountDesc": "サインインボタンで別のアカウントを追加できます。×をクリックすると保存済みアカウントを削除できます。", 78 - "infoSecureSignInTitle": "安全なサインイン", 79 - "infoSecureSignInDesc": "安全な認証のためにリダイレクトされます。パスキーや二要素認証が有効な場合は、それらも求められます。", 80 - "infoStaySignedInTitle": "サインイン状態を維持", 81 - "infoStaySignedInDesc": "サインイン後、アカウントはこのブラウザに保存され、次回から素早くアクセスできます。", 82 - "infoRecoveryTitle": "アカウント復旧", 83 - "infoRecoveryDesc": "パスワードやパスキーを紛失しましたか?サインインボタンの下の復旧リンクをご利用ください。" 68 + "lostPasskey": "パスキーを紛失?", 69 + "noAccount": "アカウントなし?", 70 + "createAccount": "作成", 71 + "removeAccount": "削除" 84 72 }, 85 73 "verification": { 86 74 "title": "アカウント確認", 87 - "subtitle": "アカウントの確認が必要です。確認方法に送信されたコードを入力してください。", 88 - "codeLabel": "確認コード", 89 - "codePlaceholder": "6桁のコードを入力", 90 - "verifyButton": "確認する", 91 - "resent": "確認コードを再送信しました!" 75 + "subtitle": "連絡先に送信されたコードを入力", 76 + "codeLabel": "コード", 77 + "codePlaceholder": "6桁のコード", 78 + "verifyButton": "確認", 79 + "resent": "コード送信済み" 92 80 }, 93 81 "register": { 94 82 "title": "アカウント作成", 95 83 "subtitle": "この PDS で新規アカウントを作成", 96 - "subtitleKeyChoice": "外部 did:web アイデンティティの設定方法を選択してください。", 97 - "subtitleInitialDidDoc": "続行するには DID ドキュメントをアップロードしてください。", 98 - "subtitleVerify": "続行するには{channel}を確認してください。", 99 - "subtitleUpdatedDidDoc": "PDS 署名キーで DID ドキュメントを更新してください。", 100 - "subtitleActivating": "アカウントを有効化しています...", 101 - "subtitleComplete": "アカウントが正常に作成されました!", 102 - "redirecting": "ダッシュボードへ移動中...", 103 - "infoIdentityDesc": "アイデンティティは、ATProto ネットワーク上でアカウントがどのように識別されるかを決定します。ほとんどのユーザーは標準オプションを選択してください。", 104 - "infoContactDesc": "この情報はアカウントの確認と、アカウントセキュリティに関する重要な通知の送信に使用されます。", 105 - "infoNextTitle": "次のステップは?", 106 - "infoNextDesc": "アカウント作成後、連絡方法を確認すると、新しいアイデンティティで任意の ATProto アプリを使用できます。", 107 - "migrateTitle": "すでにBlueskyアカウントをお持ちですか?", 108 - "migrateDescription": "新しいアカウントを作成する代わりに、既存のアカウントをこのPDSに移行できます。フォロワー、投稿、IDも一緒に移行されます。", 109 - "migrateLink": "PDS Mooverで移行する", 84 + "subtitleKeyChoice": "did:web アイデンティティを設定", 85 + "subtitleInitialDidDoc": "DID ドキュメントをアップロード", 86 + "subtitleVerify": "{channel}を確認", 87 + "subtitleUpdatedDidDoc": "DID ドキュメントを更新", 88 + "subtitleActivating": "有効化中", 89 + "subtitleComplete": "アカウント作成完了", 90 + "redirecting": "リダイレクト中", 91 + "migrateTitle": "すでにアカウントをお持ちですか?", 92 + "migrateDescription": "既存のアカウントを移行", 93 + "migrateLink": "アカウントを移行", 110 94 "handle": "ハンドル", 111 95 "handlePlaceholder": "あなたの名前", 112 96 "handleHint": "完全なハンドル: @{handle}", 97 + "handleTaken": "このハンドルは既に使用されています", 113 98 "handleDotWarning": "カスタムドメインハンドルはアカウント作成後に設定で構成できます。", 114 99 "password": "パスワード", 115 100 "passwordPlaceholder": "8文字以上", 116 101 "confirmPassword": "パスワード確認", 117 102 "confirmPasswordPlaceholder": "パスワードを再入力", 118 103 "identityType": "アイデンティティタイプ", 119 - "identityHint": "分散型アイデンティティの管理方法を選択してください。", 120 104 "didPlc": "did:plc", 121 - "didPlcRecommended": "(推奨)", 122 105 "didPlcHint": "PLC ディレクトリで管理されるポータブルアイデンティティ", 123 106 "didWeb": "did:web", 124 107 "didWebHint": "この PDS でホストされるアイデンティティ(下記の警告をお読みください)", ··· 138 121 "externalDidPlaceholder": "did:web:yourdomain.com", 139 122 "externalDidHint": "ドメインは /.well-known/did.json でこの PDS を指す有効な DID ドキュメントを提供する必要があります", 140 123 "contactMethod": "連絡方法", 141 - "contactMethodHint": "アカウントの確認と通知の受信方法を選択してください。1つだけ必要です。", 142 124 "verificationMethod": "確認方法", 143 125 "email": "メール", 144 126 "emailAddress": "メールアドレス", ··· 170 152 "ssoAccount": "SSO", 171 153 "ssoSubtitle": "外部プロバイダーを使用してアカウントを作成", 172 154 "noSsoProviders": "このサーバーにはSSOプロバイダーが設定されていません。", 173 - "ssoHint": "プロバイダーを選択してアカウントを作成:", 174 155 "continueWith": "{provider}で続行", 175 156 "validation": { 176 157 "handleRequired": "ハンドルは必須です", ··· 680 661 "oauth": { 681 662 "login": { 682 663 "title": "サインイン", 683 - "subtitle": "アプリを続行するにはサインインしてください", 684 - "signingIn": "サインイン中...", 685 - "authenticating": "認証中...", 686 - "checkingPasskey": "パスキーを確認中...", 687 - "signInWithPasskey": "パスキーでサインイン", 688 - "passkeyNotSetUp": "パスキーは設定されていません", 689 - "orUsePassword": "またはパスワードを使用", 664 + "subtitle": "サインイン中", 665 + "signingIn": "サインイン中", 666 + "authenticating": "認証中", 667 + "checkingPasskey": "確認中", 668 + "signInWithPasskey": "パスキー", 669 + "passkeyNotSetUp": "パスキーなし", 670 + "orUsePassword": "または", 690 671 "password": "パスワード", 691 - "rememberDevice": "このデバイスを記憶する", 692 - "passkeyHintChecking": "パスキーの状態を確認中...", 693 - "passkeyHintAvailable": "パスキーでサインイン", 694 - "passkeyHintNotAvailable": "このアカウントにはパスキーが登録されていません", 695 - "passkeyHint": "デバイスの生体認証またはセキュリティキーを使用", 696 - "passwordPlaceholder": "パスワードを入力", 672 + "rememberDevice": "このデバイスを記憶", 673 + "passkeyHintChecking": "確認中", 674 + "passkeyHintAvailable": "パスキーを使用", 675 + "passkeyHintNotAvailable": "パスキーなし", 676 + "passwordPlaceholder": "パスワード", 697 677 "usePasskey": "パスキーを使用", 698 - "orContinueWith": "または次の方法で続行", 699 - "orUseCredentials": "または認証情報でサインイン" 678 + "orContinueWith": "または", 679 + "orUseCredentials": "または" 700 680 }, 701 681 "register": { 702 682 "title": "アカウント作成", 703 - "subtitle": "続行するにはアカウントを作成してください", 704 - "subtitleGeneric": "続行するにはアカウントを作成してください", 705 - "haveAccount": "すでにアカウントをお持ちですか?サインイン" 683 + "subtitle": "アプリ", 684 + "subtitleGeneric": "アカウントを作成", 685 + "haveAccount": "アカウントをお持ちですか?サインイン" 706 686 }, 707 687 "sso": { 708 688 "linkedAccounts": "連携アカウント", ··· 787 767 }, 788 768 "accounts": { 789 769 "title": "アカウントを選択", 790 - "subtitle": "続行するアカウントを選択", 791 770 "useAnother": "別のアカウントを使用" 792 771 }, 793 772 "twoFactor": { 794 - "title": "二要素認証", 795 - "subtitle": "追加の確認が必要です", 773 + "title": "確認", 796 774 "usePasskey": "パスキーを使用", 797 775 "useTotp": "認証アプリを使用" 798 776 }, 799 777 "twoFactorCode": { 800 - "title": "二要素認証", 801 - "subtitle": "{channel} に確認コードを送信しました。以下にコードを入力して続行してください。", 802 - "codeLabel": "確認コード", 803 - "codePlaceholder": "6桁のコードを入力", 778 + "title": "確認", 779 + "subtitle": "{channel} にコード送信済み", 780 + "codeLabel": "コード", 781 + "codePlaceholder": "6桁のコード", 804 782 "errors": { 805 - "missingRequestUri": "request_uri パラメータがありません", 806 - "verificationFailed": "確認に失敗しました", 807 - "connectionFailed": "サーバーへの接続に失敗しました", 808 - "unexpectedResponse": "サーバーからの予期しない応答" 783 + "missingRequestUri": "リクエストURIがありません", 784 + "verificationFailed": "確認失敗", 785 + "connectionFailed": "接続失敗", 786 + "unexpectedResponse": "予期しない応答" 809 787 } 810 788 }, 811 789 "totp": { 812 - "title": "認証コードを入力", 813 - "subtitle": "認証アプリの6桁のコードを入力", 814 - "codePlaceholder": "6桁のコードを入力", 790 + "title": "認証コード", 791 + "codePlaceholder": "6桁のコード", 815 792 "useBackupCode": "バックアップコードを使用", 816 - "backupCodePlaceholder": "バックアップコードを入力", 817 - "trustDevice": "このデバイスを30日間信頼する", 818 - "hintBackupCode": "バックアップコードを使用中", 819 - "hintTotpCode": "認証コードを使用中", 820 - "hintDefault": "認証アプリは6桁、バックアップコードは8文字" 793 + "backupCodePlaceholder": "バックアップコード", 794 + "trustDevice": "このデバイスを30日間信頼", 795 + "hintBackupCode": "バックアップコード", 796 + "hintTotpCode": "認証コード" 821 797 }, 822 798 "passkey": { 823 - "title": "パスキー確認", 824 - "subtitle": "パスキーで本人確認を行います", 825 - "waiting": "パスキーを待機中...", 799 + "title": "パスキー", 800 + "waiting": "待機中", 826 801 "useTotp": "認証アプリを使用" 827 802 }, 828 803 "error": { 829 - "title": "承認エラー", 830 - "genericError": "承認中にエラーが発生しました。", 804 + "title": "承認失敗", 831 805 "tryAgain": "再試行", 832 - "backToApp": "アプリに戻る" 806 + "backToApp": "戻る" 833 807 } 834 808 }, 835 809 "sso_register": { ··· 855 829 "subtitle": "{channel} に確認コードを送信しました。以下に入力して登録を完了してください。", 856 830 "tokenTitle": "確認", 857 831 "tokenSubtitle": "確認コードと送信先の識別子を入力してください。", 858 - "codePlaceholder": "XXXX-XXXX-XXXX-XXXX...", 832 + "codePlaceholder": "Paste verification code", 859 833 "codeLabel": "確認コード", 860 - "codeHelp": "ダッシュを含む完全なコードをメッセージからコピーしてください", 834 + "codeHelp": "完全なコードをメッセージからコピーしてください", 861 835 "verifyButton": "アカウントを確認", 862 836 "pleaseWait": "お待ちください...", 863 837 "codeResent": "確認コードを再送信しました!", ··· 958 932 }, 959 933 "registerPasskey": { 960 934 "title": "パスキーアカウントを作成", 961 - "subtitle": "パスワードの代わりにパスキーを使用して超安全なアカウントを作成します。", 962 - "subtitleKeyChoice": "外部 did:web アイデンティティの設定方法を選択してください。", 963 - "subtitleVerify": "{channel} に確認コードを送信しました。コードを入力して続行してください。", 964 - "subtitlePasskey": "パスキーを作成してアカウント設定を完了します。", 935 + "subtitleKeyChoice": "did:web アイデンティティを設定", 936 + "subtitleInitialDidDoc": "DID ドキュメントをアップロード", 937 + "subtitleCreating": "アカウント作成中", 938 + "subtitlePasskey": "パスキーを登録", 939 + "subtitleAppPassword": "アプリパスワードを保存", 940 + "subtitleVerify": "{channel}を確認", 941 + "subtitleUpdatedDidDoc": "DID ドキュメントを更新", 942 + "subtitleActivating": "有効化中", 943 + "subtitleComplete": "アカウント作成完了", 965 944 "handle": "ハンドル", 966 945 "handlePlaceholder": "あなたの名前", 967 946 "handleHint": "完全なハンドル: @{handle}", 968 947 "contactMethod": "連絡方法", 969 - "contactMethodHint": "アカウントの確認と通知の受信方法を選択してください。", 970 948 "verificationMethod": "確認方法", 971 949 "email": "メールアドレス", 972 950 "emailPlaceholder": "you@example.com", ··· 993 971 "externalDidFormat": "外部DIDはdid:web:で始まる必要があります", 994 972 "discordRequired": "Discord認証にはDiscord IDが必要です" 995 973 }, 996 - "whyPasskeyBullet1": "フィッシングやデータ侵害で盗まれない", 997 - "whyPasskeyBullet2": "ハードウェア支援の暗号鍵を使用", 998 - "whyPasskeyBullet3": "生体認証またはデバイスPINが必要", 999 - "infoWhyPasskey": "なぜパスキーを使うのですか?", 1000 - "infoWhyPasskeyDesc": "パスキーはデバイスに保存される暗号化資格情報です。パスワードのようにフィッシング、推測、データ侵害による盗難の被害を受けません。", 1001 - "infoHowItWorks": "仕組み", 1002 - "infoHowItWorksDesc": "サインイン時、デバイスがFace ID、Touch ID、またはデバイスPINでの確認を求めます。覚えたり入力したりするパスワードはありません。", 1003 - "infoAppAccess": "サードパーティアプリの使用", 1004 - "infoAppAccessDesc": "アカウント作成後、アプリパスワードが発行されます。Blueskyアプリやその他のAT Protocolクライアントへのサインインに使用してください。", 1005 - "whyPasskeyOnly": "なぜパスキーのみ?", 1006 - "whyPasskeyOnlyDesc": "パスキーアカウントはパスワードベースのアカウントより安全です:", 1007 - "subtitleInitialDidDoc": "続行するにはDIDドキュメントをアップロードしてください。", 1008 - "subtitleUpdatedDidDoc": "PDS署名鍵でDIDドキュメントを更新してください。", 1009 - "subtitleActivating": "アカウントを有効化しています...", 1010 - "subtitleComplete": "アカウントが正常に作成されました!", 1011 - "subtitleCreating": "アカウントを作成しています...", 1012 - "subtitleAppPassword": "サードパーティアプリ用のアプリパスワードを保存してください。", 1013 - "creatingPasskey": "パスキーを作成中...", 1014 - "passkeyPrompt": "下のボタンをクリックしてパスキーを作成してください。以下の使用を求められます:", 1015 - "passkeyPromptBullet1": "Touch IDまたはFace ID", 1016 - "passkeyPromptBullet2": "デバイスのPINまたはパスワード", 1017 - "passkeyPromptBullet3": "セキュリティキー(お持ちの場合)", 974 + "creatingPasskey": "作成中", 1018 975 "identityType": "アイデンティティタイプ", 1019 976 "identityTypeHint": "分散型アイデンティティの管理方法を選択してください。", 1020 - "passkeyNameLabel": "パスキー名(任意)", 1021 977 "passkeyNamePlaceholder": "例:MacBook Touch ID", 1022 978 "passkeyNameHint": "このパスキーを識別するための名前", 1023 979 "createPasskey": "パスキーを作成", ··· 1044 1000 "redirecting": "ダッシュボードに移動中...", 1045 1001 "handleDotWarning": "カスタムドメインハンドルはアカウント作成後に設定できます。", 1046 1002 "wantTraditional": "従来のパスワードを使用しますか?", 1047 - "registerWithPassword": "パスワードで登録" 1003 + "registerWithPassword": "パスワードで登録", 1004 + "activatingAccount": "Activating", 1005 + "creatingAccount": "Creating account", 1006 + "passkeyDescription": "Register a passkey for this account", 1007 + "passkeyName": "Passkey Name", 1008 + "setupPasskey": "Create Passkey" 1048 1009 }, 1049 1010 "trustedDevices": { 1050 1011 "title": "信頼済みデバイス", ··· 1068 1029 "unknownDevice": "不明なデバイス" 1069 1030 }, 1070 1031 "reauth": { 1071 - "title": "再認証が必要です", 1072 - "subtitle": "続行するには本人確認を行ってください。", 1032 + "title": "再認証", 1073 1033 "password": "パスワード", 1074 1034 "totp": "TOTP", 1075 1035 "passkey": "パスキー", 1076 1036 "authenticatorCode": "認証コード", 1077 - "usePassword": "パスワードを使用", 1078 - "usePasskey": "パスキーを使用", 1079 - "useTotp": "認証アプリを使用", 1037 + "usePassword": "パスワード", 1038 + "usePasskey": "パスキー", 1039 + "useTotp": "認証アプリ", 1080 1040 "passwordPlaceholder": "パスワードを入力", 1081 - "totpPlaceholder": "6桁のコードを入力", 1082 - "authenticating": "認証中...", 1083 - "passkeyPrompt": "下のボタンをクリックしてパスキーで認証してください。", 1041 + "totpPlaceholder": "6桁のコード", 1042 + "authenticating": "認証中", 1084 1043 "cancel": "キャンセル" 1085 1044 }, 1086 1045 "verifyChannel": {
+93 -134
frontend/src/locales/ko.json
··· 1 1 { 2 2 "common": { 3 - "loading": "로딩 중...", 3 + "loading": "로딩 중", 4 4 "error": "오류", 5 5 "save": "저장", 6 6 "cancel": "취소", ··· 16 16 "name": "이름", 17 17 "dashboard": "대시보드", 18 18 "backToDashboard": "← 대시보드", 19 - "copied": "복사됨!", 20 - "copyToClipboard": "클립보드에 복사", 21 - "verifying": "확인 중...", 22 - "saving": "저장 중...", 23 - "creating": "생성 중...", 24 - "updating": "업데이트 중...", 25 - "sending": "전송 중...", 26 - "authenticating": "인증 중...", 27 - "checking": "확인 중...", 28 - "redirecting": "리디렉션 중...", 19 + "copied": "복사됨", 20 + "copyToClipboard": "복사", 21 + "verifying": "확인 중", 22 + "saving": "저장 중", 23 + "creating": "생성 중", 24 + "updating": "업데이트 중", 25 + "sending": "전송 중", 26 + "authenticating": "인증 중", 27 + "checking": "확인 중", 28 + "redirecting": "리디렉션 중", 29 29 "signIn": "로그인", 30 30 "verify": "확인", 31 31 "remove": "삭제", 32 32 "revoke": "취소", 33 - "resendCode": "코드 재전송", 33 + "resendCode": "재전송", 34 34 "startOver": "처음부터 다시", 35 - "tryAgain": "다시 시도", 35 + "tryAgain": "재시도", 36 36 "password": "비밀번호", 37 37 "email": "이메일", 38 38 "emailAddress": "이메일 주소", ··· 60 60 }, 61 61 "login": { 62 62 "title": "로그인", 63 - "subtitle": "PDS 계정을 관리하려면 로그인하세요", 64 63 "button": "로그인", 65 - "redirecting": "리디렉션 중...", 64 + "redirecting": "리디렉션 중", 66 65 "chooseAccount": "계정 선택", 67 - "signInToAnother": "다른 계정으로 로그인", 68 - "backToSaved": "← 저장된 계정으로 돌아가기", 66 + "signInToAnother": "또는 다른 계정으로 로그인", 69 67 "forgotPassword": "비밀번호를 잊으셨나요?", 70 68 "lostPasskey": "패스키를 분실하셨나요?", 71 - "noAccount": "계정이 없으신가요?", 72 - "createAccount": "계정 만들기", 73 - "removeAccount": "저장된 계정에서 삭제", 74 - "infoSavedAccountsTitle": "저장된 계정", 75 - "infoSavedAccountsDesc": "계정을 클릭하면 즉시 로그인할 수 있습니다. 세션 토큰은 이 브라우저에 안전하게 저장됩니다.", 76 - "infoNewAccountTitle": "새 계정", 77 - "infoNewAccountDesc": "로그인 버튼을 사용하여 다른 계정을 추가하세요. ×를 클릭하여 저장된 계정을 제거할 수 있습니다.", 78 - "infoSecureSignInTitle": "안전한 로그인", 79 - "infoSecureSignInDesc": "안전한 인증을 위해 리디렉션됩니다. 패스키나 2단계 인증이 활성화되어 있으면 해당 인증도 요청됩니다.", 80 - "infoStaySignedInTitle": "로그인 유지", 81 - "infoStaySignedInDesc": "로그인 후 계정이 이 브라우저에 저장되어 다음에 빠르게 접속할 수 있습니다.", 82 - "infoRecoveryTitle": "계정 복구", 83 - "infoRecoveryDesc": "비밀번호나 패스키를 분실하셨나요? 로그인 버튼 아래의 복구 링크를 사용하세요." 69 + "noAccount": "계정이 없나요?", 70 + "createAccount": "만들기", 71 + "removeAccount": "삭제" 84 72 }, 85 73 "verification": { 86 74 "title": "계정 인증", 87 - "subtitle": "계정 인증이 필요합니다. 인증 방법으로 전송된 코드를 입력하세요.", 88 - "codeLabel": "인증 코드", 89 - "codePlaceholder": "6자리 코드 입력", 90 - "verifyButton": "계정 인증", 91 - "resent": "인증 코드를 다시 보냈습니다!" 75 + "subtitle": "연락처로 전송된 코드를 입력하세요", 76 + "codeLabel": "코드", 77 + "codePlaceholder": "6자리 코드", 78 + "verifyButton": "인증", 79 + "resent": "코드 전송됨" 92 80 }, 93 81 "register": { 94 82 "title": "계정 만들기", 95 83 "subtitle": "이 PDS에 새 계정을 만듭니다", 96 - "subtitleKeyChoice": "외부 did:web 신원을 설정하는 방법을 선택하세요.", 97 - "subtitleInitialDidDoc": "계속하려면 DID 문서를 업로드하세요.", 98 - "subtitleVerify": "계속하려면 {channel}을(를) 인증하세요.", 99 - "subtitleUpdatedDidDoc": "PDS 서명 키로 DID 문서를 업데이트하세요.", 100 - "subtitleActivating": "계정을 활성화하는 중...", 101 - "subtitleComplete": "계정이 성공적으로 생성되었습니다!", 102 - "redirecting": "대시보드로 이동 중...", 103 - "infoIdentityDesc": "신원은 ATProto 네트워크에서 계정이 어떻게 식별되는지를 결정합니다. 대부분의 사용자는 표준 옵션을 선택해야 합니다.", 104 - "infoContactDesc": "이 정보는 계정 인증과 계정 보안에 관한 중요한 알림을 보내는 데 사용됩니다.", 105 - "infoNextTitle": "다음 단계는?", 106 - "infoNextDesc": "계정 생성 후 연락 방법을 인증하면 새로운 신원으로 모든 ATProto 앱을 사용할 수 있습니다.", 107 - "migrateTitle": "이미 Bluesky 계정이 있으신가요?", 108 - "migrateDescription": "새 계정을 만드는 대신 기존 계정을 이 PDS로 마이그레이션할 수 있습니다. 팔로워, 게시물, ID가 함께 이전됩니다.", 109 - "migrateLink": "PDS Moover로 마이그레이션", 84 + "subtitleKeyChoice": "did:web 신원 설정", 85 + "subtitleInitialDidDoc": "DID 문서 업로드", 86 + "subtitleVerify": "{channel} 인증", 87 + "subtitleUpdatedDidDoc": "DID 문서 업데이트", 88 + "subtitleActivating": "활성화 중", 89 + "subtitleComplete": "계정 생성됨", 90 + "redirecting": "리디렉션 중", 91 + "migrateTitle": "이미 계정이 있으신가요?", 92 + "migrateDescription": "기존 계정 마이그레이션", 93 + "migrateLink": "계정 마이그레이션", 110 94 "handle": "핸들", 111 95 "handlePlaceholder": "사용자 이름", 112 96 "handleHint": "전체 핸들: @{handle}", 97 + "handleTaken": "이 핸들은 이미 사용 중입니다", 113 98 "handleDotWarning": "사용자 정의 도메인 핸들은 계정 생성 후 설정에서 구성할 수 있습니다.", 114 99 "password": "비밀번호", 115 100 "passwordPlaceholder": "8자 이상", 116 101 "confirmPassword": "비밀번호 확인", 117 102 "confirmPasswordPlaceholder": "비밀번호 재입력", 118 103 "identityType": "ID 유형", 119 - "identityHint": "분산 ID를 관리하는 방법을 선택하세요.", 120 104 "didPlc": "did:plc", 121 - "didPlcRecommended": "(권장)", 122 105 "didPlcHint": "PLC 디렉토리에서 관리하는 이동 가능한 ID", 123 106 "didWeb": "did:web", 124 107 "didWebHint": "이 PDS에서 호스팅되는 ID (아래 경고 참조)", ··· 138 121 "externalDidPlaceholder": "did:web:yourdomain.com", 139 122 "externalDidHint": "도메인은 /.well-known/did.json에서 이 PDS를 가리키는 유효한 DID 문서를 제공해야 합니다", 140 123 "contactMethod": "연락 방법", 141 - "contactMethodHint": "계정 인증 및 알림 수신 방법을 선택하세요. 하나만 필요합니다.", 142 124 "verificationMethod": "인증 방법", 143 125 "email": "이메일", 144 126 "emailAddress": "이메일 주소", ··· 170 152 "ssoAccount": "SSO", 171 153 "ssoSubtitle": "외부 제공자를 사용하여 계정 만들기", 172 154 "noSsoProviders": "이 서버에 SSO 제공자가 설정되어 있지 않습니다.", 173 - "ssoHint": "계정을 만들 제공자를 선택하세요:", 174 155 "continueWith": "{provider}로 계속", 175 156 "validation": { 176 157 "handleRequired": "핸들은 필수입니다", ··· 680 661 "oauth": { 681 662 "login": { 682 663 "title": "로그인", 683 - "subtitle": "앱을 계속하려면 로그인하세요", 684 - "signingIn": "로그인 중...", 685 - "authenticating": "인증 중...", 686 - "checkingPasskey": "패스키 확인 중...", 687 - "signInWithPasskey": "패스키로 로그인", 688 - "passkeyNotSetUp": "패스키가 설정되지 않음", 689 - "orUsePassword": "또는 비밀번호 사용", 664 + "subtitle": "로그인 중", 665 + "signingIn": "로그인 중", 666 + "authenticating": "인증 중", 667 + "checkingPasskey": "확인 중", 668 + "signInWithPasskey": "패스키", 669 + "passkeyNotSetUp": "패스키 없음", 670 + "orUsePassword": "또는", 690 671 "password": "비밀번호", 691 672 "rememberDevice": "이 기기 기억하기", 692 - "passkeyHintChecking": "패스키 상태 확인 중...", 693 - "passkeyHintAvailable": "패스키로 로그인", 694 - "passkeyHintNotAvailable": "이 계정에 등록된 패스키가 없습니다", 695 - "passkeyHint": "기기의 생체 인식 또는 보안 키 사용", 696 - "passwordPlaceholder": "비밀번호 입력", 673 + "passkeyHintChecking": "확인 중", 674 + "passkeyHintAvailable": "패스키 사용", 675 + "passkeyHintNotAvailable": "패스키 없음", 676 + "passwordPlaceholder": "비밀번호", 697 677 "usePasskey": "패스키 사용", 698 - "orContinueWith": "또는 다음으로 계속", 699 - "orUseCredentials": "또는 자격 증명으로 로그인" 678 + "orContinueWith": "또는", 679 + "orUseCredentials": "또는" 700 680 }, 701 681 "register": { 702 682 "title": "계정 만들기", 703 - "subtitle": "계속하려면 계정을 만드세요", 704 - "subtitleGeneric": "계속하려면 계정을 만드세요", 705 - "haveAccount": "이미 계정이 있으신가요? 로그인" 683 + "subtitle": "앱", 684 + "subtitleGeneric": "계정 만들기", 685 + "haveAccount": "계정이 있으신가요? 로그인" 706 686 }, 707 687 "sso": { 708 688 "linkedAccounts": "연결된 계정", ··· 787 767 }, 788 768 "accounts": { 789 769 "title": "계정 선택", 790 - "subtitle": "계속할 계정 선택", 791 770 "useAnother": "다른 계정 사용" 792 771 }, 793 772 "twoFactor": { 794 - "title": "2단계 인증", 795 - "subtitle": "추가 확인이 필요합니다", 773 + "title": "인증", 796 774 "usePasskey": "패스키 사용", 797 775 "useTotp": "인증 앱 사용" 798 776 }, 799 777 "twoFactorCode": { 800 - "title": "2단계 인증", 801 - "subtitle": "{channel}(으)로 인증 코드를 보냈습니다. 아래에 코드를 입력하여 계속하세요.", 802 - "codeLabel": "인증 코드", 803 - "codePlaceholder": "6자리 코드 입력", 778 + "title": "인증", 779 + "subtitle": "{channel}(으)로 코드 전송됨", 780 + "codeLabel": "코드", 781 + "codePlaceholder": "6자리 코드", 804 782 "errors": { 805 - "missingRequestUri": "request_uri 매개변수가 없습니다", 806 - "verificationFailed": "인증에 실패했습니다", 807 - "connectionFailed": "서버에 연결하지 못했습니다", 808 - "unexpectedResponse": "서버로부터 예기치 않은 응답" 783 + "missingRequestUri": "요청 URI 없음", 784 + "verificationFailed": "인증 실패", 785 + "connectionFailed": "연결 실패", 786 + "unexpectedResponse": "예기치 않은 응답" 809 787 } 810 788 }, 811 789 "totp": { 812 - "title": "인증 코드 입력", 813 - "subtitle": "인증 앱의 6자리 코드를 입력하세요", 814 - "codePlaceholder": "6자리 코드 입력", 790 + "title": "인증 코드", 791 + "codePlaceholder": "6자리 코드", 815 792 "useBackupCode": "백업 코드 사용", 816 - "backupCodePlaceholder": "백업 코드 입력", 793 + "backupCodePlaceholder": "백업 코드", 817 794 "trustDevice": "이 기기를 30일간 신뢰", 818 - "hintBackupCode": "백업 코드 사용 중", 819 - "hintTotpCode": "인증 코드 사용 중", 820 - "hintDefault": "인증 앱은 6자리, 백업 코드는 8자" 795 + "hintBackupCode": "백업 코드", 796 + "hintTotpCode": "인증 코드" 821 797 }, 822 798 "passkey": { 823 - "title": "패스키 확인", 824 - "subtitle": "패스키를 사용하여 본인 확인", 825 - "waiting": "패스키 대기 중...", 799 + "title": "패스키", 800 + "waiting": "대기 중", 826 801 "useTotp": "인증 앱 사용" 827 802 }, 828 803 "error": { 829 - "title": "승인 오류", 830 - "genericError": "승인 중 오류가 발생했습니다.", 804 + "title": "승인 실패", 831 805 "tryAgain": "다시 시도", 832 - "backToApp": "앱으로 돌아가기" 806 + "backToApp": "돌아가기" 833 807 } 834 808 }, 835 809 "sso_register": { ··· 855 829 "subtitle": "{channel}(으)로 인증 코드를 보냈습니다. 아래에 입력하여 등록을 완료하세요.", 856 830 "tokenTitle": "인증", 857 831 "tokenSubtitle": "인증 코드와 전송된 식별자를 입력하세요.", 858 - "codePlaceholder": "XXXX-XXXX-XXXX-XXXX...", 832 + "codePlaceholder": "Paste verification code", 859 833 "codeLabel": "인증 코드", 860 - "codeHelp": "메시지에서 하이픈을 포함한 전체 코드를 복사하세요", 834 + "codeHelp": "메시지에서 하이픈을 를 복사하세요", 861 835 "verifyButton": "계정 인증", 862 836 "pleaseWait": "잠시 기다려 주세요...", 863 837 "codeResent": "인증 코드를 다시 보냈습니다!", ··· 958 932 }, 959 933 "registerPasskey": { 960 934 "title": "패스키 계정 만들기", 961 - "subtitle": "비밀번호 대신 패스키를 사용하여 초안전 계정을 만듭니다.", 962 - "subtitleKeyChoice": "외부 did:web 아이덴티티 설정 방법을 선택하세요.", 963 - "subtitleVerify": "{channel}(으)로 인증 코드를 보냈습니다. 코드를 입력하여 계속하세요.", 964 - "subtitlePasskey": "패스키를 만들어 계정 설정을 완료하세요.", 935 + "subtitleKeyChoice": "did:web 아이덴티티 설정", 936 + "subtitleInitialDidDoc": "DID 문서 업로드", 937 + "subtitleCreating": "계정 생성 중", 938 + "subtitlePasskey": "패스키 등록", 939 + "subtitleAppPassword": "앱 비밀번호 저장", 940 + "subtitleVerify": "{channel} 인증", 941 + "subtitleUpdatedDidDoc": "DID 문서 업데이트", 942 + "subtitleActivating": "활성화 중", 943 + "subtitleComplete": "계정 생성됨", 965 944 "handle": "핸들", 966 945 "handlePlaceholder": "사용자 이름", 967 946 "handleHint": "전체 핸들: @{handle}", 968 947 "contactMethod": "연락 방법", 969 - "contactMethodHint": "계정 인증 및 알림 수신 방법을 선택하세요.", 970 948 "verificationMethod": "인증 방법", 971 949 "email": "이메일 주소", 972 950 "emailPlaceholder": "you@example.com", ··· 993 971 "externalDidFormat": "외부 DID는 did:web:으로 시작해야 합니다", 994 972 "discordRequired": "Discord 인증에는 Discord ID가 필요합니다" 995 973 }, 996 - "whyPasskeyBullet1": "피싱이나 데이터 유출로 도난당할 수 없음", 997 - "whyPasskeyBullet2": "하드웨어 기반 암호화 키 사용", 998 - "whyPasskeyBullet3": "생체 인식 또는 기기 PIN 필요", 999 - "infoWhyPasskey": "왜 패스키를 사용하나요?", 1000 - "infoWhyPasskeyDesc": "패스키는 기기에 저장된 암호화 자격 증명입니다. 비밀번호처럼 피싱, 추측 또는 데이터 유출로 도난당할 수 없습니다.", 1001 - "infoHowItWorks": "작동 방식", 1002 - "infoHowItWorksDesc": "로그인할 때 기기에서 Face ID, Touch ID 또는 기기 PIN으로 인증하라는 메시지가 표시됩니다. 기억하거나 입력할 비밀번호가 없습니다.", 1003 - "infoAppAccess": "서드파티 앱 사용", 1004 - "infoAppAccessDesc": "계정 생성 후 앱 비밀번호를 받게 됩니다. Bluesky 앱 및 기타 AT Protocol 클라이언트에 로그인할 때 사용하세요.", 1005 - "whyPasskeyOnly": "왜 패스키만 사용하나요?", 1006 - "whyPasskeyOnlyDesc": "패스키 계정은 비밀번호 기반 계정보다 안전합니다:", 1007 - "subtitleInitialDidDoc": "계속하려면 DID 문서를 업로드하세요.", 1008 - "subtitleUpdatedDidDoc": "PDS 서명 키로 DID 문서를 업데이트하세요.", 1009 - "subtitleActivating": "계정을 활성화하는 중...", 1010 - "subtitleComplete": "계정이 성공적으로 생성되었습니다!", 1011 - "subtitleCreating": "계정을 생성하는 중...", 1012 - "subtitleAppPassword": "서드파티 앱용 앱 비밀번호를 저장하세요.", 1013 - "creatingPasskey": "패스키 생성 중...", 1014 - "passkeyPrompt": "아래 버튼을 클릭하여 패스키를 생성하세요. 다음을 사용하라는 메시지가 표시됩니다:", 1015 - "passkeyPromptBullet1": "Touch ID 또는 Face ID", 1016 - "passkeyPromptBullet2": "기기 PIN 또는 비밀번호", 1017 - "passkeyPromptBullet3": "보안 키 (있는 경우)", 974 + "creatingPasskey": "생성 중", 1018 975 "identityType": "아이덴티티 유형", 1019 976 "identityTypeHint": "분산 아이덴티티 관리 방법을 선택하세요.", 1020 - "passkeyNameLabel": "패스키 이름 (선택사항)", 1021 977 "passkeyNamePlaceholder": "예: MacBook Touch ID", 1022 978 "passkeyNameHint": "이 패스키를 식별할 수 있는 이름", 1023 979 "createPasskey": "패스키 생성", ··· 1044 1000 "redirecting": "대시보드로 이동 중...", 1045 1001 "handleDotWarning": "사용자 정의 도메인 핸들은 계정 생성 후 설정할 수 있습니다.", 1046 1002 "wantTraditional": "기존 비밀번호를 원하시나요?", 1047 - "registerWithPassword": "비밀번호로 가입" 1003 + "registerWithPassword": "비밀번호로 가입", 1004 + "activatingAccount": "Activating", 1005 + "creatingAccount": "Creating account", 1006 + "passkeyDescription": "Register a passkey for this account", 1007 + "passkeyName": "Passkey Name", 1008 + "setupPasskey": "Create Passkey" 1048 1009 }, 1049 1010 "trustedDevices": { 1050 1011 "title": "신뢰할 수 있는 기기", ··· 1068 1029 "unknownDevice": "알 수 없는 기기" 1069 1030 }, 1070 1031 "reauth": { 1071 - "title": "재인증 필요", 1072 - "subtitle": "계속하려면 본인 확인을 해주세요.", 1032 + "title": "재인증", 1073 1033 "password": "비밀번호", 1074 1034 "totp": "TOTP", 1075 1035 "passkey": "패스키", 1076 1036 "authenticatorCode": "인증 코드", 1077 - "usePassword": "비밀번호 사용", 1078 - "usePasskey": "패스키 사용", 1079 - "useTotp": "인증 앱 사용", 1037 + "usePassword": "비밀번호", 1038 + "usePasskey": "패스키", 1039 + "useTotp": "인증 앱", 1080 1040 "passwordPlaceholder": "비밀번호 입력", 1081 - "totpPlaceholder": "6자리 코드 입력", 1082 - "authenticating": "인증 중...", 1083 - "passkeyPrompt": "아래 버튼을 클릭하여 패스키로 인증하세요.", 1041 + "totpPlaceholder": "6자리 코드", 1042 + "authenticating": "인증 중", 1084 1043 "cancel": "취소" 1085 1044 }, 1086 1045 "verifyChannel": { ··· 1100 1059 "identifierPlaceholder": "이메일, Discord ID 등", 1101 1060 "identifierHelp": "인증할 이메일 주소, Discord ID, Telegram 사용자 이름 또는 Signal 번호.", 1102 1061 "codeLabel": "인증 코드", 1103 - "codeHelp": "메시지에서 하이픈을 포함한 전체 코드를 복사하세요.", 1062 + "codeHelp": "메시지에서 하이픈을 를 복사하세요.", 1104 1063 "verifyButton": "인증" 1105 1064 }, 1106 1065 "delegation": {
+95 -136
frontend/src/locales/sv.json
··· 1 1 { 2 2 "common": { 3 - "loading": "Laddar...", 3 + "loading": "Laddar", 4 4 "error": "Fel", 5 5 "save": "Spara", 6 6 "cancel": "Avbryt", ··· 16 16 "name": "Namn", 17 17 "dashboard": "Kontrollpanel", 18 18 "backToDashboard": "← Kontrollpanel", 19 - "copied": "Kopierat!", 19 + "copied": "Kopierat", 20 20 "copyToClipboard": "Kopiera", 21 - "verifying": "Verifierar...", 22 - "saving": "Sparar...", 23 - "creating": "Skapar...", 24 - "updating": "Uppdaterar...", 25 - "sending": "Skickar...", 26 - "authenticating": "Autentiserar...", 27 - "checking": "Kontrollerar...", 28 - "redirecting": "Omdirigerar...", 21 + "verifying": "Verifierar", 22 + "saving": "Sparar", 23 + "creating": "Skapar", 24 + "updating": "Uppdaterar", 25 + "sending": "Skickar", 26 + "authenticating": "Autentiserar", 27 + "checking": "Kontrollerar", 28 + "redirecting": "Omdirigerar", 29 29 "signIn": "Logga in", 30 30 "verify": "Verifiera", 31 31 "remove": "Ta bort", 32 32 "revoke": "Återkalla", 33 - "resendCode": "Skicka kod igen", 33 + "resendCode": "Skicka kod", 34 34 "startOver": "Börja om", 35 35 "tryAgain": "Försök igen", 36 36 "password": "Lösenord", ··· 60 60 }, 61 61 "login": { 62 62 "title": "Logga in", 63 - "subtitle": "Logga in för att hantera ditt PDS-konto", 64 63 "button": "Logga in", 65 - "redirecting": "Omdirigerar...", 64 + "redirecting": "Omdirigerar", 66 65 "chooseAccount": "Välj ett konto", 67 - "signInToAnother": "Logga in med ett annat konto", 68 - "backToSaved": "← Tillbaka till sparade konton", 66 + "signInToAnother": "Eller logga in med ett annat konto", 69 67 "forgotPassword": "Glömt lösenordet?", 70 68 "lostPasskey": "Tappat bort nyckeln?", 71 - "noAccount": "Har du inget konto?", 72 - "createAccount": "Skapa konto", 73 - "removeAccount": "Ta bort från sparade konton", 74 - "infoSavedAccountsTitle": "Sparade konton", 75 - "infoSavedAccountsDesc": "Klicka på ett konto för att logga in direkt. Dina sessionstoken lagras säkert i denna webbläsare.", 76 - "infoNewAccountTitle": "Nytt konto", 77 - "infoNewAccountDesc": "Använd inloggningsknappen för att lägga till ett annat konto. Klicka på × för att ta bort sparade konton.", 78 - "infoSecureSignInTitle": "Säker inloggning", 79 - "infoSecureSignInDesc": "Du omdirigeras för säker autentisering. Om du har aktiverat nycklar eller tvåfaktorsautentisering kommer du också att behöva ange dessa.", 80 - "infoStaySignedInTitle": "Förbli inloggad", 81 - "infoStaySignedInDesc": "Efter inloggning sparas ditt konto i denna webbläsare för snabb åtkomst nästa gång.", 82 - "infoRecoveryTitle": "Kontoåterställning", 83 - "infoRecoveryDesc": "Har du tappat bort ditt lösenord eller din nyckel? Använd återställningslänkarna under inloggningsknappen." 69 + "noAccount": "Inget konto?", 70 + "createAccount": "Skapa ett", 71 + "removeAccount": "Ta bort" 84 72 }, 85 73 "verification": { 86 - "title": "Verifiera ditt konto", 87 - "subtitle": "Ditt konto behöver verifieras. Ange koden som skickades till din verifieringsmetod.", 88 - "codeLabel": "Verifieringskod", 89 - "codePlaceholder": "Ange 6-siffrig kod", 90 - "verifyButton": "Verifiera konto", 91 - "resent": "Verifieringskod skickad igen!" 74 + "title": "Verifiera konto", 75 + "subtitle": "Ange koden som skickades till din kontaktmetod", 76 + "codeLabel": "Kod", 77 + "codePlaceholder": "6-siffrig kod", 78 + "verifyButton": "Verifiera", 79 + "resent": "Kod skickad" 92 80 }, 93 81 "register": { 94 82 "title": "Skapa konto", 95 83 "subtitle": "Skapa ett nytt konto på denna PDS", 96 - "subtitleKeyChoice": "Välj hur du vill konfigurera din externa did:web-identitet.", 97 - "subtitleInitialDidDoc": "Ladda upp ditt DID-dokument för att fortsätta.", 98 - "subtitleVerify": "Verifiera din {channel} för att fortsätta.", 99 - "subtitleUpdatedDidDoc": "Uppdatera ditt DID-dokument med PDS-signeringsnyckeln.", 100 - "subtitleActivating": "Aktiverar ditt konto...", 101 - "subtitleComplete": "Ditt konto har skapats!", 102 - "redirecting": "Omdirigerar till kontrollpanelen...", 103 - "infoIdentityDesc": "Din identitet avgör hur ditt konto identifieras i ATProto-nätverket. De flesta användare bör välja standardalternativet.", 104 - "infoContactDesc": "Vi använder detta för att verifiera ditt konto och skicka viktiga meddelanden om din kontosäkerhet.", 105 - "infoNextTitle": "Vad händer härnäst?", 106 - "infoNextDesc": "Efter att du skapat ditt konto verifierar du din kontaktmetod och sedan är du redo att använda vilken ATProto-app som helst med din nya identitet.", 107 - "migrateTitle": "Har du redan ett Bluesky-konto?", 108 - "migrateDescription": "Du kan flytta ditt befintliga konto till denna PDS istället för att skapa ett nytt. Dina följare, inlägg och identitet följer med.", 109 - "migrateLink": "Flytta med PDS Moover", 84 + "subtitleKeyChoice": "Konfigurera din did:web-identitet", 85 + "subtitleInitialDidDoc": "Ladda upp ditt DID-dokument", 86 + "subtitleVerify": "Verifiera din {channel}", 87 + "subtitleUpdatedDidDoc": "Uppdatera ditt DID-dokument", 88 + "subtitleActivating": "Aktiverar", 89 + "subtitleComplete": "Konto skapat", 90 + "redirecting": "Omdirigerar", 91 + "migrateTitle": "Har du redan ett konto?", 92 + "migrateDescription": "Migrera istället för att skapa nytt", 93 + "migrateLink": "Migrera ditt konto", 110 94 "handle": "Användarnamn", 111 95 "handlePlaceholder": "dittnamn", 112 96 "handleHint": "Ditt fullständiga användarnamn blir: @{handle}", 97 + "handleTaken": "Detta användarnamn är redan taget", 113 98 "handleDotWarning": "Egna domännamn kan konfigureras efter att kontot skapats i Inställningar.", 114 99 "password": "Lösenord", 115 100 "passwordPlaceholder": "Minst 8 tecken", 116 101 "confirmPassword": "Bekräfta lösenord", 117 102 "confirmPasswordPlaceholder": "Bekräfta ditt lösenord", 118 103 "identityType": "Identitetstyp", 119 - "identityHint": "Välj hur din decentraliserade identitet ska hanteras.", 120 104 "didPlc": "did:plc", 121 - "didPlcRecommended": "(Rekommenderas)", 122 105 "didPlcHint": "Portabel identitet hanterad av PLC Directory", 123 106 "didWeb": "did:web", 124 107 "didWebHint": "Identitet lagrad på denna PDS (läs varningen nedan)", ··· 138 121 "externalDidPlaceholder": "did:web:dindomän.se", 139 122 "externalDidHint": "Din domän måste tillhandahålla ett giltigt DID-dokument på /.well-known/did.json som pekar på denna PDS", 140 123 "contactMethod": "Kontaktmetod", 141 - "contactMethodHint": "Välj hur du vill verifiera ditt konto och ta emot meddelanden. Du behöver bara en.", 142 124 "verificationMethod": "Verifieringsmetod", 143 125 "email": "E-post", 144 126 "emailAddress": "E-postadress", ··· 170 152 "ssoAccount": "SSO", 171 153 "ssoSubtitle": "Skapa ett konto med en extern leverantör", 172 154 "noSsoProviders": "Inga SSO-leverantörer är konfigurerade på denna server.", 173 - "ssoHint": "Välj en leverantör för att skapa ditt konto:", 174 155 "continueWith": "Fortsätt med {provider}", 175 156 "validation": { 176 157 "handleRequired": "Användarnamn krävs", ··· 680 661 "oauth": { 681 662 "login": { 682 663 "title": "Logga in", 683 - "subtitle": "Logga in för att fortsätta till applikationen", 684 - "signingIn": "Loggar in...", 685 - "authenticating": "Autentiserar...", 686 - "checkingPasskey": "Kontrollerar nyckel...", 687 - "signInWithPasskey": "Logga in med nyckel", 688 - "passkeyNotSetUp": "Nyckel inte konfigurerad", 689 - "orUsePassword": "eller använd lösenord", 664 + "subtitle": "Loggar in på", 665 + "signingIn": "Loggar in", 666 + "authenticating": "Autentiserar", 667 + "checkingPasskey": "Kontrollerar", 668 + "signInWithPasskey": "Nyckel", 669 + "passkeyNotSetUp": "Ingen nyckel", 670 + "orUsePassword": "eller", 690 671 "password": "Lösenord", 691 672 "rememberDevice": "Kom ihåg denna enhet", 692 - "passkeyHintChecking": "Kontrollerar nyckelstatus...", 693 - "passkeyHintAvailable": "Logga in med din nyckel", 694 - "passkeyHintNotAvailable": "Inga nycklar registrerade för detta konto", 695 - "passkeyHint": "Använd enhetens biometri eller säkerhetsnyckel", 696 - "passwordPlaceholder": "Ange ditt lösenord", 673 + "passkeyHintChecking": "Kontrollerar", 674 + "passkeyHintAvailable": "Använd nyckel", 675 + "passkeyHintNotAvailable": "Ingen nyckel registrerad", 676 + "passwordPlaceholder": "Lösenord", 697 677 "usePasskey": "Använd nyckel", 698 - "orContinueWith": "Eller fortsätt med", 699 - "orUseCredentials": "Eller logga in med uppgifter" 678 + "orContinueWith": "eller", 679 + "orUseCredentials": "eller" 700 680 }, 701 681 "register": { 702 682 "title": "Skapa konto", 703 - "subtitle": "Skapa ett konto med {app}", 704 - "subtitleGeneric": "Skapa ett konto för att fortsätta", 705 - "haveAccount": "Har du redan ett konto?" 683 + "subtitle": "för", 684 + "subtitleGeneric": "Skapa ett konto", 685 + "haveAccount": "Har du ett konto? Logga in" 706 686 }, 707 687 "sso": { 708 688 "linkedAccounts": "Länkade konton", ··· 787 767 }, 788 768 "accounts": { 789 769 "title": "Välj konto", 790 - "subtitle": "Välj ett konto för att fortsätta", 791 770 "useAnother": "Använd ett annat konto" 792 771 }, 793 772 "twoFactor": { 794 - "title": "Tvåfaktorsautentisering", 795 - "subtitle": "Ytterligare verifiering krävs", 773 + "title": "Verifiering", 796 774 "usePasskey": "Använd nyckel", 797 - "useTotp": "Använd autentiseringsapp" 775 + "useTotp": "Använd autentiserare" 798 776 }, 799 777 "twoFactorCode": { 800 - "title": "Tvåfaktorsautentisering", 801 - "subtitle": "En verifieringskod har skickats till din {channel}. Ange koden nedan för att fortsätta.", 802 - "codeLabel": "Verifieringskod", 803 - "codePlaceholder": "Ange 6-siffrig kod", 778 + "title": "Verifiering", 779 + "subtitle": "Kod skickad till {channel}", 780 + "codeLabel": "Kod", 781 + "codePlaceholder": "6-siffrig kod", 804 782 "errors": { 805 - "missingRequestUri": "Saknar request_uri-parameter", 783 + "missingRequestUri": "Saknar request URI", 806 784 "verificationFailed": "Verifiering misslyckades", 807 - "connectionFailed": "Kunde inte ansluta till servern", 808 - "unexpectedResponse": "Oväntat svar från servern" 785 + "connectionFailed": "Anslutning misslyckades", 786 + "unexpectedResponse": "Oväntat svar" 809 787 } 810 788 }, 811 789 "totp": { 812 - "title": "Ange autentiseringskod", 813 - "subtitle": "Ange den 6-siffriga koden från din autentiseringsapp", 814 - "codePlaceholder": "Ange 6-siffrig kod", 815 - "useBackupCode": "Använd reservkod istället", 816 - "backupCodePlaceholder": "Ange reservkod", 790 + "title": "Autentiseringskod", 791 + "codePlaceholder": "6-siffrig kod", 792 + "useBackupCode": "Använd reservkod", 793 + "backupCodePlaceholder": "Reservkod", 817 794 "trustDevice": "Lita på denna enhet i 30 dagar", 818 - "hintBackupCode": "Använder reservkod", 819 - "hintTotpCode": "Använder autentiseringskod", 820 - "hintDefault": "6 siffror för autentiserare, 8 tecken för reservkod" 795 + "hintBackupCode": "Reservkod", 796 + "hintTotpCode": "Autentiseringskod" 821 797 }, 822 798 "passkey": { 823 - "title": "Nyckelverifiering", 824 - "subtitle": "Använd din nyckel för att verifiera din identitet", 825 - "waiting": "Väntar på nyckel...", 826 - "useTotp": "Använd autentiseringsapp istället" 799 + "title": "Nyckel", 800 + "waiting": "Väntar", 801 + "useTotp": "Använd autentiserare" 827 802 }, 828 803 "error": { 829 - "title": "Auktoriseringsfel", 830 - "genericError": "Ett fel uppstod under auktorisering.", 804 + "title": "Auktorisering misslyckades", 831 805 "tryAgain": "Försök igen", 832 - "backToApp": "Tillbaka till applikationen" 806 + "backToApp": "Tillbaka" 833 807 } 834 808 }, 835 809 "sso_register": { ··· 855 829 "subtitle": "Vi har skickat en verifieringskod till din {channel}. Ange den nedan för att slutföra registreringen.", 856 830 "tokenTitle": "Verifiera", 857 831 "tokenSubtitle": "Ange verifieringskoden och identifieraren den skickades till.", 858 - "codePlaceholder": "XXXX-XXXX-XXXX-XXXX...", 832 + "codePlaceholder": "Paste verification code", 859 833 "codeLabel": "Verifieringskod", 860 - "codeHelp": "Kopiera hela koden från ditt meddelande, inklusive bindestreck", 834 + "codeHelp": "Kopiera hela koden från ditt meddelande, ", 861 835 "verifyButton": "Verifiera konto", 862 836 "pleaseWait": "Vänta...", 863 837 "codeResent": "Verifieringskod skickad igen!", ··· 958 932 }, 959 933 "registerPasskey": { 960 934 "title": "Skapa nyckelkonto", 961 - "subtitle": "Skapa ett ultrasäkert konto med en nyckel istället för ett lösenord.", 962 - "subtitleKeyChoice": "Välj hur du vill konfigurera din externa did:web-identitet.", 963 - "subtitleVerify": "Vi har skickat en verifieringskod till din {channel}. Ange koden för att fortsätta.", 964 - "subtitlePasskey": "Skapa din nyckel för att slutföra kontokonfigurationen.", 935 + "subtitleKeyChoice": "Konfigurera din did:web-identitet", 936 + "subtitleInitialDidDoc": "Ladda upp ditt DID-dokument", 937 + "subtitleCreating": "Skapar konto", 938 + "subtitlePasskey": "Registrera din nyckel", 939 + "subtitleAppPassword": "Spara ditt applösenord", 940 + "subtitleVerify": "Verifiera din {channel}", 941 + "subtitleUpdatedDidDoc": "Uppdatera ditt DID-dokument", 942 + "subtitleActivating": "Aktiverar", 943 + "subtitleComplete": "Konto skapat", 965 944 "handle": "Användarnamn", 966 945 "handlePlaceholder": "dittnamn", 967 946 "handleHint": "Ditt fullständiga användarnamn blir: @{handle}", 968 947 "contactMethod": "Kontaktmetod", 969 - "contactMethodHint": "Välj hur du vill verifiera ditt konto och ta emot meddelanden.", 970 948 "verificationMethod": "Verifieringsmetod", 971 949 "email": "E-postadress", 972 950 "emailPlaceholder": "du@exempel.se", ··· 993 971 "externalDidFormat": "Extern DID måste börja med did:web:", 994 972 "discordRequired": "Discord-ID krävs för Discord-verifiering" 995 973 }, 996 - "whyPasskeyBullet1": "Kan inte nätfiskas eller stjälas vid dataintrång", 997 - "whyPasskeyBullet2": "Använder hårdvarubaserade kryptografiska nycklar", 998 - "whyPasskeyBullet3": "Kräver din biometri eller enhets-PIN för att använda", 999 - "infoWhyPasskey": "Varfor anvanda nyckel?", 1000 - "infoWhyPasskeyDesc": "Nycklar ar kryptografiska uppgifter som lagras pa din enhet. De kan inte nätfiskas, gissas eller stjälas vid dataintrång som losenord kan.", 1001 - "infoHowItWorks": "Hur det fungerar", 1002 - "infoHowItWorksDesc": "När du loggar in kommer din enhet att be dig verifiera med Face ID, Touch ID eller din enhets-PIN. Inget lösenord att komma ihåg eller skriva.", 1003 - "infoAppAccess": "Använda tredjepartsappar", 1004 - "infoAppAccessDesc": "Efter att du skapat ditt konto får du ett applösenord. Använd detta för att logga in på Bluesky-appar och andra AT Protocol-klienter.", 1005 - "whyPasskeyOnly": "Varför endast nyckel?", 1006 - "whyPasskeyOnlyDesc": "Nyckelkonton är säkrare än lösenordsbaserade konton eftersom de:", 1007 - "subtitleInitialDidDoc": "Ladda upp ditt DID-dokument för att fortsätta.", 1008 - "subtitleUpdatedDidDoc": "Uppdatera ditt DID-dokument med PDS-signeringsnyckeln.", 1009 - "subtitleActivating": "Aktiverar ditt konto...", 1010 - "subtitleComplete": "Ditt konto har skapats!", 1011 - "subtitleCreating": "Skapar ditt konto...", 1012 - "subtitleAppPassword": "Spara ditt applösenord för tredjepartsappar.", 1013 - "creatingPasskey": "Skapar nyckel...", 1014 - "passkeyPrompt": "Klicka på knappen nedan för att skapa din nyckel. Du kommer att uppmanas att använda:", 1015 - "passkeyPromptBullet1": "Touch ID eller Face ID", 1016 - "passkeyPromptBullet2": "Din enhets PIN-kod eller lösenord", 1017 - "passkeyPromptBullet3": "En säkerhetsnyckel (om du har en)", 974 + "creatingPasskey": "Skapar", 1018 975 "identityType": "Identitetstyp", 1019 976 "identityTypeHint": "Välj hur din decentraliserade identitet ska hanteras.", 1020 - "passkeyNameLabel": "Nyckelnamn (valfritt)", 1021 977 "passkeyNamePlaceholder": "t.ex. MacBook Touch ID", 1022 978 "passkeyNameHint": "Ett vänligt namn för att identifiera denna nyckel", 1023 979 "createPasskey": "Skapa nyckel", ··· 1044 1000 "redirecting": "Omdirigerar till instrumentpanelen...", 1045 1001 "handleDotWarning": "Egna domännamn kan konfigureras efter att kontot skapats.", 1046 1002 "wantTraditional": "Vill du ha ett traditionellt lösenord?", 1047 - "registerWithPassword": "Registrera med lösenord" 1003 + "registerWithPassword": "Registrera med lösenord", 1004 + "activatingAccount": "Activating", 1005 + "creatingAccount": "Creating account", 1006 + "passkeyDescription": "Register a passkey for this account", 1007 + "passkeyName": "Passkey Name", 1008 + "setupPasskey": "Create Passkey" 1048 1009 }, 1049 1010 "trustedDevices": { 1050 1011 "title": "Betrodda enheter", ··· 1068 1029 "unknownDevice": "Okänd enhet" 1069 1030 }, 1070 1031 "reauth": { 1071 - "title": "Återautentisering krävs", 1072 - "subtitle": "Verifiera din identitet för att fortsätta.", 1032 + "title": "Återautentisera", 1073 1033 "password": "Lösenord", 1074 1034 "totp": "TOTP", 1075 - "passkey": "Passkey", 1035 + "passkey": "Nyckel", 1076 1036 "authenticatorCode": "Autentiseringskod", 1077 - "usePassword": "Använd lösenord", 1078 - "usePasskey": "Använd nyckel", 1079 - "useTotp": "Använd autentiserare", 1037 + "usePassword": "Lösenord", 1038 + "usePasskey": "Nyckel", 1039 + "useTotp": "Autentiserare", 1080 1040 "passwordPlaceholder": "Ange ditt lösenord", 1081 - "totpPlaceholder": "Ange 6-siffrig kod", 1082 - "authenticating": "Autentiserar...", 1083 - "passkeyPrompt": "Klicka på knappen nedan för att autentisera med din passkey.", 1041 + "totpPlaceholder": "6-siffrig kod", 1042 + "authenticating": "Autentiserar", 1084 1043 "cancel": "Avbryt" 1085 1044 }, 1086 1045 "verifyChannel": { ··· 1100 1059 "identifierPlaceholder": "E-post, Discord ID, etc.", 1101 1060 "identifierHelp": "E-postadressen, Discord ID, Telegram-användarnamn eller Signal-nummer som verifieras.", 1102 1061 "codeLabel": "Verifieringskod", 1103 - "codeHelp": "Kopiera hela koden från ditt meddelande, inklusive bindestreck.", 1062 + "codeHelp": "Kopiera hela koden från ditt meddelande, .", 1104 1063 "verifyButton": "Verifiera" 1105 1064 }, 1106 1065 "delegation": {
+91 -132
frontend/src/locales/zh.json
··· 1 1 { 2 2 "common": { 3 - "loading": "加载中...", 3 + "loading": "加载中", 4 4 "error": "错误", 5 5 "save": "保存", 6 6 "cancel": "取消", ··· 16 16 "name": "名称", 17 17 "dashboard": "控制台", 18 18 "backToDashboard": "← 返回控制台", 19 - "copied": "已复制!", 19 + "copied": "已复制", 20 20 "copyToClipboard": "复制", 21 - "verifying": "验证中...", 22 - "saving": "保存中...", 23 - "creating": "创建中...", 24 - "updating": "更新中...", 25 - "sending": "发送中...", 26 - "authenticating": "认证中...", 27 - "checking": "检查中...", 28 - "redirecting": "跳转中...", 21 + "verifying": "验证中", 22 + "saving": "保存中", 23 + "creating": "创建中", 24 + "updating": "更新中", 25 + "sending": "发送中", 26 + "authenticating": "认证中", 27 + "checking": "检查中", 28 + "redirecting": "跳转中", 29 29 "signIn": "登录", 30 30 "verify": "验证", 31 31 "remove": "移除", 32 32 "revoke": "撤销", 33 - "resendCode": "重新发送验证码", 33 + "resendCode": "重新发送", 34 34 "startOver": "重新开始", 35 35 "tryAgain": "重试", 36 36 "password": "密码", ··· 60 60 }, 61 61 "login": { 62 62 "title": "登录", 63 - "subtitle": "登录以管理您的 PDS 账户", 64 63 "button": "登录", 65 - "redirecting": "跳转中...", 64 + "redirecting": "跳转中", 66 65 "chooseAccount": "选择账户", 67 - "signInToAnother": "登录其他账户", 68 - "backToSaved": "← 返回已保存账户", 66 + "signInToAnother": "或登录其他账户", 69 67 "forgotPassword": "忘记密码?", 70 68 "lostPasskey": "丢失通行密钥?", 71 - "noAccount": "还没有账户?", 72 - "createAccount": "立即注册", 73 - "removeAccount": "从已保存账户中移除", 74 - "infoSavedAccountsTitle": "已保存账户", 75 - "infoSavedAccountsDesc": "点击账户即可快速登录。您的会话令牌安全存储在此浏览器中。", 76 - "infoNewAccountTitle": "新账户", 77 - "infoNewAccountDesc": "使用登录按钮添加其他账户。点击 × 可从此浏览器中移除已保存的账户。", 78 - "infoSecureSignInTitle": "安全登录", 79 - "infoSecureSignInDesc": "您将被重定向进行安全认证。如果您启用了通行密钥或双重身份验证,也会提示您进行验证。", 80 - "infoStaySignedInTitle": "保持登录", 81 - "infoStaySignedInDesc": "登录后,您的账户将保存在此浏览器中,方便下次快速访问。", 82 - "infoRecoveryTitle": "账户恢复", 83 - "infoRecoveryDesc": "忘记密码或丢失通行密钥?使用登录按钮下方的恢复链接。" 69 + "noAccount": "没有账户?", 70 + "createAccount": "注册", 71 + "removeAccount": "移除" 84 72 }, 85 73 "verification": { 86 74 "title": "验证账户", 87 - "subtitle": "您的账户需要验证。请输入发送到您验证方式的验证码。", 75 + "subtitle": "请输入发送到您联系方式的验证码", 88 76 "codeLabel": "验证码", 89 - "codePlaceholder": "输入6位验证码", 90 - "verifyButton": "验证账户", 91 - "resent": "验证码已重新发送!" 77 + "codePlaceholder": "6位验证码", 78 + "verifyButton": "验证", 79 + "resent": "验证码已发送" 92 80 }, 93 81 "register": { 94 82 "title": "创建账户", 95 83 "subtitle": "在此 PDS 上创建新账户", 96 - "subtitleKeyChoice": "选择如何设置您的外部 did:web 身份。", 97 - "subtitleInitialDidDoc": "上传您的 DID 文档以继续。", 98 - "subtitleVerify": "验证您的{channel}以继续。", 99 - "subtitleUpdatedDidDoc": "使用 PDS 签名密钥更新您的 DID 文档。", 100 - "subtitleActivating": "正在激活您的账户...", 101 - "subtitleComplete": "您的账户已成功创建!", 102 - "redirecting": "正在跳转到控制台...", 103 - "infoIdentityDesc": "您的身份决定了您的账户在 ATProto 网络中的识别方式。大多数用户应选择标准选项。", 104 - "infoContactDesc": "我们将使用此信息验证您的账户并发送有关账户安全的重要通知。", 105 - "infoNextTitle": "接下来会发生什么?", 106 - "infoNextDesc": "创建账户后,您需要验证联系方式,然后即可使用任何 ATProto 应用程序。", 107 - "migrateTitle": "已有 Bluesky 账户?", 108 - "migrateDescription": "您可以将现有账户迁移到此 PDS,而无需创建新账户。您的关注者、帖子和身份都会一起迁移。", 109 - "migrateLink": "使用 PDS Moover 迁移", 84 + "subtitleKeyChoice": "设置 did:web 身份", 85 + "subtitleInitialDidDoc": "上传 DID 文档", 86 + "subtitleVerify": "验证您的{channel}", 87 + "subtitleUpdatedDidDoc": "更新 DID 文档", 88 + "subtitleActivating": "激活中", 89 + "subtitleComplete": "账户已创建", 90 + "redirecting": "跳转中", 91 + "migrateTitle": "已有账户?", 92 + "migrateDescription": "迁移现有账户", 93 + "migrateLink": "迁移账户", 110 94 "handle": "用户名", 111 95 "handlePlaceholder": "您的用户名", 112 96 "handleHint": "您的完整用户名将是:@{handle}", 97 + "handleTaken": "此用户名已被占用", 113 98 "handleDotWarning": "自定义域名可以在创建账户后在设置中配置。", 114 99 "password": "密码", 115 100 "passwordPlaceholder": "至少8位字符", 116 101 "confirmPassword": "确认密码", 117 102 "confirmPasswordPlaceholder": "再次输入密码", 118 103 "identityType": "身份类型", 119 - "identityHint": "选择如何管理您的去中心化身份。", 120 104 "didPlc": "did:plc", 121 - "didPlcRecommended": "(推荐)", 122 105 "didPlcHint": "由 PLC 目录管理的可迁移身份", 123 106 "didWeb": "did:web", 124 107 "didWebHint": "托管在此 PDS 上的身份(请阅读下方警告)", ··· 138 121 "externalDidPlaceholder": "did:web:yourdomain.com", 139 122 "externalDidHint": "您的域名必须在 /.well-known/did.json 提供指向此 PDS 的有效 DID 文档", 140 123 "contactMethod": "联系方式", 141 - "contactMethodHint": "选择您希望如何验证账户和接收通知。您只需选择一种。", 142 124 "verificationMethod": "验证方式", 143 125 "email": "电子邮件", 144 126 "emailAddress": "电子邮件地址", ··· 170 152 "ssoAccount": "SSO", 171 153 "ssoSubtitle": "使用外部提供商创建账户", 172 154 "noSsoProviders": "此服务器未配置SSO提供商。", 173 - "ssoHint": "选择一个提供商来创建您的账户:", 174 155 "continueWith": "使用{provider}继续", 175 156 "validation": { 176 157 "handleRequired": "请输入用户名", ··· 680 661 "oauth": { 681 662 "login": { 682 663 "title": "登录", 683 - "subtitle": "登录以继续使用应用", 684 - "signingIn": "登录中...", 685 - "authenticating": "验证中...", 686 - "checkingPasskey": "检查通行密钥...", 687 - "signInWithPasskey": "使用通行密钥登录", 664 + "subtitle": "登录到", 665 + "signingIn": "登录中", 666 + "authenticating": "验证中", 667 + "checkingPasskey": "检查中", 668 + "signInWithPasskey": "通行密钥", 688 669 "passkeyNotSetUp": "未设置通行密钥", 689 - "orUsePassword": "或使用密码", 670 + "orUsePassword": "或", 690 671 "password": "密码", 691 672 "rememberDevice": "记住此设备", 692 - "passkeyHintChecking": "正在检查通行密钥状态...", 693 - "passkeyHintAvailable": "使用您的通行密钥登录", 694 - "passkeyHintNotAvailable": "此账户未注册通行密钥", 695 - "passkeyHint": "使用设备的生物识别或安全密钥", 696 - "passwordPlaceholder": "输入您的密码", 673 + "passkeyHintChecking": "检查中", 674 + "passkeyHintAvailable": "使用通行密钥", 675 + "passkeyHintNotAvailable": "未注册通行密钥", 676 + "passwordPlaceholder": "密码", 697 677 "usePasskey": "使用通行密钥", 698 - "orContinueWith": "或使用以下方式继续", 699 - "orUseCredentials": "或使用凭证登录" 678 + "orContinueWith": "或", 679 + "orUseCredentials": "或" 700 680 }, 701 681 "register": { 702 682 "title": "创建账户", 703 - "subtitle": "使用 {app} 创建账户", 704 - "subtitleGeneric": "创建账户以继续", 705 - "haveAccount": "已有账户?" 683 + "subtitle": "为", 684 + "subtitleGeneric": "创建账户", 685 + "haveAccount": "已有账户?登录" 706 686 }, 707 687 "sso": { 708 688 "linkedAccounts": "已关联账户", ··· 787 767 }, 788 768 "accounts": { 789 769 "title": "选择账户", 790 - "subtitle": "选择一个账户继续", 791 770 "useAnother": "使用其他账户" 792 771 }, 793 772 "twoFactor": { 794 - "title": "双重身份验证", 795 - "subtitle": "需要额外验证", 773 + "title": "验证", 796 774 "usePasskey": "使用通行密钥", 797 - "useTotp": "使用身份验证器" 775 + "useTotp": "使用验证器" 798 776 }, 799 777 "twoFactorCode": { 800 - "title": "双重身份验证", 801 - "subtitle": "验证码已发送到您的 {channel}。请在下方输入验证码继续。", 778 + "title": "验证", 779 + "subtitle": "验证码已发送到 {channel}", 802 780 "codeLabel": "验证码", 803 - "codePlaceholder": "输入6位验证码", 781 + "codePlaceholder": "6位验证码", 804 782 "errors": { 805 - "missingRequestUri": "缺少 request_uri 参数", 783 + "missingRequestUri": "缺少请求 URI", 806 784 "verificationFailed": "验证失败", 807 - "connectionFailed": "无法连接到服务器", 808 - "unexpectedResponse": "服务器返回意外响应" 785 + "connectionFailed": "连接失败", 786 + "unexpectedResponse": "意外响应" 809 787 } 810 788 }, 811 789 "totp": { 812 - "title": "输入验证码", 813 - "subtitle": "请输入身份验证器应用中的6位验证码", 814 - "codePlaceholder": "输入6位验证码", 815 - "useBackupCode": "使用备用验证码", 816 - "backupCodePlaceholder": "输入备用验证码", 790 + "title": "验证器验证码", 791 + "codePlaceholder": "6位验证码", 792 + "useBackupCode": "使用备用码", 793 + "backupCodePlaceholder": "备用码", 817 794 "trustDevice": "信任此设备30天", 818 - "hintBackupCode": "正在使用备用验证码", 819 - "hintTotpCode": "正在使用身份验证器验证码", 820 - "hintDefault": "身份验证器为6位数字,备用码为8位字符" 795 + "hintBackupCode": "备用码", 796 + "hintTotpCode": "验证器验证码" 821 797 }, 822 798 "passkey": { 823 - "title": "通行密钥验证", 824 - "subtitle": "使用您的通行密钥验证身份", 825 - "waiting": "等待通行密钥...", 826 - "useTotp": "改用身份验证器" 799 + "title": "通行密钥", 800 + "waiting": "等待中", 801 + "useTotp": "使用验证器" 827 802 }, 828 803 "error": { 829 - "title": "授权错误", 830 - "genericError": "授权过程中发生错误。", 804 + "title": "授权失败", 831 805 "tryAgain": "重试", 832 - "backToApp": "返回应用" 806 + "backToApp": "返回" 833 807 } 834 808 }, 835 809 "sso_register": { ··· 855 829 "subtitle": "我们已将验证码发送到您的{channel}。请在下方输入以完成注册。", 856 830 "tokenSubtitle": "输入验证码和接收验证码的标识符。", 857 831 "tokenTitle": "验证", 858 - "codePlaceholder": "XXXX-XXXX-XXXX-XXXX...", 832 + "codePlaceholder": "Paste verification code", 859 833 "codeLabel": "验证码", 860 - "codeHelp": "复制消息中的完整验证码,包括横线", 834 + "codeHelp": "复制消息中的完整验证码,", 861 835 "verifyButton": "验证账户", 862 836 "pleaseWait": "请稍候...", 863 837 "codeResent": "验证码已重新发送!", ··· 958 932 }, 959 933 "registerPasskey": { 960 934 "title": "创建通行密钥账户", 961 - "subtitle": "使用通行密钥创建超安全账户,无需密码。", 962 - "subtitleKeyChoice": "选择如何设置您的外部 did:web 身份。", 963 - "subtitleInitialDidDoc": "上传您的 DID 文档以继续。", 964 - "subtitleCreating": "正在创建您的账户...", 965 - "subtitlePasskey": "注册通行密钥以保护您的账户。", 966 - "subtitleAppPassword": "保存您的应用专用密码以使用第三方应用。", 967 - "subtitleVerify": "验证您的{channel}以继续。", 968 - "subtitleUpdatedDidDoc": "使用 PDS 签名密钥更新您的 DID 文档。", 969 - "subtitleActivating": "正在激活您的账户...", 970 - "subtitleComplete": "您的账户已成功创建!", 935 + "subtitleKeyChoice": "设置 did:web 身份", 936 + "subtitleInitialDidDoc": "上传 DID 文档", 937 + "subtitleCreating": "创建账户", 938 + "subtitlePasskey": "注册通行密钥", 939 + "subtitleAppPassword": "保存应用专用密码", 940 + "subtitleVerify": "验证{channel}", 941 + "subtitleUpdatedDidDoc": "更新 DID 文档", 942 + "subtitleActivating": "激活中", 943 + "subtitleComplete": "账户已创建", 971 944 "handle": "用户名", 972 945 "handlePlaceholder": "您的用户名", 973 946 "handleHint": "您的完整用户名将是:@{handle}", ··· 986 959 "wantTraditional": "想使用传统密码?", 987 960 "registerWithPassword": "使用密码注册", 988 961 "contactMethod": "联系方式", 989 - "contactMethodHint": "选择您希望如何验证账户和接收通知。", 990 962 "verificationMethod": "验证方式", 991 963 "identityType": "身份类型", 992 964 "identityTypeHint": "选择如何管理您的去中心化身份。", ··· 1008 980 "externalDid": "您的 did:web", 1009 981 "externalDidPlaceholder": "did:web:yourdomain.com", 1010 982 "externalDidHint": "您需要在以下地址提供 DID 文档", 1011 - "whyPasskeyOnly": "为什么选择仅通行密钥?", 1012 - "whyPasskeyOnlyDesc": "通行密钥账户比密码账户更安全,因为它们:", 1013 - "whyPasskeyBullet1": "无法被钓鱼或在数据泄露中被盗", 1014 - "whyPasskeyBullet2": "使用硬件支持的加密密钥", 1015 - "whyPasskeyBullet3": "需要您的生物识别或设备 PIN 才能使用", 1016 - "infoWhyPasskey": "为什么使用通行密钥?", 1017 - "infoWhyPasskeyDesc": "通行密钥是存储在您设备上的加密凭证。与密码不同,它们无法被钓鱼、猜测或在数据泄露中被盗。", 1018 - "infoHowItWorks": "工作原理", 1019 - "infoHowItWorksDesc": "登录时,您的设备会提示您使用 Face ID、Touch ID 或设备 PIN 进行验证。无需记住或输入密码。", 1020 - "infoAppAccess": "使用第三方应用", 1021 - "infoAppAccessDesc": "创建账户后,您将收到一个应用密码。使用它登录 Bluesky 应用和其他 AT Protocol 客户端。", 1022 - "passkeyNameLabel": "通行密钥名称(可选)", 1023 - "passkeyNamePlaceholder": "如 MacBook Touch ID", 1024 - "passkeyNameHint": "用于识别此通行密钥的友好名称", 1025 - "passkeyPrompt": "点击下方按钮创建通行密钥。系统会提示您使用:", 1026 - "passkeyPromptBullet1": "Touch ID 或 Face ID", 1027 - "passkeyPromptBullet2": "设备 PIN 或密码", 1028 - "passkeyPromptBullet3": "安全密钥(如果有的话)", 983 + "passkeyName": "Passkey Name", 984 + "passkeyNamePlaceholder": "MacBook Touch ID", 985 + "passkeyNameHint": "可选标识", 1029 986 "createPasskey": "创建通行密钥", 1030 987 "creatingPasskey": "正在创建通行密钥...", 1031 988 "redirecting": "正在跳转到控制台...", ··· 1044 1001 "passkeyCancelled": "通行密钥创建已取消", 1045 1002 "passkeyFailed": "通行密钥注册失败" 1046 1003 }, 1047 - "didWebWarning1Detail": "您的身份将是 {did}。" 1004 + "didWebWarning1Detail": "您的身份将是 {did}。", 1005 + "activatingAccount": "Activating", 1006 + "creatingAccount": "Creating account", 1007 + "passkeyDescription": "Register a passkey for this account", 1008 + "setupPasskey": "Create Passkey" 1048 1009 }, 1049 1010 "trustedDevices": { 1050 1011 "title": "受信任设备", ··· 1068 1029 "unknownDevice": "未知设备" 1069 1030 }, 1070 1031 "reauth": { 1071 - "title": "需要重新验证", 1072 - "subtitle": "请验证您的身份以继续。", 1032 + "title": "重新验证", 1073 1033 "password": "密码", 1074 1034 "totp": "TOTP", 1075 1035 "passkey": "通行密钥", 1076 1036 "authenticatorCode": "验证码", 1077 - "usePassword": "使用密码", 1078 - "usePasskey": "使用通行密钥", 1079 - "useTotp": "使用身份验证器", 1037 + "usePassword": "密码", 1038 + "usePasskey": "通行密钥", 1039 + "useTotp": "身份验证器", 1080 1040 "passwordPlaceholder": "输入您的密码", 1081 - "totpPlaceholder": "输入6位验证码", 1082 - "authenticating": "正在验证...", 1083 - "passkeyPrompt": "点击下方按钮使用通行密钥进行验证。", 1041 + "totpPlaceholder": "6位验证码", 1042 + "authenticating": "验证中", 1084 1043 "cancel": "取消" 1085 1044 }, 1086 1045 "verifyChannel": { ··· 1100 1059 "identifierPlaceholder": "邮箱、Discord ID 等", 1101 1060 "identifierHelp": "正在验证的邮箱地址、Discord ID、Telegram 用户名或 Signal 号码。", 1102 1061 "codeLabel": "验证码", 1103 - "codeHelp": "复制消息中的完整验证码,包括横线。", 1062 + "codeHelp": "复制消息中的完整验证码,。", 1104 1063 "verifyButton": "验证" 1105 1064 }, 1106 1065 "delegation": {
+62 -64
frontend/src/routes/Login.svelte
··· 156 156 {:else} 157 157 <header class="page-header"> 158 158 <h1>{$_('login.title')}</h1> 159 - <p class="subtitle">{savedAccounts.length > 0 ? $_('login.chooseAccount') : $_('login.subtitle')}</p> 159 + {#if savedAccounts.length > 0} 160 + <p class="subtitle">{$_('login.chooseAccount')}</p> 161 + {/if} 160 162 </header> 161 163 162 - <div class="split-layout sidebar-right"> 163 - <div class="main-section"> 164 - {#if savedAccounts.length > 0} 165 - <div class="saved-accounts"> 166 - {#each savedAccounts as account} 167 - <div 168 - class="account-item" 169 - class:disabled={submitting} 170 - role="button" 171 - tabindex="0" 172 - onclick={() => !submitting && handleSwitchAccount(account.did)} 173 - onkeydown={(e) => e.key === 'Enter' && !submitting && handleSwitchAccount(account.did)} 174 - > 175 - <div class="account-info"> 176 - <span class="account-handle">@{account.handle}</span> 177 - <span class="account-did">{account.did}</span> 178 - </div> 179 - <button 180 - type="button" 181 - class="forget-btn" 182 - onclick={(e) => handleForgetAccount(account.did, e)} 183 - title={$_('login.removeAccount')} 184 - > 185 - &times; 186 - </button> 164 + <div class="login-content"> 165 + {#if savedAccounts.length > 0} 166 + <div class="saved-accounts" class:grid={savedAccounts.length > 1}> 167 + {#each savedAccounts as account} 168 + <div 169 + class="account-item" 170 + class:disabled={submitting} 171 + role="button" 172 + tabindex="0" 173 + onclick={() => !submitting && handleSwitchAccount(account.did)} 174 + onkeydown={(e) => e.key === 'Enter' && !submitting && handleSwitchAccount(account.did)} 175 + > 176 + <div class="account-info"> 177 + <span class="account-handle">@{account.handle}</span> 178 + <span class="account-did">{account.did}</span> 187 179 </div> 188 - {/each} 189 - </div> 190 - 191 - <p class="or-divider">{$_('login.signInToAnother')}</p> 192 - {/if} 193 - 194 - <button type="button" class="oauth-btn" onclick={handleOAuthLogin} disabled={submitting || loading}> 195 - {submitting ? $_('login.redirecting') : $_('login.button')} 196 - </button> 197 - 198 - <p class="forgot-links"> 199 - <a href="/app/reset-password">{$_('login.forgotPassword')}</a> 200 - <span class="separator">&middot;</span> 201 - <a href="/app/request-passkey-recovery">{$_('login.lostPasskey')}</a> 202 - </p> 203 - 204 - <p class="link-text"> 205 - {$_('login.noAccount')} <a href="/app/register">{$_('login.createAccount')}</a> 206 - </p> 207 - </div> 180 + <button 181 + type="button" 182 + class="forget-btn" 183 + onclick={(e) => handleForgetAccount(account.did, e)} 184 + title={$_('login.removeAccount')} 185 + > 186 + &times; 187 + </button> 188 + </div> 189 + {/each} 190 + </div> 208 191 209 - <aside class="info-panel"> 210 - {#if savedAccounts.length > 0} 211 - <h3>{$_('login.infoSavedAccountsTitle')}</h3> 212 - <p>{$_('login.infoSavedAccountsDesc')}</p> 192 + <p class="or-divider">{$_('login.signInToAnother')}</p> 193 + {/if} 213 194 214 - <h3>{$_('login.infoNewAccountTitle')}</h3> 215 - <p>{$_('login.infoNewAccountDesc')}</p> 216 - {:else} 217 - <h3>{$_('login.infoSecureSignInTitle')}</h3> 218 - <p>{$_('login.infoSecureSignInDesc')}</p> 195 + <button type="button" class="oauth-btn" onclick={handleOAuthLogin} disabled={submitting || loading}> 196 + {submitting ? $_('login.redirecting') : $_('login.button')} 197 + </button> 219 198 220 - <h3>{$_('login.infoStaySignedInTitle')}</h3> 221 - <p>{$_('login.infoStaySignedInDesc')}</p> 222 - {/if} 199 + <p class="forgot-links"> 200 + <a href="/app/reset-password">{$_('login.forgotPassword')}</a> 201 + <span class="separator">&middot;</span> 202 + <a href="/app/request-passkey-recovery">{$_('login.lostPasskey')}</a> 203 + </p> 223 204 224 - <h3>{$_('login.infoRecoveryTitle')}</h3> 225 - <p>{$_('login.infoRecoveryDesc')}</p> 226 - </aside> 205 + <p class="link-text"> 206 + {$_('login.noAccount')} <a href="/app/register">{$_('login.createAccount')}</a> 207 + </p> 227 208 </div> 228 209 {/if} 229 210 </div> ··· 237 218 238 219 .page-header { 239 220 margin-bottom: var(--space-6); 221 + text-align: center; 240 222 } 241 223 242 224 h1 { ··· 248 230 margin: 0; 249 231 } 250 232 251 - .main-section { 252 - min-width: 0; 233 + .login-content { 234 + max-width: var(--width-md); 235 + margin: 0 auto; 253 236 } 254 237 255 238 form { ··· 257 240 flex-direction: column; 258 241 gap: var(--space-4); 259 242 max-width: var(--width-sm); 243 + margin: 0 auto; 260 244 } 261 245 262 246 .actions { ··· 286 270 margin-top: var(--space-4); 287 271 font-size: var(--text-sm); 288 272 color: var(--text-secondary); 273 + text-align: center; 289 274 } 290 275 291 276 .forgot-links a { ··· 300 285 margin-top: var(--space-6); 301 286 font-size: var(--text-sm); 302 287 color: var(--text-secondary); 288 + text-align: center; 303 289 } 304 290 305 291 .link-text a { ··· 313 299 margin-bottom: var(--space-5); 314 300 } 315 301 302 + .saved-accounts.grid { 303 + display: grid; 304 + grid-template-columns: 1fr; 305 + } 306 + 307 + @media (min-width: 700px) { 308 + .saved-accounts.grid { 309 + grid-template-columns: repeat(2, 1fr); 310 + } 311 + } 312 + 316 313 .account-item { 317 314 display: flex; 318 315 align-items: center; ··· 339 336 display: flex; 340 337 flex-direction: column; 341 338 gap: var(--space-1); 339 + min-width: 0; 342 340 } 343 341 344 342 .account-handle { ··· 352 350 font-family: var(--font-mono); 353 351 overflow: hidden; 354 352 text-overflow: ellipsis; 355 - max-width: 250px; 356 353 } 357 354 358 355 .forget-btn { 356 + flex-shrink: 0; 359 357 padding: var(--space-2) var(--space-3); 360 358 background: transparent; 361 359 border: none;
+1 -7
frontend/src/routes/OAuthAccounts.svelte
··· 124 124 </div> 125 125 {:else} 126 126 <h1>{$_('oauth.accounts.title')}</h1> 127 - <p class="subtitle">{$_('oauth.accounts.subtitle')}</p> 128 127 129 128 <div class="accounts-list"> 130 129 {#each accounts as account} ··· 156 155 } 157 156 158 157 h1 { 159 - margin: 0 0 var(--space-2) 0; 160 - } 161 - 162 - .subtitle { 163 - color: var(--text-secondary); 164 - margin: 0 0 var(--space-7) 0; 158 + margin: 0 0 var(--space-6) 0; 165 159 } 166 160 167 161 .loading {
+7 -14
frontend/src/routes/OAuthLogin.svelte
··· 8 8 type WebAuthnRequestOptionsResponse, 9 9 } from '../lib/webauthn' 10 10 import SsoIcon from '../components/SsoIcon.svelte' 11 + import { getRandomHandle } from '../components/RandomHandle.svelte' 12 + 13 + const handlePlaceholder = getRandomHandle() 11 14 12 15 interface SsoProvider { 13 16 provider: string ··· 345 348 <div class="page-sm"> 346 349 <header class="page-header"> 347 350 <h1>{$_('oauth.login.title')}</h1> 348 - <p class="subtitle"> 349 - {#if clientName} 350 - {$_('oauth.login.subtitle')} <strong>{clientName}</strong> 351 - {:else} 352 - {$_('oauth.login.subtitle')} 353 - {/if} 354 - </p> 351 + {#if clientName} 352 + <p class="subtitle">{$_('oauth.login.subtitle')} <strong>{clientName}</strong></p> 353 + {/if} 355 354 </header> 356 355 357 356 {#if error} ··· 365 364 id="username" 366 365 type="text" 367 366 bind:value={username} 368 - placeholder={$_('register.emailPlaceholder')} 367 + placeholder={handlePlaceholder} 369 368 disabled={submitting} 370 369 required 371 370 autocomplete="username" ··· 426 425 {/if} 427 426 </span> 428 427 </button> 429 - <p class="method-hint">{$_('oauth.login.passkeyHint')}</p> 430 428 </div> 431 429 432 430 {#if hasPassword} ··· 572 570 letter-spacing: 0.05em; 573 571 } 574 572 575 - .method-hint { 576 - margin: 0; 577 - font-size: var(--text-xs); 578 - color: var(--text-muted); 579 - } 580 573 581 574 .method-divider { 582 575 display: flex;
+1 -9
frontend/src/routes/OAuthPasskey.svelte
··· 119 119 120 120 <div class="oauth-passkey-container"> 121 121 <h1>{t('oauth.passkey.title')}</h1> 122 - <p class="subtitle"> 123 - {t('oauth.passkey.subtitle')} 124 - </p> 125 122 126 123 {#if error} 127 124 <div class="error">{error}</div> ··· 156 153 } 157 154 158 155 h1 { 159 - margin: 0 0 0.5rem 0; 160 - } 161 - 162 - .subtitle { 163 - color: var(--text-secondary); 164 - margin: 0 0 2rem 0; 156 + margin: 0 0 1.5rem 0; 165 157 } 166 158 167 159 .error {
+6 -18
frontend/src/routes/OAuthTotp.svelte
··· 74 74 75 75 <div class="oauth-totp-container"> 76 76 <h1>{$_('oauth.totp.title')}</h1> 77 - <p class="subtitle"> 78 - {$_('oauth.totp.subtitle')} 79 - </p> 80 77 81 78 {#if error} 82 79 <div class="error">{error}</div> ··· 96 93 autocomplete="one-time-code" 97 94 autocapitalize="characters" 98 95 /> 99 - <p class="hint"> 100 - {#if isBackupCode} 101 - {$_('oauth.totp.hintBackupCode')} 102 - {:else if isTotpCode} 103 - {$_('oauth.totp.hintTotpCode')} 104 - {:else} 105 - {$_('oauth.totp.hintDefault')} 106 - {/if} 107 - </p> 96 + {#if isBackupCode || isTotpCode} 97 + <p class="hint"> 98 + {isBackupCode ? $_('oauth.totp.hintBackupCode') : $_('oauth.totp.hintTotpCode')} 99 + </p> 100 + {/if} 108 101 </div> 109 102 110 103 <label class="trust-device-label"> ··· 135 128 } 136 129 137 130 h1 { 138 - margin: 0 0 var(--space-2) 0; 139 - } 140 - 141 - .subtitle { 142 - color: var(--text-secondary); 143 - margin: 0 0 var(--space-7) 0; 131 + margin: 0 0 var(--space-6) 0; 144 132 } 145 133 146 134 form {
+199 -145
frontend/src/routes/Register.svelte
··· 31 31 let flow = $state<ReturnType<typeof createRegistrationFlow> | null>(null) 32 32 let passkeyName = $state('') 33 33 let clientName = $state<string | null>(null) 34 + let checkHandleTimeout: ReturnType<typeof setTimeout> | null = null 35 + 36 + $effect(() => { 37 + if (!flow) return 38 + const handle = flow.info.handle 39 + if (checkHandleTimeout) { 40 + clearTimeout(checkHandleTimeout) 41 + } 42 + if (handle.length >= 3 && !handle.includes('.')) { 43 + checkHandleTimeout = setTimeout(() => flow?.checkHandleAvailability(handle), 400) 44 + } 45 + }) 34 46 35 47 $effect(() => { 36 48 if (!serverInfoLoaded) { ··· 284 296 body: JSON.stringify({ request_uri: requestUri }) 285 297 }) 286 298 299 + if (!response.ok) { 300 + window.history.back() 301 + return 302 + } 303 + 287 304 const data = await response.json() 288 305 if (data.redirect_uri) { 289 306 window.location.href = data.redirect_uri 307 + } else { 308 + window.history.back() 290 309 } 291 - } catch (err) { 292 - console.error('OAuth deny failed:', err) 310 + } catch { 293 311 window.history.back() 294 312 } 295 313 } ··· 313 331 {:else if flow} 314 332 <header class="page-header"> 315 333 <h1>{$_('oauth.register.title')}</h1> 316 - <p class="subtitle"> 317 - {#if clientName} 318 - {$_('oauth.register.subtitle')} <strong>{clientName}</strong> 319 - {:else} 320 - {$_('oauth.register.subtitleGeneric')} 321 - {/if} 322 - </p> 334 + {#if clientName} 335 + <p class="subtitle">{$_('oauth.register.subtitle')} <strong>{clientName}</strong></p> 336 + {/if} 323 337 </header> 324 338 325 339 {#if flow.state.error} ··· 340 354 341 355 <AccountTypeSwitcher active="passkey" {ssoAvailable} oauthRequestUri={getRequestUriFromUrl()} /> 342 356 343 - <div class="split-layout"> 344 - <div class="form-section"> 345 - <form onsubmit={handleInfoSubmit}> 357 + <form class="register-form" onsubmit={handleInfoSubmit}> 346 358 <div class="field"> 347 359 <label for="handle">{$_('register.handle')}</label> 348 360 <input ··· 354 366 required 355 367 autocomplete="off" 356 368 /> 357 - {#if fullHandle()} 369 + {#if flow.info.handle.includes('.')} 370 + <p class="hint warning">{$_('register.handleDotWarning')}</p> 371 + {:else if flow.state.checkingHandle} 372 + <p class="hint">{$_('common.checking')}</p> 373 + {:else if flow.state.handleAvailable === false} 374 + <p class="hint warning">{$_('register.handleTaken')}</p> 375 + {:else if flow.state.handleAvailable === true && fullHandle()} 376 + <p class="hint success">{$_('register.handleHint', { values: { handle: fullHandle() } })}</p> 377 + {:else if fullHandle()} 358 378 <p class="hint">{$_('register.handleHint', { values: { handle: fullHandle() } })}</p> 359 379 {/if} 360 380 </div> 361 381 362 - <fieldset> 363 - <legend>{$_('register.contactMethod')}</legend> 364 - <div class="contact-fields"> 365 - <div class="field"> 366 - <label for="verification-channel">{$_('register.verificationMethod')}</label> 367 - <select id="verification-channel" bind:value={flow.info.verificationChannel} disabled={flow.state.submitting}> 368 - <option value="email">{channelLabel('email')}</option> 369 - {#if isChannelAvailable('discord')} 370 - <option value="discord">{channelLabel('discord')}</option> 371 - {/if} 372 - {#if isChannelAvailable('telegram')} 373 - <option value="telegram">{channelLabel('telegram')}</option> 374 - {/if} 375 - {#if isChannelAvailable('signal')} 376 - <option value="signal">{channelLabel('signal')}</option> 377 - {/if} 378 - </select> 379 - </div> 380 - 381 - {#if flow.info.verificationChannel === 'email'} 382 - <div class="field"> 383 - <label for="email">{$_('register.emailAddress')}</label> 384 - <input 385 - id="email" 386 - type="email" 387 - bind:value={flow.info.email} 388 - placeholder={$_('register.emailPlaceholder')} 389 - disabled={flow.state.submitting} 390 - required 391 - /> 392 - </div> 393 - {:else if flow.info.verificationChannel === 'discord'} 394 - <div class="field"> 395 - <label for="discord-id">{$_('register.discordId')}</label> 396 - <input 397 - id="discord-id" 398 - type="text" 399 - bind:value={flow.info.discordId} 400 - placeholder={$_('register.discordIdPlaceholder')} 401 - disabled={flow.state.submitting} 402 - required 403 - /> 404 - <p class="hint">{$_('register.discordIdHint')}</p> 405 - </div> 406 - {:else if flow.info.verificationChannel === 'telegram'} 407 - <div class="field"> 408 - <label for="telegram-username">{$_('register.telegramUsername')}</label> 409 - <input 410 - id="telegram-username" 411 - type="text" 412 - bind:value={flow.info.telegramUsername} 413 - placeholder={$_('register.telegramUsernamePlaceholder')} 414 - disabled={flow.state.submitting} 415 - required 416 - /> 417 - </div> 418 - {:else if flow.info.verificationChannel === 'signal'} 419 - <div class="field"> 420 - <label for="signal-number">{$_('register.signalNumber')}</label> 421 - <input 422 - id="signal-number" 423 - type="tel" 424 - bind:value={flow.info.signalNumber} 425 - placeholder={$_('register.signalNumberPlaceholder')} 426 - disabled={flow.state.submitting} 427 - required 428 - /> 429 - <p class="hint">{$_('register.signalNumberHint')}</p> 430 - </div> 382 + <div class="field"> 383 + <label for="verification-channel">{$_('register.verificationMethod')}</label> 384 + <select id="verification-channel" bind:value={flow.info.verificationChannel} disabled={flow.state.submitting}> 385 + <option value="email">{channelLabel('email')}</option> 386 + {#if isChannelAvailable('discord')} 387 + <option value="discord">{channelLabel('discord')}</option> 431 388 {/if} 389 + {#if isChannelAvailable('telegram')} 390 + <option value="telegram">{channelLabel('telegram')}</option> 391 + {/if} 392 + {#if isChannelAvailable('signal')} 393 + <option value="signal">{channelLabel('signal')}</option> 394 + {/if} 395 + </select> 396 + </div> 397 + 398 + {#if flow.info.verificationChannel === 'email'} 399 + <div class="field"> 400 + <label for="email">{$_('register.emailAddress')}</label> 401 + <input 402 + id="email" 403 + type="email" 404 + bind:value={flow.info.email} 405 + placeholder={$_('register.emailPlaceholder')} 406 + disabled={flow.state.submitting} 407 + required 408 + /> 432 409 </div> 433 - </fieldset> 410 + {:else if flow.info.verificationChannel === 'discord'} 411 + <div class="field"> 412 + <label for="discord-id">{$_('register.discordId')}</label> 413 + <input 414 + id="discord-id" 415 + type="text" 416 + bind:value={flow.info.discordId} 417 + placeholder={$_('register.discordIdPlaceholder')} 418 + disabled={flow.state.submitting} 419 + required 420 + /> 421 + <p class="hint">{$_('register.discordIdHint')}</p> 422 + </div> 423 + {:else if flow.info.verificationChannel === 'telegram'} 424 + <div class="field"> 425 + <label for="telegram-username">{$_('register.telegramUsername')}</label> 426 + <input 427 + id="telegram-username" 428 + type="text" 429 + bind:value={flow.info.telegramUsername} 430 + placeholder={$_('register.telegramUsernamePlaceholder')} 431 + disabled={flow.state.submitting} 432 + required 433 + /> 434 + </div> 435 + {:else if flow.info.verificationChannel === 'signal'} 436 + <div class="field"> 437 + <label for="signal-number">{$_('register.signalNumber')}</label> 438 + <input 439 + id="signal-number" 440 + type="tel" 441 + bind:value={flow.info.signalNumber} 442 + placeholder={$_('register.signalNumberPlaceholder')} 443 + disabled={flow.state.submitting} 444 + required 445 + /> 446 + <p class="hint">{$_('register.signalNumberHint')}</p> 447 + </div> 448 + {/if} 434 449 435 - <fieldset> 450 + <fieldset class="identity-section"> 436 451 <legend>{$_('registerPasskey.identityType')}</legend> 437 - <p class="section-hint">{$_('registerPasskey.identityTypeHint')}</p> 438 452 <div class="radio-group"> 439 453 <label class="radio-label"> 440 454 <input type="radio" name="didType" value="plc" bind:group={flow.info.didType} disabled={flow.state.submitting} /> ··· 462 476 </span> 463 477 </label> 464 478 </div> 465 - {#if flow.info.didType === 'web'} 466 - <div class="warning-box"> 467 - <strong>{$_('registerPasskey.didWebWarningTitle')}</strong> 468 - <ul> 469 - <li><strong>{$_('registerPasskey.didWebWarning1')}</strong> {@html $_('registerPasskey.didWebWarning1Detail', { values: { did: `<code>did:web:yourhandle.${serverInfo?.availableUserDomains?.[0] || 'this-pds.com'}</code>` } })}</li> 470 - <li><strong>{$_('registerPasskey.didWebWarning2')}</strong> {$_('registerPasskey.didWebWarning2Detail')}</li> 479 + </fieldset> 480 + 481 + {#if flow.info.didType === 'web'} 482 + <div class="warning-box"> 483 + <strong>{$_('registerPasskey.didWebWarningTitle')}</strong> 484 + <ul> 485 + <li><strong>{$_('registerPasskey.didWebWarning1')}</strong> {@html $_('registerPasskey.didWebWarning1Detail', { values: { did: `<code>did:web:yourhandle.${serverInfo?.availableUserDomains?.[0] || 'this-pds.com'}</code>` } })}</li> 486 + <li><strong>{$_('registerPasskey.didWebWarning2')}</strong> {$_('registerPasskey.didWebWarning2Detail')}</li> 487 + {#if $_('registerPasskey.didWebWarning3')} 471 488 <li><strong>{$_('registerPasskey.didWebWarning3')}</strong> {$_('registerPasskey.didWebWarning3Detail')}</li> 472 - <li><strong>{$_('registerPasskey.didWebWarning4')}</strong> {$_('registerPasskey.didWebWarning4Detail')}</li> 473 - </ul> 474 - </div> 475 - {/if} 476 - {#if flow.info.didType === 'web-external'} 477 - <div class="field"> 478 - <label for="external-did">{$_('registerPasskey.externalDid')}</label> 479 - <input id="external-did" type="text" bind:value={flow.info.externalDid} placeholder={$_('registerPasskey.externalDidPlaceholder')} disabled={flow.state.submitting} required /> 480 - <p class="hint">{$_('registerPasskey.externalDidHint')} <code>https://{flow.info.externalDid ? flow.extractDomain(flow.info.externalDid) : 'yourdomain.com'}/.well-known/did.json</code></p> 481 - </div> 482 - {/if} 483 - </fieldset> 489 + {/if} 490 + </ul> 491 + </div> 492 + {/if} 493 + 494 + {#if flow.info.didType === 'web-external'} 495 + <div class="field"> 496 + <label for="external-did">{$_('registerPasskey.externalDid')}</label> 497 + <input id="external-did" type="text" bind:value={flow.info.externalDid} placeholder={$_('registerPasskey.externalDidPlaceholder')} disabled={flow.state.submitting} required /> 498 + <p class="hint">{$_('registerPasskey.externalDidHint')} <code>https://{flow.info.externalDid ? flow.extractDomain(flow.info.externalDid) : 'yourdomain.com'}/.well-known/did.json</code></p> 499 + </div> 500 + {/if} 484 501 485 502 {#if serverInfo?.inviteCodeRequired} 486 503 <div class="field"> 487 - <label for="invite-code">{$_('register.inviteCode')} <span class="required">*</span></label> 504 + <label for="invite-code">{$_('register.inviteCode')}</label> 488 505 <input 489 506 id="invite-code" 490 507 type="text" ··· 496 513 </div> 497 514 {/if} 498 515 499 - <div class="actions"> 500 - <button type="submit" class="primary" disabled={flow.state.submitting}> 516 + <div class="form-actions"> 517 + <button type="button" class="secondary" onclick={handleCancel} disabled={flow.state.submitting}> 518 + {$_('common.cancel')} 519 + </button> 520 + <button type="submit" class="primary" disabled={flow.state.submitting || flow.state.handleAvailable === false || flow.state.checkingHandle}> 501 521 {flow.state.submitting ? $_('common.loading') : $_('common.continue')} 502 522 </button> 503 523 </div> 504 - 505 - <div class="secondary-actions"> 506 - <button type="button" class="link" onclick={goToLogin}> 507 - {$_('oauth.register.haveAccount')} 508 - </button> 509 - <button type="button" class="link" onclick={handleCancel}> 510 - {$_('common.cancel')} 511 - </button> 512 - </div> 513 - </form> 514 - 515 - <div class="form-links"> 516 - <p class="link-text"> 517 - {$_('register.alreadyHaveAccount')} <a href="/app/login">{$_('register.signIn')}</a> 518 - </p> 519 - </div> 520 - </div> 521 - 522 - <aside class="info-panel"> 523 - <h3>{$_('registerPasskey.infoWhyPasskey')}</h3> 524 - <p>{$_('registerPasskey.infoWhyPasskeyDesc')}</p> 525 - 526 - <h3>{$_('registerPasskey.infoHowItWorks')}</h3> 527 - <p>{$_('registerPasskey.infoHowItWorksDesc')}</p> 528 - 529 - <h3>{$_('registerPasskey.infoAppAccess')}</h3> 530 - <p>{$_('registerPasskey.infoAppAccessDesc')}</p> 531 - </aside> 532 - </div> 524 + </form> 533 525 534 526 {:else if flow.state.step === 'key-choice'} 535 527 <KeyChoiceStep {flow} /> ··· 589 581 </div> 590 582 591 583 <style> 592 - form { 584 + .register-form { 585 + display: flex; 586 + flex-direction: column; 587 + gap: var(--space-3); 588 + max-width: 500px; 589 + } 590 + 591 + .identity-section { 592 + border: 1px solid var(--border-color); 593 + border-radius: var(--radius-md); 594 + padding: var(--space-4); 595 + margin: 0; 596 + margin-top: var(--space-5); 597 + } 598 + 599 + .identity-section legend { 600 + font-weight: var(--font-medium); 601 + font-size: var(--text-sm); 602 + padding: 0 var(--space-2); 603 + } 604 + 605 + .radio-group { 593 606 display: flex; 594 607 flex-direction: column; 595 - gap: var(--space-5); 608 + gap: var(--space-3); 596 609 } 597 610 598 - .actions { 611 + .radio-label { 599 612 display: flex; 600 - gap: var(--space-4); 601 - margin-top: var(--space-2); 613 + align-items: flex-start; 614 + gap: var(--space-2); 615 + cursor: pointer; 602 616 } 603 617 604 - .actions button { 605 - flex: 1; 618 + .radio-label.disabled { 619 + opacity: 0.5; 620 + cursor: not-allowed; 606 621 } 607 622 608 - .secondary-actions { 623 + .radio-label input { 624 + margin-top: 2px; 625 + } 626 + 627 + .radio-content { 609 628 display: flex; 610 - justify-content: center; 629 + flex-direction: column; 630 + gap: var(--space-1); 631 + } 632 + 633 + .radio-hint { 634 + font-size: var(--text-sm); 635 + color: var(--text-secondary); 636 + } 637 + 638 + .radio-hint.disabled-hint { 639 + color: var(--text-muted); 640 + } 641 + 642 + .warning-box { 643 + padding: var(--space-4); 644 + background: var(--warning-bg); 645 + border: 1px solid var(--warning-border); 646 + border-radius: var(--radius-md); 647 + } 648 + 649 + .warning-box ul { 650 + margin: var(--space-2) 0 0 0; 651 + padding-left: var(--space-5); 652 + } 653 + 654 + .warning-box li { 655 + margin-top: var(--space-2); 656 + } 657 + 658 + .form-actions { 659 + display: flex; 611 660 gap: var(--space-4); 612 - margin-top: var(--space-4); 661 + margin-top: var(--space-5); 662 + } 663 + 664 + .form-actions .primary { 665 + flex: 1; 613 666 } 614 667 615 668 .passkey-step { 616 669 display: flex; 617 670 flex-direction: column; 618 671 gap: var(--space-4); 672 + max-width: 500px; 619 673 } 620 674 621 675 .passkey-step h2 {
+319 -241
frontend/src/routes/RegisterPassword.svelte
··· 25 25 let flow = $state<ReturnType<typeof createRegistrationFlow> | null>(null) 26 26 let confirmPassword = $state('') 27 27 let clientName = $state<string | null>(null) 28 + let checkHandleTimeout: ReturnType<typeof setTimeout> | null = null 29 + 30 + $effect(() => { 31 + if (!flow) return 32 + const handle = flow.info.handle 33 + if (checkHandleTimeout) { 34 + clearTimeout(checkHandleTimeout) 35 + } 36 + if (handle.length >= 3 && !handle.includes('.')) { 37 + checkHandleTimeout = setTimeout(() => flow?.checkHandleAvailability(handle), 400) 38 + } 39 + }) 28 40 29 41 $effect(() => { 30 42 if (!serverInfoLoaded) { ··· 176 188 body: JSON.stringify({ 177 189 request_uri: requestUri, 178 190 did: flow.account.did, 179 - app_password: flow.account.appPassword, 191 + app_password: flow.account.appPassword || flow.info.password, 180 192 }), 181 193 }) 182 194 ··· 226 238 return did.replace('did:web:', '').replace(/%3A/g, ':') 227 239 } 228 240 229 - function getSubtitle(): string { 230 - if (!flow) return '' 231 - switch (flow.state.step) { 232 - case 'info': return $_('register.subtitle') 233 - case 'key-choice': return $_('register.subtitleKeyChoice') 234 - case 'initial-did-doc': return $_('register.subtitleInitialDidDoc') 235 - case 'creating': return $_('common.creating') 236 - case 'verify': return $_('register.subtitleVerify', { values: { channel: channelLabel(flow.info.verificationChannel) } }) 237 - case 'updated-did-doc': return $_('register.subtitleUpdatedDidDoc') 238 - case 'activating': return $_('register.subtitleActivating') 239 - case 'redirect-to-dashboard': return $_('register.subtitleComplete') 240 - default: return '' 241 + async function handleCancel() { 242 + const requestUri = getRequestUriFromUrl() 243 + if (!requestUri) { 244 + window.history.back() 245 + return 246 + } 247 + 248 + try { 249 + const response = await fetch('/oauth/authorize/deny', { 250 + method: 'POST', 251 + headers: { 252 + 'Content-Type': 'application/json', 253 + 'Accept': 'application/json' 254 + }, 255 + body: JSON.stringify({ request_uri: requestUri }) 256 + }) 257 + 258 + if (!response.ok) { 259 + window.history.back() 260 + return 261 + } 262 + 263 + const data = await response.json() 264 + if (data.redirect_uri) { 265 + window.location.href = data.redirect_uri 266 + } else { 267 + window.history.back() 268 + } 269 + } catch { 270 + window.history.back() 241 271 } 242 272 } 243 273 </script> ··· 245 275 <div class="page"> 246 276 <header class="page-header"> 247 277 <h1>{$_('register.title')}</h1> 248 - <p class="subtitle">{getSubtitle()}</p> 249 278 {#if clientName} 250 - <p class="client-name">{$_('oauth.login.subtitle')} <strong>{clientName}</strong></p> 279 + <p class="subtitle">{$_('oauth.register.subtitle')} <strong>{clientName}</strong></p> 251 280 {/if} 252 281 </header> 253 282 ··· 273 302 274 303 <AccountTypeSwitcher active="password" {ssoAvailable} oauthRequestUri={getRequestUriFromUrl()} /> 275 304 276 - <div class="split-layout sidebar-right"> 277 - <div class="form-section"> 278 - <form onsubmit={handleInfoSubmit}> 279 - <div class="field"> 280 - <label for="handle">{$_('register.handle')}</label> 281 - <input 282 - id="handle" 283 - type="text" 284 - bind:value={flow.info.handle} 285 - placeholder={$_('register.handlePlaceholder')} 286 - disabled={flow.state.submitting} 287 - required 288 - /> 289 - {#if flow.info.handle.includes('.')} 290 - <p class="hint warning">{$_('register.handleDotWarning')}</p> 291 - {:else if fullHandle()} 292 - <p class="hint">{$_('register.handleHint', { values: { handle: fullHandle() } })}</p> 293 - {/if} 294 - </div> 295 - 296 - <div class="form-row"> 297 - <div class="field"> 298 - <label for="password">{$_('register.password')}</label> 299 - <input 300 - id="password" 301 - type="password" 302 - bind:value={flow.info.password} 303 - placeholder={$_('register.passwordPlaceholder')} 304 - disabled={flow.state.submitting} 305 - required 306 - minlength="8" 307 - /> 308 - </div> 309 - 310 - <div class="field"> 311 - <label for="confirm-password">{$_('register.confirmPassword')}</label> 312 - <input 313 - id="confirm-password" 314 - type="password" 315 - bind:value={confirmPassword} 316 - placeholder={$_('register.confirmPasswordPlaceholder')} 317 - disabled={flow.state.submitting} 318 - required 319 - /> 320 - </div> 321 - </div> 305 + <form class="register-form" onsubmit={handleInfoSubmit}> 306 + <div class="field"> 307 + <label for="handle">{$_('register.handle')}</label> 308 + <input 309 + id="handle" 310 + type="text" 311 + bind:value={flow.info.handle} 312 + placeholder={$_('register.handlePlaceholder')} 313 + disabled={flow.state.submitting} 314 + required 315 + /> 316 + {#if flow.info.handle.includes('.')} 317 + <p class="hint warning">{$_('register.handleDotWarning')}</p> 318 + {:else if flow.state.checkingHandle} 319 + <p class="hint">{$_('common.checking')}</p> 320 + {:else if flow.state.handleAvailable === false} 321 + <p class="hint warning">{$_('register.handleTaken')}</p> 322 + {:else if flow.state.handleAvailable === true && fullHandle()} 323 + <p class="hint success">{$_('register.handleHint', { values: { handle: fullHandle() } })}</p> 324 + {:else if fullHandle()} 325 + <p class="hint">{$_('register.handleHint', { values: { handle: fullHandle() } })}</p> 326 + {/if} 327 + </div> 322 328 323 - <fieldset class="section-fieldset"> 324 - <legend>{$_('register.identityType')}</legend> 325 - <div class="radio-group"> 326 - <label class="radio-label"> 327 - <input type="radio" name="didType" value="plc" bind:group={flow.info.didType} disabled={flow.state.submitting} /> 328 - <span class="radio-content"> 329 - <strong>{$_('register.didPlc')}</strong> {$_('register.didPlcRecommended')} 330 - <span class="radio-hint">{$_('register.didPlcHint')}</span> 331 - </span> 332 - </label> 333 - 334 - <label class="radio-label" class:disabled={serverInfo?.selfHostedDidWebEnabled === false}> 335 - <input type="radio" name="didType" value="web" bind:group={flow.info.didType} disabled={flow.state.submitting || serverInfo?.selfHostedDidWebEnabled === false} /> 336 - <span class="radio-content"> 337 - <strong>{$_('register.didWeb')}</strong> 338 - {#if serverInfo?.selfHostedDidWebEnabled === false} 339 - <span class="radio-hint disabled-hint">{$_('register.didWebDisabledHint')}</span> 340 - {:else} 341 - <span class="radio-hint">{$_('register.didWebHint')}</span> 342 - {/if} 343 - </span> 344 - </label> 329 + <div class="field"> 330 + <label for="password">{$_('register.password')}</label> 331 + <input 332 + id="password" 333 + type="password" 334 + bind:value={flow.info.password} 335 + placeholder={$_('register.passwordPlaceholder')} 336 + disabled={flow.state.submitting} 337 + required 338 + minlength="8" 339 + /> 340 + </div> 345 341 346 - <label class="radio-label"> 347 - <input type="radio" name="didType" value="web-external" bind:group={flow.info.didType} disabled={flow.state.submitting} /> 348 - <span class="radio-content"> 349 - <strong>{$_('register.didWebBYOD')}</strong> 350 - <span class="radio-hint">{$_('register.didWebBYODHint')}</span> 351 - </span> 352 - </label> 353 - </div> 342 + <div class="field"> 343 + <label for="confirm-password">{$_('register.confirmPassword')}</label> 344 + <input 345 + id="confirm-password" 346 + type="password" 347 + bind:value={confirmPassword} 348 + placeholder={$_('register.confirmPasswordPlaceholder')} 349 + disabled={flow.state.submitting} 350 + required 351 + /> 352 + </div> 354 353 355 - {#if flow.info.didType === 'web'} 356 - <div class="warning-box"> 357 - <strong>{$_('register.didWebWarningTitle')}</strong> 358 - <ul> 359 - <li><strong>{$_('register.didWebWarning1')}</strong> {$_('register.didWebWarning1Detail', { values: { did: `did:web:yourhandle.${serverInfo?.availableUserDomains?.[0] || 'this-pds.com'}` } })}</li> 360 - <li><strong>{$_('register.didWebWarning2')}</strong> {$_('register.didWebWarning2Detail')}</li> 361 - <li><strong>{$_('register.didWebWarning3')}</strong> {$_('register.didWebWarning3Detail')}</li> 362 - <li><strong>{$_('register.didWebWarning4')}</strong> {$_('register.didWebWarning4Detail')}</li> 363 - </ul> 364 - </div> 365 - {/if} 354 + <div class="field"> 355 + <label for="verification-channel">{$_('register.verificationMethod')}</label> 356 + <select id="verification-channel" bind:value={flow.info.verificationChannel} disabled={flow.state.submitting}> 357 + <option value="email">{$_('register.email')}</option> 358 + {#if isChannelAvailable('discord')} 359 + <option value="discord">{$_('register.discord')}</option> 360 + {/if} 361 + {#if isChannelAvailable('telegram')} 362 + <option value="telegram">{$_('register.telegram')}</option> 363 + {/if} 364 + {#if isChannelAvailable('signal')} 365 + <option value="signal">{$_('register.signal')}</option> 366 + {/if} 367 + </select> 368 + </div> 366 369 367 - {#if flow.info.didType === 'web-external'} 368 - <div class="field"> 369 - <label for="external-did">{$_('register.externalDid')}</label> 370 - <input 371 - id="external-did" 372 - type="text" 373 - bind:value={flow.info.externalDid} 374 - placeholder={$_('register.externalDidPlaceholder')} 375 - disabled={flow.state.submitting} 376 - required 377 - /> 378 - <p class="hint">{$_('register.externalDidHint')}</p> 379 - </div> 380 - {/if} 381 - </fieldset> 370 + {#if flow.info.verificationChannel === 'email'} 371 + <div class="field"> 372 + <label for="email">{$_('register.emailAddress')}</label> 373 + <input 374 + id="email" 375 + type="email" 376 + bind:value={flow.info.email} 377 + placeholder={$_('register.emailPlaceholder')} 378 + disabled={flow.state.submitting} 379 + required 380 + /> 381 + </div> 382 + {:else if flow.info.verificationChannel === 'discord'} 383 + <div class="field"> 384 + <label for="discord-id">{$_('register.discordId')}</label> 385 + <input 386 + id="discord-id" 387 + type="text" 388 + bind:value={flow.info.discordId} 389 + onblur={() => flow?.checkCommsChannelInUse('discord', flow.info.discordId ?? '')} 390 + placeholder={$_('register.discordIdPlaceholder')} 391 + disabled={flow.state.submitting} 392 + required 393 + /> 394 + <p class="hint">{$_('register.discordIdHint')}</p> 395 + {#if flow.state.discordInUse} 396 + <p class="hint warning">{$_('register.discordInUseWarning')}</p> 397 + {/if} 398 + </div> 399 + {:else if flow.info.verificationChannel === 'telegram'} 400 + <div class="field"> 401 + <label for="telegram-username">{$_('register.telegramUsername')}</label> 402 + <input 403 + id="telegram-username" 404 + type="text" 405 + bind:value={flow.info.telegramUsername} 406 + onblur={() => flow?.checkCommsChannelInUse('telegram', flow.info.telegramUsername ?? '')} 407 + placeholder={$_('register.telegramUsernamePlaceholder')} 408 + disabled={flow.state.submitting} 409 + required 410 + /> 411 + {#if flow.state.telegramInUse} 412 + <p class="hint warning">{$_('register.telegramInUseWarning')}</p> 413 + {/if} 414 + </div> 415 + {:else if flow.info.verificationChannel === 'signal'} 416 + <div class="field"> 417 + <label for="signal-number">{$_('register.signalNumber')}</label> 418 + <input 419 + id="signal-number" 420 + type="tel" 421 + bind:value={flow.info.signalNumber} 422 + onblur={() => flow?.checkCommsChannelInUse('signal', flow.info.signalNumber ?? '')} 423 + placeholder={$_('register.signalNumberPlaceholder')} 424 + disabled={flow.state.submitting} 425 + required 426 + /> 427 + <p class="hint">{$_('register.signalNumberHint')}</p> 428 + {#if flow.state.signalInUse} 429 + <p class="hint warning">{$_('register.signalInUseWarning')}</p> 430 + {/if} 431 + </div> 432 + {/if} 382 433 383 - <fieldset class="section-fieldset"> 384 - <legend>{$_('register.contactMethod')}</legend> 385 - <div class="contact-fields"> 386 - <div class="field"> 387 - <label for="verification-channel">{$_('register.verificationMethod')}</label> 388 - <select id="verification-channel" bind:value={flow.info.verificationChannel} disabled={flow.state.submitting}> 389 - <option value="email">{$_('register.email')}</option> 390 - <option value="discord" disabled={!isChannelAvailable('discord')}> 391 - {$_('register.discord')}{isChannelAvailable('discord') ? '' : ` (${$_('register.notConfigured')})`} 392 - </option> 393 - <option value="telegram" disabled={!isChannelAvailable('telegram')}> 394 - {$_('register.telegram')}{isChannelAvailable('telegram') ? '' : ` (${$_('register.notConfigured')})`} 395 - </option> 396 - <option value="signal" disabled={!isChannelAvailable('signal')}> 397 - {$_('register.signal')}{isChannelAvailable('signal') ? '' : ` (${$_('register.notConfigured')})`} 398 - </option> 399 - </select> 400 - </div> 434 + <fieldset class="identity-section"> 435 + <legend>{$_('register.identityType')}</legend> 436 + <div class="radio-group"> 437 + <label class="radio-label"> 438 + <input type="radio" name="didType" value="plc" bind:group={flow.info.didType} disabled={flow.state.submitting} /> 439 + <span class="radio-content"> 440 + <strong>{$_('register.didPlc')}</strong> 441 + <span class="radio-hint">{$_('register.didPlcHint')}</span> 442 + </span> 443 + </label> 401 444 402 - {#if flow.info.verificationChannel === 'email'} 403 - <div class="field"> 404 - <label for="email">{$_('register.emailAddress')}</label> 405 - <input 406 - id="email" 407 - type="email" 408 - bind:value={flow.info.email} 409 - onblur={() => flow?.checkEmailInUse(flow.info.email)} 410 - placeholder={$_('register.emailPlaceholder')} 411 - disabled={flow.state.submitting} 412 - required 413 - /> 414 - {#if flow.state.emailInUse} 415 - <p class="hint warning">{$_('register.emailInUseWarning')}</p> 416 - {/if} 417 - </div> 418 - {:else if flow.info.verificationChannel === 'discord'} 419 - <div class="field"> 420 - <label for="discord-id">{$_('register.discordId')}</label> 421 - <input 422 - id="discord-id" 423 - type="text" 424 - bind:value={flow.info.discordId} 425 - onblur={() => flow?.checkCommsChannelInUse('discord', flow.info.discordId ?? '')} 426 - placeholder={$_('register.discordIdPlaceholder')} 427 - disabled={flow.state.submitting} 428 - required 429 - /> 430 - <p class="hint">{$_('register.discordIdHint')}</p> 431 - {#if flow.state.discordInUse} 432 - <p class="hint warning">{$_('register.discordInUseWarning')}</p> 433 - {/if} 434 - </div> 435 - {:else if flow.info.verificationChannel === 'telegram'} 436 - <div class="field"> 437 - <label for="telegram-username">{$_('register.telegramUsername')}</label> 438 - <input 439 - id="telegram-username" 440 - type="text" 441 - bind:value={flow.info.telegramUsername} 442 - onblur={() => flow?.checkCommsChannelInUse('telegram', flow.info.telegramUsername ?? '')} 443 - placeholder={$_('register.telegramUsernamePlaceholder')} 444 - disabled={flow.state.submitting} 445 - required 446 - /> 447 - {#if flow.state.telegramInUse} 448 - <p class="hint warning">{$_('register.telegramInUseWarning')}</p> 449 - {/if} 450 - </div> 451 - {:else if flow.info.verificationChannel === 'signal'} 452 - <div class="field"> 453 - <label for="signal-number">{$_('register.signalNumber')}</label> 454 - <input 455 - id="signal-number" 456 - type="tel" 457 - bind:value={flow.info.signalNumber} 458 - onblur={() => flow?.checkCommsChannelInUse('signal', flow.info.signalNumber ?? '')} 459 - placeholder={$_('register.signalNumberPlaceholder')} 460 - disabled={flow.state.submitting} 461 - required 462 - /> 463 - <p class="hint">{$_('register.signalNumberHint')}</p> 464 - {#if flow.state.signalInUse} 465 - <p class="hint warning">{$_('register.signalInUseWarning')}</p> 466 - {/if} 467 - </div> 445 + <label class="radio-label" class:disabled={serverInfo?.selfHostedDidWebEnabled === false}> 446 + <input type="radio" name="didType" value="web" bind:group={flow.info.didType} disabled={flow.state.submitting || serverInfo?.selfHostedDidWebEnabled === false} /> 447 + <span class="radio-content"> 448 + <strong>{$_('register.didWeb')}</strong> 449 + {#if serverInfo?.selfHostedDidWebEnabled === false} 450 + <span class="radio-hint disabled-hint">{$_('register.didWebDisabledHint')}</span> 451 + {:else} 452 + <span class="radio-hint">{$_('register.didWebHint')}</span> 468 453 {/if} 469 - </div> 470 - </fieldset> 454 + </span> 455 + </label> 471 456 472 - {#if serverInfo?.inviteCodeRequired} 473 - <div class="field"> 474 - <label for="invite-code">{$_('register.inviteCode')} <span class="required">{$_('register.inviteCodeRequired')}</span></label> 475 - <input 476 - id="invite-code" 477 - type="text" 478 - bind:value={flow.info.inviteCode} 479 - placeholder={$_('register.inviteCodePlaceholder')} 480 - disabled={flow.state.submitting} 481 - required 482 - /> 483 - </div> 484 - {/if} 457 + <label class="radio-label"> 458 + <input type="radio" name="didType" value="web-external" bind:group={flow.info.didType} disabled={flow.state.submitting} /> 459 + <span class="radio-content"> 460 + <strong>{$_('register.didWebBYOD')}</strong> 461 + <span class="radio-hint">{$_('register.didWebBYODHint')}</span> 462 + </span> 463 + </label> 464 + </div> 465 + </fieldset> 485 466 486 - <button type="submit" disabled={flow.state.submitting}> 487 - {flow.state.submitting ? $_('common.creating') : $_('register.createButton')} 488 - </button> 489 - </form> 490 - 491 - <div class="form-links"> 492 - <p class="link-text"> 493 - {$_('register.alreadyHaveAccount')} <a href={getFullUrl(routes.login)}>{$_('register.signIn')}</a> 494 - </p> 467 + {#if flow.info.didType === 'web'} 468 + <div class="warning-box"> 469 + <strong>{$_('register.didWebWarningTitle')}</strong> 470 + <ul> 471 + <li><strong>{$_('register.didWebWarning1')}</strong> {$_('register.didWebWarning1Detail', { values: { did: `did:web:yourhandle.${serverInfo?.availableUserDomains?.[0] || 'this-pds.com'}` } })}</li> 472 + <li><strong>{$_('register.didWebWarning2')}</strong> {$_('register.didWebWarning2Detail')}</li> 473 + {#if $_('register.didWebWarning3')} 474 + <li><strong>{$_('register.didWebWarning3')}</strong> {$_('register.didWebWarning3Detail')}</li> 475 + {/if} 476 + </ul> 495 477 </div> 496 - </div> 478 + {/if} 497 479 498 - <aside class="info-panel"> 499 - <h3>{$_('register.identityHint')}</h3> 500 - <p>{$_('register.infoIdentityDesc')}</p> 480 + {#if flow.info.didType === 'web-external'} 481 + <div class="field"> 482 + <label for="external-did">{$_('register.externalDid')}</label> 483 + <input 484 + id="external-did" 485 + type="text" 486 + bind:value={flow.info.externalDid} 487 + placeholder={$_('register.externalDidPlaceholder')} 488 + disabled={flow.state.submitting} 489 + required 490 + /> 491 + <p class="hint">{$_('register.externalDidHint')}</p> 492 + </div> 493 + {/if} 501 494 502 - <h3>{$_('register.contactMethodHint')}</h3> 503 - <p>{$_('register.infoContactDesc')}</p> 495 + {#if serverInfo?.inviteCodeRequired} 496 + <div class="field"> 497 + <label for="invite-code">{$_('register.inviteCode')}</label> 498 + <input 499 + id="invite-code" 500 + type="text" 501 + bind:value={flow.info.inviteCode} 502 + placeholder={$_('register.inviteCodePlaceholder')} 503 + disabled={flow.state.submitting} 504 + required 505 + /> 506 + </div> 507 + {/if} 504 508 505 - <h3>{$_('register.infoNextTitle')}</h3> 506 - <p>{$_('register.infoNextDesc')}</p> 507 - </aside> 508 - </div> 509 + <div class="form-actions"> 510 + <button type="button" class="secondary" onclick={handleCancel} disabled={flow.state.submitting}> 511 + {$_('common.cancel')} 512 + </button> 513 + <button type="submit" class="primary" disabled={flow.state.submitting || flow.state.handleAvailable === false || flow.state.checkingHandle}> 514 + {flow.state.submitting ? $_('common.loading') : $_('common.continue')} 515 + </button> 516 + </div> 517 + </form> 509 518 510 519 {:else if flow.state.step === 'key-choice'} 511 520 <KeyChoiceStep {flow} /> ··· 543 552 </div> 544 553 545 554 <style> 546 - .client-name { 555 + .register-form { 556 + display: flex; 557 + flex-direction: column; 558 + gap: var(--space-3); 559 + max-width: 500px; 560 + } 561 + 562 + .identity-section { 563 + border: 1px solid var(--border-color); 564 + border-radius: var(--radius-md); 565 + padding: var(--space-4); 566 + margin: 0; 567 + margin-top: var(--space-5); 568 + } 569 + 570 + .identity-section legend { 571 + font-weight: var(--font-medium); 572 + font-size: var(--text-sm); 573 + padding: 0 var(--space-2); 574 + } 575 + 576 + .radio-group { 577 + display: flex; 578 + flex-direction: column; 579 + gap: var(--space-3); 580 + } 581 + 582 + .radio-label { 583 + display: flex; 584 + align-items: flex-start; 585 + gap: var(--space-2); 586 + cursor: pointer; 587 + } 588 + 589 + .radio-label.disabled { 590 + opacity: 0.5; 591 + cursor: not-allowed; 592 + } 593 + 594 + .radio-label input { 595 + margin-top: 2px; 596 + } 597 + 598 + .radio-content { 599 + display: flex; 600 + flex-direction: column; 601 + gap: var(--space-1); 602 + } 603 + 604 + .radio-hint { 605 + font-size: var(--text-sm); 547 606 color: var(--text-secondary); 607 + } 608 + 609 + .radio-hint.disabled-hint { 610 + color: var(--text-muted); 611 + } 612 + 613 + .warning-box { 614 + padding: var(--space-4); 615 + background: var(--warning-bg); 616 + border: 1px solid var(--warning-border); 617 + border-radius: var(--radius-md); 618 + } 619 + 620 + .warning-box ul { 621 + margin: var(--space-2) 0 0 0; 622 + padding-left: var(--space-5); 623 + } 624 + 625 + .warning-box li { 548 626 margin-top: var(--space-2); 549 627 } 550 628 551 - form { 629 + .form-actions { 552 630 display: flex; 553 - flex-direction: column; 554 - gap: var(--space-5); 631 + gap: var(--space-4); 632 + margin-top: var(--space-5); 555 633 } 556 634 557 - button[type="submit"] { 558 - margin-top: var(--space-3); 635 + .form-actions .primary { 636 + flex: 1; 559 637 } 560 638 </style>
+42 -15
frontend/src/routes/RegisterSso.svelte
··· 94 94 initiating = null 95 95 } 96 96 } 97 + 98 + async function handleCancel() { 99 + const requestUri = getRequestUriFromUrl() 100 + if (!requestUri) { 101 + window.history.back() 102 + return 103 + } 104 + 105 + try { 106 + const response = await fetch('/oauth/authorize/deny', { 107 + method: 'POST', 108 + headers: { 109 + 'Content-Type': 'application/json', 110 + 'Accept': 'application/json' 111 + }, 112 + body: JSON.stringify({ request_uri: requestUri }) 113 + }) 114 + 115 + if (!response.ok) { 116 + window.history.back() 117 + return 118 + } 119 + 120 + const data = await response.json() 121 + if (data.redirect_uri) { 122 + window.location.href = data.redirect_uri 123 + } else { 124 + window.history.back() 125 + } 126 + } catch { 127 + window.history.back() 128 + } 129 + } 97 130 </script> 98 131 99 132 <div class="page"> 100 133 <header class="page-header"> 101 134 <h1>{$_('register.title')}</h1> 102 - <p class="subtitle">{$_('register.ssoSubtitle')}</p> 103 135 </header> 104 136 105 137 <div class="migrate-callout"> ··· 125 157 </div> 126 158 {:else} 127 159 <div class="provider-list"> 128 - <p class="provider-hint">{$_('register.ssoHint')}</p> 129 160 <div class="provider-grid"> 130 161 {#each providers as provider} 131 162 <button ··· 147 178 </div> 148 179 {/if} 149 180 150 - <div class="form-links"> 151 - <p class="link-text"> 152 - {$_('register.alreadyHaveAccount')} <a href={getFullUrl(routes.login)}>{$_('register.signIn')}</a> 153 - </p> 181 + <div class="form-actions"> 182 + <button type="button" class="secondary" onclick={handleCancel} disabled={initiating !== null}> 183 + {$_('common.cancel')} 184 + </button> 154 185 </div> 155 186 </div> 156 187 ··· 162 193 } 163 194 164 195 .provider-list { 165 - display: flex; 166 - flex-direction: column; 167 - gap: var(--space-3); 168 196 max-width: var(--width-md); 169 - } 170 - 171 - .provider-hint { 172 - color: var(--text-secondary); 173 - font-size: var(--text-sm); 174 - margin: 0 0 var(--space-4) 0; 175 197 } 176 198 177 199 .provider-grid { ··· 215 237 216 238 .provider-button .provider-name { 217 239 flex: 1; 240 + } 241 + 242 + .form-actions { 243 + margin-top: var(--space-5); 244 + max-width: var(--width-md); 218 245 } 219 246 </style>
+14 -7
frontend/src/routes/Verify.svelte
··· 451 451 <p class="field-help">{$_('verify.codeHelp')}</p> 452 452 </div> 453 453 454 - <button type="submit" disabled={submitting || !verificationCode.trim()}> 455 - {submitting ? $_('common.verifying') : $_('common.verify')} 456 - </button> 457 - 458 - <button type="button" class="secondary" onclick={handleResendCode} disabled={resendingCode}> 459 - {resendingCode ? $_('common.sending') : $_('common.resendCode')} 460 - </button> 454 + <div class="form-actions"> 455 + <button type="button" class="secondary" onclick={handleResendCode} disabled={resendingCode}> 456 + {resendingCode ? $_('common.sending') : $_('common.resendCode')} 457 + </button> 458 + <button type="submit" disabled={submitting || !verificationCode.trim()}> 459 + {submitting ? $_('common.verifying') : $_('common.verify')} 460 + </button> 461 + </div> 461 462 </form> 462 463 463 464 <p class="link-text"> ··· 517 518 .token-input { 518 519 font-family: var(--font-mono); 519 520 letter-spacing: 0.05em; 521 + } 522 + 523 + .form-actions { 524 + display: flex; 525 + gap: var(--space-4); 526 + margin-top: var(--space-4); 520 527 } 521 528 522 529 .link-text {