fix: account creation bug & frontend style improvements

This commit is contained in:
lewis
2026-01-30 20:53:31 +01:00
parent bee0bba6f4
commit b73c91b561
32 changed files with 1631 additions and 1750 deletions
+16 -7
View File
@@ -2334,7 +2334,12 @@ impl UserRepository for PostgresUserRepository {
tranquil_db_traits::CreatePasswordAccountResult,
tranquil_db_traits::CreateAccountError,
> {
tracing::info!(did = %input.did, handle = %input.handle, "create_password_account: starting transaction");
let mut tx = self.pool.begin().await.map_err(|e: sqlx::Error| {
tracing::error!(
"create_password_account: failed to begin transaction: {}",
e
);
tranquil_db_traits::CreateAccountError::Database(e.to_string())
})?;
@@ -2366,8 +2371,12 @@ impl UserRepository for PostgresUserRepository {
.await;
let user_id = match user_insert {
Ok((id,)) => id,
Ok((id,)) => {
tracing::info!(did = %input.did, user_id = %id, "create_password_account: user row inserted");
id
}
Err(e) => {
tracing::error!(did = %input.did, error = %e, "create_password_account: user insert failed");
if let Some(db_err) = e.as_database_error()
&& db_err.code().as_deref() == Some("23505")
{
@@ -2455,8 +2464,7 @@ impl UserRepository for PostgresUserRepository {
if let Some(birthdate_pref) = &input.birthdate_pref {
let _ = sqlx::query!(
"INSERT INTO account_preferences (user_id, name, value_json) VALUES ($1, $2, $3)
ON CONFLICT (user_id, name) DO NOTHING",
"INSERT INTO account_preferences (user_id, name, value_json) VALUES ($1, $2, $3)",
user_id,
"app.bsky.actor.defs#personalDetailsPref",
birthdate_pref
@@ -2465,9 +2473,12 @@ impl UserRepository for PostgresUserRepository {
.await;
}
tracing::info!(did = %input.did, user_id = %user_id, "create_password_account: committing transaction");
tx.commit().await.map_err(|e: sqlx::Error| {
tracing::error!(did = %input.did, user_id = %user_id, error = %e, "create_password_account: commit failed");
tranquil_db_traits::CreateAccountError::Database(e.to_string())
})?;
tracing::info!(did = %input.did, user_id = %user_id, "create_password_account: transaction committed successfully");
Ok(tranquil_db_traits::CreatePasswordAccountResult {
user_id,
@@ -2716,8 +2727,7 @@ impl UserRepository for PostgresUserRepository {
if let Some(birthdate_pref) = &input.birthdate_pref {
let _ = sqlx::query!(
"INSERT INTO account_preferences (user_id, name, value_json) VALUES ($1, $2, $3)
ON CONFLICT (user_id, name) DO NOTHING",
"INSERT INTO account_preferences (user_id, name, value_json) VALUES ($1, $2, $3)",
user_id,
"app.bsky.actor.defs#personalDetailsPref",
birthdate_pref
@@ -2865,8 +2875,7 @@ impl UserRepository for PostgresUserRepository {
if let Some(birthdate_pref) = &input.birthdate_pref {
let _ = sqlx::query!(
"INSERT INTO account_preferences (user_id, name, value_json) VALUES ($1, $2, $3)
ON CONFLICT (user_id, name) DO NOTHING",
"INSERT INTO account_preferences (user_id, name, value_json) VALUES ($1, $2, $3)",
user_id,
"app.bsky.actor.defs#personalDetailsPref",
birthdate_pref
+22 -24
View File
@@ -245,36 +245,34 @@ async fn proxy_handler(
let key_bytes = match auth_user.key_bytes {
Some(kb) => kb,
None => {
match state.user_repo.get_user_info_by_did(&auth_user.did).await {
Ok(Some(info)) => match info.key_bytes {
Some(key_bytes_enc) => {
match crate::config::decrypt_key(
&key_bytes_enc,
info.encryption_version,
) {
Ok(key) => key,
Err(e) => {
error!(error = ?e, "Failed to decrypt user key for proxy");
return ApiError::UpstreamFailure.into_response();
}
None => match state.user_repo.get_user_info_by_did(&auth_user.did).await {
Ok(Some(info)) => match info.key_bytes {
Some(key_bytes_enc) => {
match crate::config::decrypt_key(
&key_bytes_enc,
info.encryption_version,
) {
Ok(key) => key,
Err(e) => {
error!(error = ?e, "Failed to decrypt user key for proxy");
return ApiError::UpstreamFailure.into_response();
}
}
None => {
warn!(did = %auth_user.did, "User has no signing key for proxy");
return ApiError::UpstreamFailure.into_response();
}
},
Ok(None) => {
warn!(did = %auth_user.did, "User not found for proxy service auth");
return ApiError::UpstreamFailure.into_response();
}
Err(e) => {
error!(error = ?e, "DB error fetching user key for proxy");
None => {
warn!(did = %auth_user.did, "User has no signing key for proxy");
return ApiError::UpstreamFailure.into_response();
}
},
Ok(None) => {
warn!(did = %auth_user.did, "User not found for proxy service auth");
return ApiError::UpstreamFailure.into_response();
}
}
Err(e) => {
error!(error = ?e, "DB error fetching user key for proxy");
return ApiError::UpstreamFailure.into_response();
}
},
};
match crate::auth::create_service_token(
@@ -296,21 +296,11 @@ pub fn verify_token_signature(token: &str) -> Result<VerificationToken, VerifyEr
}
pub fn format_token_for_display(token: &str) -> String {
token
.replace(['-', ' '], "")
.chars()
.collect::<Vec<_>>()
.chunks(4)
.map(|chunk| chunk.iter().collect::<String>())
.collect::<Vec<_>>()
.join("-")
token.to_string()
}
pub fn normalize_token_input(input: &str) -> String {
input
.chars()
.filter(|c| c.is_ascii_alphanumeric() || *c == '_' || *c == '=')
.collect()
input.trim().to_string()
}
#[cfg(test)]
@@ -410,12 +400,12 @@ mod tests {
fn test_format_token_for_display() {
let token = "ABCDEFGHIJKLMNOP";
let formatted = format_token_for_display(token);
assert_eq!(formatted, "ABCD-EFGH-IJKL-MNOP");
assert_eq!(formatted, "ABCDEFGHIJKLMNOP");
}
#[test]
fn test_normalize_token_input() {
let input = "ABCD-EFGH IJKL-MNOP";
let input = " ABCDEFGHIJKLMNOP ";
let normalized = normalize_token_input(input);
assert_eq!(normalized, "ABCDEFGHIJKLMNOP");
}
@@ -3277,10 +3277,16 @@ pub async fn register_complete(
}
};
let password_valid = password_hashes.iter().fold(false, |acc, hash| {
let mut password_valid = password_hashes.iter().fold(false, |acc, hash| {
acc | bcrypt::verify(&form.app_password, hash).unwrap_or(false)
});
if !password_valid
&& let Ok(Some(account_hash)) = state.user_repo.get_password_hash_by_did(&did).await
{
password_valid = bcrypt::verify(&form.app_password, &account_hash).unwrap_or(false);
}
if !password_valid {
return (
StatusCode::FORBIDDEN,
@@ -227,13 +227,13 @@ async fn test_update_email_via_notification_prefs() {
.skip_while(|line| !line.contains("verification code"))
.nth(1)
.map(|line| line.trim().to_string())
.filter(|line| !line.is_empty() && line.contains('-'))
.filter(|line| !line.is_empty())
.unwrap_or_else(|| {
body_text
.lines()
.find(|line| {
let trimmed = line.trim();
trimmed.starts_with("MX") && trimmed.contains('-')
trimmed.len() == 11 && trimmed.chars().nth(5) == Some('-')
})
.map(|s| s.trim().to_string())
.unwrap_or_default()
+6 -8
View File
@@ -605,9 +605,9 @@ pub async fn verify_new_account(client: &Client, did: &str) -> String {
.and_then(|(i, _)| lines.get(i + 1).map(|s| s.trim().to_string()))
.or_else(|| {
body_text
.split_whitespace()
.find(|word| word.contains('-') && word.chars().filter(|c| *c == '-').count() >= 3)
.map(|s| s.to_string())
.lines()
.find(|line| line.trim().starts_with("MX"))
.map(|s| s.trim().to_string())
})
.unwrap_or_else(|| body_text.clone());
@@ -774,11 +774,9 @@ async fn create_account_and_login_internal(client: &Client, make_admin: bool) ->
.and_then(|(i, _)| lines.get(i + 1).map(|s: &&str| s.trim().to_string()))
.or_else(|| {
body_text
.split_whitespace()
.find(|word: &&str| {
word.contains('-') && word.chars().filter(|c| *c == '-').count() >= 3
})
.map(|s: &str| s.to_string())
.lines()
.find(|line| line.trim().starts_with("MX"))
.map(|s| s.trim().to_string())
})
.unwrap_or_else(|| body_text.clone());
+7 -4
View File
@@ -17,11 +17,14 @@ async fn get_email_update_token(pool: &PgPool, did: &str) -> String {
.skip_while(|line| !line.contains("verification code"))
.nth(1)
.map(|line| line.trim().to_string())
.filter(|line| !line.is_empty() && line.contains('-'))
.filter(|line| !line.is_empty())
.unwrap_or_else(|| {
body_text
.lines()
.find(|line| line.trim().starts_with("MX") && line.contains('-'))
.find(|line| {
let trimmed = line.trim();
trimmed.len() == 11 && trimmed.chars().nth(5) == Some('-')
})
.map(|s| s.trim().to_string())
.unwrap_or_default()
})
@@ -271,7 +274,7 @@ async fn test_confirm_email_confirms_existing_email() {
let code = body_text
.lines()
.find(|line| line.trim().starts_with("MX") && line.contains('-'))
.find(|line| line.trim().starts_with("MX"))
.map(|s| s.trim().to_string())
.unwrap_or_default();
@@ -334,7 +337,7 @@ async fn test_confirm_email_rejects_wrong_email() {
let code = body_text
.lines()
.find(|line| line.trim().starts_with("MX") && line.contains('-'))
.find(|line| line.trim().starts_with("MX"))
.map(|s| s.trim().to_string())
.unwrap_or_default();
+3 -3
View File
@@ -696,9 +696,9 @@ async fn test_refresh_token_replay_protection() {
.and_then(|(i, _)| lines.get(i + 1).map(|s| s.trim().to_string()))
.or_else(|| {
body_text
.split_whitespace()
.find(|word| word.contains('-') && word.chars().filter(|c| *c == '-').count() >= 3)
.map(|s| s.to_string())
.lines()
.find(|line| line.trim().starts_with("MX"))
.map(|s| s.trim().to_string())
})
.unwrap_or_else(|| body_text.clone());
+167 -218
View File
@@ -129,36 +129,14 @@
-webkit-font-smoothing: antialiased;
}
.pattern-container {
.pattern-canvas {
position: fixed;
top: -32px;
left: -32px;
right: -32px;
bottom: -32px;
pointer-events: none;
z-index: 1;
overflow: hidden;
}
.pattern {
position: absolute;
top: 0;
left: 0;
width: calc(100% + 500px);
width: 100%;
height: 100%;
animation: drift 80s linear infinite;
}
.dot {
position: absolute;
width: 10px;
height: 10px;
background: rgba(0, 0, 0, 0.06);
border-radius: 50%;
transition: transform 0.04s linear;
}
@media (prefers-color-scheme: dark) {
.dot {
background: rgba(255, 255, 255, 0.1);
}
pointer-events: none;
z-index: 1;
}
.pattern-fade {
position: fixed;
@@ -174,14 +152,6 @@
pointer-events: none;
z-index: 2;
}
@keyframes drift {
0% {
transform: translateX(-500px);
}
100% {
transform: translateX(0);
}
}
nav {
position: fixed;
@@ -246,9 +216,8 @@
padding: 72px 32px 32px;
}
.hero {
padding: var(--space-7) 0 var(--space-8);
border-bottom: 1px solid var(--border-color);
margin-bottom: var(--space-8);
padding: var(--space-7) 0 var(--space-6);
margin-bottom: var(--space-5);
}
h1 {
font-size: var(--text-4xl);
@@ -294,16 +263,16 @@
border: 1px solid transparent;
}
.btn.primary {
background: var(--secondary);
background: var(--accent);
color: var(--text-inverse);
border-color: var(--secondary);
border-color: var(--accent);
}
.btn.primary:hover {
background: var(--secondary-hover);
border-color: var(--secondary-hover);
background: var(--accent-hover);
border-color: var(--accent-hover);
}
.btn.secondary {
background: transparent;
background: var(--bg-primary);
color: var(--text-primary);
border-color: var(--border-color);
}
@@ -349,34 +318,20 @@
margin-bottom: var(--space-5);
line-height: var(--leading-relaxed);
}
.features {
display: grid;
grid-template-columns: repeat(2, 1fr);
gap: var(--space-6);
margin: var(--space-6) 0 var(--space-8);
}
.feature {
padding: var(--space-5);
padding: var(--space-6);
background: var(--bg-secondary);
border-radius: var(--radius-xl);
border: 1px solid var(--border-color);
}
.feature h3 {
font-size: var(--text-base);
font-weight: var(--font-semibold);
color: var(--text-primary);
margin-bottom: var(--space-3);
margin: var(--space-6) 0 var(--space-8);
}
.feature p {
font-size: var(--text-sm);
font-size: var(--text-base);
color: var(--text-secondary);
margin: 0;
line-height: var(--leading-relaxed);
}
@media (max-width: 700px) {
.features {
grid-template-columns: 1fr;
}
h1 {
font-size: var(--text-3xl);
}
@@ -407,9 +362,7 @@
</style>
</head>
<body>
<div class="pattern-container">
<div class="pattern" id="dotPattern"></div>
</div>
<canvas class="pattern-canvas" id="patternCanvas"></canvas>
<div class="pattern-fade"></div>
<nav>
@@ -458,101 +411,18 @@
<section class="content">
<h2>What you get</h2>
<div class="features">
<div class="feature">
<h3>Real security</h3>
<p>
Sign in with passkeys or SSO, add two-factor authentication, set
up backup codes, and mark devices you trust. Your account stays
yours.
</p>
</div>
<div class="feature">
<h3>Your own identity</h3>
<p>
Use your own domain as your handle, or get a subdomain on ours.
Either way, your identity moves with you if you ever leave.
</p>
</div>
<div class="feature">
<h3>Stay in the loop</h3>
<p>
Get important alerts where you actually see them: email, Discord,
Telegram, or Signal.
</p>
</div>
<div class="feature">
<h3>You decide what apps can do</h3>
<p>
When an app asks for access, you'll see exactly what it wants in
plain language. Grant what makes sense, deny what doesn't.
</p>
</div>
<div class="feature">
<h3>App passwords with guardrails</h3>
<p>
Create app passwords that can only do specific things: read-only
for feed readers, post-only for bots. Full control over what each
password can access.
</p>
</div>
<div class="feature">
<h3>Delegate without sharing passwords</h3>
<p>
Let team members or tools manage your account with specific
permission levels. They authenticate with their own credentials,
you see everything they do in an audit log.
</p>
</div>
<div class="feature">
<h3>Automatic backups</h3>
<p>
Your repository is backed up daily to object storage. Download any
backup or restore with one click. You own your data, even if the
worst happens.
</p>
</div>
</div>
<h2>Everything in one place</h2>
<p>
Manage your profile, security settings, connected apps, and more from
a clean dashboard. No command line or 3rd party apps required.
</p>
<h2>Works with everything</h2>
<p>
Use any ATProto app you already like. Tranquil PDS speaks the same
language as Bluesky's servers, so all your favorite clients and tools
just work.
</p>
<h2>Ready to try it?</h2>
<p>
Join this server, or grab the source and run your own. Either way, you
can migrate an existing account over and your followers, posts, and
identity come with you.
</p>
<div class="actions" id="footerActions">
<a href="/app/register" class="btn primary" id="footerPrimary"
>Join This Server</a>
<a href="/app/login" class="btn secondary" id="footerLogin">Login</a>
<a
href="https://tangled.org/tranquil.farm/tranquil-pds"
class="btn secondary"
target="_blank"
rel="noopener"
>View Source</a>
<div class="feature">
<p>
A superset of the reference PDS: passkeys and 2FA (TOTP, backup
codes, trusted devices), SSO login and signup, did:web support
(PDS-hosted subdomains or bring-your-own), multi-channel
notifications (email, Discord, Telegram, Signal) for verification
and alerts, granular OAuth scopes with human-readable descriptions,
app passwords with configurable permissions (read-only, post-only,
or custom scopes), account delegation with permission levels and
audit logging, and a built-in web UI for account management, OAuth
consent, repo browsing, and admin.
</p>
</div>
</section>
@@ -565,35 +435,21 @@
<script>
(function checkSession() {
try {
const stored = localStorage.getItem("tranquil_pds_session");
var stored = localStorage.getItem("tranquil_pds_session");
if (stored) {
const session = JSON.parse(stored);
var session = JSON.parse(stored);
if (session && session.handle) {
const handle = "@" + session.handle;
const heroPrimary = document.getElementById(
"heroPrimary",
);
const footerPrimary = document.getElementById(
"footerPrimary",
);
const heroSecondary = document.getElementById(
var heroPrimary = document.getElementById("heroPrimary");
var heroLogin = document.getElementById("heroLogin");
var heroSecondary = document.getElementById(
"heroSecondary",
);
if (heroPrimary) {
heroPrimary.href = "/app/dashboard";
heroPrimary.textContent = handle;
heroPrimary.textContent = "@" + session.handle;
}
if (footerPrimary) {
footerPrimary.href = "/app/dashboard";
footerPrimary.textContent = handle;
}
var heroLogin = document.getElementById("heroLogin");
var footerLogin = document.getElementById("footerLogin");
if (heroLogin) heroLogin.classList.add("hidden");
if (footerLogin) footerLogin.classList.add("hidden");
if (heroSecondary) {
heroSecondary.classList.add("hidden");
}
if (heroSecondary) heroSecondary.classList.add("hidden");
}
}
} catch (e) {}
@@ -624,21 +480,77 @@
setTimeout(cycleWord, 2000);
fetch("/xrpc/com.atproto.server.describeServer")
.then((r) => r.json())
.then((info) => {
if (info.availableUserDomains?.length) {
document.getElementById("hostname").textContent =
info.availableUserDomains[0];
document.getElementById("hostname").classList.remove(
"placeholder",
);
.then(function (r) {
return r.json();
})
.then(function (info) {
var hostnameEl = document.getElementById("hostname");
if (
info.availableUserDomains &&
info.availableUserDomains.length
) {
hostnameEl.textContent = info.availableUserDomains[0];
} else {
hostnameEl.textContent = "Tranquil PDS";
}
hostnameEl.classList.remove("placeholder");
if (info.version) {
document.getElementById("version").textContent =
info.version;
}
})
.catch(() => {});
.catch(function () {
var hostnameEl = document.getElementById("hostname");
hostnameEl.textContent = "Tranquil PDS";
hostnameEl.classList.remove("placeholder");
});
(function loadServerColors() {
var darkMode =
window.matchMedia("(prefers-color-scheme: dark)").matches;
var root = document.documentElement;
function applyColors(config) {
if (darkMode) {
if (config.primaryColorDark) {
root.style.setProperty(
"--accent",
config.primaryColorDark,
);
}
if (config.secondaryColorDark) {
root.style.setProperty(
"--secondary",
config.secondaryColorDark,
);
}
} else {
if (config.primaryColor) {
root.style.setProperty("--accent", config.primaryColor);
}
if (config.secondaryColor) {
root.style.setProperty(
"--secondary",
config.secondaryColor,
);
}
}
}
fetch("/xrpc/_server.getConfig")
.then(function (r) {
return r.json();
})
.then(function (config) {
applyColors(config);
window.matchMedia("(prefers-color-scheme: dark)")
.addEventListener("change", function (e) {
darkMode = e.matches;
applyColors(config);
});
})
.catch(function () {});
})();
fetch("/xrpc/com.atproto.sync.listRepos?limit=1000")
.then((r) => r.json())
@@ -661,42 +573,79 @@
})
.catch(() => {});
const pattern = document.getElementById("dotPattern");
const spacing = 32;
const cols = Math.ceil((window.innerWidth + 600) / spacing);
const rows = Math.ceil((window.innerHeight + 100) / spacing);
const dots = [];
(function initPattern() {
var canvas = document.getElementById("patternCanvas");
var ctx = canvas.getContext("2d");
var dpr = window.devicePixelRatio || 1;
var spacing = 32;
var baseRadius = 5;
var maxDist = 120;
var driftSpeed = 500 / 80;
var driftOffset = 0;
var mouseX = -1000;
var mouseY = -1000;
var darkMode =
window.matchMedia("(prefers-color-scheme: dark)").matches;
for (let y = 0; y < rows; y++) {
for (let x = 0; x < cols; x++) {
const dot = document.createElement("div");
dot.className = "dot";
dot.style.left = (x * spacing) + "px";
dot.style.top = (y * spacing) + "px";
pattern.appendChild(dot);
dots.push({ el: dot, x: x * spacing, y: y * spacing });
function resize() {
canvas.width = window.innerWidth * dpr;
canvas.height = window.innerHeight * dpr;
canvas.style.width = window.innerWidth + "px";
canvas.style.height = window.innerHeight + "px";
ctx.scale(dpr, dpr);
}
}
resize();
window.addEventListener("resize", resize);
let mouseX = -1000, mouseY = -1000;
document.addEventListener("mousemove", (e) => {
mouseX = e.clientX;
mouseY = e.clientY;
});
window.matchMedia("(prefers-color-scheme: dark)")
.addEventListener("change", function (e) {
darkMode = e.matches;
});
function updateDots() {
const patternRect = pattern.getBoundingClientRect();
dots.forEach((dot) => {
const dotX = patternRect.left + dot.x + 5;
const dotY = patternRect.top + dot.y + 5;
const dist = Math.hypot(mouseX - dotX, mouseY - dotY);
const maxDist = 120;
const scale = Math.min(1, Math.max(0.1, dist / maxDist));
dot.el.style.transform = "scale(" + scale + ")";
document.addEventListener("mousemove", function (e) {
mouseX = e.clientX;
mouseY = e.clientY;
});
requestAnimationFrame(updateDots);
}
updateDots();
document.addEventListener("mouseleave", function () {
mouseX = -1000;
mouseY = -1000;
});
var lastTime = 0;
function draw(time) {
var dt = lastTime ? (time - lastTime) / 1000 : 0;
lastTime = time;
driftOffset = (driftOffset + driftSpeed * dt) % spacing;
ctx.clearRect(0, 0, canvas.width / dpr, canvas.height / dpr);
var fillColor = darkMode
? "rgba(255, 255, 255, 0.1)"
: "rgba(0, 0, 0, 0.06)";
ctx.fillStyle = fillColor;
var startX = -spacing + driftOffset;
var startY = -spacing;
var endX = window.innerWidth + spacing;
var endY = window.innerHeight + spacing;
for (var y = startY; y < endY; y += spacing) {
for (var x = startX; x < endX; x += spacing) {
var dist = Math.hypot(mouseX - x, mouseY - y);
var scale = Math.min(1, Math.max(0.1, dist / maxDist));
var radius = baseRadius * scale;
ctx.beginPath();
ctx.arc(x, y, radius, 0, Math.PI * 2);
ctx.fill();
}
}
requestAnimationFrame(draw);
}
requestAnimationFrame(draw);
})();
</script>
</body>
</html>
@@ -0,0 +1,21 @@
<script lang="ts" module>
const EXAMPLE_HANDLES = [
"nel.pet",
"lewis.moe",
"llaama.bsky.social",
"debugman.wizardry.systems",
"nonbinary.computer",
"mary.my.id",
"olaren.dev",
] as const;
export function getRandomHandle(): string {
return EXAMPLE_HANDLES[Math.floor(Math.random() * EXAMPLE_HANDLES.length)];
}
</script>
<script lang="ts">
let handle = $state(getRandomHandle());
</script>
@{handle}
@@ -143,10 +143,6 @@
<button class="close-btn" onclick={handleClose} aria-label="Close">&times;</button>
</div>
<p class="modal-description">
{$_('reauth.subtitle')}
</p>
{#if error}
<div class="error-message">{error}</div>
{/if}
@@ -221,7 +217,6 @@
</form>
{:else if activeMethod === 'passkey'}
<div class="passkey-auth">
<p>{$_('reauth.passkeyPrompt')}</p>
<button onclick={handlePasskeyAuth} disabled={loading}>
{loading ? $_('reauth.authenticating') : $_('reauth.usePasskey')}
</button>
+7 -5
View File
@@ -253,7 +253,9 @@ export type { AppPassword, DidDocument, InviteCodeInfo as InviteCode, Session };
export type { DidType, VerificationChannel };
function buildContactState(s: Record<string, unknown>): ContactState {
const preferredChannel = s.preferredChannel as VerificationChannel | undefined;
const preferredChannel = s.preferredChannel as
| VerificationChannel
| undefined;
const email = s.email ? unsafeAsEmail(s.email as string) : undefined;
if (preferredChannel) {
@@ -319,7 +321,7 @@ export function castSession(raw: unknown): Session {
};
}
function castDelegationController(raw: unknown): DelegationController {
function _castDelegationController(raw: unknown): DelegationController {
const c = raw as Record<string, unknown>;
return {
did: unsafeAsDid(c.did as string),
@@ -328,7 +330,7 @@ function castDelegationController(raw: unknown): DelegationController {
};
}
function castDelegationControlledAccount(
function _castDelegationControlledAccount(
raw: unknown,
): DelegationControlledAccount {
const a = raw as Record<string, unknown>;
@@ -339,7 +341,7 @@ function castDelegationControlledAccount(
};
}
function castDelegationAuditEntry(raw: unknown): DelegationAuditEntry {
function _castDelegationAuditEntry(raw: unknown): DelegationAuditEntry {
const e = raw as Record<string, unknown>;
return {
id: e.id as string,
@@ -351,7 +353,7 @@ function castDelegationAuditEntry(raw: unknown): DelegationAuditEntry {
};
}
function castSsoLinkedAccount(raw: unknown): SsoLinkedAccount {
function _castSsoLinkedAccount(raw: unknown): SsoLinkedAccount {
const a = raw as Record<string, unknown>;
return {
id: a.id as string,
+1 -1
View File
@@ -1,4 +1,4 @@
import { api, ApiError, typedApi, castSession } from "./api.ts";
import { api, ApiError, castSession, typedApi } from "./api.ts";
import type {
CreateAccountParams,
CreateAccountResult,
+7 -1
View File
@@ -1,4 +1,10 @@
import type { AccessToken, Did, EmailAddress, Handle, ScopeSet } from "./types/branded.ts";
import type {
AccessToken,
Did,
EmailAddress,
Handle,
ScopeSet,
} from "./types/branded.ts";
import type { Session } from "./types/api.ts";
import type {
DelegationAuditEntry,
+3 -1
View File
@@ -240,7 +240,9 @@ export class AtprotoClient {
error: "Unknown",
message: res.statusText,
}));
const retryError = new Error(retryErr.message || retryErr.error || res.statusText) as
const retryError = new Error(
retryErr.message || retryErr.error || res.statusText,
) as
& Error
& { status: number; error: string };
retryError.status = res.status;
+30 -10
View File
@@ -34,7 +34,8 @@ export interface RegistrationFlowState {
error: string | null;
submitting: boolean;
pdsHostname: string;
emailInUse: boolean;
handleAvailable: boolean | null;
checkingHandle: boolean;
discordInUse: boolean;
telegramInUse: boolean;
signalInUse: boolean;
@@ -67,7 +68,8 @@ export function createRegistrationFlow(
error: null,
submitting: false,
pdsHostname,
emailInUse: false,
handleAvailable: null,
checkingHandle: false,
discordInUse: false,
telegramInUse: false,
signalInUse: false,
@@ -105,7 +107,16 @@ export function createRegistrationFlow(
function setError(err: unknown) {
if (err instanceof ApiError) {
state.error = err.message || "An error occurred";
const errorMessages: Record<string, string> = {
HandleNotAvailable: "This handle is already taken",
InvalidHandle: "Invalid handle format",
InvalidInviteCode: "Invalid invite code",
InviteCodeRequired: "An invite code is required",
InvalidEmail: "Invalid email address",
InvalidPassword: "Password must be at least 8 characters",
};
state.error = errorMessages[err.error] || err.message ||
"An error occurred";
} else if (err instanceof Error) {
state.error = err.message || "An error occurred";
} else {
@@ -113,16 +124,25 @@ export function createRegistrationFlow(
}
}
async function checkEmailInUse(email: string): Promise<void> {
if (!email.trim() || !email.includes("@")) {
state.emailInUse = false;
async function checkHandleAvailability(handle: string): Promise<void> {
if (!handle.trim() || handle.length < 3) {
state.handleAvailable = null;
state.checkingHandle = false;
return;
}
state.checkingHandle = true;
try {
const result = await api.checkEmailInUse(email.trim());
state.emailInUse = result.inUse;
const response = await fetch(
`${getPdsEndpoint()}/oauth/sso/check-handle-available?handle=${
encodeURIComponent(handle)
}`,
);
const data = await response.json();
state.handleAvailable = data.available === true;
} catch {
state.emailInUse = false;
state.handleAvailable = null;
} finally {
state.checkingHandle = false;
}
}
@@ -522,7 +542,7 @@ export function createRegistrationFlow(
activateAccount,
finalizeSession,
goBack,
checkEmailInUse,
checkHandleAvailability,
checkCommsChannelInUse,
setError(msg: string) {
+14 -3
View File
@@ -34,16 +34,27 @@ export function qrState(qrBase64: string, totpUri: string): TotpQr {
}
export function verifyState(state: TotpQr): TotpVerify {
return { step: "verify", qrBase64: state.qrBase64, totpUri: state.totpUri } as TotpVerify;
return {
step: "verify",
qrBase64: state.qrBase64,
totpUri: state.totpUri,
} as TotpVerify;
}
export function backupState(state: TotpVerify, backupCodes: readonly string[]): TotpBackup {
export function backupState(
state: TotpVerify,
backupCodes: readonly string[],
): TotpBackup {
void state;
return { step: "backup", backupCodes } as TotpBackup;
}
export function goBackToQr(state: TotpVerify): TotpQr {
return { step: "qr", qrBase64: state.qrBase64, totpUri: state.totpUri } as TotpQr;
return {
step: "qr",
qrBase64: state.qrBase64,
totpUri: state.totpUri,
} as TotpQr;
}
export function finish(_state: TotpBackup): TotpIdle {
+185 -233
View File
@@ -1,6 +1,6 @@
{
"common": {
"loading": "Loading...",
"loading": "Loading",
"error": "Error",
"save": "Save",
"cancel": "Cancel",
@@ -16,26 +16,23 @@
"name": "Name",
"dashboard": "Dashboard",
"backToDashboard": "← Dashboard",
"copied": "Copied!",
"copyToClipboard": "Copy to Clipboard",
"verifying": "Verifying...",
"saving": "Saving...",
"creating": "Creating...",
"updating": "Updating...",
"sending": "Sending...",
"authenticating": "Authenticating...",
"checking": "Checking...",
"redirecting": "Redirecting...",
"signIn": "Sign In",
"copied": "Copied",
"copyToClipboard": "Copy",
"verifying": "Verifying",
"saving": "Saving",
"creating": "Creating",
"updating": "Updating",
"sending": "Sending",
"authenticating": "Authenticating",
"checking": "Checking",
"redirecting": "Redirecting",
"signIn": "Sign in",
"verify": "Verify",
"remove": "Remove",
"revoke": "Revoke",
"resendCode": "Resend Code",
"startOver": "Start Over",
"tryAgain": "Try Again",
"resendCode": "Resend code",
"startOver": "Start over",
"tryAgain": "Retry",
"password": "Password",
"email": "Email",
"emailAddress": "Email Address",
@@ -45,126 +42,104 @@
"inviteCode": "Invite Code",
"newPassword": "New Password",
"confirmPassword": "Confirm Password",
"enterSixDigitCode": "Enter 6-digit code",
"passwordHint": "At least 8 characters",
"enterPassword": "Enter your password",
"emailPlaceholder": "you@example.com",
"verified": "Verified",
"disabled": "Disabled",
"available": "Available",
"deactivated": "Deactivated",
"unverified": "Unverified",
"backToLogin": "Back to Login",
"backToSettings": "Back to Settings",
"alreadyHaveAccount": "Already have an account?",
"createAccount": "Create account",
"passwordsMismatch": "Passwords do not match",
"passwordTooShort": "Password must be at least 8 characters"
},
"login": {
"title": "Sign In",
"subtitle": "Sign in to manage your PDS account",
"button": "Sign In",
"redirecting": "Redirecting...",
"button": "Sign in",
"redirecting": "Redirecting",
"chooseAccount": "Choose an account",
"signInToAnother": "Sign in to another account",
"backToSaved": "← Back to saved accounts",
"signInToAnother": "Or sign in to another account",
"forgotPassword": "Forgot password?",
"lostPasskey": "Lost passkey?",
"noAccount": "Don't have an account?",
"createAccount": "Create account",
"removeAccount": "Remove from saved accounts",
"infoSavedAccountsTitle": "Saved accounts",
"infoSavedAccountsDesc": "Click an account to sign in instantly. Your session tokens are stored securely in this browser.",
"infoNewAccountTitle": "New account",
"infoNewAccountDesc": "Use the sign-in button to add a different account. Click the × to remove saved accounts from this browser.",
"infoSecureSignInTitle": "Secure sign-in",
"infoSecureSignInDesc": "You'll be redirected to authenticate securely. If you have passkeys or two-factor authentication enabled, you'll be prompted for those too.",
"infoStaySignedInTitle": "Stay signed in",
"infoStaySignedInDesc": "After signing in, your account will be saved to this browser for quick access next time.",
"infoRecoveryTitle": "Account recovery",
"infoRecoveryDesc": "Lost your password or passkey? Use the recovery links below the sign-in button."
"noAccount": "No account?",
"createAccount": "Create one",
"removeAccount": "Remove"
},
"verification": {
"title": "Verify Your Account",
"subtitle": "Your account needs verification. Enter the code sent to your verification method.",
"codeLabel": "Verification Code",
"codePlaceholder": "Enter 6-digit code",
"verifyButton": "Verify Account",
"resent": "Verification code resent!"
"title": "Verify account",
"subtitle": "Enter the code sent to your contact method",
"codeLabel": "Code",
"codePlaceholder": "6-digit code",
"verifyButton": "Verify",
"resent": "Code resent"
},
"register": {
"title": "Create Account",
"subtitle": "Create a new account on this PDS",
"subtitleKeyChoice": "Choose how to set up your external did:web identity.",
"subtitleInitialDidDoc": "Upload your DID document to continue.",
"subtitleVerify": "Verify your {channel} to continue.",
"subtitleUpdatedDidDoc": "Update your DID document with the PDS signing key.",
"subtitleActivating": "Activating your account...",
"subtitleComplete": "Your account has been created successfully!",
"redirecting": "Redirecting to dashboard...",
"infoIdentityDesc": "Your identity determines how your account is identified across the ATProto network. Most users should choose the standard option.",
"infoContactDesc": "We'll use this to verify your account and send important notifications about your account security.",
"infoNextTitle": "What happens next?",
"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.",
"migrateTitle": "Already have a Bluesky account?",
"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.",
"subtitleKeyChoice": "Set up your did:web identity",
"subtitleInitialDidDoc": "Upload your DID document",
"subtitleVerify": "Verify your {channel}",
"subtitleUpdatedDidDoc": "Update your DID document",
"subtitleActivating": "Activating",
"subtitleComplete": "Account created",
"redirecting": "Redirecting",
"migrateTitle": "Already have an account?",
"migrateDescription": "Migrate instead of creating a new account",
"migrateLink": "Migrate your account",
"handle": "Handle",
"handlePlaceholder": "yourname",
"handleHint": "Your full handle will be: @{handle}",
"handleDotWarning": "Custom domain handles can be set up after account creation in Settings.",
"handleTaken": "This handle is already taken",
"handleDotWarning": "Custom domains can be set up in Settings",
"password": "Password",
"passwordPlaceholder": "At least 8 characters",
"confirmPassword": "Confirm Password",
"confirmPasswordPlaceholder": "Confirm your password",
"identityType": "Identity Type",
"identityHint": "Choose how your decentralized identity will be managed.",
"didPlc": "did:plc",
"didPlcRecommended": "(Recommended)",
"didPlcHint": "Portable identity managed by PLC Directory",
"didPlcHint": "Portable identity via PLC Directory",
"didWeb": "did:web",
"didWebHint": "Identity hosted on this PDS (read warning below)",
"didWebDisabledHint": "Not available on this PDS - use did:plc or bring your own did:web",
"didWebHint": "Identity hosted on this PDS",
"didWebDisabledHint": "Not available on this PDS",
"didWebBYOD": "did:web (BYOD)",
"didWebBYODHint": "Bring your own domain",
"didWebWarningTitle": "Important: Understand the trade-offs",
"didWebWarning1": "Permanent tie to this PDS:",
"didWebWarning1Detail": "Your identity will be {did}. Even if you migrate to another PDS later, this server must continue hosting your DID document.",
"didWebWarning2": "No recovery mechanism:",
"didWebWarning2Detail": "Unlike did:plc, did:web has no rotation keys. If this PDS goes offline permanently, your identity cannot be recovered.",
"didWebWarning3": "We commit to you:",
"didWebWarning3Detail": "If you migrate away, we will continue serving a minimal DID document pointing to your new PDS. Your identity will remain functional.",
"didWebWarning4": "Recommendation:",
"didWebWarning4Detail": "Choose did:plc unless you have a specific reason to prefer did:web.",
"didWebWarningTitle": "did:web trade-offs",
"didWebWarning1": "Permanent tie:",
"didWebWarning1Detail": "Your identity will be {did}",
"didWebWarning2": "No recovery:",
"didWebWarning2Detail": "No rotation keys like did:plc",
"didWebWarning3": "Our commitment:",
"didWebWarning3Detail": "We will continue hosting your DID document if you migrate",
"didWebWarning4": "",
"didWebWarning4Detail": "",
"externalDid": "Your did:web",
"externalDidPlaceholder": "did:web:yourdomain.com",
"externalDidHint": "Your domain must serve a valid DID document at /.well-known/did.json pointing to this PDS",
"externalDidHint": "Serve DID document at",
"contactMethod": "Contact Method",
"contactMethodHint": "Choose how you'd like to verify your account and receive notifications. You only need one.",
"verificationMethod": "Verification Method",
"email": "Email",
"emailAddress": "Email Address",
"emailPlaceholder": "you@example.com",
"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.",
"emailInUseWarning": "Email in use by another account",
"discord": "Discord",
"discordId": "Discord User ID",
"discordIdPlaceholder": "Your Discord user ID",
"discordIdHint": "Your numeric Discord user ID (enable Developer Mode to find it)",
"discordInUseWarning": "This Discord ID is already associated with another account.",
"discordIdPlaceholder": "123456789012345678",
"discordIdHint": "Enable Developer Mode to find your ID",
"discordInUseWarning": "Discord ID in use by another account",
"telegram": "Telegram",
"telegramUsername": "Telegram Username",
"telegramUsernamePlaceholder": "@yourusername",
"telegramInUseWarning": "This Telegram username is already associated with another account.",
"telegramInUseWarning": "Telegram username in use by another account",
"signal": "Signal",
"signalNumber": "Signal Phone Number",
"signalNumberPlaceholder": "+1234567890",
"signalNumberHint": "Include country code (eg., +1 for US)",
"signalInUseWarning": "This Signal number is already associated with another account.",
"signalNumberHint": "Include country code",
"signalInUseWarning": "Signal number in use by another account",
"notConfigured": "not configured",
"inviteCode": "Invite Code",
"inviteCodePlaceholder": "Enter your invite code",
@@ -175,9 +150,8 @@
"passkeyAccount": "Passkey",
"passwordAccount": "Password",
"ssoAccount": "SSO",
"ssoSubtitle": "Create an account using an external provider",
"noSsoProviders": "No SSO providers are configured on this server.",
"ssoHint": "Choose a provider to create your account:",
"ssoSubtitle": "Create account with external provider",
"noSsoProviders": "No SSO providers configured",
"continueWith": "Continue with {provider}",
"validation": {
"handleRequired": "Handle is required",
@@ -686,24 +660,23 @@
},
"oauth": {
"login": {
"title": "Sign In",
"subtitle": "Sign in to continue to the application",
"signingIn": "Signing in...",
"authenticating": "Authenticating...",
"checkingPasskey": "Checking passkey...",
"signInWithPasskey": "Sign in with passkey",
"passkeyNotSetUp": "Passkey not set up",
"orUsePassword": "Or use password",
"title": "Sign in",
"subtitle": "Signing in to",
"signingIn": "Signing in",
"authenticating": "Authenticating",
"checkingPasskey": "Checking",
"signInWithPasskey": "Passkey",
"passkeyNotSetUp": "No passkey",
"orUsePassword": "or",
"password": "Password",
"rememberDevice": "Remember this device",
"passkeyHintChecking": "Checking passkey status...",
"passkeyHintAvailable": "Sign in with your passkey",
"passkeyHintNotAvailable": "No passkeys registered for this account",
"passkeyHint": "Use your device's biometrics or security key",
"passwordPlaceholder": "Enter your password",
"usePasskey": "Use Passkey",
"orContinueWith": "Or continue with",
"orUseCredentials": "Or sign in with credentials"
"passkeyHintChecking": "Checking",
"passkeyHintAvailable": "Use passkey",
"passkeyHintNotAvailable": "No passkey registered",
"passwordPlaceholder": "Password",
"usePasskey": "Use passkey",
"orContinueWith": "or",
"orUseCredentials": "or"
},
"sso": {
"linkedAccounts": "Linked Accounts",
@@ -787,56 +760,50 @@
}
},
"accounts": {
"title": "Choose Account",
"subtitle": "Select an account to continue",
"useAnother": "Use a different account"
"title": "Choose account",
"useAnother": "Use another account"
},
"register": {
"title": "Create Account",
"subtitle": "Create an account to continue to",
"subtitleGeneric": "Create an account to continue",
"haveAccount": "Already have an account? Sign in"
"title": "Create account",
"subtitle": "for",
"subtitleGeneric": "Create an account",
"haveAccount": "Have an account? Sign in"
},
"twoFactor": {
"title": "Two-Factor Authentication",
"subtitle": "Additional verification is required",
"usePasskey": "Use Passkey",
"useTotp": "Use Authenticator App"
"title": "Verification",
"usePasskey": "Use passkey",
"useTotp": "Use authenticator"
},
"twoFactorCode": {
"title": "Two-Factor Authentication",
"subtitle": "A verification code has been sent to your {channel}. Enter the code below to continue.",
"codeLabel": "Verification Code",
"codePlaceholder": "Enter 6-digit code",
"title": "Verification",
"subtitle": "Code sent to {channel}",
"codeLabel": "Code",
"codePlaceholder": "6-digit code",
"errors": {
"missingRequestUri": "Missing request_uri parameter",
"missingRequestUri": "Missing request URI",
"verificationFailed": "Verification failed",
"connectionFailed": "Failed to connect to server",
"unexpectedResponse": "Unexpected response from server"
"connectionFailed": "Connection failed",
"unexpectedResponse": "Unexpected response"
}
},
"totp": {
"title": "Enter Authenticator Code",
"subtitle": "Enter the 6-digit code from your authenticator app",
"codePlaceholder": "Enter 6-digit code",
"useBackupCode": "Use backup code instead",
"backupCodePlaceholder": "Enter backup code",
"title": "Authenticator code",
"codePlaceholder": "6-digit code",
"useBackupCode": "Use backup code",
"backupCodePlaceholder": "Backup code",
"trustDevice": "Trust this device for 30 days",
"hintBackupCode": "Using backup code",
"hintTotpCode": "Using authenticator code",
"hintDefault": "6 digits for authenticator, 8 characters for backup code"
"hintBackupCode": "Backup code",
"hintTotpCode": "Authenticator code"
},
"passkey": {
"title": "Passkey Verification",
"subtitle": "Use your passkey to verify your identity",
"waiting": "Waiting for passkey...",
"useTotp": "Use authenticator app instead"
"title": "Passkey",
"waiting": "Waiting",
"useTotp": "Use authenticator"
},
"error": {
"title": "Authorization Error",
"genericError": "An error occurred during authorization.",
"tryAgain": "Try Again",
"backToApp": "Back to Application"
"title": "Authorization failed",
"tryAgain": "Retry",
"backToApp": "Back"
}
},
"sso_register": {
@@ -862,9 +829,9 @@
"subtitle": "We've sent a verification code to your {channel}. Enter it below to complete registration.",
"tokenSubtitle": "Enter the verification code and the identifier it was sent to.",
"tokenTitle": "Verify",
"codePlaceholder": "XXXX-XXXX-XXXX-XXXX...",
"codePlaceholder": "Paste verification code",
"codeLabel": "Verification Code",
"codeHelp": "Copy the entire code from your message, including dashes",
"codeHelp": "Copy the entire code from your message",
"verifyButton": "Verify Account",
"pleaseWait": "Please wait...",
"codeResent": "Verification code resent!",
@@ -965,20 +932,19 @@
},
"registerPasskey": {
"title": "Create Passkey Account",
"subtitle": "Create an ultra-secure account using a passkey instead of a password.",
"subtitleKeyChoice": "Choose how to set up your external did:web identity.",
"subtitleInitialDidDoc": "Upload your DID document to continue.",
"subtitleCreating": "Creating your account...",
"subtitlePasskey": "Register your passkey to secure your account.",
"subtitleAppPassword": "Save your app password for third-party apps.",
"subtitleVerify": "Verify your {channel} to continue.",
"subtitleUpdatedDidDoc": "Update your DID document with the PDS signing key.",
"subtitleActivating": "Activating your account...",
"subtitleComplete": "Your account has been created successfully!",
"subtitleKeyChoice": "Set up your did:web identity",
"subtitleInitialDidDoc": "Upload your DID document",
"subtitleCreating": "Creating account",
"subtitlePasskey": "Register your passkey",
"subtitleAppPassword": "Save your app password",
"subtitleVerify": "Verify your {channel}",
"subtitleUpdatedDidDoc": "Update your DID document",
"subtitleActivating": "Activating",
"subtitleComplete": "Account created",
"handle": "Handle",
"handlePlaceholder": "yourname",
"handleHint": "Your full handle will be: @{handle}",
"handleDotWarning": "Custom domain handles can be set up after account creation.",
"handleHint": "Your full handle: @{handle}",
"handleDotWarning": "Custom domains can be set up in Settings",
"email": "Email Address",
"emailPlaceholder": "you@example.com",
"inviteCode": "Invite Code",
@@ -988,56 +954,44 @@
"back": "Back",
"alreadyHaveAccount": "Already have an account?",
"signIn": "Sign in",
"wantPassword": "Want to use a password?",
"createPasswordAccount": "Create a password account",
"wantTraditional": "Want a traditional password?",
"registerWithPassword": "Register with password",
"wantPassword": "Use a password instead?",
"createPasswordAccount": "Password account",
"wantTraditional": "Use a password instead?",
"registerWithPassword": "Password account",
"contactMethod": "Contact Method",
"contactMethodHint": "Choose how you'd like to verify your account and receive notifications.",
"verificationMethod": "Verification Method",
"identityType": "Identity Type",
"identityTypeHint": "Choose how your decentralized identity will be managed.",
"identityTypeHint": "How your decentralized identity is managed",
"didPlcRecommended": "did:plc (Recommended)",
"didPlcHint": "Portable identity managed by PLC Directory",
"didPlcHint": "Portable identity via PLC Directory",
"didWeb": "did:web",
"didWebHint": "Identity hosted on this PDS (read warning below)",
"didWebDisabledHint": "Not available on this PDS - use did:plc or bring your own did:web",
"didWebHint": "Identity hosted on this PDS",
"didWebDisabledHint": "Not available on this PDS",
"didWebBYOD": "did:web (BYOD)",
"didWebBYODHint": "Bring your own domain",
"didWebWarningTitle": "Important: Understand the trade-offs",
"didWebWarning1": "Permanent tie to this PDS:",
"didWebWarning1Detail": "Your identity will be {did}.",
"didWebWarning2": "No recovery mechanism:",
"didWebWarning2Detail": "Unlike did:plc, did:web has no rotation keys.",
"didWebWarning3": "We commit to you:",
"didWebWarning3Detail": "If you migrate away, we will continue serving a minimal DID document.",
"didWebWarning4": "Recommendation:",
"didWebWarning4Detail": "Choose did:plc unless you have a specific reason to prefer did:web.",
"didWebWarningTitle": "did:web trade-offs",
"didWebWarning1": "Permanent tie:",
"didWebWarning1Detail": "Your identity will be {did}",
"didWebWarning2": "No recovery:",
"didWebWarning2Detail": "No rotation keys like did:plc",
"didWebWarning3": "Our commitment:",
"didWebWarning3Detail": "We will continue hosting your DID document if you migrate",
"didWebWarning4": "",
"didWebWarning4Detail": "",
"externalDid": "Your did:web",
"externalDidPlaceholder": "did:web:yourdomain.com",
"externalDidHint": "You'll need to serve a DID document at",
"whyPasskeyOnly": "Why passkey-only?",
"whyPasskeyOnlyDesc": "Passkey accounts are more secure than password-based accounts because they:",
"whyPasskeyBullet1": "Cannot be phished or stolen in data breaches",
"whyPasskeyBullet2": "Use hardware-backed cryptographic keys",
"whyPasskeyBullet3": "Require your biometric or device PIN to use",
"infoWhyPasskey": "Why use a passkey?",
"infoWhyPasskeyDesc": "Passkeys are cryptographic credentials stored on your device. They cannot be phished, guessed, or stolen in data breaches like passwords can.",
"infoHowItWorks": "How it works",
"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.",
"infoAppAccess": "Using third-party apps",
"infoAppAccessDesc": "After creating your account, you will receive an app password. Use this to sign in to Bluesky apps and other AT Protocol clients.",
"passkeyNameLabel": "Passkey Name (optional)",
"passkeyNamePlaceholder": "eg., MacBook Touch ID",
"passkeyNameHint": "A friendly name to identify this passkey",
"passkeyPrompt": "Click the button below to create your passkey. You'll be prompted to use:",
"passkeyPromptBullet1": "Touch ID or Face ID",
"passkeyPromptBullet2": "Your device PIN or password",
"passkeyPromptBullet3": "A security key (if you have one)",
"externalDidHint": "Serve DID document at",
"passkeyName": "Passkey Name",
"passkeyNamePlaceholder": "MacBook Touch ID",
"passkeyNameHint": "Optional identifier",
"setupPasskey": "Create Passkey",
"passkeyDescription": "Register a passkey for this account",
"createPasskey": "Create Passkey",
"creatingPasskey": "Creating Passkey...",
"redirecting": "Redirecting to dashboard...",
"loading": "Loading...",
"creatingPasskey": "Creating",
"creatingAccount": "Creating account",
"activatingAccount": "Activating",
"redirecting": "Redirecting",
"loading": "Loading",
"errors": {
"handleRequired": "Handle is required",
"handleNoDots": "Handle cannot contain dots. You can set up a custom domain handle after creating your account.",
@@ -1047,55 +1001,53 @@
"emailRequired": "Email is required for email verification",
"discordRequired": "Discord ID is required for Discord verification",
"telegramRequired": "Telegram username is required for Telegram verification",
"signalRequired": "Phone number is required for Signal verification",
"passkeysNotSupported": "Passkeys are not supported in this browser. Please use a different browser or register with a password instead.",
"passkeyCancelled": "Passkey creation was cancelled",
"signalRequired": "Phone number required",
"passkeysNotSupported": "Passkeys not supported in this browser",
"passkeyCancelled": "Cancelled",
"passkeyFailed": "Passkey registration failed"
}
},
"trustedDevices": {
"title": "Trusted Devices",
"backToSecurity": "← Security Settings",
"description": "Trusted devices can skip two-factor authentication when logging in. Trust is granted for 30 days and automatically extends when you use the device.",
"failedToLoad": "Failed to load trusted devices",
"noDevices": "No trusted devices yet.",
"noDevicesHint": "When you log in with two-factor authentication enabled, you can choose to trust the device for 30 days.",
"backToSecurity": "← Security",
"failedToLoad": "Could not load trusted devices",
"noDevices": "No trusted devices",
"lastSeen": "Last seen:",
"trustedSince": "Trusted since:",
"trustExpires": "Trust expires:",
"trustExpires": "Expires:",
"expired": "Expired",
"tomorrow": "Tomorrow",
"inDays": "In {days} days",
"revoke": "Revoke Trust",
"revokeConfirm": "Are you sure you want to revoke trust for this device? You will need to enter your 2FA code next time you log in from this device.",
"deviceRevoked": "Device trust revoked",
"revoke": "Revoke",
"revokeConfirm": "Revoke trust for this device?",
"deviceRevoked": "Trust revoked",
"deviceRenamed": "Device renamed",
"deviceNamePlaceholder": "Device name",
"browser": "Browser:",
"unknownDevice": "Unknown device"
"unknownDevice": "Unknown device",
"description": "Trusted devices can skip two-factor authentication when signing in. Trust is granted for 30 days and automatically extends when you use the device.",
"noDevicesHint": "When you sign in with two-factor authentication enabled, you can choose to trust the device for 30 days."
},
"reauth": {
"title": "Re-authentication Required",
"subtitle": "Please verify your identity to continue.",
"title": "Re-authenticate",
"password": "Password",
"totp": "TOTP",
"passkey": "Passkey",
"authenticatorCode": "Authenticator Code",
"usePassword": "Use Password",
"usePasskey": "Use Passkey",
"useTotp": "Use Authenticator",
"usePassword": "Password",
"usePasskey": "Passkey",
"useTotp": "Authenticator",
"passwordPlaceholder": "Enter your password",
"totpPlaceholder": "Enter 6-digit code",
"authenticating": "Authenticating...",
"passkeyPrompt": "Click the button below to authenticate with your passkey.",
"totpPlaceholder": "6-digit code",
"authenticating": "Authenticating",
"cancel": "Cancel"
},
"delegation": {
"title": "Account Delegation",
"loading": "Loading...",
"loading": "Loading",
"controllers": "Controllers",
"controllersDesc": "Accounts that can act on your behalf",
"noControllers": "No controllers have been granted access to your account.",
"noControllers": "No controllers",
"inactive": "Inactive",
"did": "DID",
"granted": "Granted",
@@ -1168,35 +1120,35 @@
"backToControllers": "Back to Controllers"
},
"oauthDelegation": {
"loading": "Loading...",
"loading": "Loading",
"title": "Delegated Account",
"isDelegated": "{handle} is a delegated account.",
"enterControllerHandle": "Sign in with your controller account to access this account.",
"isDelegated": "{handle} is delegated",
"enterControllerHandle": "Sign in as controller",
"controllerHandle": "Controller handle",
"handlePlaceholder": "handle.example.com",
"checking": "Checking...",
"controllerNotFound": "Account not found or you don't have access to this delegated account",
"checking": "Checking",
"controllerNotFound": "Controller not found",
"missingParams": "Missing delegation parameters",
"missingInfo": "Missing required information",
"passkeyCancelled": "Passkey authentication cancelled",
"passkeyFailed": "Passkey authentication failed",
"failedPasskeyStart": "Failed to start passkey login",
"missingInfo": "Missing information",
"passkeyCancelled": "Cancelled",
"passkeyFailed": "Passkey failed",
"failedPasskeyStart": "Passkey start failed",
"authFailed": "Authentication failed",
"unexpectedResponse": "Unexpected response from server",
"signInAsController": "Sign In as Controller",
"authenticateAs": "Authenticate as {controller} to act on behalf of {delegated}",
"useDifferentController": "Use a different controller",
"signInWithPasskey": "Sign in with Passkey",
"authenticating": "Authenticating...",
"usePasskey": "Use Passkey",
"unexpectedResponse": "Unexpected response",
"signInAsController": "Controller Sign In",
"authenticateAs": "Sign in as {controller} for {delegated}",
"useDifferentController": "Different controller",
"signInWithPasskey": "Passkey",
"authenticating": "Authenticating",
"usePasskey": "Passkey",
"or": "or",
"password": "Password",
"enterPassword": "Enter password",
"rememberDevice": "Remember this device",
"signingIn": "Signing in...",
"signIn": "Sign In",
"goBack": "Go Back",
"unableToLoad": "Unable to load delegation info"
"signingIn": "Signing in",
"signIn": "Sign in",
"goBack": "Back",
"unableToLoad": "Could not load delegation info"
},
"oauthConsent": {
"delegatedAccess": "Delegated Access",
+93 -141
View File
@@ -1,6 +1,6 @@
{
"common": {
"loading": "Ladataan...",
"loading": "Ladataan",
"error": "Virhe",
"save": "Tallenna",
"cancel": "Peruuta",
@@ -16,26 +16,23 @@
"name": "Nimi",
"dashboard": "Hallintapaneeli",
"backToDashboard": "← Hallintapaneeli",
"copied": "Kopioitu!",
"copied": "Kopioitu",
"copyToClipboard": "Kopioi",
"verifying": "Vahvistetaan...",
"saving": "Tallennetaan...",
"creating": "Luodaan...",
"updating": "Päivitetään...",
"sending": "Lähetetään...",
"authenticating": "Todennetaan...",
"checking": "Tarkistetaan...",
"redirecting": "Ohjataan...",
"verifying": "Vahvistetaan",
"saving": "Tallennetaan",
"creating": "Luodaan",
"updating": "Päivitetään",
"sending": "Lähetetään",
"authenticating": "Todennetaan",
"checking": "Tarkistetaan",
"redirecting": "Ohjataan",
"signIn": "Kirjaudu sisään",
"verify": "Vahvista",
"remove": "Poista",
"revoke": "Peruuta",
"resendCode": "Lähetä koodi uudelleen",
"resendCode": "Lähetä koodi",
"startOver": "Aloita alusta",
"tryAgain": "Yritä uudelleen",
"password": "Salasana",
"email": "Sähköposti",
"emailAddress": "Sähköpostiosoite",
@@ -45,87 +42,66 @@
"inviteCode": "Kutsukoodi",
"newPassword": "Uusi salasana",
"confirmPassword": "Vahvista salasana",
"enterSixDigitCode": "Syötä 6-numeroinen koodi",
"passwordHint": "Vähintään 8 merkkiä",
"enterPassword": "Syötä salasanasi",
"emailPlaceholder": "sinä@esimerkki.com",
"verified": "Vahvistettu",
"disabled": "Poistettu käytöstä",
"available": "Saatavilla",
"deactivated": "Deaktivoitu",
"unverified": "Vahvistamaton",
"backToLogin": "Takaisin kirjautumiseen",
"backToSettings": "Takaisin asetuksiin",
"alreadyHaveAccount": "Onko sinulla jo tili?",
"createAccount": "Luo tili",
"passwordsMismatch": "Salasanat eivät täsmää",
"passwordTooShort": "Salasanan on oltava vähintään 8 merkkiä"
},
"login": {
"title": "Kirjaudu sisään",
"subtitle": "Kirjaudu sisään hallitaksesi PDS-tiliäsi",
"button": "Kirjaudu sisään",
"redirecting": "Ohjataan...",
"redirecting": "Ohjataan",
"chooseAccount": "Valitse tili",
"signInToAnother": "Kirjaudu toiselle tilille",
"backToSaved": "← Takaisin tallennettuihin tileihin",
"signInToAnother": "Tai kirjaudu toiselle tilille",
"forgotPassword": "Unohditko salasanan?",
"lostPasskey": "Kadotitko pääsyavaimen?",
"noAccount": "Eikö sinulla ole tiliä?",
"noAccount": "Ei tiliä?",
"createAccount": "Luo tili",
"removeAccount": "Poista tallennetuista tileistä",
"infoSavedAccountsTitle": "Tallennetut tilit",
"infoSavedAccountsDesc": "Napsauta tiliä kirjautuaksesi heti. Istuntotunnuksesi on tallennettu turvallisesti tähän selaimeen.",
"infoNewAccountTitle": "Uusi tili",
"infoNewAccountDesc": "Käytä kirjautumispainiketta lisätäksesi toisen tilin. Napsauta × poistaaksesi tallennettuja tilejä.",
"infoSecureSignInTitle": "Turvallinen kirjautuminen",
"infoSecureSignInDesc": "Sinut ohjataan turvalliseen todennukseen. Jos sinulla on pääsyavaimia tai kaksivaiheinen tunnistautuminen käytössä, sinulta pyydetään myös ne.",
"infoStaySignedInTitle": "Pysy kirjautuneena",
"infoStaySignedInDesc": "Kirjautumisen jälkeen tilisi tallennetaan tähän selaimeen nopeaa pääsyä varten.",
"infoRecoveryTitle": "Tilin palautus",
"infoRecoveryDesc": "Kadotitko salasanasi tai pääsyavaimesi? Käytä palautuslinkkejä kirjautumispainikkeen alla."
"removeAccount": "Poista"
},
"verification": {
"title": "Vahvista tilisi",
"subtitle": "Tilisi vaatii vahvistuksen. Syötä vahvistusmenetelmääsi lähetetty koodi.",
"codeLabel": "Vahvistuskoodi",
"codePlaceholder": "Syötä 6-numeroinen koodi",
"verifyButton": "Vahvista tili",
"resent": "Vahvistuskoodi lähetetty uudelleen!"
"title": "Vahvista tili",
"subtitle": "Syötä yhteystietoosi lähetetty koodi",
"codeLabel": "Koodi",
"codePlaceholder": "6-numeroinen koodi",
"verifyButton": "Vahvista",
"resent": "Koodi lähetetty"
},
"register": {
"title": "Luo tili",
"subtitle": "Luo uusi tili tälle PDS:lle",
"subtitleKeyChoice": "Valitse, miten haluat määrittää ulkoisen did:web-identiteettisi.",
"subtitleInitialDidDoc": "Lataa DID-dokumenttisi jatkaaksesi.",
"subtitleVerify": "Vahvista {channel} jatkaaksesi.",
"subtitleUpdatedDidDoc": "Päivitä DID-dokumenttisi PDS-allekirjoitusavaimella.",
"subtitleActivating": "Aktivoidaan tiliäsi...",
"subtitleComplete": "Tilisi on luotu onnistuneesti!",
"redirecting": "Siirrytään kojelaudalle...",
"infoIdentityDesc": "Identiteettisi määrittää, miten tilisi tunnistetaan ATProto-verkossa. Useimpien käyttäjien tulisi valita vakiovaihtoehto.",
"infoContactDesc": "Käytämme tätä tilisi vahvistamiseen ja tärkeiden turvallisuusilmoitusten lähettämiseen.",
"infoNextTitle": "Mitä tapahtuu seuraavaksi?",
"infoNextDesc": "Tilin luomisen jälkeen vahvistat yhteysmenetelmäsi ja olet valmis käyttämään mitä tahansa ATProto-sovellusta uudella identiteetilläsi.",
"migrateTitle": "Onko sinulla jo Bluesky-tili?",
"migrateDescription": "Voit siirtää olemassa olevan tilisi tälle PDS:lle uuden luomisen sijaan. Seuraajasi, julkaisusi ja identiteettisi siirtyvät mukana.",
"migrateLink": "Siirrä PDS Mooverilla",
"subtitleKeyChoice": "Määritä did:web-identiteettisi",
"subtitleInitialDidDoc": "Lataa DID-dokumenttisi",
"subtitleVerify": "Vahvista {channel}",
"subtitleUpdatedDidDoc": "Päivitä DID-dokumenttisi",
"subtitleActivating": "Aktivoidaan",
"subtitleComplete": "Tili luotu",
"redirecting": "Ohjataan",
"migrateTitle": "Onko sinulla jo tili?",
"migrateDescription": "Siirrä olemassa oleva tilisi",
"migrateLink": "Siirrä tilisi",
"handle": "Käyttäjänimi",
"handlePlaceholder": "nimesi",
"handleHint": "Täydellinen käyttäjänimesi on: @{handle}",
"handleTaken": "Tämä käyttäjänimi on jo varattu",
"handleDotWarning": "Omat verkkotunnukset voidaan määrittää tilin luomisen jälkeen Asetuksissa.",
"password": "Salasana",
"passwordPlaceholder": "Vähintään 8 merkkiä",
"confirmPassword": "Vahvista salasana",
"confirmPasswordPlaceholder": "Vahvista salasanasi",
"identityType": "Identiteettityyppi",
"identityHint": "Valitse, miten hajautettu identiteettisi hallinnoidaan.",
"didPlc": "did:plc",
"didPlcRecommended": "(Suositellaan)",
"didPlcHint": "Siirrettävä identiteetti, jota hallinnoi PLC Directory",
"didWeb": "did:web",
"didWebHint": "Identiteetti isännöidään tällä PDS:llä (lue alla oleva varoitus)",
@@ -145,7 +121,6 @@
"externalDidPlaceholder": "did:web:verkkotunnuksesi.fi",
"externalDidHint": "Verkkotunnuksesi on tarjottava kelvollinen DID-dokumentti osoitteessa /.well-known/did.json, joka osoittaa tähän PDS:ään",
"contactMethod": "Yhteysmenetelmä",
"contactMethodHint": "Valitse, miten haluat vahvistaa tilisi ja vastaanottaa ilmoituksia. Tarvitset vain yhden.",
"verificationMethod": "Vahvistusmenetelmä",
"email": "Sähköposti",
"emailAddress": "Sähköpostiosoite",
@@ -177,7 +152,6 @@
"ssoAccount": "SSO",
"ssoSubtitle": "Luo tili ulkoisen palveluntarjoajan kautta",
"noSsoProviders": "Tälle palvelimelle ei ole määritetty SSO-palveluntarjoajia.",
"ssoHint": "Valitse palveluntarjoaja tilin luomiseksi:",
"continueWith": "Jatka palvelulla {provider}",
"validation": {
"handleRequired": "Käyttäjänimi vaaditaan",
@@ -687,29 +661,28 @@
"oauth": {
"login": {
"title": "Kirjaudu sisään",
"subtitle": "Kirjaudu sisään jatkaaksesi sovellukseen",
"signingIn": "Kirjaudutaan...",
"authenticating": "Todennetaan...",
"checkingPasskey": "Tarkistetaan pääsyavainta...",
"signInWithPasskey": "Kirjaudu pääsyavaimella",
"passkeyNotSetUp": "Pääsyavainta ei ole määritetty",
"orUsePassword": "tai käytä salasanaa",
"subtitle": "Kirjaudutaan",
"signingIn": "Kirjaudutaan",
"authenticating": "Todennetaan",
"checkingPasskey": "Tarkistetaan",
"signInWithPasskey": "Pääsyavain",
"passkeyNotSetUp": "Ei pääsyavainta",
"orUsePassword": "tai",
"password": "Salasana",
"rememberDevice": "Muista tämä laite",
"passkeyHintChecking": "Tarkistetaan pääsyavaimen tilaa...",
"passkeyHintAvailable": "Kirjaudu pääsyavaimellasi",
"passkeyHintNotAvailable": "Ei rekisteröityjä pääsyavaimia tälle tilille",
"passkeyHint": "Käytä laitteesi biometriikkaa tai suojausavainta",
"passwordPlaceholder": "Syötä salasanasi",
"passkeyHintChecking": "Tarkistetaan",
"passkeyHintAvailable": "Käytä pääsyavainta",
"passkeyHintNotAvailable": "Ei pääsyavainta",
"passwordPlaceholder": "Salasana",
"usePasskey": "Käytä pääsyavainta",
"orContinueWith": "Tai jatka käyttäen",
"orUseCredentials": "Tai kirjaudu tunnuksilla"
"orContinueWith": "tai",
"orUseCredentials": "tai"
},
"register": {
"title": "Luo tili",
"subtitle": "Luo tili jatkaaksesi sovellukseen",
"subtitleGeneric": "Luo tili jatkaaksesi",
"haveAccount": "Onko sinulla jo tili? Kirjaudu sisään"
"subtitle": "sovellukseen",
"subtitleGeneric": "Luo tili",
"haveAccount": "Onko sinulla tili? Kirjaudu"
},
"sso": {
"linkedAccounts": "Linkitetyt tilit",
@@ -794,49 +767,43 @@
},
"accounts": {
"title": "Valitse tili",
"subtitle": "Valitse tili jatkaaksesi",
"useAnother": "Käytä toista tiliä"
},
"twoFactor": {
"title": "Kaksivaiheinen tunnistautuminen",
"subtitle": "Lisävahvistus vaaditaan",
"title": "Vahvistus",
"usePasskey": "Käytä pääsyavainta",
"useTotp": "Käytä todentajasovellusta"
"useTotp": "Käytä todentajaa"
},
"twoFactorCode": {
"title": "Kaksivaiheinen tunnistautuminen",
"subtitle": "Vahvistuskoodi on lähetetty {channel}. Syötä koodi alla jatkaaksesi.",
"codeLabel": "Vahvistuskoodi",
"codePlaceholder": "Syötä 6-numeroinen koodi",
"title": "Vahvistus",
"subtitle": "Koodi lähetetty: {channel}",
"codeLabel": "Koodi",
"codePlaceholder": "6-numeroinen koodi",
"errors": {
"missingRequestUri": "Puuttuva request_uri-parametri",
"missingRequestUri": "Puuttuva request URI",
"verificationFailed": "Vahvistus epäonnistui",
"connectionFailed": "Palvelimeen yhdistäminen epäonnistui",
"unexpectedResponse": "Odottamaton vastaus palvelimelta"
"connectionFailed": "Yhteys epäonnistui",
"unexpectedResponse": "Odottamaton vastaus"
}
},
"totp": {
"title": "Syötä todentajakoodi",
"subtitle": "Syötä 6-numeroinen koodi todentajasovelluksestasi",
"codePlaceholder": "Syötä 6-numeroinen koodi",
"useBackupCode": "Käytä varakoodia sen sijaan",
"backupCodePlaceholder": "Syötä varakoodi",
"title": "Todentajakoodi",
"codePlaceholder": "6-numeroinen koodi",
"useBackupCode": "Käytä varakoodia",
"backupCodePlaceholder": "Varakoodi",
"trustDevice": "Luota tähän laitteeseen 30 päivää",
"hintBackupCode": "Käytetään varakoodia",
"hintTotpCode": "Käytetään todentajakoodia",
"hintDefault": "6 numeroa todentajalle, 8 merkkiä varakoodille"
"hintBackupCode": "Varakoodi",
"hintTotpCode": "Todentajakoodi"
},
"passkey": {
"title": "Pääsyavaimen vahvistus",
"subtitle": "Käytä pääsyavaintasi vahvistaaksesi henkilöllisyytesi",
"waiting": "Odotetaan pääsyavainta...",
"useTotp": "Käytä todentajasovellusta sen sijaan"
"title": "Pääsyavain",
"waiting": "Odotetaan",
"useTotp": "Käytä todentajaa"
},
"error": {
"title": "Valtuutusvirhe",
"genericError": "Valtuutuksen aikana tapahtui virhe.",
"title": "Valtuutus epäonnistui",
"tryAgain": "Yritä uudelleen",
"backToApp": "Takaisin sovellukseen"
"backToApp": "Takaisin"
}
},
"sso_register": {
@@ -862,9 +829,9 @@
"subtitle": "Olemme lähettäneet vahvistuskoodin {channel}. Syötä se alla viimeistelläksesi rekisteröinnin.",
"tokenTitle": "Vahvista",
"tokenSubtitle": "Syötä vahvistuskoodi ja tunniste, johon se lähetettiin.",
"codePlaceholder": "XXXX-XXXX-XXXX-XXXX...",
"codePlaceholder": "Paste verification code",
"codeLabel": "Vahvistuskoodi",
"codeHelp": "Kopioi koko koodi viestistäsi, mukaan lukien väliviivat",
"codeHelp": "Kopioi koko koodi viestistäsi, ",
"verifyButton": "Vahvista tili",
"pleaseWait": "Odota...",
"codeResent": "Vahvistuskoodi lähetetty uudelleen!",
@@ -965,15 +932,19 @@
},
"registerPasskey": {
"title": "Luo pääsyavaintili",
"subtitle": "Luo erittäin turvallinen tili käyttämällä pääsyavainta salasanan sijaan.",
"subtitleKeyChoice": "Valitse, miten haluat määrittää ulkoisen did:web-identiteettisi.",
"subtitleVerify": "Olemme lähettäneet vahvistuskoodin {channel}. Syötä koodi jatkaaksesi.",
"subtitlePasskey": "Luo pääsyavain viimeistelläksesi tilin määrityksen.",
"subtitleKeyChoice": "Määritä did:web-identiteettisi",
"subtitleInitialDidDoc": "Lataa DID-dokumenttisi",
"subtitleCreating": "Luodaan tiliä",
"subtitlePasskey": "Rekisteröi pääsyavaimesi",
"subtitleAppPassword": "Tallenna sovellussalasanasi",
"subtitleVerify": "Vahvista {channel}",
"subtitleUpdatedDidDoc": "Päivitä DID-dokumenttisi",
"subtitleActivating": "Aktivoidaan",
"subtitleComplete": "Tili luotu",
"handle": "Käyttäjänimi",
"handlePlaceholder": "nimesi",
"handleHint": "Täydellinen käyttäjänimesi on: @{handle}",
"contactMethod": "Yhteysmenetelmä",
"contactMethodHint": "Valitse, miten haluat vahvistaa tilisi ja vastaanottaa ilmoituksia.",
"verificationMethod": "Vahvistusmenetelmä",
"email": "Sähköpostiosoite",
"emailPlaceholder": "sinä@esimerkki.fi",
@@ -1000,31 +971,9 @@
"externalDidFormat": "Ulkoisen DID:n on alettava did:web:",
"discordRequired": "Discord-tunnus vaaditaan Discord-vahvistukseen"
},
"whyPasskeyBullet1": "Ei voi kalastella tai varastaa tietomurroissa",
"whyPasskeyBullet2": "Käyttää laitteistopohjaisia salausavaimia",
"whyPasskeyBullet3": "Vaatii biometrisen tunnistuksen tai laitteen PIN-koodin",
"infoWhyPasskey": "Miksi käyttää pääsyavainta?",
"infoWhyPasskeyDesc": "Pääsyavaimet ovat laitteellesi tallennettuja salattuja tunnistetietoja. Niitä ei voi kalastella, arvata tai varastaa tietomurroissa kuten salasanoja.",
"infoHowItWorks": "Miten se toimii",
"infoHowItWorksDesc": "Kirjautuessasi laitteesi pyytää sinua vahvistamaan Face ID:llä, Touch ID:llä tai laitteen PIN-koodilla. Ei salasanaa muistettavaksi tai kirjoitettavaksi.",
"infoAppAccess": "Kolmannen osapuolen sovellusten käyttö",
"infoAppAccessDesc": "Tilin luomisen jälkeen saat sovellussalasanan. Käytä sitä kirjautuaksesi Bluesky-sovelluksiin ja muihin AT Protocol -asiakkaisiin.",
"whyPasskeyOnly": "Miksi vain pääsyavain?",
"whyPasskeyOnlyDesc": "Pääsyavaintilit ovat turvallisempia kuin salasanapohjaiset tilit, koska ne:",
"subtitleInitialDidDoc": "Lataa DID-dokumenttisi jatkaaksesi.",
"subtitleUpdatedDidDoc": "Päivitä DID-dokumenttisi PDS-allekirjoitusavaimella.",
"subtitleActivating": "Aktivoidaan tiliäsi...",
"subtitleComplete": "Tilisi on luotu onnistuneesti!",
"subtitleCreating": "Luodaan tiliäsi...",
"subtitleAppPassword": "Tallenna sovellussalasanasi kolmannen osapuolen sovelluksia varten.",
"creatingPasskey": "Luodaan pääsyavainta...",
"passkeyPrompt": "Napsauta alla olevaa painiketta luodaksesi pääsyavaimesi. Sinua pyydetään käyttämään:",
"passkeyPromptBullet1": "Touch ID tai Face ID",
"passkeyPromptBullet2": "Laitteesi PIN-koodi tai salasana",
"passkeyPromptBullet3": "Turva-avain (jos sinulla on sellainen)",
"creatingPasskey": "Luodaan",
"identityType": "Identiteettityyppi",
"identityTypeHint": "Valitse, miten hajautettua identiteettiäsi hallitaan.",
"passkeyNameLabel": "Pääsyavaimen nimi (valinnainen)",
"passkeyNamePlaceholder": "esim. MacBook Touch ID",
"passkeyNameHint": "Ystävällinen nimi tämän pääsyavaimen tunnistamiseksi",
"createPasskey": "Luo pääsyavain",
@@ -1051,7 +1000,12 @@
"redirecting": "Ohjataan hallintapaneeliin...",
"handleDotWarning": "Mukautetut verkkotunnuskahvat voidaan määrittää tilin luomisen jälkeen.",
"wantTraditional": "Haluatko perinteisen salasanan?",
"registerWithPassword": "Rekisteröidy salasanalla"
"registerWithPassword": "Rekisteröidy salasanalla",
"activatingAccount": "Activating",
"creatingAccount": "Creating account",
"passkeyDescription": "Register a passkey for this account",
"passkeyName": "Passkey Name",
"setupPasskey": "Create Passkey"
},
"trustedDevices": {
"title": "Luotetut laitteet",
@@ -1075,19 +1029,17 @@
"unknownDevice": "Tuntematon laite"
},
"reauth": {
"title": "Uudelleentodennus vaaditaan",
"subtitle": "Vahvista henkilöllisyytesi jatkaaksesi.",
"title": "Todenna uudelleen",
"password": "Salasana",
"totp": "TOTP",
"passkey": "Pääsyavain",
"authenticatorCode": "Todentajan koodi",
"usePassword": "Käytä salasanaa",
"usePasskey": "Käytä pääsyavainta",
"useTotp": "Käytä todentajaa",
"usePassword": "Salasana",
"usePasskey": "Pääsyavain",
"useTotp": "Todentaja",
"passwordPlaceholder": "Syötä salasanasi",
"totpPlaceholder": "Syötä 6-numeroinen koodi",
"authenticating": "Todennetaan...",
"passkeyPrompt": "Klikkaa alla olevaa painiketta todentaaksesi pääsyavaimellasi.",
"totpPlaceholder": "6-numeroinen koodi",
"authenticating": "Todennetaan",
"cancel": "Peruuta"
},
"verifyChannel": {
@@ -1107,7 +1059,7 @@
"identifierPlaceholder": "Sähköposti, Discord ID jne.",
"identifierHelp": "Vahvistettava sähköpostiosoite, Discord ID, Telegram-käyttäjänimi tai Signal-numero.",
"codeLabel": "Vahvistuskoodi",
"codeHelp": "Kopioi koko koodi viestistäsi, mukaan lukien väliviivat.",
"codeHelp": "Kopioi koko koodi viestistäsi, .",
"verifyButton": "Vahvista"
},
"delegation": {
+95 -136
View File
@@ -1,6 +1,6 @@
{
"common": {
"loading": "読み込み中...",
"loading": "読み込み中",
"error": "エラー",
"save": "保存",
"cancel": "キャンセル",
@@ -16,22 +16,22 @@
"name": "名前",
"dashboard": "ダッシュボード",
"backToDashboard": "← ダッシュボード",
"copied": "コピーしました!",
"copyToClipboard": "クリップボードにコピー",
"verifying": "確認中...",
"saving": "保存中...",
"creating": "作成中...",
"updating": "更新中...",
"sending": "送信中...",
"authenticating": "認証中...",
"checking": "確認中...",
"redirecting": "リダイレクト中...",
"copied": "コピー完了",
"copyToClipboard": "コピー",
"verifying": "確認中",
"saving": "保存中",
"creating": "作成中",
"updating": "更新中",
"sending": "送信中",
"authenticating": "認証中",
"checking": "確認中",
"redirecting": "リダイレクト中",
"signIn": "サインイン",
"verify": "確認",
"remove": "削除",
"revoke": "取り消し",
"resendCode": "コードを再送信",
"startOver": "最初からやり直す",
"resendCode": "再送信",
"startOver": "やり直す",
"tryAgain": "再試行",
"password": "パスワード",
"email": "メール",
@@ -60,65 +60,48 @@
},
"login": {
"title": "サインイン",
"subtitle": "PDS アカウントを管理するにはサインインしてください",
"button": "サインイン",
"redirecting": "リダイレクト中...",
"redirecting": "リダイレクト中",
"chooseAccount": "アカウントを選択",
"signInToAnother": "別のアカウントでサインイン",
"backToSaved": "← 保存済みアカウントに戻る",
"signInToAnother": "または別のアカウントでサインイン",
"forgotPassword": "パスワードをお忘れですか?",
"lostPasskey": "パスキーを紛失しましたか",
"noAccount": "アカウントをお持ちでないですか",
"createAccount": "アカウントを作成",
"removeAccount": "保存済みアカウントから削除",
"infoSavedAccountsTitle": "保存済みアカウント",
"infoSavedAccountsDesc": "アカウントをクリックすると即座にサインインできます。セッショントークンはこのブラウザに安全に保存されています。",
"infoNewAccountTitle": "新規アカウント",
"infoNewAccountDesc": "サインインボタンで別のアカウントを追加できます。×をクリックすると保存済みアカウントを削除できます。",
"infoSecureSignInTitle": "安全なサインイン",
"infoSecureSignInDesc": "安全な認証のためにリダイレクトされます。パスキーや二要素認証が有効な場合は、それらも求められます。",
"infoStaySignedInTitle": "サインイン状態を維持",
"infoStaySignedInDesc": "サインイン後、アカウントはこのブラウザに保存され、次回から素早くアクセスできます。",
"infoRecoveryTitle": "アカウント復旧",
"infoRecoveryDesc": "パスワードやパスキーを紛失しましたか?サインインボタンの下の復旧リンクをご利用ください。"
"lostPasskey": "パスキーを紛失?",
"noAccount": "アカウントなし",
"createAccount": "作成",
"removeAccount": "削除"
},
"verification": {
"title": "アカウント確認",
"subtitle": "アカウントの確認が必要です。確認方法に送信されたコードを入力してください。",
"codeLabel": "確認コード",
"codePlaceholder": "6桁のコードを入力",
"verifyButton": "確認する",
"resent": "確認コードを再送信しました!"
"subtitle": "連絡先に送信されたコードを入力",
"codeLabel": "コード",
"codePlaceholder": "6桁のコード",
"verifyButton": "確認",
"resent": "コード送信済み"
},
"register": {
"title": "アカウント作成",
"subtitle": "この PDS で新規アカウントを作成",
"subtitleKeyChoice": "外部 did:web アイデンティティ設定方法を選択してください。",
"subtitleInitialDidDoc": "続行するには DID ドキュメントをアップロードしてください。",
"subtitleVerify": "続行するには{channel}を確認してください。",
"subtitleUpdatedDidDoc": "PDS 署名キーで DID ドキュメントを更新してください。",
"subtitleActivating": "アカウントを有効化しています...",
"subtitleComplete": "アカウントが正常に作成されました!",
"redirecting": "ダッシュボードへ移動中...",
"infoIdentityDesc": "アイデンティティは、ATProto ネットワーク上でアカウントがどのように識別されるかを決定します。ほとんどのユーザーは標準オプションを選択してください。",
"infoContactDesc": "この情報はアカウントの確認と、アカウントセキュリティに関する重要な通知の送信に使用されます。",
"infoNextTitle": "次のステップは?",
"infoNextDesc": "アカウント作成後、連絡方法を確認すると、新しいアイデンティティで任意の ATProto アプリを使用できます。",
"migrateTitle": "すでにBlueskyアカウントをお持ちですか?",
"migrateDescription": "新しいアカウントを作成する代わりに、既存のアカウントをこのPDSに移行できます。フォロワー、投稿、IDも一緒に移行されます。",
"migrateLink": "PDS Mooverで移行する",
"subtitleKeyChoice": "did:web アイデンティティ設定",
"subtitleInitialDidDoc": "DID ドキュメントをアップロード",
"subtitleVerify": "{channel}を確認",
"subtitleUpdatedDidDoc": "DID ドキュメントを更新",
"subtitleActivating": "有効化中",
"subtitleComplete": "アカウント作成完了",
"redirecting": "リダイレクト中",
"migrateTitle": "すでにアカウントをお持ちですか?",
"migrateDescription": "既存のアカウントを移行",
"migrateLink": "アカウントを移行",
"handle": "ハンドル",
"handlePlaceholder": "あなたの名前",
"handleHint": "完全なハンドル: @{handle}",
"handleTaken": "このハンドルは既に使用されています",
"handleDotWarning": "カスタムドメインハンドルはアカウント作成後に設定で構成できます。",
"password": "パスワード",
"passwordPlaceholder": "8文字以上",
"confirmPassword": "パスワード確認",
"confirmPasswordPlaceholder": "パスワードを再入力",
"identityType": "アイデンティティタイプ",
"identityHint": "分散型アイデンティティの管理方法を選択してください。",
"didPlc": "did:plc",
"didPlcRecommended": "(推奨)",
"didPlcHint": "PLC ディレクトリで管理されるポータブルアイデンティティ",
"didWeb": "did:web",
"didWebHint": "この PDS でホストされるアイデンティティ(下記の警告をお読みください)",
@@ -138,7 +121,6 @@
"externalDidPlaceholder": "did:web:yourdomain.com",
"externalDidHint": "ドメインは /.well-known/did.json でこの PDS を指す有効な DID ドキュメントを提供する必要があります",
"contactMethod": "連絡方法",
"contactMethodHint": "アカウントの確認と通知の受信方法を選択してください。1つだけ必要です。",
"verificationMethod": "確認方法",
"email": "メール",
"emailAddress": "メールアドレス",
@@ -170,7 +152,6 @@
"ssoAccount": "SSO",
"ssoSubtitle": "外部プロバイダーを使用してアカウントを作成",
"noSsoProviders": "このサーバーにはSSOプロバイダーが設定されていません。",
"ssoHint": "プロバイダーを選択してアカウントを作成:",
"continueWith": "{provider}で続行",
"validation": {
"handleRequired": "ハンドルは必須です",
@@ -680,29 +661,28 @@
"oauth": {
"login": {
"title": "サインイン",
"subtitle": "アプリを続行するにはサインインしてください",
"signingIn": "サインイン中...",
"authenticating": "認証中...",
"checkingPasskey": "パスキーを確認中...",
"signInWithPasskey": "パスキーでサインイン",
"passkeyNotSetUp": "パスキーは設定されていません",
"orUsePassword": "またはパスワードを使用",
"subtitle": "サインイン中",
"signingIn": "サインイン中",
"authenticating": "認証中",
"checkingPasskey": "確認中",
"signInWithPasskey": "パスキー",
"passkeyNotSetUp": "パスキーなし",
"orUsePassword": "または",
"password": "パスワード",
"rememberDevice": "このデバイスを記憶する",
"passkeyHintChecking": "パスキーの状態を確認中...",
"passkeyHintAvailable": "パスキーでサインイン",
"passkeyHintNotAvailable": "このアカウントにはパスキーが登録されていません",
"passkeyHint": "デバイスの生体認証またはセキュリティキーを使用",
"passwordPlaceholder": "パスワードを入力",
"rememberDevice": "このデバイスを記憶",
"passkeyHintChecking": "確認中",
"passkeyHintAvailable": "パスキーを使用",
"passkeyHintNotAvailable": "パスキーなし",
"passwordPlaceholder": "パスワード",
"usePasskey": "パスキーを使用",
"orContinueWith": "または次の方法で続行",
"orUseCredentials": "または認証情報でサインイン"
"orContinueWith": "または",
"orUseCredentials": "または"
},
"register": {
"title": "アカウント作成",
"subtitle": "続行するにはアカウントを作成してください",
"subtitleGeneric": "続行するにはアカウントを作成してください",
"haveAccount": "すでにアカウントをお持ちですか?サインイン"
"subtitle": "アプリ",
"subtitleGeneric": "アカウントを作成",
"haveAccount": "アカウントをお持ちですか?サインイン"
},
"sso": {
"linkedAccounts": "連携アカウント",
@@ -787,49 +767,43 @@
},
"accounts": {
"title": "アカウントを選択",
"subtitle": "続行するアカウントを選択",
"useAnother": "別のアカウントを使用"
},
"twoFactor": {
"title": "二要素認証",
"subtitle": "追加の確認が必要です",
"title": "確認",
"usePasskey": "パスキーを使用",
"useTotp": "認証アプリを使用"
},
"twoFactorCode": {
"title": "二要素認証",
"subtitle": "{channel} に確認コード送信しました。以下にコードを入力して続行してください。",
"codeLabel": "確認コード",
"codePlaceholder": "6桁のコードを入力",
"title": "確認",
"subtitle": "{channel} にコード送信済み",
"codeLabel": "コード",
"codePlaceholder": "6桁のコード",
"errors": {
"missingRequestUri": "request_uri パラメータがありません",
"verificationFailed": "確認失敗しました",
"connectionFailed": "サーバーへの接続失敗しました",
"unexpectedResponse": "サーバーからの予期しない応答"
"missingRequestUri": "リクエストURIがありません",
"verificationFailed": "確認失敗",
"connectionFailed": "接続失敗",
"unexpectedResponse": "予期しない応答"
}
},
"totp": {
"title": "認証コードを入力",
"subtitle": "認証アプリの6桁のコードを入力",
"codePlaceholder": "6桁のコードを入力",
"title": "認証コード",
"codePlaceholder": "6桁のコード",
"useBackupCode": "バックアップコードを使用",
"backupCodePlaceholder": "バックアップコードを入力",
"trustDevice": "このデバイスを30日間信頼する",
"hintBackupCode": "バックアップコードを使用中",
"hintTotpCode": "認証コードを使用中",
"hintDefault": "認証アプリは6桁、バックアップコードは8文字"
"backupCodePlaceholder": "バックアップコード",
"trustDevice": "このデバイスを30日間信頼",
"hintBackupCode": "バックアップコード",
"hintTotpCode": "認証コード"
},
"passkey": {
"title": "パスキー確認",
"subtitle": "パスキーで本人確認を行います",
"waiting": "パスキーを待機中...",
"title": "パスキー",
"waiting": "待機中",
"useTotp": "認証アプリを使用"
},
"error": {
"title": "承認エラー",
"genericError": "承認中にエラーが発生しました。",
"title": "承認失敗",
"tryAgain": "再試行",
"backToApp": "アプリに戻る"
"backToApp": "戻る"
}
},
"sso_register": {
@@ -855,9 +829,9 @@
"subtitle": "{channel} に確認コードを送信しました。以下に入力して登録を完了してください。",
"tokenTitle": "確認",
"tokenSubtitle": "確認コードと送信先の識別子を入力してください。",
"codePlaceholder": "XXXX-XXXX-XXXX-XXXX...",
"codePlaceholder": "Paste verification code",
"codeLabel": "確認コード",
"codeHelp": "ダッシュを含む完全なコードをメッセージからコピーしてください",
"codeHelp": "完全なコードをメッセージからコピーしてください",
"verifyButton": "アカウントを確認",
"pleaseWait": "お待ちください...",
"codeResent": "確認コードを再送信しました!",
@@ -958,15 +932,19 @@
},
"registerPasskey": {
"title": "パスキーアカウントを作成",
"subtitle": "パスワードの代わりにパスキーを使用して超安全なアカウントを作成します。",
"subtitleKeyChoice": "外部 did:web アイデンティティの設定方法を選択してください。",
"subtitleVerify": "{channel} に確認コードを送信しました。コードを入力して続行してください。",
"subtitlePasskey": "パスキーを作成してアカウント設定を完了します。",
"subtitleKeyChoice": "did:web アイデンティティを設定",
"subtitleInitialDidDoc": "DID ドキュメントをアップロード",
"subtitleCreating": "アカウント作成中",
"subtitlePasskey": "パスキーを登録",
"subtitleAppPassword": "アプリパスワードを保存",
"subtitleVerify": "{channel}を確認",
"subtitleUpdatedDidDoc": "DID ドキュメントを更新",
"subtitleActivating": "有効化中",
"subtitleComplete": "アカウント作成完了",
"handle": "ハンドル",
"handlePlaceholder": "あなたの名前",
"handleHint": "完全なハンドル: @{handle}",
"contactMethod": "連絡方法",
"contactMethodHint": "アカウントの確認と通知の受信方法を選択してください。",
"verificationMethod": "確認方法",
"email": "メールアドレス",
"emailPlaceholder": "you@example.com",
@@ -993,31 +971,9 @@
"externalDidFormat": "外部DIDはdid:web:で始まる必要があります",
"discordRequired": "Discord認証にはDiscord IDが必要です"
},
"whyPasskeyBullet1": "フィッシングやデータ侵害で盗まれない",
"whyPasskeyBullet2": "ハードウェア支援の暗号鍵を使用",
"whyPasskeyBullet3": "生体認証またはデバイスPINが必要",
"infoWhyPasskey": "なぜパスキーを使うのですか?",
"infoWhyPasskeyDesc": "パスキーはデバイスに保存される暗号化資格情報です。パスワードのようにフィッシング、推測、データ侵害による盗難の被害を受けません。",
"infoHowItWorks": "仕組み",
"infoHowItWorksDesc": "サインイン時、デバイスがFace ID、Touch ID、またはデバイスPINでの確認を求めます。覚えたり入力したりするパスワードはありません。",
"infoAppAccess": "サードパーティアプリの使用",
"infoAppAccessDesc": "アカウント作成後、アプリパスワードが発行されます。Blueskyアプリやその他のAT Protocolクライアントへのサインインに使用してください。",
"whyPasskeyOnly": "なぜパスキーのみ?",
"whyPasskeyOnlyDesc": "パスキーアカウントはパスワードベースのアカウントより安全です:",
"subtitleInitialDidDoc": "続行するにはDIDドキュメントをアップロードしてください。",
"subtitleUpdatedDidDoc": "PDS署名鍵でDIDドキュメントを更新してください。",
"subtitleActivating": "アカウントを有効化しています...",
"subtitleComplete": "アカウントが正常に作成されました!",
"subtitleCreating": "アカウントを作成しています...",
"subtitleAppPassword": "サードパーティアプリ用のアプリパスワードを保存してください。",
"creatingPasskey": "パスキーを作成中...",
"passkeyPrompt": "下のボタンをクリックしてパスキーを作成してください。以下の使用を求められます:",
"passkeyPromptBullet1": "Touch IDまたはFace ID",
"passkeyPromptBullet2": "デバイスのPINまたはパスワード",
"passkeyPromptBullet3": "セキュリティキー(お持ちの場合)",
"creatingPasskey": "作成中",
"identityType": "アイデンティティタイプ",
"identityTypeHint": "分散型アイデンティティの管理方法を選択してください。",
"passkeyNameLabel": "パスキー名(任意)",
"passkeyNamePlaceholder": "例:MacBook Touch ID",
"passkeyNameHint": "このパスキーを識別するための名前",
"createPasskey": "パスキーを作成",
@@ -1044,7 +1000,12 @@
"redirecting": "ダッシュボードに移動中...",
"handleDotWarning": "カスタムドメインハンドルはアカウント作成後に設定できます。",
"wantTraditional": "従来のパスワードを使用しますか?",
"registerWithPassword": "パスワードで登録"
"registerWithPassword": "パスワードで登録",
"activatingAccount": "Activating",
"creatingAccount": "Creating account",
"passkeyDescription": "Register a passkey for this account",
"passkeyName": "Passkey Name",
"setupPasskey": "Create Passkey"
},
"trustedDevices": {
"title": "信頼済みデバイス",
@@ -1068,19 +1029,17 @@
"unknownDevice": "不明なデバイス"
},
"reauth": {
"title": "再認証が必要です",
"subtitle": "続行するには本人確認を行ってください。",
"title": "再認証",
"password": "パスワード",
"totp": "TOTP",
"passkey": "パスキー",
"authenticatorCode": "認証コード",
"usePassword": "パスワードを使用",
"usePasskey": "パスキーを使用",
"useTotp": "認証アプリを使用",
"usePassword": "パスワード",
"usePasskey": "パスキー",
"useTotp": "認証アプリ",
"passwordPlaceholder": "パスワードを入力",
"totpPlaceholder": "6桁のコードを入力",
"authenticating": "認証中...",
"passkeyPrompt": "下のボタンをクリックしてパスキーで認証してください。",
"totpPlaceholder": "6桁のコード",
"authenticating": "認証中",
"cancel": "キャンセル"
},
"verifyChannel": {
+93 -134
View File
@@ -1,6 +1,6 @@
{
"common": {
"loading": "로딩 중...",
"loading": "로딩 중",
"error": "오류",
"save": "저장",
"cancel": "취소",
@@ -16,23 +16,23 @@
"name": "이름",
"dashboard": "대시보드",
"backToDashboard": "← 대시보드",
"copied": "복사됨!",
"copyToClipboard": "클립보드에 복사",
"verifying": "확인 중...",
"saving": "저장 중...",
"creating": "생성 중...",
"updating": "업데이트 중...",
"sending": "전송 중...",
"authenticating": "인증 중...",
"checking": "확인 중...",
"redirecting": "리디렉션 중...",
"copied": "복사됨",
"copyToClipboard": "복사",
"verifying": "확인 중",
"saving": "저장 중",
"creating": "생성 중",
"updating": "업데이트 중",
"sending": "전송 중",
"authenticating": "인증 중",
"checking": "확인 중",
"redirecting": "리디렉션 중",
"signIn": "로그인",
"verify": "확인",
"remove": "삭제",
"revoke": "취소",
"resendCode": "코드 재전송",
"resendCode": "재전송",
"startOver": "처음부터 다시",
"tryAgain": "다시 시도",
"tryAgain": "시도",
"password": "비밀번호",
"email": "이메일",
"emailAddress": "이메일 주소",
@@ -60,65 +60,48 @@
},
"login": {
"title": "로그인",
"subtitle": "PDS 계정을 관리하려면 로그인하세요",
"button": "로그인",
"redirecting": "리디렉션 중...",
"redirecting": "리디렉션 중",
"chooseAccount": "계정 선택",
"signInToAnother": "다른 계정으로 로그인",
"backToSaved": "← 저장된 계정으로 돌아가기",
"signInToAnother": "또는 다른 계정으로 로그인",
"forgotPassword": "비밀번호를 잊으셨나요?",
"lostPasskey": "패스키를 분실하셨나요?",
"noAccount": "계정이 없으신가요?",
"createAccount": "계정 만들기",
"removeAccount": "저장된 계정에서 삭제",
"infoSavedAccountsTitle": "저장된 계정",
"infoSavedAccountsDesc": "계정을 클릭하면 즉시 로그인할 수 있습니다. 세션 토큰은 이 브라우저에 안전하게 저장됩니다.",
"infoNewAccountTitle": "새 계정",
"infoNewAccountDesc": "로그인 버튼을 사용하여 다른 계정을 추가하세요. ×를 클릭하여 저장된 계정을 제거할 수 있습니다.",
"infoSecureSignInTitle": "안전한 로그인",
"infoSecureSignInDesc": "안전한 인증을 위해 리디렉션됩니다. 패스키나 2단계 인증이 활성화되어 있으면 해당 인증도 요청됩니다.",
"infoStaySignedInTitle": "로그인 유지",
"infoStaySignedInDesc": "로그인 후 계정이 이 브라우저에 저장되어 다음에 빠르게 접속할 수 있습니다.",
"infoRecoveryTitle": "계정 복구",
"infoRecoveryDesc": "비밀번호나 패스키를 분실하셨나요? 로그인 버튼 아래의 복구 링크를 사용하세요."
"noAccount": "계정이 없요?",
"createAccount": "만들기",
"removeAccount": "삭제"
},
"verification": {
"title": "계정 인증",
"subtitle": "계정 인증이 필요합니다. 인증 방법으로 전송된 코드를 입력하세요.",
"codeLabel": "인증 코드",
"codePlaceholder": "6자리 코드 입력",
"verifyButton": "계정 인증",
"resent": "인증 코드를 다시 보냈습니다!"
"subtitle": "연락처로 전송된 코드를 입력하세요",
"codeLabel": "코드",
"codePlaceholder": "6자리 코드",
"verifyButton": "인증",
"resent": "코드 전송됨"
},
"register": {
"title": "계정 만들기",
"subtitle": "이 PDS에 새 계정을 만듭니다",
"subtitleKeyChoice": "외부 did:web 신원 설정하는 방법을 선택하세요.",
"subtitleInitialDidDoc": "계속하려면 DID 문서 업로드하세요.",
"subtitleVerify": "계속하려면 {channel}을(를) 인증하세요.",
"subtitleUpdatedDidDoc": "PDS 서명 키로 DID 문서 업데이트하세요.",
"subtitleActivating": "계정을 활성화하는 중...",
"subtitleComplete": "계정이 성공적으로 생성되었습니다!",
"redirecting": "대시보드로 이동 중...",
"infoIdentityDesc": "신원은 ATProto 네트워크에서 계정이 어떻게 식별되는지를 결정합니다. 대부분의 사용자는 표준 옵션을 선택해야 합니다.",
"infoContactDesc": "이 정보는 계정 인증과 계정 보안에 관한 중요한 알림을 보내는 데 사용됩니다.",
"infoNextTitle": "다음 단계는?",
"infoNextDesc": "계정 생성 후 연락 방법을 인증하면 새로운 신원으로 모든 ATProto 앱을 사용할 수 있습니다.",
"migrateTitle": "이미 Bluesky 계정이 있으신가요?",
"migrateDescription": "새 계정을 만드는 대신 기존 계정을 이 PDS로 마이그레이션할 수 있습니다. 팔로워, 게시물, ID가 함께 이전됩니다.",
"migrateLink": "PDS Moover로 마이그레이션",
"subtitleKeyChoice": "did:web 신원 설정",
"subtitleInitialDidDoc": "DID 문서 업로드",
"subtitleVerify": "{channel} 인증",
"subtitleUpdatedDidDoc": "DID 문서 업데이트",
"subtitleActivating": "활성화 중",
"subtitleComplete": "계정 생성됨",
"redirecting": "리디렉션 중",
"migrateTitle": "이미 계정이 있으신가요?",
"migrateDescription": "기존 계정 마이그레이션",
"migrateLink": "계정 마이그레이션",
"handle": "핸들",
"handlePlaceholder": "사용자 이름",
"handleHint": "전체 핸들: @{handle}",
"handleTaken": "이 핸들은 이미 사용 중입니다",
"handleDotWarning": "사용자 정의 도메인 핸들은 계정 생성 후 설정에서 구성할 수 있습니다.",
"password": "비밀번호",
"passwordPlaceholder": "8자 이상",
"confirmPassword": "비밀번호 확인",
"confirmPasswordPlaceholder": "비밀번호 재입력",
"identityType": "ID 유형",
"identityHint": "분산 ID를 관리하는 방법을 선택하세요.",
"didPlc": "did:plc",
"didPlcRecommended": "(권장)",
"didPlcHint": "PLC 디렉토리에서 관리하는 이동 가능한 ID",
"didWeb": "did:web",
"didWebHint": "이 PDS에서 호스팅되는 ID (아래 경고 참조)",
@@ -138,7 +121,6 @@
"externalDidPlaceholder": "did:web:yourdomain.com",
"externalDidHint": "도메인은 /.well-known/did.json에서 이 PDS를 가리키는 유효한 DID 문서를 제공해야 합니다",
"contactMethod": "연락 방법",
"contactMethodHint": "계정 인증 및 알림 수신 방법을 선택하세요. 하나만 필요합니다.",
"verificationMethod": "인증 방법",
"email": "이메일",
"emailAddress": "이메일 주소",
@@ -170,7 +152,6 @@
"ssoAccount": "SSO",
"ssoSubtitle": "외부 제공자를 사용하여 계정 만들기",
"noSsoProviders": "이 서버에 SSO 제공자가 설정되어 있지 않습니다.",
"ssoHint": "계정을 만들 제공자를 선택하세요:",
"continueWith": "{provider}로 계속",
"validation": {
"handleRequired": "핸들은 필수입니다",
@@ -680,29 +661,28 @@
"oauth": {
"login": {
"title": "로그인",
"subtitle": "앱을 계속하려면 로그인하세요",
"signingIn": "로그인 중...",
"authenticating": "인증 중...",
"checkingPasskey": "패스키 확인 중...",
"signInWithPasskey": "패스키로 로그인",
"passkeyNotSetUp": "패스키가 설정되지 않음",
"orUsePassword": "또는 비밀번호 사용",
"subtitle": "로그인 중",
"signingIn": "로그인 중",
"authenticating": "인증 중",
"checkingPasskey": "확인 중",
"signInWithPasskey": "패스키",
"passkeyNotSetUp": "패스키음",
"orUsePassword": "또는",
"password": "비밀번호",
"rememberDevice": "이 기기 기억하기",
"passkeyHintChecking": "패스키 상태 확인 중...",
"passkeyHintAvailable": "패스키로 로그인",
"passkeyHintNotAvailable": "이 계정에 등록된 패스키습니다",
"passkeyHint": "기기의 생체 인식 또는 보안 키 사용",
"passwordPlaceholder": "비밀번호 입력",
"passkeyHintChecking": "확인 중",
"passkeyHintAvailable": "패스키 사용",
"passkeyHintNotAvailable": "패스키 없",
"passwordPlaceholder": "비밀번호",
"usePasskey": "패스키 사용",
"orContinueWith": "또는 다음으로 계속",
"orUseCredentials": "또는 자격 증명으로 로그인"
"orContinueWith": "또는",
"orUseCredentials": "또는"
},
"register": {
"title": "계정 만들기",
"subtitle": "계속하려면 계정을 만드세요",
"subtitleGeneric": "계속하려면 계정을 만드세요",
"haveAccount": "이미 계정이 있으신가요? 로그인"
"subtitle": "",
"subtitleGeneric": "계정 만들기",
"haveAccount": "계정이 있으신가요? 로그인"
},
"sso": {
"linkedAccounts": "연결된 계정",
@@ -787,49 +767,43 @@
},
"accounts": {
"title": "계정 선택",
"subtitle": "계속할 계정 선택",
"useAnother": "다른 계정 사용"
},
"twoFactor": {
"title": "2단계 인증",
"subtitle": "추가 확인이 필요합니다",
"title": "인증",
"usePasskey": "패스키 사용",
"useTotp": "인증 앱 사용"
},
"twoFactorCode": {
"title": "2단계 인증",
"subtitle": "{channel}(으)로 인증 코드를 보냈습니다. 아래에 코드를 입력하여 계속하세요.",
"codeLabel": "인증 코드",
"codePlaceholder": "6자리 코드 입력",
"title": "인증",
"subtitle": "{channel}(으)로 코드 전송됨",
"codeLabel": "코드",
"codePlaceholder": "6자리 코드",
"errors": {
"missingRequestUri": "request_uri 매개변수가 없습니다",
"verificationFailed": "인증 실패했습니다",
"connectionFailed": "서버에 연결하지 못했습니다",
"unexpectedResponse": "서버로부터 예기치 않은 응답"
"missingRequestUri": "요청 URI 없음",
"verificationFailed": "인증 실패",
"connectionFailed": "연결 실패",
"unexpectedResponse": "예기치 않은 응답"
}
},
"totp": {
"title": "인증 코드 입력",
"subtitle": "인증 앱의 6자리 코드를 입력하세요",
"codePlaceholder": "6자리 코드 입력",
"title": "인증 코드",
"codePlaceholder": "6자리 코드",
"useBackupCode": "백업 코드 사용",
"backupCodePlaceholder": "백업 코드 입력",
"backupCodePlaceholder": "백업 코드",
"trustDevice": "이 기기를 30일간 신뢰",
"hintBackupCode": "백업 코드 사용 중",
"hintTotpCode": "인증 코드 사용 중",
"hintDefault": "인증 앱은 6자리, 백업 코드는 8자"
"hintBackupCode": "백업 코드",
"hintTotpCode": "인증 코드"
},
"passkey": {
"title": "패스키 확인",
"subtitle": "패스키를 사용하여 본인 확인",
"waiting": "패스키 대기 중...",
"title": "패스키",
"waiting": "대기 중",
"useTotp": "인증 앱 사용"
},
"error": {
"title": "승인 오류",
"genericError": "승인 중 오류가 발생했습니다.",
"title": "승인 실패",
"tryAgain": "다시 시도",
"backToApp": "앱으로 돌아가기"
"backToApp": "돌아가기"
}
},
"sso_register": {
@@ -855,9 +829,9 @@
"subtitle": "{channel}(으)로 인증 코드를 보냈습니다. 아래에 입력하여 등록을 완료하세요.",
"tokenTitle": "인증",
"tokenSubtitle": "인증 코드와 전송된 식별자를 입력하세요.",
"codePlaceholder": "XXXX-XXXX-XXXX-XXXX...",
"codePlaceholder": "Paste verification code",
"codeLabel": "인증 코드",
"codeHelp": "메시지에서 하이픈을 포함한 전체 코드를 복사하세요",
"codeHelp": "메시지에서 하이픈을 를 복사하세요",
"verifyButton": "계정 인증",
"pleaseWait": "잠시 기다려 주세요...",
"codeResent": "인증 코드를 다시 보냈습니다!",
@@ -958,15 +932,19 @@
},
"registerPasskey": {
"title": "패스키 계정 만들기",
"subtitle": "비밀번호 대신 패스키를 사용하여 초안전 계정을 만듭니다.",
"subtitleKeyChoice": "외부 did:web 아이덴티티 설정 방법을 선택하세요.",
"subtitleVerify": "{channel}(으)로 인증 코드를 보냈습니다. 코드를 입력하여 계속하세요.",
"subtitlePasskey": "패스키를 만들어 계정 설정을 완료하세요.",
"subtitleKeyChoice": "did:web 아이덴티티 설정",
"subtitleInitialDidDoc": "DID 문서 업로드",
"subtitleCreating": "계정 생성 중",
"subtitlePasskey": "패스키 등록",
"subtitleAppPassword": "앱 비밀번호 저장",
"subtitleVerify": "{channel} 인증",
"subtitleUpdatedDidDoc": "DID 문서 업데이트",
"subtitleActivating": "활성화 중",
"subtitleComplete": "계정 생성됨",
"handle": "핸들",
"handlePlaceholder": "사용자 이름",
"handleHint": "전체 핸들: @{handle}",
"contactMethod": "연락 방법",
"contactMethodHint": "계정 인증 및 알림 수신 방법을 선택하세요.",
"verificationMethod": "인증 방법",
"email": "이메일 주소",
"emailPlaceholder": "you@example.com",
@@ -993,31 +971,9 @@
"externalDidFormat": "외부 DID는 did:web:으로 시작해야 합니다",
"discordRequired": "Discord 인증에는 Discord ID가 필요합니다"
},
"whyPasskeyBullet1": "피싱이나 데이터 유출로 도난당할 수 없음",
"whyPasskeyBullet2": "하드웨어 기반 암호화 키 사용",
"whyPasskeyBullet3": "생체 인식 또는 기기 PIN 필요",
"infoWhyPasskey": "왜 패스키를 사용하나요?",
"infoWhyPasskeyDesc": "패스키는 기기에 저장된 암호화 자격 증명입니다. 비밀번호처럼 피싱, 추측 또는 데이터 유출로 도난당할 수 없습니다.",
"infoHowItWorks": "작동 방식",
"infoHowItWorksDesc": "로그인할 때 기기에서 Face ID, Touch ID 또는 기기 PIN으로 인증하라는 메시지가 표시됩니다. 기억하거나 입력할 비밀번호가 없습니다.",
"infoAppAccess": "서드파티 앱 사용",
"infoAppAccessDesc": "계정 생성 후 앱 비밀번호를 받게 됩니다. Bluesky 앱 및 기타 AT Protocol 클라이언트에 로그인할 때 사용하세요.",
"whyPasskeyOnly": "왜 패스키만 사용하나요?",
"whyPasskeyOnlyDesc": "패스키 계정은 비밀번호 기반 계정보다 안전합니다:",
"subtitleInitialDidDoc": "계속하려면 DID 문서를 업로드하세요.",
"subtitleUpdatedDidDoc": "PDS 서명 키로 DID 문서를 업데이트하세요.",
"subtitleActivating": "계정을 활성화하는 중...",
"subtitleComplete": "계정이 성공적으로 생성되었습니다!",
"subtitleCreating": "계정을 생성하는 중...",
"subtitleAppPassword": "서드파티 앱용 앱 비밀번호를 저장하세요.",
"creatingPasskey": "패스키 생성 중...",
"passkeyPrompt": "아래 버튼을 클릭하여 패스키를 생성하세요. 다음을 사용하라는 메시지가 표시됩니다:",
"passkeyPromptBullet1": "Touch ID 또는 Face ID",
"passkeyPromptBullet2": "기기 PIN 또는 비밀번호",
"passkeyPromptBullet3": "보안 키 (있는 경우)",
"creatingPasskey": "생성 중",
"identityType": "아이덴티티 유형",
"identityTypeHint": "분산 아이덴티티 관리 방법을 선택하세요.",
"passkeyNameLabel": "패스키 이름 (선택사항)",
"passkeyNamePlaceholder": "예: MacBook Touch ID",
"passkeyNameHint": "이 패스키를 식별할 수 있는 이름",
"createPasskey": "패스키 생성",
@@ -1044,7 +1000,12 @@
"redirecting": "대시보드로 이동 중...",
"handleDotWarning": "사용자 정의 도메인 핸들은 계정 생성 후 설정할 수 있습니다.",
"wantTraditional": "기존 비밀번호를 원하시나요?",
"registerWithPassword": "비밀번호로 가입"
"registerWithPassword": "비밀번호로 가입",
"activatingAccount": "Activating",
"creatingAccount": "Creating account",
"passkeyDescription": "Register a passkey for this account",
"passkeyName": "Passkey Name",
"setupPasskey": "Create Passkey"
},
"trustedDevices": {
"title": "신뢰할 수 있는 기기",
@@ -1068,19 +1029,17 @@
"unknownDevice": "알 수 없는 기기"
},
"reauth": {
"title": "재인증 필요",
"subtitle": "계속하려면 본인 확인을 해주세요.",
"title": "재인증",
"password": "비밀번호",
"totp": "TOTP",
"passkey": "패스키",
"authenticatorCode": "인증 코드",
"usePassword": "비밀번호 사용",
"usePasskey": "패스키 사용",
"useTotp": "인증 앱 사용",
"usePassword": "비밀번호",
"usePasskey": "패스키",
"useTotp": "인증 앱",
"passwordPlaceholder": "비밀번호 입력",
"totpPlaceholder": "6자리 코드 입력",
"authenticating": "인증 중...",
"passkeyPrompt": "아래 버튼을 클릭하여 패스키로 인증하세요.",
"totpPlaceholder": "6자리 코드",
"authenticating": "인증 중",
"cancel": "취소"
},
"verifyChannel": {
@@ -1100,7 +1059,7 @@
"identifierPlaceholder": "이메일, Discord ID 등",
"identifierHelp": "인증할 이메일 주소, Discord ID, Telegram 사용자 이름 또는 Signal 번호.",
"codeLabel": "인증 코드",
"codeHelp": "메시지에서 하이픈을 포함한 전체 코드를 복사하세요.",
"codeHelp": "메시지에서 하이픈을 를 복사하세요.",
"verifyButton": "인증"
},
"delegation": {
+95 -136
View File
@@ -1,6 +1,6 @@
{
"common": {
"loading": "Laddar...",
"loading": "Laddar",
"error": "Fel",
"save": "Spara",
"cancel": "Avbryt",
@@ -16,21 +16,21 @@
"name": "Namn",
"dashboard": "Kontrollpanel",
"backToDashboard": "← Kontrollpanel",
"copied": "Kopierat!",
"copied": "Kopierat",
"copyToClipboard": "Kopiera",
"verifying": "Verifierar...",
"saving": "Sparar...",
"creating": "Skapar...",
"updating": "Uppdaterar...",
"sending": "Skickar...",
"authenticating": "Autentiserar...",
"checking": "Kontrollerar...",
"redirecting": "Omdirigerar...",
"verifying": "Verifierar",
"saving": "Sparar",
"creating": "Skapar",
"updating": "Uppdaterar",
"sending": "Skickar",
"authenticating": "Autentiserar",
"checking": "Kontrollerar",
"redirecting": "Omdirigerar",
"signIn": "Logga in",
"verify": "Verifiera",
"remove": "Ta bort",
"revoke": "Återkalla",
"resendCode": "Skicka kod igen",
"resendCode": "Skicka kod",
"startOver": "Börja om",
"tryAgain": "Försök igen",
"password": "Lösenord",
@@ -60,65 +60,48 @@
},
"login": {
"title": "Logga in",
"subtitle": "Logga in för att hantera ditt PDS-konto",
"button": "Logga in",
"redirecting": "Omdirigerar...",
"redirecting": "Omdirigerar",
"chooseAccount": "Välj ett konto",
"signInToAnother": "Logga in med ett annat konto",
"backToSaved": "← Tillbaka till sparade konton",
"signInToAnother": "Eller logga in med ett annat konto",
"forgotPassword": "Glömt lösenordet?",
"lostPasskey": "Tappat bort nyckeln?",
"noAccount": "Har du inget konto?",
"createAccount": "Skapa konto",
"removeAccount": "Ta bort från sparade konton",
"infoSavedAccountsTitle": "Sparade konton",
"infoSavedAccountsDesc": "Klicka på ett konto för att logga in direkt. Dina sessionstoken lagras säkert i denna webbläsare.",
"infoNewAccountTitle": "Nytt konto",
"infoNewAccountDesc": "Använd inloggningsknappen för att lägga till ett annat konto. Klicka på × för att ta bort sparade konton.",
"infoSecureSignInTitle": "Säker inloggning",
"infoSecureSignInDesc": "Du omdirigeras för säker autentisering. Om du har aktiverat nycklar eller tvåfaktorsautentisering kommer du också att behöva ange dessa.",
"infoStaySignedInTitle": "Förbli inloggad",
"infoStaySignedInDesc": "Efter inloggning sparas ditt konto i denna webbläsare för snabb åtkomst nästa gång.",
"infoRecoveryTitle": "Kontoåterställning",
"infoRecoveryDesc": "Har du tappat bort ditt lösenord eller din nyckel? Använd återställningslänkarna under inloggningsknappen."
"noAccount": "Inget konto?",
"createAccount": "Skapa ett",
"removeAccount": "Ta bort"
},
"verification": {
"title": "Verifiera ditt konto",
"subtitle": "Ditt konto behöver verifieras. Ange koden som skickades till din verifieringsmetod.",
"codeLabel": "Verifieringskod",
"codePlaceholder": "Ange 6-siffrig kod",
"verifyButton": "Verifiera konto",
"resent": "Verifieringskod skickad igen!"
"title": "Verifiera konto",
"subtitle": "Ange koden som skickades till din kontaktmetod",
"codeLabel": "Kod",
"codePlaceholder": "6-siffrig kod",
"verifyButton": "Verifiera",
"resent": "Kod skickad"
},
"register": {
"title": "Skapa konto",
"subtitle": "Skapa ett nytt konto på denna PDS",
"subtitleKeyChoice": "Välj hur du vill konfigurera din externa did:web-identitet.",
"subtitleInitialDidDoc": "Ladda upp ditt DID-dokument för att fortsätta.",
"subtitleVerify": "Verifiera din {channel} för att fortsätta.",
"subtitleUpdatedDidDoc": "Uppdatera ditt DID-dokument med PDS-signeringsnyckeln.",
"subtitleActivating": "Aktiverar ditt konto...",
"subtitleComplete": "Ditt konto har skapats!",
"redirecting": "Omdirigerar till kontrollpanelen...",
"infoIdentityDesc": "Din identitet avgör hur ditt konto identifieras i ATProto-nätverket. De flesta användare bör välja standardalternativet.",
"infoContactDesc": "Vi använder detta för att verifiera ditt konto och skicka viktiga meddelanden om din kontosäkerhet.",
"infoNextTitle": "Vad händer härnäst?",
"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.",
"migrateTitle": "Har du redan ett Bluesky-konto?",
"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.",
"migrateLink": "Flytta med PDS Moover",
"subtitleKeyChoice": "Konfigurera din did:web-identitet",
"subtitleInitialDidDoc": "Ladda upp ditt DID-dokument",
"subtitleVerify": "Verifiera din {channel}",
"subtitleUpdatedDidDoc": "Uppdatera ditt DID-dokument",
"subtitleActivating": "Aktiverar",
"subtitleComplete": "Konto skapat",
"redirecting": "Omdirigerar",
"migrateTitle": "Har du redan ett konto?",
"migrateDescription": "Migrera istället för att skapa nytt",
"migrateLink": "Migrera ditt konto",
"handle": "Användarnamn",
"handlePlaceholder": "dittnamn",
"handleHint": "Ditt fullständiga användarnamn blir: @{handle}",
"handleTaken": "Detta användarnamn är redan taget",
"handleDotWarning": "Egna domännamn kan konfigureras efter att kontot skapats i Inställningar.",
"password": "Lösenord",
"passwordPlaceholder": "Minst 8 tecken",
"confirmPassword": "Bekräfta lösenord",
"confirmPasswordPlaceholder": "Bekräfta ditt lösenord",
"identityType": "Identitetstyp",
"identityHint": "Välj hur din decentraliserade identitet ska hanteras.",
"didPlc": "did:plc",
"didPlcRecommended": "(Rekommenderas)",
"didPlcHint": "Portabel identitet hanterad av PLC Directory",
"didWeb": "did:web",
"didWebHint": "Identitet lagrad på denna PDS (läs varningen nedan)",
@@ -138,7 +121,6 @@
"externalDidPlaceholder": "did:web:dindomän.se",
"externalDidHint": "Din domän måste tillhandahålla ett giltigt DID-dokument på /.well-known/did.json som pekar på denna PDS",
"contactMethod": "Kontaktmetod",
"contactMethodHint": "Välj hur du vill verifiera ditt konto och ta emot meddelanden. Du behöver bara en.",
"verificationMethod": "Verifieringsmetod",
"email": "E-post",
"emailAddress": "E-postadress",
@@ -170,7 +152,6 @@
"ssoAccount": "SSO",
"ssoSubtitle": "Skapa ett konto med en extern leverantör",
"noSsoProviders": "Inga SSO-leverantörer är konfigurerade på denna server.",
"ssoHint": "Välj en leverantör för att skapa ditt konto:",
"continueWith": "Fortsätt med {provider}",
"validation": {
"handleRequired": "Användarnamn krävs",
@@ -680,29 +661,28 @@
"oauth": {
"login": {
"title": "Logga in",
"subtitle": "Logga in för att fortsätta till applikationen",
"signingIn": "Loggar in...",
"authenticating": "Autentiserar...",
"checkingPasskey": "Kontrollerar nyckel...",
"signInWithPasskey": "Logga in med nyckel",
"passkeyNotSetUp": "Nyckel inte konfigurerad",
"orUsePassword": "eller använd lösenord",
"subtitle": "Loggar in ",
"signingIn": "Loggar in",
"authenticating": "Autentiserar",
"checkingPasskey": "Kontrollerar",
"signInWithPasskey": "Nyckel",
"passkeyNotSetUp": "Ingen nyckel",
"orUsePassword": "eller",
"password": "Lösenord",
"rememberDevice": "Kom ihåg denna enhet",
"passkeyHintChecking": "Kontrollerar nyckelstatus...",
"passkeyHintAvailable": "Logga in med din nyckel",
"passkeyHintNotAvailable": "Inga nycklar registrerade för detta konto",
"passkeyHint": "Använd enhetens biometri eller säkerhetsnyckel",
"passwordPlaceholder": "Ange ditt lösenord",
"passkeyHintChecking": "Kontrollerar",
"passkeyHintAvailable": "Använd nyckel",
"passkeyHintNotAvailable": "Ingen nyckel registrerad",
"passwordPlaceholder": "Lösenord",
"usePasskey": "Använd nyckel",
"orContinueWith": "Eller fortsätt med",
"orUseCredentials": "Eller logga in med uppgifter"
"orContinueWith": "eller",
"orUseCredentials": "eller"
},
"register": {
"title": "Skapa konto",
"subtitle": "Skapa ett konto med {app}",
"subtitleGeneric": "Skapa ett konto för att fortsätta",
"haveAccount": "Har du redan ett konto?"
"subtitle": "för",
"subtitleGeneric": "Skapa ett konto",
"haveAccount": "Har du ett konto? Logga in"
},
"sso": {
"linkedAccounts": "Länkade konton",
@@ -787,49 +767,43 @@
},
"accounts": {
"title": "Välj konto",
"subtitle": "Välj ett konto för att fortsätta",
"useAnother": "Använd ett annat konto"
},
"twoFactor": {
"title": "Tvåfaktorsautentisering",
"subtitle": "Ytterligare verifiering krävs",
"title": "Verifiering",
"usePasskey": "Använd nyckel",
"useTotp": "Använd autentiseringsapp"
"useTotp": "Använd autentiserare"
},
"twoFactorCode": {
"title": "Tvåfaktorsautentisering",
"subtitle": "En verifieringskod har skickats till din {channel}. Ange koden nedan för att fortsätta.",
"codeLabel": "Verifieringskod",
"codePlaceholder": "Ange 6-siffrig kod",
"title": "Verifiering",
"subtitle": "Kod skickad till {channel}",
"codeLabel": "Kod",
"codePlaceholder": "6-siffrig kod",
"errors": {
"missingRequestUri": "Saknar request_uri-parameter",
"missingRequestUri": "Saknar request URI",
"verificationFailed": "Verifiering misslyckades",
"connectionFailed": "Kunde inte ansluta till servern",
"unexpectedResponse": "Oväntat svar från servern"
"connectionFailed": "Anslutning misslyckades",
"unexpectedResponse": "Oväntat svar"
}
},
"totp": {
"title": "Ange autentiseringskod",
"subtitle": "Ange den 6-siffriga koden från din autentiseringsapp",
"codePlaceholder": "Ange 6-siffrig kod",
"useBackupCode": "Använd reservkod istället",
"backupCodePlaceholder": "Ange reservkod",
"title": "Autentiseringskod",
"codePlaceholder": "6-siffrig kod",
"useBackupCode": "Använd reservkod",
"backupCodePlaceholder": "Reservkod",
"trustDevice": "Lita på denna enhet i 30 dagar",
"hintBackupCode": "Använder reservkod",
"hintTotpCode": "Använder autentiseringskod",
"hintDefault": "6 siffror för autentiserare, 8 tecken för reservkod"
"hintBackupCode": "Reservkod",
"hintTotpCode": "Autentiseringskod"
},
"passkey": {
"title": "Nyckelverifiering",
"subtitle": "Använd din nyckel för att verifiera din identitet",
"waiting": "Väntar på nyckel...",
"useTotp": "Använd autentiseringsapp istället"
"title": "Nyckel",
"waiting": "Väntar",
"useTotp": "Använd autentiserare"
},
"error": {
"title": "Auktoriseringsfel",
"genericError": "Ett fel uppstod under auktorisering.",
"title": "Auktorisering misslyckades",
"tryAgain": "Försök igen",
"backToApp": "Tillbaka till applikationen"
"backToApp": "Tillbaka"
}
},
"sso_register": {
@@ -855,9 +829,9 @@
"subtitle": "Vi har skickat en verifieringskod till din {channel}. Ange den nedan för att slutföra registreringen.",
"tokenTitle": "Verifiera",
"tokenSubtitle": "Ange verifieringskoden och identifieraren den skickades till.",
"codePlaceholder": "XXXX-XXXX-XXXX-XXXX...",
"codePlaceholder": "Paste verification code",
"codeLabel": "Verifieringskod",
"codeHelp": "Kopiera hela koden från ditt meddelande, inklusive bindestreck",
"codeHelp": "Kopiera hela koden från ditt meddelande, ",
"verifyButton": "Verifiera konto",
"pleaseWait": "Vänta...",
"codeResent": "Verifieringskod skickad igen!",
@@ -958,15 +932,19 @@
},
"registerPasskey": {
"title": "Skapa nyckelkonto",
"subtitle": "Skapa ett ultrasäkert konto med en nyckel istället för ett lösenord.",
"subtitleKeyChoice": "Välj hur du vill konfigurera din externa did:web-identitet.",
"subtitleVerify": "Vi har skickat en verifieringskod till din {channel}. Ange koden för att fortsätta.",
"subtitlePasskey": "Skapa din nyckel för att slutföra kontokonfigurationen.",
"subtitleKeyChoice": "Konfigurera din did:web-identitet",
"subtitleInitialDidDoc": "Ladda upp ditt DID-dokument",
"subtitleCreating": "Skapar konto",
"subtitlePasskey": "Registrera din nyckel",
"subtitleAppPassword": "Spara ditt applösenord",
"subtitleVerify": "Verifiera din {channel}",
"subtitleUpdatedDidDoc": "Uppdatera ditt DID-dokument",
"subtitleActivating": "Aktiverar",
"subtitleComplete": "Konto skapat",
"handle": "Användarnamn",
"handlePlaceholder": "dittnamn",
"handleHint": "Ditt fullständiga användarnamn blir: @{handle}",
"contactMethod": "Kontaktmetod",
"contactMethodHint": "Välj hur du vill verifiera ditt konto och ta emot meddelanden.",
"verificationMethod": "Verifieringsmetod",
"email": "E-postadress",
"emailPlaceholder": "du@exempel.se",
@@ -993,31 +971,9 @@
"externalDidFormat": "Extern DID måste börja med did:web:",
"discordRequired": "Discord-ID krävs för Discord-verifiering"
},
"whyPasskeyBullet1": "Kan inte nätfiskas eller stjälas vid dataintrång",
"whyPasskeyBullet2": "Använder hårdvarubaserade kryptografiska nycklar",
"whyPasskeyBullet3": "Kräver din biometri eller enhets-PIN för att använda",
"infoWhyPasskey": "Varfor anvanda nyckel?",
"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.",
"infoHowItWorks": "Hur det fungerar",
"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.",
"infoAppAccess": "Använda tredjepartsappar",
"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.",
"whyPasskeyOnly": "Varför endast nyckel?",
"whyPasskeyOnlyDesc": "Nyckelkonton är säkrare än lösenordsbaserade konton eftersom de:",
"subtitleInitialDidDoc": "Ladda upp ditt DID-dokument för att fortsätta.",
"subtitleUpdatedDidDoc": "Uppdatera ditt DID-dokument med PDS-signeringsnyckeln.",
"subtitleActivating": "Aktiverar ditt konto...",
"subtitleComplete": "Ditt konto har skapats!",
"subtitleCreating": "Skapar ditt konto...",
"subtitleAppPassword": "Spara ditt applösenord för tredjepartsappar.",
"creatingPasskey": "Skapar nyckel...",
"passkeyPrompt": "Klicka på knappen nedan för att skapa din nyckel. Du kommer att uppmanas att använda:",
"passkeyPromptBullet1": "Touch ID eller Face ID",
"passkeyPromptBullet2": "Din enhets PIN-kod eller lösenord",
"passkeyPromptBullet3": "En säkerhetsnyckel (om du har en)",
"creatingPasskey": "Skapar",
"identityType": "Identitetstyp",
"identityTypeHint": "Välj hur din decentraliserade identitet ska hanteras.",
"passkeyNameLabel": "Nyckelnamn (valfritt)",
"passkeyNamePlaceholder": "t.ex. MacBook Touch ID",
"passkeyNameHint": "Ett vänligt namn för att identifiera denna nyckel",
"createPasskey": "Skapa nyckel",
@@ -1044,7 +1000,12 @@
"redirecting": "Omdirigerar till instrumentpanelen...",
"handleDotWarning": "Egna domännamn kan konfigureras efter att kontot skapats.",
"wantTraditional": "Vill du ha ett traditionellt lösenord?",
"registerWithPassword": "Registrera med lösenord"
"registerWithPassword": "Registrera med lösenord",
"activatingAccount": "Activating",
"creatingAccount": "Creating account",
"passkeyDescription": "Register a passkey for this account",
"passkeyName": "Passkey Name",
"setupPasskey": "Create Passkey"
},
"trustedDevices": {
"title": "Betrodda enheter",
@@ -1068,19 +1029,17 @@
"unknownDevice": "Okänd enhet"
},
"reauth": {
"title": "Återautentisering krävs",
"subtitle": "Verifiera din identitet för att fortsätta.",
"title": "Återautentisera",
"password": "Lösenord",
"totp": "TOTP",
"passkey": "Passkey",
"passkey": "Nyckel",
"authenticatorCode": "Autentiseringskod",
"usePassword": "Använd lösenord",
"usePasskey": "Använd nyckel",
"useTotp": "Använd autentiserare",
"usePassword": "Lösenord",
"usePasskey": "Nyckel",
"useTotp": "Autentiserare",
"passwordPlaceholder": "Ange ditt lösenord",
"totpPlaceholder": "Ange 6-siffrig kod",
"authenticating": "Autentiserar...",
"passkeyPrompt": "Klicka på knappen nedan för att autentisera med din passkey.",
"totpPlaceholder": "6-siffrig kod",
"authenticating": "Autentiserar",
"cancel": "Avbryt"
},
"verifyChannel": {
@@ -1100,7 +1059,7 @@
"identifierPlaceholder": "E-post, Discord ID, etc.",
"identifierHelp": "E-postadressen, Discord ID, Telegram-användarnamn eller Signal-nummer som verifieras.",
"codeLabel": "Verifieringskod",
"codeHelp": "Kopiera hela koden från ditt meddelande, inklusive bindestreck.",
"codeHelp": "Kopiera hela koden från ditt meddelande, .",
"verifyButton": "Verifiera"
},
"delegation": {
+91 -132
View File
@@ -1,6 +1,6 @@
{
"common": {
"loading": "加载中...",
"loading": "加载中",
"error": "错误",
"save": "保存",
"cancel": "取消",
@@ -16,21 +16,21 @@
"name": "名称",
"dashboard": "控制台",
"backToDashboard": "← 返回控制台",
"copied": "已复制",
"copied": "已复制",
"copyToClipboard": "复制",
"verifying": "验证中...",
"saving": "保存中...",
"creating": "创建中...",
"updating": "更新中...",
"sending": "发送中...",
"authenticating": "认证中...",
"checking": "检查中...",
"redirecting": "跳转中...",
"verifying": "验证中",
"saving": "保存中",
"creating": "创建中",
"updating": "更新中",
"sending": "发送中",
"authenticating": "认证中",
"checking": "检查中",
"redirecting": "跳转中",
"signIn": "登录",
"verify": "验证",
"remove": "移除",
"revoke": "撤销",
"resendCode": "重新发送验证码",
"resendCode": "重新发送",
"startOver": "重新开始",
"tryAgain": "重试",
"password": "密码",
@@ -60,65 +60,48 @@
},
"login": {
"title": "登录",
"subtitle": "登录以管理您的 PDS 账户",
"button": "登录",
"redirecting": "跳转中...",
"redirecting": "跳转中",
"chooseAccount": "选择账户",
"signInToAnother": "登录其他账户",
"backToSaved": "← 返回已保存账户",
"signInToAnother": "登录其他账户",
"forgotPassword": "忘记密码?",
"lostPasskey": "丢失通行密钥?",
"noAccount": "没有账户?",
"createAccount": "立即注册",
"removeAccount": "从已保存账户中移除",
"infoSavedAccountsTitle": "已保存账户",
"infoSavedAccountsDesc": "点击账户即可快速登录。您的会话令牌安全存储在此浏览器中。",
"infoNewAccountTitle": "新账户",
"infoNewAccountDesc": "使用登录按钮添加其他账户。点击 × 可从此浏览器中移除已保存的账户。",
"infoSecureSignInTitle": "安全登录",
"infoSecureSignInDesc": "您将被重定向进行安全认证。如果您启用了通行密钥或双重身份验证,也会提示您进行验证。",
"infoStaySignedInTitle": "保持登录",
"infoStaySignedInDesc": "登录后,您的账户将保存在此浏览器中,方便下次快速访问。",
"infoRecoveryTitle": "账户恢复",
"infoRecoveryDesc": "忘记密码或丢失通行密钥?使用登录按钮下方的恢复链接。"
"noAccount": "没有账户?",
"createAccount": "注册",
"removeAccount": "移除"
},
"verification": {
"title": "验证账户",
"subtitle": "您的账户需要验证。请输入发送到您验证方式的验证码",
"subtitle": "请输入发送到您联系方式的验证码",
"codeLabel": "验证码",
"codePlaceholder": "输入6位验证码",
"verifyButton": "验证账户",
"resent": "验证码已重新发送"
"codePlaceholder": "6位验证码",
"verifyButton": "验证",
"resent": "验证码已发送"
},
"register": {
"title": "创建账户",
"subtitle": "在此 PDS 上创建新账户",
"subtitleKeyChoice": "选择如何设置您的外部 did:web 身份",
"subtitleInitialDidDoc": "上传您的 DID 文档以继续。",
"subtitleVerify": "验证您的{channel}以继续。",
"subtitleUpdatedDidDoc": "使用 PDS 签名密钥更新您的 DID 文档",
"subtitleActivating": "正在激活您的账户...",
"subtitleComplete": "您的账户已成功创建",
"redirecting": "正在跳转到控制台...",
"infoIdentityDesc": "您的身份决定了您的账户在 ATProto 网络中的识别方式。大多数用户应选择标准选项。",
"infoContactDesc": "我们将使用此信息验证您的账户并发送有关账户安全的重要通知。",
"infoNextTitle": "接下来会发生什么?",
"infoNextDesc": "创建账户后,您需要验证联系方式,然后即可使用任何 ATProto 应用程序。",
"migrateTitle": "已有 Bluesky 账户?",
"migrateDescription": "您可以将现有账户迁移到此 PDS,而无需创建新账户。您的关注者、帖子和身份都会一起迁移。",
"migrateLink": "使用 PDS Moover 迁移",
"subtitleKeyChoice": "设置 did:web 身份",
"subtitleInitialDidDoc": "上传 DID 文档",
"subtitleVerify": "验证您的{channel}",
"subtitleUpdatedDidDoc": "更新 DID 文档",
"subtitleActivating": "激活中",
"subtitleComplete": "账户已创建",
"redirecting": "跳转中",
"migrateTitle": "已有账户?",
"migrateDescription": "迁移现有账户",
"migrateLink": "迁移账户",
"handle": "用户名",
"handlePlaceholder": "您的用户名",
"handleHint": "您的完整用户名将是:@{handle}",
"handleTaken": "此用户名已被占用",
"handleDotWarning": "自定义域名可以在创建账户后在设置中配置。",
"password": "密码",
"passwordPlaceholder": "至少8位字符",
"confirmPassword": "确认密码",
"confirmPasswordPlaceholder": "再次输入密码",
"identityType": "身份类型",
"identityHint": "选择如何管理您的去中心化身份。",
"didPlc": "did:plc",
"didPlcRecommended": "(推荐)",
"didPlcHint": "由 PLC 目录管理的可迁移身份",
"didWeb": "did:web",
"didWebHint": "托管在此 PDS 上的身份(请阅读下方警告)",
@@ -138,7 +121,6 @@
"externalDidPlaceholder": "did:web:yourdomain.com",
"externalDidHint": "您的域名必须在 /.well-known/did.json 提供指向此 PDS 的有效 DID 文档",
"contactMethod": "联系方式",
"contactMethodHint": "选择您希望如何验证账户和接收通知。您只需选择一种。",
"verificationMethod": "验证方式",
"email": "电子邮件",
"emailAddress": "电子邮件地址",
@@ -170,7 +152,6 @@
"ssoAccount": "SSO",
"ssoSubtitle": "使用外部提供商创建账户",
"noSsoProviders": "此服务器未配置SSO提供商。",
"ssoHint": "选择一个提供商来创建您的账户:",
"continueWith": "使用{provider}继续",
"validation": {
"handleRequired": "请输入用户名",
@@ -680,29 +661,28 @@
"oauth": {
"login": {
"title": "登录",
"subtitle": "登录以继续使用应用",
"signingIn": "登录中...",
"authenticating": "验证中...",
"checkingPasskey": "检查通行密钥...",
"signInWithPasskey": "使用通行密钥登录",
"subtitle": "登录",
"signingIn": "登录中",
"authenticating": "验证中",
"checkingPasskey": "检查",
"signInWithPasskey": "通行密钥",
"passkeyNotSetUp": "未设置通行密钥",
"orUsePassword": "或使用密码",
"orUsePassword": "或",
"password": "密码",
"rememberDevice": "记住此设备",
"passkeyHintChecking": "正在检查通行密钥状态...",
"passkeyHintAvailable": "使用您的通行密钥登录",
"passkeyHintNotAvailable": "此账户未注册通行密钥",
"passkeyHint": "使用设备的生物识别或安全密钥",
"passwordPlaceholder": "输入您的密码",
"passkeyHintChecking": "检查中",
"passkeyHintAvailable": "使用通行密钥",
"passkeyHintNotAvailable": "未注册通行密钥",
"passwordPlaceholder": "密码",
"usePasskey": "使用通行密钥",
"orContinueWith": "或使用以下方式继续",
"orUseCredentials": "或使用凭证登录"
"orContinueWith": "或",
"orUseCredentials": "或"
},
"register": {
"title": "创建账户",
"subtitle": "使用 {app} 创建账户",
"subtitleGeneric": "创建账户以继续",
"haveAccount": "已有账户?"
"subtitle": "",
"subtitleGeneric": "创建账户",
"haveAccount": "已有账户?登录"
},
"sso": {
"linkedAccounts": "已关联账户",
@@ -787,49 +767,43 @@
},
"accounts": {
"title": "选择账户",
"subtitle": "选择一个账户继续",
"useAnother": "使用其他账户"
},
"twoFactor": {
"title": "双重身份验证",
"subtitle": "需要额外验证",
"title": "验证",
"usePasskey": "使用通行密钥",
"useTotp": "使用身份验证器"
"useTotp": "使用验证器"
},
"twoFactorCode": {
"title": "双重身份验证",
"subtitle": "验证码已发送到您的 {channel}。请在下方输入验证码继续。",
"title": "验证",
"subtitle": "验证码已发送到 {channel}",
"codeLabel": "验证码",
"codePlaceholder": "输入6位验证码",
"codePlaceholder": "6位验证码",
"errors": {
"missingRequestUri": "缺少 request_uri 参数",
"missingRequestUri": "缺少请求 URI",
"verificationFailed": "验证失败",
"connectionFailed": "无法连接到服务器",
"unexpectedResponse": "服务器返回意外响应"
"connectionFailed": "连接失败",
"unexpectedResponse": "意外响应"
}
},
"totp": {
"title": "输入验证码",
"subtitle": "请输入身份验证器应用中的6位验证码",
"codePlaceholder": "输入6位验证码",
"useBackupCode": "使用备用验证码",
"backupCodePlaceholder": "输入备用验证码",
"title": "验证器验证码",
"codePlaceholder": "6位验证码",
"useBackupCode": "使用备用码",
"backupCodePlaceholder": "备用码",
"trustDevice": "信任此设备30天",
"hintBackupCode": "正在使用备用验证码",
"hintTotpCode": "正在使用身份验证器验证码",
"hintDefault": "身份验证器为6位数字,备用码为8位字符"
"hintBackupCode": "备用码",
"hintTotpCode": "验证器验证码"
},
"passkey": {
"title": "通行密钥验证",
"subtitle": "使用您的通行密钥验证身份",
"waiting": "等待通行密钥...",
"useTotp": "改用身份验证器"
"title": "通行密钥",
"waiting": "等待中",
"useTotp": "使用验证器"
},
"error": {
"title": "授权错误",
"genericError": "授权过程中发生错误。",
"title": "授权失败",
"tryAgain": "重试",
"backToApp": "返回应用"
"backToApp": "返回"
}
},
"sso_register": {
@@ -855,9 +829,9 @@
"subtitle": "我们已将验证码发送到您的{channel}。请在下方输入以完成注册。",
"tokenSubtitle": "输入验证码和接收验证码的标识符。",
"tokenTitle": "验证",
"codePlaceholder": "XXXX-XXXX-XXXX-XXXX...",
"codePlaceholder": "Paste verification code",
"codeLabel": "验证码",
"codeHelp": "复制消息中的完整验证码,包括横线",
"codeHelp": "复制消息中的完整验证码,",
"verifyButton": "验证账户",
"pleaseWait": "请稍候...",
"codeResent": "验证码已重新发送!",
@@ -958,16 +932,15 @@
},
"registerPasskey": {
"title": "创建通行密钥账户",
"subtitle": "使用通行密钥创建超安全账户,无需密码。",
"subtitleKeyChoice": "选择如何设置您的外部 did:web 身份。",
"subtitleInitialDidDoc": "上传您的 DID 文档以继续。",
"subtitleCreating": "正在创建您的账户...",
"subtitlePasskey": "注册通行密钥以保护您的账户。",
"subtitleAppPassword": "保存您的应用专用密码以使用第三方应用。",
"subtitleVerify": "验证您的{channel}以继续。",
"subtitleUpdatedDidDoc": "使用 PDS 签名密钥更新您的 DID 文档。",
"subtitleActivating": "正在激活您的账户...",
"subtitleComplete": "您的账户已成功创建!",
"subtitleKeyChoice": "设置 did:web 身份",
"subtitleInitialDidDoc": "上传 DID 文档",
"subtitleCreating": "创建账户",
"subtitlePasskey": "注册通行密钥",
"subtitleAppPassword": "保存应用专用密码",
"subtitleVerify": "验证{channel}",
"subtitleUpdatedDidDoc": "更新 DID 文档",
"subtitleActivating": "激活中",
"subtitleComplete": "账户已创建",
"handle": "用户名",
"handlePlaceholder": "您的用户名",
"handleHint": "您的完整用户名将是:@{handle}",
@@ -986,7 +959,6 @@
"wantTraditional": "想使用传统密码?",
"registerWithPassword": "使用密码注册",
"contactMethod": "联系方式",
"contactMethodHint": "选择您希望如何验证账户和接收通知。",
"verificationMethod": "验证方式",
"identityType": "身份类型",
"identityTypeHint": "选择如何管理您的去中心化身份。",
@@ -1008,24 +980,9 @@
"externalDid": "您的 did:web",
"externalDidPlaceholder": "did:web:yourdomain.com",
"externalDidHint": "您需要在以下地址提供 DID 文档",
"whyPasskeyOnly": "为什么选择仅通行密钥?",
"whyPasskeyOnlyDesc": "通行密钥账户比密码账户更安全,因为它们:",
"whyPasskeyBullet1": "无法被钓鱼或在数据泄露中被盗",
"whyPasskeyBullet2": "使用硬件支持的加密密钥",
"whyPasskeyBullet3": "需要您的生物识别或设备 PIN 才能使用",
"infoWhyPasskey": "为什么使用通行密钥?",
"infoWhyPasskeyDesc": "通行密钥是存储在您设备上的加密凭证。与密码不同,它们无法被钓鱼、猜测或在数据泄露中被盗。",
"infoHowItWorks": "工作原理",
"infoHowItWorksDesc": "登录时,您的设备会提示您使用 Face ID、Touch ID 或设备 PIN 进行验证。无需记住或输入密码。",
"infoAppAccess": "使用第三方应用",
"infoAppAccessDesc": "创建账户后,您将收到一个应用密码。使用它登录 Bluesky 应用和其他 AT Protocol 客户端。",
"passkeyNameLabel": "通行密钥名称(可选)",
"passkeyNamePlaceholder": "如 MacBook Touch ID",
"passkeyNameHint": "用于识别此通行密钥的友好名称",
"passkeyPrompt": "点击下方按钮创建通行密钥。系统会提示您使用:",
"passkeyPromptBullet1": "Touch ID 或 Face ID",
"passkeyPromptBullet2": "设备 PIN 或密码",
"passkeyPromptBullet3": "安全密钥(如果有的话)",
"passkeyName": "Passkey Name",
"passkeyNamePlaceholder": "MacBook Touch ID",
"passkeyNameHint": "可选标识",
"createPasskey": "创建通行密钥",
"creatingPasskey": "正在创建通行密钥...",
"redirecting": "正在跳转到控制台...",
@@ -1044,7 +1001,11 @@
"passkeyCancelled": "通行密钥创建已取消",
"passkeyFailed": "通行密钥注册失败"
},
"didWebWarning1Detail": "您的身份将是 {did}。"
"didWebWarning1Detail": "您的身份将是 {did}。",
"activatingAccount": "Activating",
"creatingAccount": "Creating account",
"passkeyDescription": "Register a passkey for this account",
"setupPasskey": "Create Passkey"
},
"trustedDevices": {
"title": "受信任设备",
@@ -1068,19 +1029,17 @@
"unknownDevice": "未知设备"
},
"reauth": {
"title": "需要重新验证",
"subtitle": "请验证您的身份以继续。",
"title": "重新验证",
"password": "密码",
"totp": "TOTP",
"passkey": "通行密钥",
"authenticatorCode": "验证码",
"usePassword": "使用密码",
"usePasskey": "使用通行密钥",
"useTotp": "使用身份验证器",
"usePassword": "密码",
"usePasskey": "通行密钥",
"useTotp": "身份验证器",
"passwordPlaceholder": "输入您的密码",
"totpPlaceholder": "输入6位验证码",
"authenticating": "正在验证...",
"passkeyPrompt": "点击下方按钮使用通行密钥进行验证。",
"totpPlaceholder": "6位验证码",
"authenticating": "验证中",
"cancel": "取消"
},
"verifyChannel": {
@@ -1100,7 +1059,7 @@
"identifierPlaceholder": "邮箱、Discord ID 等",
"identifierHelp": "正在验证的邮箱地址、Discord ID、Telegram 用户名或 Signal 号码。",
"codeLabel": "验证码",
"codeHelp": "复制消息中的完整验证码,包括横线。",
"codeHelp": "复制消息中的完整验证码,。",
"verifyButton": "验证"
},
"delegation": {
+62 -64
View File
@@ -156,74 +156,55 @@
{:else}
<header class="page-header">
<h1>{$_('login.title')}</h1>
<p class="subtitle">{savedAccounts.length > 0 ? $_('login.chooseAccount') : $_('login.subtitle')}</p>
{#if savedAccounts.length > 0}
<p class="subtitle">{$_('login.chooseAccount')}</p>
{/if}
</header>
<div class="split-layout sidebar-right">
<div class="main-section">
{#if savedAccounts.length > 0}
<div class="saved-accounts">
{#each savedAccounts as account}
<div
class="account-item"
class:disabled={submitting}
role="button"
tabindex="0"
onclick={() => !submitting && handleSwitchAccount(account.did)}
onkeydown={(e) => e.key === 'Enter' && !submitting && handleSwitchAccount(account.did)}
>
<div class="account-info">
<span class="account-handle">@{account.handle}</span>
<span class="account-did">{account.did}</span>
</div>
<button
type="button"
class="forget-btn"
onclick={(e) => handleForgetAccount(account.did, e)}
title={$_('login.removeAccount')}
>
&times;
</button>
<div class="login-content">
{#if savedAccounts.length > 0}
<div class="saved-accounts" class:grid={savedAccounts.length > 1}>
{#each savedAccounts as account}
<div
class="account-item"
class:disabled={submitting}
role="button"
tabindex="0"
onclick={() => !submitting && handleSwitchAccount(account.did)}
onkeydown={(e) => e.key === 'Enter' && !submitting && handleSwitchAccount(account.did)}
>
<div class="account-info">
<span class="account-handle">@{account.handle}</span>
<span class="account-did">{account.did}</span>
</div>
{/each}
</div>
<button
type="button"
class="forget-btn"
onclick={(e) => handleForgetAccount(account.did, e)}
title={$_('login.removeAccount')}
>
&times;
</button>
</div>
{/each}
</div>
<p class="or-divider">{$_('login.signInToAnother')}</p>
{/if}
<p class="or-divider">{$_('login.signInToAnother')}</p>
{/if}
<button type="button" class="oauth-btn" onclick={handleOAuthLogin} disabled={submitting || loading}>
{submitting ? $_('login.redirecting') : $_('login.button')}
</button>
<button type="button" class="oauth-btn" onclick={handleOAuthLogin} disabled={submitting || loading}>
{submitting ? $_('login.redirecting') : $_('login.button')}
</button>
<p class="forgot-links">
<a href="/app/reset-password">{$_('login.forgotPassword')}</a>
<span class="separator">&middot;</span>
<a href="/app/request-passkey-recovery">{$_('login.lostPasskey')}</a>
</p>
<p class="forgot-links">
<a href="/app/reset-password">{$_('login.forgotPassword')}</a>
<span class="separator">&middot;</span>
<a href="/app/request-passkey-recovery">{$_('login.lostPasskey')}</a>
</p>
<p class="link-text">
{$_('login.noAccount')} <a href="/app/register">{$_('login.createAccount')}</a>
</p>
</div>
<aside class="info-panel">
{#if savedAccounts.length > 0}
<h3>{$_('login.infoSavedAccountsTitle')}</h3>
<p>{$_('login.infoSavedAccountsDesc')}</p>
<h3>{$_('login.infoNewAccountTitle')}</h3>
<p>{$_('login.infoNewAccountDesc')}</p>
{:else}
<h3>{$_('login.infoSecureSignInTitle')}</h3>
<p>{$_('login.infoSecureSignInDesc')}</p>
<h3>{$_('login.infoStaySignedInTitle')}</h3>
<p>{$_('login.infoStaySignedInDesc')}</p>
{/if}
<h3>{$_('login.infoRecoveryTitle')}</h3>
<p>{$_('login.infoRecoveryDesc')}</p>
</aside>
<p class="link-text">
{$_('login.noAccount')} <a href="/app/register">{$_('login.createAccount')}</a>
</p>
</div>
{/if}
</div>
@@ -237,6 +218,7 @@
.page-header {
margin-bottom: var(--space-6);
text-align: center;
}
h1 {
@@ -248,8 +230,9 @@
margin: 0;
}
.main-section {
min-width: 0;
.login-content {
max-width: var(--width-md);
margin: 0 auto;
}
form {
@@ -257,6 +240,7 @@
flex-direction: column;
gap: var(--space-4);
max-width: var(--width-sm);
margin: 0 auto;
}
.actions {
@@ -286,6 +270,7 @@
margin-top: var(--space-4);
font-size: var(--text-sm);
color: var(--text-secondary);
text-align: center;
}
.forgot-links a {
@@ -300,6 +285,7 @@
margin-top: var(--space-6);
font-size: var(--text-sm);
color: var(--text-secondary);
text-align: center;
}
.link-text a {
@@ -313,6 +299,17 @@
margin-bottom: var(--space-5);
}
.saved-accounts.grid {
display: grid;
grid-template-columns: 1fr;
}
@media (min-width: 700px) {
.saved-accounts.grid {
grid-template-columns: repeat(2, 1fr);
}
}
.account-item {
display: flex;
align-items: center;
@@ -339,6 +336,7 @@
display: flex;
flex-direction: column;
gap: var(--space-1);
min-width: 0;
}
.account-handle {
@@ -352,10 +350,10 @@
font-family: var(--font-mono);
overflow: hidden;
text-overflow: ellipsis;
max-width: 250px;
}
.forget-btn {
flex-shrink: 0;
padding: var(--space-2) var(--space-3);
background: transparent;
border: none;
+1 -7
View File
@@ -124,7 +124,6 @@
</div>
{:else}
<h1>{$_('oauth.accounts.title')}</h1>
<p class="subtitle">{$_('oauth.accounts.subtitle')}</p>
<div class="accounts-list">
{#each accounts as account}
@@ -156,12 +155,7 @@
}
h1 {
margin: 0 0 var(--space-2) 0;
}
.subtitle {
color: var(--text-secondary);
margin: 0 0 var(--space-7) 0;
margin: 0 0 var(--space-6) 0;
}
.loading {
+7 -14
View File
@@ -8,6 +8,9 @@
type WebAuthnRequestOptionsResponse,
} from '../lib/webauthn'
import SsoIcon from '../components/SsoIcon.svelte'
import { getRandomHandle } from '../components/RandomHandle.svelte'
const handlePlaceholder = getRandomHandle()
interface SsoProvider {
provider: string
@@ -345,13 +348,9 @@
<div class="page-sm">
<header class="page-header">
<h1>{$_('oauth.login.title')}</h1>
<p class="subtitle">
{#if clientName}
{$_('oauth.login.subtitle')} <strong>{clientName}</strong>
{:else}
{$_('oauth.login.subtitle')}
{/if}
</p>
{#if clientName}
<p class="subtitle">{$_('oauth.login.subtitle')} <strong>{clientName}</strong></p>
{/if}
</header>
{#if error}
@@ -365,7 +364,7 @@
id="username"
type="text"
bind:value={username}
placeholder={$_('register.emailPlaceholder')}
placeholder={handlePlaceholder}
disabled={submitting}
required
autocomplete="username"
@@ -426,7 +425,6 @@
{/if}
</span>
</button>
<p class="method-hint">{$_('oauth.login.passkeyHint')}</p>
</div>
{#if hasPassword}
@@ -572,11 +570,6 @@
letter-spacing: 0.05em;
}
.method-hint {
margin: 0;
font-size: var(--text-xs);
color: var(--text-muted);
}
.method-divider {
display: flex;
+1 -9
View File
@@ -119,9 +119,6 @@
<div class="oauth-passkey-container">
<h1>{t('oauth.passkey.title')}</h1>
<p class="subtitle">
{t('oauth.passkey.subtitle')}
</p>
{#if error}
<div class="error">{error}</div>
@@ -156,12 +153,7 @@
}
h1 {
margin: 0 0 0.5rem 0;
}
.subtitle {
color: var(--text-secondary);
margin: 0 0 2rem 0;
margin: 0 0 1.5rem 0;
}
.error {
+6 -18
View File
@@ -74,9 +74,6 @@
<div class="oauth-totp-container">
<h1>{$_('oauth.totp.title')}</h1>
<p class="subtitle">
{$_('oauth.totp.subtitle')}
</p>
{#if error}
<div class="error">{error}</div>
@@ -96,15 +93,11 @@
autocomplete="one-time-code"
autocapitalize="characters"
/>
<p class="hint">
{#if isBackupCode}
{$_('oauth.totp.hintBackupCode')}
{:else if isTotpCode}
{$_('oauth.totp.hintTotpCode')}
{:else}
{$_('oauth.totp.hintDefault')}
{/if}
</p>
{#if isBackupCode || isTotpCode}
<p class="hint">
{isBackupCode ? $_('oauth.totp.hintBackupCode') : $_('oauth.totp.hintTotpCode')}
</p>
{/if}
</div>
<label class="trust-device-label">
@@ -135,12 +128,7 @@
}
h1 {
margin: 0 0 var(--space-2) 0;
}
.subtitle {
color: var(--text-secondary);
margin: 0 0 var(--space-7) 0;
margin: 0 0 var(--space-6) 0;
}
form {
+201 -147
View File
@@ -31,6 +31,18 @@
let flow = $state<ReturnType<typeof createRegistrationFlow> | null>(null)
let passkeyName = $state('')
let clientName = $state<string | null>(null)
let checkHandleTimeout: ReturnType<typeof setTimeout> | null = null
$effect(() => {
if (!flow) return
const handle = flow.info.handle
if (checkHandleTimeout) {
clearTimeout(checkHandleTimeout)
}
if (handle.length >= 3 && !handle.includes('.')) {
checkHandleTimeout = setTimeout(() => flow?.checkHandleAvailability(handle), 400)
}
})
$effect(() => {
if (!serverInfoLoaded) {
@@ -284,12 +296,18 @@
body: JSON.stringify({ request_uri: requestUri })
})
if (!response.ok) {
window.history.back()
return
}
const data = await response.json()
if (data.redirect_uri) {
window.location.href = data.redirect_uri
} else {
window.history.back()
}
} catch (err) {
console.error('OAuth deny failed:', err)
} catch {
window.history.back()
}
}
@@ -313,13 +331,9 @@
{:else if flow}
<header class="page-header">
<h1>{$_('oauth.register.title')}</h1>
<p class="subtitle">
{#if clientName}
{$_('oauth.register.subtitle')} <strong>{clientName}</strong>
{:else}
{$_('oauth.register.subtitleGeneric')}
{/if}
</p>
{#if clientName}
<p class="subtitle">{$_('oauth.register.subtitle')} <strong>{clientName}</strong></p>
{/if}
</header>
{#if flow.state.error}
@@ -340,9 +354,7 @@
<AccountTypeSwitcher active="passkey" {ssoAvailable} oauthRequestUri={getRequestUriFromUrl()} />
<div class="split-layout">
<div class="form-section">
<form onsubmit={handleInfoSubmit}>
<form class="register-form" onsubmit={handleInfoSubmit}>
<div class="field">
<label for="handle">{$_('register.handle')}</label>
<input
@@ -354,87 +366,89 @@
required
autocomplete="off"
/>
{#if fullHandle()}
{#if flow.info.handle.includes('.')}
<p class="hint warning">{$_('register.handleDotWarning')}</p>
{:else if flow.state.checkingHandle}
<p class="hint">{$_('common.checking')}</p>
{:else if flow.state.handleAvailable === false}
<p class="hint warning">{$_('register.handleTaken')}</p>
{:else if flow.state.handleAvailable === true && fullHandle()}
<p class="hint success">{$_('register.handleHint', { values: { handle: fullHandle() } })}</p>
{:else if fullHandle()}
<p class="hint">{$_('register.handleHint', { values: { handle: fullHandle() } })}</p>
{/if}
</div>
<fieldset>
<legend>{$_('register.contactMethod')}</legend>
<div class="contact-fields">
<div class="field">
<label for="verification-channel">{$_('register.verificationMethod')}</label>
<select id="verification-channel" bind:value={flow.info.verificationChannel} disabled={flow.state.submitting}>
<option value="email">{channelLabel('email')}</option>
{#if isChannelAvailable('discord')}
<option value="discord">{channelLabel('discord')}</option>
{/if}
{#if isChannelAvailable('telegram')}
<option value="telegram">{channelLabel('telegram')}</option>
{/if}
{#if isChannelAvailable('signal')}
<option value="signal">{channelLabel('signal')}</option>
{/if}
</select>
</div>
{#if flow.info.verificationChannel === 'email'}
<div class="field">
<label for="email">{$_('register.emailAddress')}</label>
<input
id="email"
type="email"
bind:value={flow.info.email}
placeholder={$_('register.emailPlaceholder')}
disabled={flow.state.submitting}
required
/>
</div>
{:else if flow.info.verificationChannel === 'discord'}
<div class="field">
<label for="discord-id">{$_('register.discordId')}</label>
<input
id="discord-id"
type="text"
bind:value={flow.info.discordId}
placeholder={$_('register.discordIdPlaceholder')}
disabled={flow.state.submitting}
required
/>
<p class="hint">{$_('register.discordIdHint')}</p>
</div>
{:else if flow.info.verificationChannel === 'telegram'}
<div class="field">
<label for="telegram-username">{$_('register.telegramUsername')}</label>
<input
id="telegram-username"
type="text"
bind:value={flow.info.telegramUsername}
placeholder={$_('register.telegramUsernamePlaceholder')}
disabled={flow.state.submitting}
required
/>
</div>
{:else if flow.info.verificationChannel === 'signal'}
<div class="field">
<label for="signal-number">{$_('register.signalNumber')}</label>
<input
id="signal-number"
type="tel"
bind:value={flow.info.signalNumber}
placeholder={$_('register.signalNumberPlaceholder')}
disabled={flow.state.submitting}
required
/>
<p class="hint">{$_('register.signalNumberHint')}</p>
</div>
<div class="field">
<label for="verification-channel">{$_('register.verificationMethod')}</label>
<select id="verification-channel" bind:value={flow.info.verificationChannel} disabled={flow.state.submitting}>
<option value="email">{channelLabel('email')}</option>
{#if isChannelAvailable('discord')}
<option value="discord">{channelLabel('discord')}</option>
{/if}
</div>
</fieldset>
{#if isChannelAvailable('telegram')}
<option value="telegram">{channelLabel('telegram')}</option>
{/if}
{#if isChannelAvailable('signal')}
<option value="signal">{channelLabel('signal')}</option>
{/if}
</select>
</div>
<fieldset>
{#if flow.info.verificationChannel === 'email'}
<div class="field">
<label for="email">{$_('register.emailAddress')}</label>
<input
id="email"
type="email"
bind:value={flow.info.email}
placeholder={$_('register.emailPlaceholder')}
disabled={flow.state.submitting}
required
/>
</div>
{:else if flow.info.verificationChannel === 'discord'}
<div class="field">
<label for="discord-id">{$_('register.discordId')}</label>
<input
id="discord-id"
type="text"
bind:value={flow.info.discordId}
placeholder={$_('register.discordIdPlaceholder')}
disabled={flow.state.submitting}
required
/>
<p class="hint">{$_('register.discordIdHint')}</p>
</div>
{:else if flow.info.verificationChannel === 'telegram'}
<div class="field">
<label for="telegram-username">{$_('register.telegramUsername')}</label>
<input
id="telegram-username"
type="text"
bind:value={flow.info.telegramUsername}
placeholder={$_('register.telegramUsernamePlaceholder')}
disabled={flow.state.submitting}
required
/>
</div>
{:else if flow.info.verificationChannel === 'signal'}
<div class="field">
<label for="signal-number">{$_('register.signalNumber')}</label>
<input
id="signal-number"
type="tel"
bind:value={flow.info.signalNumber}
placeholder={$_('register.signalNumberPlaceholder')}
disabled={flow.state.submitting}
required
/>
<p class="hint">{$_('register.signalNumberHint')}</p>
</div>
{/if}
<fieldset class="identity-section">
<legend>{$_('registerPasskey.identityType')}</legend>
<p class="section-hint">{$_('registerPasskey.identityTypeHint')}</p>
<div class="radio-group">
<label class="radio-label">
<input type="radio" name="didType" value="plc" bind:group={flow.info.didType} disabled={flow.state.submitting} />
@@ -462,29 +476,32 @@
</span>
</label>
</div>
{#if flow.info.didType === 'web'}
<div class="warning-box">
<strong>{$_('registerPasskey.didWebWarningTitle')}</strong>
<ul>
<li><strong>{$_('registerPasskey.didWebWarning1')}</strong> {@html $_('registerPasskey.didWebWarning1Detail', { values: { did: `<code>did:web:yourhandle.${serverInfo?.availableUserDomains?.[0] || 'this-pds.com'}</code>` } })}</li>
<li><strong>{$_('registerPasskey.didWebWarning2')}</strong> {$_('registerPasskey.didWebWarning2Detail')}</li>
<li><strong>{$_('registerPasskey.didWebWarning3')}</strong> {$_('registerPasskey.didWebWarning3Detail')}</li>
<li><strong>{$_('registerPasskey.didWebWarning4')}</strong> {$_('registerPasskey.didWebWarning4Detail')}</li>
</ul>
</div>
{/if}
{#if flow.info.didType === 'web-external'}
<div class="field">
<label for="external-did">{$_('registerPasskey.externalDid')}</label>
<input id="external-did" type="text" bind:value={flow.info.externalDid} placeholder={$_('registerPasskey.externalDidPlaceholder')} disabled={flow.state.submitting} required />
<p class="hint">{$_('registerPasskey.externalDidHint')} <code>https://{flow.info.externalDid ? flow.extractDomain(flow.info.externalDid) : 'yourdomain.com'}/.well-known/did.json</code></p>
</div>
{/if}
</fieldset>
{#if flow.info.didType === 'web'}
<div class="warning-box">
<strong>{$_('registerPasskey.didWebWarningTitle')}</strong>
<ul>
<li><strong>{$_('registerPasskey.didWebWarning1')}</strong> {@html $_('registerPasskey.didWebWarning1Detail', { values: { did: `<code>did:web:yourhandle.${serverInfo?.availableUserDomains?.[0] || 'this-pds.com'}</code>` } })}</li>
<li><strong>{$_('registerPasskey.didWebWarning2')}</strong> {$_('registerPasskey.didWebWarning2Detail')}</li>
{#if $_('registerPasskey.didWebWarning3')}
<li><strong>{$_('registerPasskey.didWebWarning3')}</strong> {$_('registerPasskey.didWebWarning3Detail')}</li>
{/if}
</ul>
</div>
{/if}
{#if flow.info.didType === 'web-external'}
<div class="field">
<label for="external-did">{$_('registerPasskey.externalDid')}</label>
<input id="external-did" type="text" bind:value={flow.info.externalDid} placeholder={$_('registerPasskey.externalDidPlaceholder')} disabled={flow.state.submitting} required />
<p class="hint">{$_('registerPasskey.externalDidHint')} <code>https://{flow.info.externalDid ? flow.extractDomain(flow.info.externalDid) : 'yourdomain.com'}/.well-known/did.json</code></p>
</div>
{/if}
{#if serverInfo?.inviteCodeRequired}
<div class="field">
<label for="invite-code">{$_('register.inviteCode')} <span class="required">*</span></label>
<label for="invite-code">{$_('register.inviteCode')}</label>
<input
id="invite-code"
type="text"
@@ -496,40 +513,15 @@
</div>
{/if}
<div class="actions">
<button type="submit" class="primary" disabled={flow.state.submitting}>
<div class="form-actions">
<button type="button" class="secondary" onclick={handleCancel} disabled={flow.state.submitting}>
{$_('common.cancel')}
</button>
<button type="submit" class="primary" disabled={flow.state.submitting || flow.state.handleAvailable === false || flow.state.checkingHandle}>
{flow.state.submitting ? $_('common.loading') : $_('common.continue')}
</button>
</div>
<div class="secondary-actions">
<button type="button" class="link" onclick={goToLogin}>
{$_('oauth.register.haveAccount')}
</button>
<button type="button" class="link" onclick={handleCancel}>
{$_('common.cancel')}
</button>
</div>
</form>
<div class="form-links">
<p class="link-text">
{$_('register.alreadyHaveAccount')} <a href="/app/login">{$_('register.signIn')}</a>
</p>
</div>
</div>
<aside class="info-panel">
<h3>{$_('registerPasskey.infoWhyPasskey')}</h3>
<p>{$_('registerPasskey.infoWhyPasskeyDesc')}</p>
<h3>{$_('registerPasskey.infoHowItWorks')}</h3>
<p>{$_('registerPasskey.infoHowItWorksDesc')}</p>
<h3>{$_('registerPasskey.infoAppAccess')}</h3>
<p>{$_('registerPasskey.infoAppAccessDesc')}</p>
</aside>
</div>
</form>
{:else if flow.state.step === 'key-choice'}
<KeyChoiceStep {flow} />
@@ -589,33 +581,95 @@
</div>
<style>
form {
.register-form {
display: flex;
flex-direction: column;
gap: var(--space-5);
gap: var(--space-3);
max-width: 500px;
}
.actions {
.identity-section {
border: 1px solid var(--border-color);
border-radius: var(--radius-md);
padding: var(--space-4);
margin: 0;
margin-top: var(--space-5);
}
.identity-section legend {
font-weight: var(--font-medium);
font-size: var(--text-sm);
padding: 0 var(--space-2);
}
.radio-group {
display: flex;
gap: var(--space-4);
flex-direction: column;
gap: var(--space-3);
}
.radio-label {
display: flex;
align-items: flex-start;
gap: var(--space-2);
cursor: pointer;
}
.radio-label.disabled {
opacity: 0.5;
cursor: not-allowed;
}
.radio-label input {
margin-top: 2px;
}
.radio-content {
display: flex;
flex-direction: column;
gap: var(--space-1);
}
.radio-hint {
font-size: var(--text-sm);
color: var(--text-secondary);
}
.radio-hint.disabled-hint {
color: var(--text-muted);
}
.warning-box {
padding: var(--space-4);
background: var(--warning-bg);
border: 1px solid var(--warning-border);
border-radius: var(--radius-md);
}
.warning-box ul {
margin: var(--space-2) 0 0 0;
padding-left: var(--space-5);
}
.warning-box li {
margin-top: var(--space-2);
}
.actions button {
flex: 1;
.form-actions {
display: flex;
gap: var(--space-4);
margin-top: var(--space-5);
}
.secondary-actions {
display: flex;
justify-content: center;
gap: var(--space-4);
margin-top: var(--space-4);
.form-actions .primary {
flex: 1;
}
.passkey-step {
display: flex;
flex-direction: column;
gap: var(--space-4);
max-width: 500px;
}
.passkey-step h2 {
+328 -250
View File
@@ -25,6 +25,18 @@
let flow = $state<ReturnType<typeof createRegistrationFlow> | null>(null)
let confirmPassword = $state('')
let clientName = $state<string | null>(null)
let checkHandleTimeout: ReturnType<typeof setTimeout> | null = null
$effect(() => {
if (!flow) return
const handle = flow.info.handle
if (checkHandleTimeout) {
clearTimeout(checkHandleTimeout)
}
if (handle.length >= 3 && !handle.includes('.')) {
checkHandleTimeout = setTimeout(() => flow?.checkHandleAvailability(handle), 400)
}
})
$effect(() => {
if (!serverInfoLoaded) {
@@ -176,7 +188,7 @@
body: JSON.stringify({
request_uri: requestUri,
did: flow.account.did,
app_password: flow.account.appPassword,
app_password: flow.account.appPassword || flow.info.password,
}),
})
@@ -226,18 +238,36 @@
return did.replace('did:web:', '').replace(/%3A/g, ':')
}
function getSubtitle(): string {
if (!flow) return ''
switch (flow.state.step) {
case 'info': return $_('register.subtitle')
case 'key-choice': return $_('register.subtitleKeyChoice')
case 'initial-did-doc': return $_('register.subtitleInitialDidDoc')
case 'creating': return $_('common.creating')
case 'verify': return $_('register.subtitleVerify', { values: { channel: channelLabel(flow.info.verificationChannel) } })
case 'updated-did-doc': return $_('register.subtitleUpdatedDidDoc')
case 'activating': return $_('register.subtitleActivating')
case 'redirect-to-dashboard': return $_('register.subtitleComplete')
default: return ''
async function handleCancel() {
const requestUri = getRequestUriFromUrl()
if (!requestUri) {
window.history.back()
return
}
try {
const response = await fetch('/oauth/authorize/deny', {
method: 'POST',
headers: {
'Content-Type': 'application/json',
'Accept': 'application/json'
},
body: JSON.stringify({ request_uri: requestUri })
})
if (!response.ok) {
window.history.back()
return
}
const data = await response.json()
if (data.redirect_uri) {
window.location.href = data.redirect_uri
} else {
window.history.back()
}
} catch {
window.history.back()
}
}
</script>
@@ -245,9 +275,8 @@
<div class="page">
<header class="page-header">
<h1>{$_('register.title')}</h1>
<p class="subtitle">{getSubtitle()}</p>
{#if clientName}
<p class="client-name">{$_('oauth.login.subtitle')} <strong>{clientName}</strong></p>
<p class="subtitle">{$_('oauth.register.subtitle')} <strong>{clientName}</strong></p>
{/if}
</header>
@@ -273,239 +302,219 @@
<AccountTypeSwitcher active="password" {ssoAvailable} oauthRequestUri={getRequestUriFromUrl()} />
<div class="split-layout sidebar-right">
<div class="form-section">
<form onsubmit={handleInfoSubmit}>
<div class="field">
<label for="handle">{$_('register.handle')}</label>
<input
id="handle"
type="text"
bind:value={flow.info.handle}
placeholder={$_('register.handlePlaceholder')}
disabled={flow.state.submitting}
required
/>
{#if flow.info.handle.includes('.')}
<p class="hint warning">{$_('register.handleDotWarning')}</p>
{:else if fullHandle()}
<p class="hint">{$_('register.handleHint', { values: { handle: fullHandle() } })}</p>
{/if}
</div>
<div class="form-row">
<div class="field">
<label for="password">{$_('register.password')}</label>
<input
id="password"
type="password"
bind:value={flow.info.password}
placeholder={$_('register.passwordPlaceholder')}
disabled={flow.state.submitting}
required
minlength="8"
/>
</div>
<div class="field">
<label for="confirm-password">{$_('register.confirmPassword')}</label>
<input
id="confirm-password"
type="password"
bind:value={confirmPassword}
placeholder={$_('register.confirmPasswordPlaceholder')}
disabled={flow.state.submitting}
required
/>
</div>
</div>
<fieldset class="section-fieldset">
<legend>{$_('register.identityType')}</legend>
<div class="radio-group">
<label class="radio-label">
<input type="radio" name="didType" value="plc" bind:group={flow.info.didType} disabled={flow.state.submitting} />
<span class="radio-content">
<strong>{$_('register.didPlc')}</strong> {$_('register.didPlcRecommended')}
<span class="radio-hint">{$_('register.didPlcHint')}</span>
</span>
</label>
<label class="radio-label" class:disabled={serverInfo?.selfHostedDidWebEnabled === false}>
<input type="radio" name="didType" value="web" bind:group={flow.info.didType} disabled={flow.state.submitting || serverInfo?.selfHostedDidWebEnabled === false} />
<span class="radio-content">
<strong>{$_('register.didWeb')}</strong>
{#if serverInfo?.selfHostedDidWebEnabled === false}
<span class="radio-hint disabled-hint">{$_('register.didWebDisabledHint')}</span>
{:else}
<span class="radio-hint">{$_('register.didWebHint')}</span>
{/if}
</span>
</label>
<label class="radio-label">
<input type="radio" name="didType" value="web-external" bind:group={flow.info.didType} disabled={flow.state.submitting} />
<span class="radio-content">
<strong>{$_('register.didWebBYOD')}</strong>
<span class="radio-hint">{$_('register.didWebBYODHint')}</span>
</span>
</label>
</div>
{#if flow.info.didType === 'web'}
<div class="warning-box">
<strong>{$_('register.didWebWarningTitle')}</strong>
<ul>
<li><strong>{$_('register.didWebWarning1')}</strong> {$_('register.didWebWarning1Detail', { values: { did: `did:web:yourhandle.${serverInfo?.availableUserDomains?.[0] || 'this-pds.com'}` } })}</li>
<li><strong>{$_('register.didWebWarning2')}</strong> {$_('register.didWebWarning2Detail')}</li>
<li><strong>{$_('register.didWebWarning3')}</strong> {$_('register.didWebWarning3Detail')}</li>
<li><strong>{$_('register.didWebWarning4')}</strong> {$_('register.didWebWarning4Detail')}</li>
</ul>
</div>
{/if}
{#if flow.info.didType === 'web-external'}
<div class="field">
<label for="external-did">{$_('register.externalDid')}</label>
<input
id="external-did"
type="text"
bind:value={flow.info.externalDid}
placeholder={$_('register.externalDidPlaceholder')}
disabled={flow.state.submitting}
required
/>
<p class="hint">{$_('register.externalDidHint')}</p>
</div>
{/if}
</fieldset>
<fieldset class="section-fieldset">
<legend>{$_('register.contactMethod')}</legend>
<div class="contact-fields">
<div class="field">
<label for="verification-channel">{$_('register.verificationMethod')}</label>
<select id="verification-channel" bind:value={flow.info.verificationChannel} disabled={flow.state.submitting}>
<option value="email">{$_('register.email')}</option>
<option value="discord" disabled={!isChannelAvailable('discord')}>
{$_('register.discord')}{isChannelAvailable('discord') ? '' : ` (${$_('register.notConfigured')})`}
</option>
<option value="telegram" disabled={!isChannelAvailable('telegram')}>
{$_('register.telegram')}{isChannelAvailable('telegram') ? '' : ` (${$_('register.notConfigured')})`}
</option>
<option value="signal" disabled={!isChannelAvailable('signal')}>
{$_('register.signal')}{isChannelAvailable('signal') ? '' : ` (${$_('register.notConfigured')})`}
</option>
</select>
</div>
{#if flow.info.verificationChannel === 'email'}
<div class="field">
<label for="email">{$_('register.emailAddress')}</label>
<input
id="email"
type="email"
bind:value={flow.info.email}
onblur={() => flow?.checkEmailInUse(flow.info.email)}
placeholder={$_('register.emailPlaceholder')}
disabled={flow.state.submitting}
required
/>
{#if flow.state.emailInUse}
<p class="hint warning">{$_('register.emailInUseWarning')}</p>
{/if}
</div>
{:else if flow.info.verificationChannel === 'discord'}
<div class="field">
<label for="discord-id">{$_('register.discordId')}</label>
<input
id="discord-id"
type="text"
bind:value={flow.info.discordId}
onblur={() => flow?.checkCommsChannelInUse('discord', flow.info.discordId ?? '')}
placeholder={$_('register.discordIdPlaceholder')}
disabled={flow.state.submitting}
required
/>
<p class="hint">{$_('register.discordIdHint')}</p>
{#if flow.state.discordInUse}
<p class="hint warning">{$_('register.discordInUseWarning')}</p>
{/if}
</div>
{:else if flow.info.verificationChannel === 'telegram'}
<div class="field">
<label for="telegram-username">{$_('register.telegramUsername')}</label>
<input
id="telegram-username"
type="text"
bind:value={flow.info.telegramUsername}
onblur={() => flow?.checkCommsChannelInUse('telegram', flow.info.telegramUsername ?? '')}
placeholder={$_('register.telegramUsernamePlaceholder')}
disabled={flow.state.submitting}
required
/>
{#if flow.state.telegramInUse}
<p class="hint warning">{$_('register.telegramInUseWarning')}</p>
{/if}
</div>
{:else if flow.info.verificationChannel === 'signal'}
<div class="field">
<label for="signal-number">{$_('register.signalNumber')}</label>
<input
id="signal-number"
type="tel"
bind:value={flow.info.signalNumber}
onblur={() => flow?.checkCommsChannelInUse('signal', flow.info.signalNumber ?? '')}
placeholder={$_('register.signalNumberPlaceholder')}
disabled={flow.state.submitting}
required
/>
<p class="hint">{$_('register.signalNumberHint')}</p>
{#if flow.state.signalInUse}
<p class="hint warning">{$_('register.signalInUseWarning')}</p>
{/if}
</div>
{/if}
</div>
</fieldset>
{#if serverInfo?.inviteCodeRequired}
<div class="field">
<label for="invite-code">{$_('register.inviteCode')} <span class="required">{$_('register.inviteCodeRequired')}</span></label>
<input
id="invite-code"
type="text"
bind:value={flow.info.inviteCode}
placeholder={$_('register.inviteCodePlaceholder')}
disabled={flow.state.submitting}
required
/>
</div>
{/if}
<button type="submit" disabled={flow.state.submitting}>
{flow.state.submitting ? $_('common.creating') : $_('register.createButton')}
</button>
</form>
<div class="form-links">
<p class="link-text">
{$_('register.alreadyHaveAccount')} <a href={getFullUrl(routes.login)}>{$_('register.signIn')}</a>
</p>
</div>
<form class="register-form" onsubmit={handleInfoSubmit}>
<div class="field">
<label for="handle">{$_('register.handle')}</label>
<input
id="handle"
type="text"
bind:value={flow.info.handle}
placeholder={$_('register.handlePlaceholder')}
disabled={flow.state.submitting}
required
/>
{#if flow.info.handle.includes('.')}
<p class="hint warning">{$_('register.handleDotWarning')}</p>
{:else if flow.state.checkingHandle}
<p class="hint">{$_('common.checking')}</p>
{:else if flow.state.handleAvailable === false}
<p class="hint warning">{$_('register.handleTaken')}</p>
{:else if flow.state.handleAvailable === true && fullHandle()}
<p class="hint success">{$_('register.handleHint', { values: { handle: fullHandle() } })}</p>
{:else if fullHandle()}
<p class="hint">{$_('register.handleHint', { values: { handle: fullHandle() } })}</p>
{/if}
</div>
<aside class="info-panel">
<h3>{$_('register.identityHint')}</h3>
<p>{$_('register.infoIdentityDesc')}</p>
<div class="field">
<label for="password">{$_('register.password')}</label>
<input
id="password"
type="password"
bind:value={flow.info.password}
placeholder={$_('register.passwordPlaceholder')}
disabled={flow.state.submitting}
required
minlength="8"
/>
</div>
<h3>{$_('register.contactMethodHint')}</h3>
<p>{$_('register.infoContactDesc')}</p>
<div class="field">
<label for="confirm-password">{$_('register.confirmPassword')}</label>
<input
id="confirm-password"
type="password"
bind:value={confirmPassword}
placeholder={$_('register.confirmPasswordPlaceholder')}
disabled={flow.state.submitting}
required
/>
</div>
<h3>{$_('register.infoNextTitle')}</h3>
<p>{$_('register.infoNextDesc')}</p>
</aside>
</div>
<div class="field">
<label for="verification-channel">{$_('register.verificationMethod')}</label>
<select id="verification-channel" bind:value={flow.info.verificationChannel} disabled={flow.state.submitting}>
<option value="email">{$_('register.email')}</option>
{#if isChannelAvailable('discord')}
<option value="discord">{$_('register.discord')}</option>
{/if}
{#if isChannelAvailable('telegram')}
<option value="telegram">{$_('register.telegram')}</option>
{/if}
{#if isChannelAvailable('signal')}
<option value="signal">{$_('register.signal')}</option>
{/if}
</select>
</div>
{#if flow.info.verificationChannel === 'email'}
<div class="field">
<label for="email">{$_('register.emailAddress')}</label>
<input
id="email"
type="email"
bind:value={flow.info.email}
placeholder={$_('register.emailPlaceholder')}
disabled={flow.state.submitting}
required
/>
</div>
{:else if flow.info.verificationChannel === 'discord'}
<div class="field">
<label for="discord-id">{$_('register.discordId')}</label>
<input
id="discord-id"
type="text"
bind:value={flow.info.discordId}
onblur={() => flow?.checkCommsChannelInUse('discord', flow.info.discordId ?? '')}
placeholder={$_('register.discordIdPlaceholder')}
disabled={flow.state.submitting}
required
/>
<p class="hint">{$_('register.discordIdHint')}</p>
{#if flow.state.discordInUse}
<p class="hint warning">{$_('register.discordInUseWarning')}</p>
{/if}
</div>
{:else if flow.info.verificationChannel === 'telegram'}
<div class="field">
<label for="telegram-username">{$_('register.telegramUsername')}</label>
<input
id="telegram-username"
type="text"
bind:value={flow.info.telegramUsername}
onblur={() => flow?.checkCommsChannelInUse('telegram', flow.info.telegramUsername ?? '')}
placeholder={$_('register.telegramUsernamePlaceholder')}
disabled={flow.state.submitting}
required
/>
{#if flow.state.telegramInUse}
<p class="hint warning">{$_('register.telegramInUseWarning')}</p>
{/if}
</div>
{:else if flow.info.verificationChannel === 'signal'}
<div class="field">
<label for="signal-number">{$_('register.signalNumber')}</label>
<input
id="signal-number"
type="tel"
bind:value={flow.info.signalNumber}
onblur={() => flow?.checkCommsChannelInUse('signal', flow.info.signalNumber ?? '')}
placeholder={$_('register.signalNumberPlaceholder')}
disabled={flow.state.submitting}
required
/>
<p class="hint">{$_('register.signalNumberHint')}</p>
{#if flow.state.signalInUse}
<p class="hint warning">{$_('register.signalInUseWarning')}</p>
{/if}
</div>
{/if}
<fieldset class="identity-section">
<legend>{$_('register.identityType')}</legend>
<div class="radio-group">
<label class="radio-label">
<input type="radio" name="didType" value="plc" bind:group={flow.info.didType} disabled={flow.state.submitting} />
<span class="radio-content">
<strong>{$_('register.didPlc')}</strong>
<span class="radio-hint">{$_('register.didPlcHint')}</span>
</span>
</label>
<label class="radio-label" class:disabled={serverInfo?.selfHostedDidWebEnabled === false}>
<input type="radio" name="didType" value="web" bind:group={flow.info.didType} disabled={flow.state.submitting || serverInfo?.selfHostedDidWebEnabled === false} />
<span class="radio-content">
<strong>{$_('register.didWeb')}</strong>
{#if serverInfo?.selfHostedDidWebEnabled === false}
<span class="radio-hint disabled-hint">{$_('register.didWebDisabledHint')}</span>
{:else}
<span class="radio-hint">{$_('register.didWebHint')}</span>
{/if}
</span>
</label>
<label class="radio-label">
<input type="radio" name="didType" value="web-external" bind:group={flow.info.didType} disabled={flow.state.submitting} />
<span class="radio-content">
<strong>{$_('register.didWebBYOD')}</strong>
<span class="radio-hint">{$_('register.didWebBYODHint')}</span>
</span>
</label>
</div>
</fieldset>
{#if flow.info.didType === 'web'}
<div class="warning-box">
<strong>{$_('register.didWebWarningTitle')}</strong>
<ul>
<li><strong>{$_('register.didWebWarning1')}</strong> {$_('register.didWebWarning1Detail', { values: { did: `did:web:yourhandle.${serverInfo?.availableUserDomains?.[0] || 'this-pds.com'}` } })}</li>
<li><strong>{$_('register.didWebWarning2')}</strong> {$_('register.didWebWarning2Detail')}</li>
{#if $_('register.didWebWarning3')}
<li><strong>{$_('register.didWebWarning3')}</strong> {$_('register.didWebWarning3Detail')}</li>
{/if}
</ul>
</div>
{/if}
{#if flow.info.didType === 'web-external'}
<div class="field">
<label for="external-did">{$_('register.externalDid')}</label>
<input
id="external-did"
type="text"
bind:value={flow.info.externalDid}
placeholder={$_('register.externalDidPlaceholder')}
disabled={flow.state.submitting}
required
/>
<p class="hint">{$_('register.externalDidHint')}</p>
</div>
{/if}
{#if serverInfo?.inviteCodeRequired}
<div class="field">
<label for="invite-code">{$_('register.inviteCode')}</label>
<input
id="invite-code"
type="text"
bind:value={flow.info.inviteCode}
placeholder={$_('register.inviteCodePlaceholder')}
disabled={flow.state.submitting}
required
/>
</div>
{/if}
<div class="form-actions">
<button type="button" class="secondary" onclick={handleCancel} disabled={flow.state.submitting}>
{$_('common.cancel')}
</button>
<button type="submit" class="primary" disabled={flow.state.submitting || flow.state.handleAvailable === false || flow.state.checkingHandle}>
{flow.state.submitting ? $_('common.loading') : $_('common.continue')}
</button>
</div>
</form>
{:else if flow.state.step === 'key-choice'}
<KeyChoiceStep {flow} />
@@ -543,18 +552,87 @@
</div>
<style>
.client-name {
.register-form {
display: flex;
flex-direction: column;
gap: var(--space-3);
max-width: 500px;
}
.identity-section {
border: 1px solid var(--border-color);
border-radius: var(--radius-md);
padding: var(--space-4);
margin: 0;
margin-top: var(--space-5);
}
.identity-section legend {
font-weight: var(--font-medium);
font-size: var(--text-sm);
padding: 0 var(--space-2);
}
.radio-group {
display: flex;
flex-direction: column;
gap: var(--space-3);
}
.radio-label {
display: flex;
align-items: flex-start;
gap: var(--space-2);
cursor: pointer;
}
.radio-label.disabled {
opacity: 0.5;
cursor: not-allowed;
}
.radio-label input {
margin-top: 2px;
}
.radio-content {
display: flex;
flex-direction: column;
gap: var(--space-1);
}
.radio-hint {
font-size: var(--text-sm);
color: var(--text-secondary);
}
.radio-hint.disabled-hint {
color: var(--text-muted);
}
.warning-box {
padding: var(--space-4);
background: var(--warning-bg);
border: 1px solid var(--warning-border);
border-radius: var(--radius-md);
}
.warning-box ul {
margin: var(--space-2) 0 0 0;
padding-left: var(--space-5);
}
.warning-box li {
margin-top: var(--space-2);
}
form {
.form-actions {
display: flex;
flex-direction: column;
gap: var(--space-5);
gap: var(--space-4);
margin-top: var(--space-5);
}
button[type="submit"] {
margin-top: var(--space-3);
.form-actions .primary {
flex: 1;
}
</style>
+42 -15
View File
@@ -94,12 +94,44 @@
initiating = null
}
}
async function handleCancel() {
const requestUri = getRequestUriFromUrl()
if (!requestUri) {
window.history.back()
return
}
try {
const response = await fetch('/oauth/authorize/deny', {
method: 'POST',
headers: {
'Content-Type': 'application/json',
'Accept': 'application/json'
},
body: JSON.stringify({ request_uri: requestUri })
})
if (!response.ok) {
window.history.back()
return
}
const data = await response.json()
if (data.redirect_uri) {
window.location.href = data.redirect_uri
} else {
window.history.back()
}
} catch {
window.history.back()
}
}
</script>
<div class="page">
<header class="page-header">
<h1>{$_('register.title')}</h1>
<p class="subtitle">{$_('register.ssoSubtitle')}</p>
</header>
<div class="migrate-callout">
@@ -125,7 +157,6 @@
</div>
{:else}
<div class="provider-list">
<p class="provider-hint">{$_('register.ssoHint')}</p>
<div class="provider-grid">
{#each providers as provider}
<button
@@ -147,10 +178,10 @@
</div>
{/if}
<div class="form-links">
<p class="link-text">
{$_('register.alreadyHaveAccount')} <a href={getFullUrl(routes.login)}>{$_('register.signIn')}</a>
</p>
<div class="form-actions">
<button type="button" class="secondary" onclick={handleCancel} disabled={initiating !== null}>
{$_('common.cancel')}
</button>
</div>
</div>
@@ -162,18 +193,9 @@
}
.provider-list {
display: flex;
flex-direction: column;
gap: var(--space-3);
max-width: var(--width-md);
}
.provider-hint {
color: var(--text-secondary);
font-size: var(--text-sm);
margin: 0 0 var(--space-4) 0;
}
.provider-grid {
display: grid;
grid-template-columns: 1fr;
@@ -216,4 +238,9 @@
.provider-button .provider-name {
flex: 1;
}
.form-actions {
margin-top: var(--space-5);
max-width: var(--width-md);
}
</style>
+14 -7
View File
@@ -451,13 +451,14 @@
<p class="field-help">{$_('verify.codeHelp')}</p>
</div>
<button type="submit" disabled={submitting || !verificationCode.trim()}>
{submitting ? $_('common.verifying') : $_('common.verify')}
</button>
<button type="button" class="secondary" onclick={handleResendCode} disabled={resendingCode}>
{resendingCode ? $_('common.sending') : $_('common.resendCode')}
</button>
<div class="form-actions">
<button type="button" class="secondary" onclick={handleResendCode} disabled={resendingCode}>
{resendingCode ? $_('common.sending') : $_('common.resendCode')}
</button>
<button type="submit" disabled={submitting || !verificationCode.trim()}>
{submitting ? $_('common.verifying') : $_('common.verify')}
</button>
</div>
</form>
<p class="link-text">
@@ -519,6 +520,12 @@
letter-spacing: 0.05em;
}
.form-actions {
display: flex;
gap: var(--space-4);
margin-top: var(--space-4);
}
.link-text {
text-align: center;
margin-top: var(--space-6);