Account management page (#3659)

---------

Co-authored-by: Eric Bailey <git@esb.lol>
This commit is contained in:
Matthieu Sieben 2025-04-15 17:15:27 +02:00 committed by GitHub
parent 8b98fec885
commit 371e04aad2
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
370 changed files with 24123 additions and 6252 deletions

View File

@ -0,0 +1,5 @@
---
"@atproto/oauth-provider": patch
---
Security fix: Properly validate JWT `exp` claim when it is zero.

View File

@ -0,0 +1,5 @@
---
"@atproto/oauth-provider": minor
---
OAuthProvider will now always generate JWT access tokens. This will prevent "leaked" `tokenId` values from being used as access tokens directly. This change also introduces an `AccessTokenMode` that allows generating "stateless" tokens (when the AS and RS are different servers), or shorter "light" tokens (that only act as wrapper around `tokenId` values).

View File

@ -0,0 +1,5 @@
---
"@atproto/oauth-provider": minor
---
Remove unused `getAuthorizationDetails` hook

View File

@ -0,0 +1,5 @@
---
"@atproto/oauth-provider": minor
---
Change name of `onSignupAttempt` hook to `onSignUpAttempt`

View File

@ -0,0 +1,5 @@
---
"@atproto/oauth-types": patch
---
Add `OAuthAuthenticationErrorResponse`

View File

@ -0,0 +1,5 @@
---
"@atproto-labs/rollup-plugin-bundle-manifest": minor
---
Export plugin as named export

View File

@ -0,0 +1,5 @@
---
"@atproto/oauth-provider-api": minor
---
Various adaptations

View File

@ -0,0 +1,5 @@
---
"@atproto/oauth-provider": minor
---
Store & verify new authorization requests against previously approved scopes for the same client

View File

@ -0,0 +1,5 @@
---
"@atproto/oauth-provider": minor
---
Split oauth endpoints & authorization page routes from `OAuthProvider`

View File

@ -0,0 +1,5 @@
---
"@atproto/oauth-provider": patch
---
Always log to console in dev mode

View File

@ -0,0 +1,5 @@
---
"@atproto/pds": patch
---
Add account management page for oauth sessions

View File

@ -0,0 +1,5 @@
---
"@atproto/oauth-provider": minor
---
Fix bug allowing to authenticate using previous account even if the "remember me" checkbox was left unchecked

View File

@ -0,0 +1,5 @@
---
"@atproto/oauth-provider": minor
---
Change "brand" color to "primary"

View File

@ -0,0 +1,6 @@
---
"@atproto/oauth-provider-frontend": minor
"@atproto/oauth-provider-ui": minor
---
New build system

View File

@ -0,0 +1,5 @@
---
"@atproto/oauth-provider": patch
---
Do not return invalid authorization response errors

View File

@ -0,0 +1,5 @@
---
"@atproto/oauth-provider": minor
---
Remove instrospection endpoint

View File

@ -0,0 +1,5 @@
---
"@atproto/oauth-provider": patch
---
Apply time mitigation strategy on the sensitive part of the operation only.

View File

@ -87,6 +87,7 @@
"project": [
"tsconfig.json",
"packages/oauth/*/tsconfig.json",
"packages/oauth/*/tsconfig.src.json",
"packages/internal/*/tsconfig.json",
"packages/*/tsconfig.json"
]

View File

@ -35,7 +35,7 @@ jobs:
path: |
packages/*/dist
packages/*/*/dist
packages/oauth/oauth-provider-ui/src/locales/*/messages.ts
packages/oauth/*/src/locales/*/messages.ts
retention-days: 1
test:
name: Test

View File

@ -14,4 +14,6 @@ packages/pds/src/lexicon
packages/ozone/src/lexicon
# Automatically generated by lingui
packages/oauth/oauth-provider-ui/src/locales/*/messages.ts
packages/oauth/*/src/locales/*/messages.ts
packages/oauth/oauth-provider-frontend/src/routeTree.gen.ts

View File

@ -3,6 +3,7 @@
"tabWidth": 2,
"semi": false,
"singleQuote": true,
"plugins": ["prettier-plugin-tailwindcss"],
"overrides": [
{
"files": "*.hbs",

View File

@ -20,8 +20,8 @@
"verify:types": "tsc --build tsconfig.json",
"format": "pnpm lint:fix && pnpm style:fix",
"precodegen": "pnpm run --recursive --stream --filter '@atproto/lex-cli...' build --force",
"codegen": "pnpm run --recursive --stream --parallel codegen",
"build": "pnpm run --recursive --stream '/^(build|build:.+)$/'",
"codegen": "pnpm run --sort --recursive --stream --parallel codegen",
"build": "pnpm run --sort --recursive --stream '/^(build|build:.+)$/'",
"dev": "NODE_ENV=development pnpm run --recursive --parallel --stream '/^(dev|dev:.+)$/'",
"dev:tsc": "tsc --build tsconfig.json --preserveWatchOutput --watch",
"test": "LOG_ENABLED=false ./packages/dev-infra/with-test-redis-and-db.sh pnpm test --stream --recursive",
@ -52,7 +52,8 @@
"pino-pretty": "^9.1.0",
"prettier": "^3.2.5",
"prettier-config-standard": "^7.0.0",
"typescript": "^5.6.3"
"prettier-plugin-tailwindcss": "^0.6.11",
"typescript": "^5.8.2"
},
"workspaces": {
"packages": [

View File

@ -17,8 +17,8 @@
"bin": "dist/bin.js",
"scripts": {
"build": "tsc --build tsconfig.build.json",
"start": "../dev-infra/with-test-redis-and-db.sh node dist/bin.js",
"dev": "../dev-infra/with-test-redis-and-db.sh node --watch dist/bin.js"
"start": "../dev-infra/with-test-redis-and-db.sh node --enable-source-maps dist/bin.js",
"dev": "../dev-infra/with-test-redis-and-db.sh node --enable-source-maps --watch dist/bin.js"
},
"engines": {
"node": ">=18.7.0"

View File

@ -49,17 +49,13 @@ export class TestPds {
inviteRequired: false,
disableSsrfProtection: true,
serviceName: 'Development PDS',
brandColor: '#8338ec',
errorColor: '#ff006e',
// Purposefully not setting warningColor to ensure that not all branding
// colors are required from a config perspective.
warningColor: undefined,
successColor: '#02c39a',
primaryColor: '#f0828d',
errorColor: '#a5414b',
logoUrl:
// Using a "data:" instead of a real URL to avoid making CORS requests in dev.
// License: https://uxwing.com/license/
// Source: https://uxwing.com/bee-icon/
`data:image/svg+xml;base64,${Buffer.from('<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 117.47 122.88"><defs><style>.cls-1,.cls-2{fill-rule:evenodd;}.cls-2{fill:#ffcb1e;}</style></defs><title>bee</title><path class="cls-1" d="M72.69,49.18c5.93.81,12.68,1.76,19.09,3.3,41.08,9.87,25.49,44.79,4.33,40.78A23.57,23.57,0,0,1,83.48,86.2c0,1.23-.12,2.45-.24,3.65a1.47,1.47,0,0,1-.07.72,49.21,49.21,0,0,1-6.78,20.68c-4.49,7.15-10.75,11.63-17.72,11.63S45.4,118.52,40.9,111.46a41.91,41.91,0,0,1-4-8.23l-.06-.18a54.7,54.7,0,0,1-3-17.22,24.75,24.75,0,0,1-13,7.43C.68,97.49-15.49,62.44,25.22,52.48A184,184,0,0,1,44.4,49.16l-.09-.09a9.18,9.18,0,0,1-1.9-2.74,28,28,0,0,1-2.26-10.81,17.15,17.15,0,0,1,5.41-12.45,18.57,18.57,0,0,1,7.78-4.42,19.21,19.21,0,0,0-2.19-7.07A8.05,8.05,0,0,0,47.4,8.13a4.77,4.77,0,1,1,1.38-3.36c0,.22,0,.43,0,.64a11,11,0,0,1,5,4.64,21.82,21.82,0,0,1,2.56,8,20.17,20.17,0,0,1,2.21-.13c.56,0,1.11,0,1.65.07a21.65,21.65,0,0,1,2.66-8.14,10.84,10.84,0,0,1,5.45-4.68c0-.14,0-.28,0-.42A4.77,4.77,0,1,1,69.49,8a7.8,7.8,0,0,0-4.07,3.48,18.73,18.73,0,0,0-2.26,7.06,18.57,18.57,0,0,1,8.31,4.56,17.11,17.11,0,0,1,5.41,12.45,27.65,27.65,0,0,1-2.6,11.38,10,10,0,0,1-1.59,2.28Z"/><path class="cls-2" d="M40.15,103.25a37.55,37.55,0,0,0,3.3,6.59c3.94,6.18,9.33,10,15.22,10s11.22-3.94,15.16-10.21A38.55,38.55,0,0,0,77,103.28q-9.49-5.66-18.7-5.66a33.91,33.91,0,0,0-18.17,5.63Zm31-37.85c-.65-1.51-1.29-3-1.92-4.42-1.84-4.19-3.37-7.81-5.18-12a21.24,21.24,0,0,0-5.76-.9,22,22,0,0,0-5.23.54C51.2,53,49.64,56.67,47.74,61l-1.89,4.36a41.7,41.7,0,0,1,12.58-2.09A37.6,37.6,0,0,1,71.17,65.4ZM69.39,25.26A15.7,15.7,0,0,0,58.52,21a15.89,15.89,0,0,0-3,.28,1.21,1.21,0,0,1-.33.07h0a15.56,15.56,0,0,0-7.47,3.93,14.13,14.13,0,0,0-4.46,10.26,24.67,24.67,0,0,0,2,9.51,6.21,6.21,0,0,0,1.24,1.83,1.43,1.43,0,0,0,1,.47,1.51,1.51,0,0,0,.64-.16,24,24,0,0,1,20.6.37,1.55,1.55,0,0,0,.73.2,1.57,1.57,0,0,0,1-.46,6.41,6.41,0,0,0,1.3-1.78,24.28,24.28,0,0,0,2.25-10,14.17,14.17,0,0,0-4.46-10.26Zm9.38,55c-1.86-2.83-4.59-10.46-7.11-11.45a35.4,35.4,0,0,0-13.21-2.54A40,40,0,0,0,45,68.82c-1.8.66-5.3,9.18-6.78,11.44-1.65,2.51-1.33,1.13-1.36,3.52,0,.12,0,.45,0,1s0,1.32,0,2c.29-.28.59-.55.89-.81C43.56,81,51.53,79.15,59.3,79.48S74.62,82.3,79.69,86.1l.73.58c0-.82.06-3.55.06-4.38,0-.39-.85-.74-1.71-2ZM37.19,90.9a50.67,50.67,0,0,0,1.94,9.42A36.54,36.54,0,0,1,58.32,94.6q9.78,0,19.73,5.78a52,52,0,0,0,2.08-9.86,17.62,17.62,0,0,0-2.26-2c-4.6-3.45-11.55-5.72-18.69-6S44.9,83.86,39.76,88.26a19.46,19.46,0,0,0-2.57,2.64Z"/></svg>', 'utf8').toString('base64')}`,
`data:image/svg+xml;base64,${Buffer.from('<svg xmlns="http://www.w3.org/2000/svg" shape-rendering="geometricPrecision" text-rendering="geometricPrecision" image-rendering="optimizeQuality" fill-rule="evenodd" clip-rule="evenodd" viewBox="0 0 503 511.623"><path fill="#FFB9B9" d="M379.75 85.311l90.022 89.879C503.264 128.804 502.534 31.13 476.188 0c-27.441 31.966-59.103 60.767-96.438 85.311z"/><path fill="#E2828D" d="M399.445 104.976l70.327 70.214c26.443-36.622 31.549-105.205 19.563-147.778-26.692 28.309-56.413 54.344-89.89 77.564z"/><path fill="#FFB9B9" d="M119.595 85.311L29.573 175.19C-3.919 128.804-3.189 31.13 23.156 0c27.441 31.966 59.103 60.767 96.439 85.311z"/><path fill="#E2828D" d="M99.899 104.976L29.573 175.19C3.13 138.568-1.976 69.985 10.01 27.412c26.692 28.309 56.413 54.344 89.889 77.564z"/><path fill="#FFB9B9" d="M251.5 51.303c138.898 0 251.5 103.046 251.5 230.16 0 127.114-112.602 230.16-251.5 230.16C112.6 511.623 0 408.577 0 281.463c0-127.114 112.6-230.16 251.5-230.16z"/><path fill="#331400" d="M138.142 188.245c16.387 0 29.672 13.283 29.672 29.672 0 16.389-13.285 29.673-29.672 29.673-16.389 0-29.675-13.284-29.675-29.673 0-16.389 13.286-29.672 29.675-29.672zM360.695 188.245c16.389 0 29.674 13.283 29.674 29.672 0 16.389-13.285 29.673-29.674 29.673-16.387 0-29.673-13.284-29.673-29.673 0-16.389 13.286-29.672 29.673-29.672z"/><path fill="#F0828D" fill-rule="nonzero" d="M251.5 255.548c37.407 0 71.438 11.136 96.213 29.138 25.886 18.808 41.905 45.125 41.905 74.487 0 29.36-16.017 55.679-41.908 74.49-24.772 18.001-58.805 29.138-96.21 29.138-37.405 0-71.438-11.137-96.21-29.138-25.891-18.811-41.908-45.13-41.908-74.49 0-29.362 16.019-55.679 41.905-74.487 24.775-18.002 58.808-29.138 96.213-29.138z"/><circle fill="#A5414B" cx="203.259" cy="358.515" r="29.673"/><circle fill="#A5414B" cx="298.744" cy="358.515" r="29.673"/></svg>', 'utf8').toString('base64')}`,
homeUrl: 'https://bsky.social/',
termsOfServiceUrl: 'https://bsky.social/about/support/tos',
privacyPolicyUrl: 'https://bsky.social/about/support/privacy-policy',

View File

@ -1,7 +1,7 @@
import { createHash } from 'node:crypto'
import { extname } from 'node:path'
import mime from 'mime'
import { Plugin } from 'rollup'
import type { Plugin } from 'rollup'
type AssetItem = {
type: 'asset'
@ -26,7 +26,7 @@ export type ManifestItem = AssetItem | ChunkItem
export type Manifest = Record<string, ManifestItem>
export default function bundleManifest({
export function bundleManifest({
name = 'bundle-manifest.json',
data = false,
}: {

View File

@ -7,7 +7,7 @@ const { default: nodeResolve } = require('@rollup/plugin-node-resolve')
const { default: swc } = require('@rollup/plugin-swc')
const { defineConfig } = require('rollup')
const {
default: manifest,
bundleManifest,
} = require('@atproto-labs/rollup-plugin-bundle-manifest')
const postcss = ((m) => m.default || m)(require('rollup-plugin-postcss'))
const serve = ((m) => m.default || m)(require('rollup-plugin-serve'))
@ -106,7 +106,7 @@ module.exports = defineConfig((commandLineArguments) => {
</html>
`,
}),
manifest({ name: 'files.json', data: true }),
bundleManifest({ name: 'files.json', data: true }),
commandLineArguments.watch &&
serve({

View File

@ -35,9 +35,9 @@ export function AuthForm({ atpSignIn, oauthSignIn, signUpUrl }: AuthFormProps) {
// Tailwind css tabs
return (
<div className="p-4">
<div className="flex my-4">
<div className="my-4 flex">
<button
className={`bg-blue-500 hover:bg-blue-700 text-white font-bold py-1 px-4 rounded ${
className={`rounded bg-blue-500 px-4 py-1 font-bold text-white hover:bg-blue-700 ${
method === 'oauth' ? 'bg-blue-700' : ''
}`}
onClick={() => oauthSignIn && setMethod('oauth')}
@ -47,7 +47,7 @@ export function AuthForm({ atpSignIn, oauthSignIn, signUpUrl }: AuthFormProps) {
</button>
<button
className={`bg-blue-500 hover:bg-blue-700 text-white font-bold py-1 px-4 rounded ${
className={`rounded bg-blue-500 px-4 py-1 font-bold text-white hover:bg-blue-700 ${
method === 'credential' ? 'bg-blue-700' : ''
}`}
onClick={() => atpSignIn && setMethod('credential')}

View File

@ -24,7 +24,7 @@ export function CredentialSignInForm({
const [password, setPassword] = useState('')
const [service, setService] = useState('http://localhost:2583')
// TODO: add auth factor support ?
// @TODO: add auth factor support ?
const onSubmit = useCallback(
async (e: FormEvent<HTMLFormElement>) => {
@ -49,14 +49,14 @@ export function CredentialSignInForm({
)
return (
<form {...props} className="max-w-lg w-full" onSubmit={onSubmit}>
<fieldset className="rounded-md border border-solid border-slate-200 dark:border-slate-700 text-neutral-700 dark:text-neutral-100">
<div className="relative p-1 flex flex-col flex-wrap items-center justify-stretch">
<form {...props} className="w-full max-w-lg" onSubmit={onSubmit}>
<fieldset className="rounded-md border border-solid border-slate-200 text-neutral-700 dark:border-slate-700 dark:text-neutral-100">
<div className="relative flex flex-col flex-wrap items-center justify-stretch p-1">
<input
id="identifier"
name="identifier"
type="text"
className="relative m-0 block w-full flex-auto px-3 py-[0.25rem] leading-[1.6] bg-transparent bg-clip-padding text-base text-inherit outline-none dark:placeholder:text-neutral-100"
className="relative m-0 block w-full flex-auto bg-transparent bg-clip-padding px-3 py-[0.25rem] text-base leading-[1.6] text-inherit outline-none dark:placeholder:text-neutral-100"
placeholder="@handle or email"
aria-label="@handle or email"
required
@ -69,7 +69,7 @@ export function CredentialSignInForm({
id="password"
name="password"
type="password"
className="relative m-0 block w-full flex-auto px-3 py-[0.25rem] leading-[1.6] bg-transparent bg-clip-padding text-base text-inherit outline-none dark:placeholder:text-neutral-100"
className="relative m-0 block w-full flex-auto bg-transparent bg-clip-padding px-3 py-[0.25rem] text-base leading-[1.6] text-inherit outline-none dark:placeholder:text-neutral-100"
placeholder="Password"
aria-label="Password"
required
@ -82,7 +82,7 @@ export function CredentialSignInForm({
id="service"
name="service"
type="text"
className="relative m-0 block w-full flex-auto px-3 py-[0.25rem] leading-[1.6] bg-transparent bg-clip-padding text-base text-inherit outline-none dark:placeholder:text-neutral-100"
className="relative m-0 block w-full flex-auto bg-transparent bg-clip-padding px-3 py-[0.25rem] text-base leading-[1.6] text-inherit outline-none dark:placeholder:text-neutral-100"
placeholder="Service"
aria-label="Service"
required
@ -94,7 +94,7 @@ export function CredentialSignInForm({
<button
type="submit"
disabled={loading}
className="bg-transparent text-blue-600 rounded-md py-1 px-3 hover:bg-blue-100 focus:outline-none focus:ring-2 focus:ring-offset-2 focus:ring-blue-500 focus:ring-inset"
className="rounded-md bg-transparent px-3 py-1 text-blue-600 hover:bg-blue-100 focus:outline-none focus:ring-2 focus:ring-inset focus:ring-blue-500 focus:ring-offset-2"
>
Login
</button>

View File

@ -61,15 +61,15 @@ export function OAuthSignInForm({
return (
<form
{...props}
className={`${className || ''} max-w-lg w-full`}
className={`${className || ''} w-full max-w-lg`}
onSubmit={onSubmit}
>
<fieldset className="rounded-md border border-solid border-slate-200 dark:border-slate-700 text-neutral-700 dark:text-neutral-100">
<div className="relative p-1 flex flex-wrap items-center justify-stretch">
<fieldset className="rounded-md border border-solid border-slate-200 text-neutral-700 dark:border-slate-700 dark:text-neutral-100">
<div className="relative flex flex-wrap items-center justify-stretch p-1">
<input
name="value"
type="text"
className="relative m-0 block w-[1px] min-w-0 flex-auto px-3 py-[0.25rem] leading-[1.6] bg-transparent bg-clip-padding text-base text-inherit outline-none dark:placeholder:text-neutral-100"
className="relative m-0 block w-[1px] min-w-0 flex-auto bg-transparent bg-clip-padding px-3 py-[0.25rem] text-base leading-[1.6] text-inherit outline-none dark:placeholder:text-neutral-100"
placeholder="@handle, DID or PDS url"
aria-label="@handle, DID or PDS url"
required
@ -80,7 +80,7 @@ export function OAuthSignInForm({
<button
type="submit"
disabled={loading}
className="bg-transparent text-blue-600 rounded-md py-1 px-3 hover:bg-blue-100 focus:outline-none focus:ring-2 focus:ring-offset-2 focus:ring-blue-500 focus:ring-inset"
className="rounded-md bg-transparent px-3 py-1 text-blue-600 hover:bg-blue-100 focus:outline-none focus:ring-2 focus:ring-inset focus:ring-blue-500 focus:ring-offset-2"
>
Login
</button>
@ -93,7 +93,7 @@ export function OAuthSignInForm({
type="button"
onClick={() => signIn(signUpUrl)}
disabled={loading}
className="mt-2 bg-blue-600 text-white rounded-md py-1 px-3 hover:bg-blue-700 focus:outline-none focus:ring-2 focus:ring-offset-2 focus:ring-blue-500 focus:ring-inset"
className="mt-2 rounded-md bg-blue-600 px-3 py-1 text-white hover:bg-blue-700 focus:outline-none focus:ring-2 focus:ring-inset focus:ring-blue-500 focus:ring-offset-2"
>
Sign up
</button>

View File

@ -142,7 +142,7 @@ export class BrowserOAuthClient extends OAuthClient implements Disposable {
database.getProtectedResourceMetadataCache(),
})
// TODO: replace with AsyncDisposableStack once they are standardized
// @TODO replace with AsyncDisposableStack once they are standardized
const ac = new AbortController()
const { signal } = ac
this[Symbol.dispose] = () => ac.abort()
@ -293,7 +293,7 @@ export class BrowserOAuthClient extends OAuthClient implements Disposable {
}
const cancel = () => {
// @TODO: Store fact that the request was cancelled, allowing any
// @TODO Store fact that the request was cancelled, allowing any
// callback (e.g. in the popup) to revoke the session or credentials.
reject(new Error(options?.signal?.aborted ? 'Aborted' : 'Timeout'))

View File

@ -143,7 +143,7 @@ export class OAuthSession {
// credentials from the authorization server (e.g. because some migration
// occurred). Any ways, there is no point in keeping the session.
if (isInvalidTokenResponse(finalResponse)) {
// TODO: Is there a "softer" way to handle this, e.g. by marking the
// @TODO Is there a "softer" way to handle this, e.g. by marking the
// session as "expired" in the session store, allowing the user to trigger
// a new login (using login_hint)?
await this.sessionGetter.delStored(

View File

@ -25,6 +25,7 @@
}
},
"dependencies": {
"@atproto/jwk": "workspace:*",
"@atproto/oauth-types": "workspace:*"
},
"devDependencies": {

View File

@ -0,0 +1,207 @@
import type { SignedJwt } from '@atproto/jwk'
import type { OAuthClientMetadata } from '@atproto/oauth-types'
import type { Account, DeviceMetadata, ISODateString } from './types.js'
// These are the endpoints implemented by the OAuth provider, for its UI to
// call.
export type ApiEndpoints = {
'/verify-handle-availability': {
method: 'POST'
input: VerifyHandleAvailabilityInput
output: { available: true }
}
'/sign-up': {
method: 'POST'
input: SignUpInput
output: SignUpOutput
}
'/sign-in': {
method: 'POST'
input: SignInInput
output: SignInOutput
}
'/reset-password-request': {
method: 'POST'
input: InitiatePasswordResetInput
output: { success: true }
}
'/reset-password-confirm': {
method: 'POST'
input: ConfirmResetPasswordInput
output: { success: true }
}
'/sign-out': {
method: 'POST'
input: SignOutInput
output: { success: true }
}
/**
* Lists all the accounts that are currently active, on the current device.
*/
'/device-sessions': {
method: 'GET'
output: ActiveDeviceSession[]
}
/**
* Lists all the active OAuth sessions (access/refresh tokens) that where
* issued to OAuth clients (apps).
*
* @NOTE can be revoked using the oauth revocation endpoint (json or form
* encoded)
*
* ```http
* POST /oauth/revoke
* Content-Type: application/x-www-form-urlencoded
*
* token=<tokenId>
* ```
*/
'/oauth-sessions': {
method: 'GET'
params: { sub: string }
output: ActiveOAuthSession[]
}
'/revoke-oauth-session': {
method: 'POST'
input: RevokeOAuthSessionInput
output: { success: true }
}
/**
* Lists all the sessions that are currently active for a particular user, on
* other devices.
*/
'/account-sessions': {
method: 'GET'
params: { sub: string }
output: ActiveAccountSession[]
}
'/revoke-account-session': {
method: 'POST'
input: RevokeAccountSessionInput
output: { success: true }
}
'/accept': {
method: 'POST'
input: AcceptInput
output: { url: string }
}
'/reject': {
method: 'POST'
input: RejectInput
output: { url: string }
}
}
/**
* When a user signs in without the "remember me" option, the server returns an
* ephemeral token. When used as `Bearer` authorization header, the token will
* be used in order to authenticate the users in place of using the user's
* cookie based session (which are only created when "remember me" is checked).
*
* Only include this token in the `Authorization` header when making requests to
* the OAuth provider API, **FOR THE ACCOUNT IT WAS GENERATED FOR**.
*/
export type EphemeralToken = SignedJwt
export type SignInInput = {
locale: string
username: string
password: string
emailOtp?: string
remember?: boolean
}
export type SignInOutput = {
account: Account
ephemeralToken?: EphemeralToken
consentRequired?: boolean
}
export type SignUpInput = {
locale: string
handle: string
email: string
password: string
inviteCode?: string
hcaptchaToken?: string
}
export type SignUpOutput = {
account: Account
ephemeralToken?: EphemeralToken
}
export type SignOutInput = {
sub: string | string[]
}
export type InitiatePasswordResetInput = {
locale: string
email: string
}
export type ConfirmResetPasswordInput = {
token: string
password: string
}
export type VerifyHandleAvailabilityInput = {
handle: string
}
export type RevokeAccountSessionInput = {
sub: string
deviceId: string
}
export type RevokeOAuthSessionInput = {
sub: string
tokenId: string
}
export type AcceptInput = {
sub: string
}
export type RejectInput = Record<string, never>
/**
* Represents an account that is currently signed-in to the Authorization
* Server. If the session was created too long ago, the user may be required to
* re-authenticate ({@link ActiveDeviceSession.loginRequired}).
*/
export type ActiveDeviceSession = {
account: Account
/**
* The session is too old and the user must re-authenticate.
*/
loginRequired: boolean
}
/**
* Represents another device on which an account is currently signed-in.
*/
export type ActiveAccountSession = {
deviceId: string
deviceMetadata: DeviceMetadata
isCurrentDevice: boolean
}
/**
* Represents an active OAuth session (access token).
*/
export type ActiveOAuthSession = {
tokenId: string
createdAt: ISODateString
updatedAt: ISODateString
clientId: string
/** An "undefined" value means that the client metadata could not be fetched */
clientMetadata?: OAuthClientMetadata
scope?: string
}

View File

@ -1,64 +0,0 @@
import type { Account } from './types.js'
// These are the endpoints implemented by the OAuth provider, for it's UI to
// call.
export type ApiEndpoints = {
'/verify-handle-availability': {
input: VerifyHandleAvailabilityData
output: { available: true }
}
'/sign-up': {
input: SignUpData
output: {
account: Account
consentRequired: boolean
}
}
'/sign-in': {
input: SignInData
output: {
account: Account
consentRequired: boolean
}
}
'/reset-password-request': {
input: InitiatePasswordResetData
output: { success: true }
}
'/reset-password-confirm': {
input: ConfirmResetPasswordData
output: { success: true }
}
}
export type SignInData = {
locale: string
username: string
password: string
emailOtp?: string
remember?: boolean
}
export type SignUpData = {
locale: string
handle: string
email: string
password: string
inviteCode?: string
hcaptchaToken?: string
}
export type InitiatePasswordResetData = {
locale: string
email: string
}
export type ConfirmResetPasswordData = {
token: string
password: string
}
export type VerifyHandleAvailabilityData = {
handle: string
}

View File

@ -1,11 +1,8 @@
import type { OAuthClientMetadata } from '@atproto/oauth-types'
import type { LinkDefinition, ScopeDetail, Session } from './types.js'
import type { LinkDefinition } from './types.js'
// These are the types of the variables that are injected into the HTML by the
// backend. They are used to configure the frontend.
export type AvailableLocales = readonly string[]
export type CustomizationData = {
// Functional customization
hcaptchaSiteKey?: string
@ -17,19 +14,3 @@ export type CustomizationData = {
logo?: string
links?: LinkDefinition[]
}
export type ErrorData = {
error: string
error_description: string
}
export type AuthorizeData = {
clientId: string
clientMetadata: OAuthClientMetadata
clientTrusted: boolean
requestUri: string
loginHint?: string
scopeDetails?: ScopeDetail[]
newSessionsRequireConsent: boolean
sessions: Session[]
}

View File

@ -0,0 +1,4 @@
export const CSRF_COOKIE_NAME = 'csrf-token'
export const CSRF_HEADER_NAME = 'x-csrf-token'
export const API_ENDPOINT_PREFIX = '/@atproto/oauth-provider/~api'

View File

@ -1,3 +1,5 @@
export type * from './api.js'
export type * from './api-endpoints.js'
export type * from './backend-types.js'
export type * from './types.js'
export * from './contants.js'

View File

@ -18,9 +18,12 @@ export type Session = {
consentRequired: boolean
}
export type LocalizedString =
| string
| ({ en: string } & Record<string, string | undefined>)
export type MultiLangString = { en: string } & Record<
string,
string | undefined
>
export type LocalizedString = string | MultiLangString
export type LinkDefinition = {
title: LocalizedString
@ -30,5 +33,13 @@ export type LinkDefinition = {
export type ScopeDetail = {
scope: string
description?: string
description?: LocalizedString
}
export type DeviceMetadata = {
userAgent: string | null
ipAddress: string
lastSeenAt: ISODateString
}
export type ISODateString = `${string}T${string}Z`

View File

@ -0,0 +1,3 @@
src/locales/*/*.ts
.swc
dist

View File

@ -0,0 +1,58 @@
{
"format": "po",
"sourceLocale": "en",
"locales": [
"en",
"an",
"ast",
"ca",
"da",
"de",
"el",
"en-GB",
"es",
"eu",
"fi",
"fr",
"ga",
"gl",
"hi",
"hu",
"ia",
"id",
"it",
"ja",
"km",
"ko",
"ne",
"nl",
"pl",
"pt-BR",
"ro",
"ru",
"sv",
"th",
"tr",
"uk",
"vi",
"zh-CN",
"zh-HK",
"zh-TW"
],
"fallbackLocales": {
"default": "en"
},
"catalogs": [
{
"path": "<rootDir>/src/locales/{locale}/messages",
"include": [
"<rootDir>/src"
],
"exclude": [
"**/dist/**",
"**/node_modules/**"
]
}
],
"compileNamespace": "ts"
}

View File

@ -0,0 +1,352 @@
<!doctype html>
<html>
<head>
<meta charset="UTF-8" />
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
<title>Mock - OAuth Provider</title>
</head>
<body>
<div id="root"></div>
<script>
/*
* This file's purpose is to provide a way to develop the UI without
* running a full featured OAuth server. It mocks the server responses and
* provides configuration data to the UI.
*
* This file is not part of the production build.
*
* Start the development server with the following command from the
* oauth-provider root:
*
* ```sh
* pnpm run start:ui
* ```
*
* Then open the browser at http://localhost:5173/
*/
</script>
<style>
:root {
--branding-color-primary: 10 122 255;
--branding-color-primary-contrast: 255 255 255;
--branding-color-primary-hue: 212.57142857142856;
--branding-color-error: 244 11 66;
--branding-color-error-contrast: 255 255 255;
--branding-color-error-hue: 345.83690987124464;
--branding-color-warning: 251 86 7;
--branding-color-warning-contrast: 255 255 255;
--branding-color-warning-hue: 19.426229508196723;
--branding-color-success: 2 195 154;
--branding-color-success-contrast: 0 0 0;
--branding-color-success-hue: 167.2538860103627;
}
</style>
<script type="module">
import { API_ENDPOINT_PREFIX } from '@atproto/oauth-provider-api'
/*
* PDS branding configuration
*/
history.replaceState(history.state, '', '/account')
const devices = new Map([
[
'device1',
{
userAgent:
'Mozilla/5.0 (Macintosh; Intel Mac OS X 10_15_7) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/135.0.0.0 Safari/537.36',
ipAddress: '192.0.0.1',
},
],
[
'device2',
{
userAgent:
'Mozilla/5.0 (Macintosh; Intel Mac OS X 10_15_7) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/135.0.0.0 Safari/537.36',
ipAddress: '192.0.0.1',
},
],
])
const accounts = new Map(
[
{
sub: 'did:plc:3jpt2mvvsumj2r7eqk4gzzjz',
email: 'eric@foobar.com',
email_verified: true,
name: 'Eric',
preferred_username: 'esb.lol',
picture:
'https://cdn.bsky.app/img/avatar/plain/did:plc:3jpt2mvvsumj2r7eqk4gzzjz/bafkreiaexnb3bkzbaxktm5q3l3txyweflh3smcruigesvroqjrqxec4zv4@jpeg',
},
{
sub: 'did:plc:dpajgwmnecpdyjyqzjzm6bnb',
email: 'eric@foobar.com',
email_verified: true,
name: 'Tom Sawyeeeeeeeeeee',
preferred_username: 'test.esb.lol',
picture:
'https://cdn.bsky.app/img/avatar/plain/did:plc:dpajgwmnecpdyjyqzjzm6bnb/bafkreia6dx7fhoi6fxwfpgm7jrxijpqci7ap53wpilkpazojwvqlmgud2m@jpeg',
},
{
sub: 'did:plc:matttmattmattmattmattmat',
email: 'matthieu@foobar.com',
email_verified: true,
name: 'Matthieu',
preferred_username: 'matthieu.bsky.test',
picture: /** @type {sting|undefined} */ (undefined),
},
].map((a) => [a.sub, a]),
)
const clients = new Map(
[
{
client_id: 'https://bsky.app/oauth-client.json',
client_name: 'Bluesky',
client_uri: 'https://bsky.app',
logo_uri: 'https://web-cdn.bsky.app/static/apple-touch-icon.png',
},
].map((c) => [c.client_id, c]),
)
// Unable to load metadata for this client:
clients.set('https://example.com/oauth-client.json', undefined)
const accountDeviceSessions = new Map([
[
'device1',
[
{
sub: 'did:plc:3jpt2mvvsumj2r7eqk4gzzjz',
remember: true,
loginRequired: true,
},
{
sub: 'did:plc:dpajgwmnecpdyjyqzjzm6bnb',
remember: false,
loginRequired: false,
},
],
],
[
'device2',
[
{
sub: 'did:plc:3jpt2mvvsumj2r7eqk4gzzjz',
remember: true,
loginRequired: false,
},
],
],
])
const accountOAuthSessions = new Map([
[
'did:plc:3jpt2mvvsumj2r7eqk4gzzjz',
[
{
tokenId: 'token1',
createdAt: '2023-10-01T00:00:00.000Z',
updatedAt: '2025-10-01T00:00:00.000Z',
clientId: 'https://bsky.app/oauth-client.json',
scope: 'atproto transition:generic transition:chat.bsky',
},
],
],
[
'did:plc:dpajgwmnecpdyjyqzjzm6bnb',
[
{
tokenId: 'token2',
createdAt: '2023-10-01T00:00:00.000Z',
updatedAt: '2023-10-01T00:00:00.000Z',
clientId: 'https://bsky.app/oauth-client.json',
scope: 'atproto transition:generic transition:chat.bsky',
},
{
tokenId: 'token3',
createdAt: '2024-08-01T00:00:00.000Z',
updatedAt: '2025-10-01T00:00:00.000Z',
clientId: 'https://example.com/oauth-client.json',
scope: /** @type {string|undefined} */ (undefined),
},
],
],
])
const currentDeviceId = 'device1' // Simulate that this device is "device1"
async function mockFetch(...args) {
const [input, init] = args
const method = init?.method ?? 'GET'
const url =
typeof input === 'string'
? new URL(input, window.location)
: input instanceof URL
? input
: undefined
if (url) {
console.log(`Fetching: ${method} ${url.pathname}${url.search}`)
switch (`${method} ${url.pathname}`) {
case `POST ${API_ENDPOINT_PREFIX}/sign-up`: {
const {
locale,
handle,
email,
password,
inviteCode,
hcaptchaToken,
} = JSON.parse(init.body)
return jsonResponse({ error: 'Not implemented' }, 400)
}
case `POST ${API_ENDPOINT_PREFIX}/sign-in`: {
const { username, remember } = JSON.parse(init.body)
for (const [sub, account] of accounts) {
if (
account.email === username ||
account.preferred_username === username ||
username === 'a'
) {
accountDeviceSessions.set(
currentDeviceId,
(
accountDeviceSessions
.get(currentDeviceId)
?.filter((s) => s.sub !== sub) ?? []
).concat({ sub, remember, loginRequired: false }),
)
return jsonResponse({ account })
}
}
return jsonResponse({ error: 'Invalid credentials' }, 400)
}
case `GET ${API_ENDPOINT_PREFIX}/device-sessions`:
return jsonResponse(
accountDeviceSessions.get(currentDeviceId)?.map((s) => ({
remembered: s.remember,
loginRequired: s.loginRequired,
account: accounts.get(s.sub),
})) ?? [],
)
case `GET ${API_ENDPOINT_PREFIX}/oauth-sessions`: {
const sub = url.searchParams.get('sub')
return jsonResponse(
accountOAuthSessions.get(sub)?.map((oauthSession) => ({
...oauthSession,
clientMetadata: clients.get(oauthSession.clientId),
})) ?? [],
)
}
case `GET ${API_ENDPOINT_PREFIX}/account-sessions`: {
const sub = url.searchParams.get('sub')
return jsonResponse(
Array.from(
accountDeviceSessions.entries(),
([deviceId, deviceSession]) =>
deviceSession
.filter((s) => s.sub === sub)
.map((s) => ({
deviceId,
deviceMetadata: devices.get(deviceId),
remember: s.remember,
isCurrentDevice: true,
})),
).flat(),
)
}
case `POST ${API_ENDPOINT_PREFIX}/sign-out`: {
const { sub } = JSON.parse(init.body)
accountDeviceSessions.set(
currentDeviceId,
accountDeviceSessions
.get(currentDeviceId)
?.filter((s) => s.sub !== sub) ?? [],
)
return jsonResponse({ success: true })
}
case `POST ${API_ENDPOINT_PREFIX}/revoke-account-session`: {
const { sub, deviceId } = JSON.parse(init.body)
accountDeviceSessions.set(
deviceId,
accountDeviceSessions
.get(deviceId)
?.filter((s) => s.sub !== sub) ?? [],
)
return jsonResponse({ success: true })
}
case `POST ${API_ENDPOINT_PREFIX}/verify-handle-availability`:
return jsonResponse({ available: true })
case `POST ${API_ENDPOINT_PREFIX}/reset-password-request`:
return jsonResponse({ available: true })
case `POST ${API_ENDPOINT_PREFIX}/reset-password-confirm`:
return jsonResponse({ available: true })
}
}
return origFetch.call(this, ...args)
}
function jsonResponse(payload, status = 200) {
console.log('Mock response:', payload)
return new Response(JSON.stringify(payload), {
status,
headers: { 'Content-Type': 'application/json' },
})
}
const origFetch = window.fetch
Object.defineProperty(window, 'fetch', {
writable: true,
configurable: true,
value: mockFetch,
})
window.__customizationData = {
availableUserDomains: ['.bsky.social', '.bsky.team'],
inviteCodeRequired: false,
hcaptchaSiteKey: undefined,
name: 'Bluesky',
links: [
{
title: { en: 'Home', fr: 'Accueil' },
href: 'https://bsky.social/',
rel: 'canonical', // prevents the login page from being indexed by search engines
},
{
title: { en: 'Terms of Service' },
href: 'https://bsky.social/about/support/tos',
rel: 'terms-of-service',
},
{
title: { en: 'Privacy Policy' },
href: 'https://bsky.social/about/support/privacy-policy',
rel: 'privacy-policy',
},
{
title: { en: 'Support' },
href: 'https://blueskyweb.zendesk.com/hc/en-us',
rel: 'help',
},
],
logo: `data:image/svg+xml,${encodeURIComponent('<svg xmlns="http://www.w3.org/2000/svg" fill="none" viewBox="0 0 320 286"><path fill="rgb(10,122,255)" d="M69.364 19.146c36.687 27.806 76.147 84.186 90.636 114.439 14.489-30.253 53.948-86.633 90.636-114.439C277.107-.917 320-16.44 320 32.957c0 9.865-5.603 82.875-8.889 94.729-11.423 41.208-53.045 51.719-90.071 45.357 64.719 11.12 81.182 47.953 45.627 84.785-80 82.874-106.667-44.333-106.667-44.333s-26.667 127.207-106.667 44.333c-35.555-36.832-19.092-73.665 45.627-84.785-37.026 6.362-78.648-4.149-90.071-45.357C5.603 115.832 0 42.822 0 32.957 0-16.44 42.893-.917 69.364 19.147Z" /></svg>')}`,
}
window.__deviceSessions =
accountDeviceSessions.get(currentDeviceId)?.map((s) => ({
remembered: s.remember,
loginRequired: s.loginRequired,
account: accounts.get(s.sub),
})) ?? []
</script>
<script src="./src/account-page.tsx" type="module"></script>
</body>
</html>

View File

@ -0,0 +1,11 @@
<!doctype html>
<html>
<head>
<meta charset="UTF-8" />
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
<title>OAuth mock pages</title>
</head>
<body>
<a href="/account.html">My Account</a>
</body>
</html>

View File

@ -0,0 +1,71 @@
{
"name": "@atproto/oauth-provider-frontend",
"version": "0.0.0",
"license": "MIT",
"homepage": "https://atproto.com",
"repository": {
"type": "git",
"url": "https://github.com/bluesky-social/atproto",
"directory": "packages/oauth/oauth-provider-frontend"
},
"engines": {
"node": ">=18.7.0"
},
"type": "commonjs",
"exports": {
"./bundle-manifest.json": {
"default": "./dist/bundle-manifest.json"
},
"./hydration-data": {
"types": "./src/hydration-data.d.ts"
}
},
"optionalDependencies": {
"@atproto-labs/rollup-plugin-bundle-manifest": "workspace:*",
"@atproto/oauth-provider-api": "workspace:*",
"@atproto/oauth-types": "workspace:*"
},
"devDependencies": {
"@atproto-labs/fetch": "workspace:*",
"@atproto-labs/rollup-plugin-bundle-manifest": "workspace:*",
"@atproto/oauth-provider-api": "workspace:*",
"@atproto/oauth-types": "workspace:*",
"@atproto/syntax": "workspace:*",
"@lingui/cli": "^5.2.0",
"@lingui/core": "^5.2.0",
"@lingui/react": "^5.2.0",
"@lingui/swc-plugin": "^5.4.0",
"@lingui/vite-plugin": "^5.2.0",
"@radix-ui/react-dialog": "^1.1.6",
"@radix-ui/react-icons": "^1.3.2",
"@radix-ui/react-popover": "^1.1.6",
"@radix-ui/react-toast": "^1.2.6",
"@tailwindcss/vite": "^4.1.3",
"@tanstack/react-form": "^1.3.0",
"@tanstack/react-query": "^5.71.10",
"@tanstack/react-router": "^1.115.0",
"@tanstack/react-router-devtools": "^1.115.0",
"@tanstack/router-plugin": "^1.115.0",
"@types/react": "^19.0.10",
"@types/react-dom": "^19.0.4",
"@vitejs/plugin-react-swc": "^3.8.0",
"clsx": "^2.1.1",
"react": "^19.0.0",
"react-dom": "^19.0.0",
"tailwindcss": "^4.0.14",
"ua-parser-js": "^2.0.3",
"vite": "^6.2.0",
"zod": "^3.24.2"
},
"scripts": {
"i18n:extract": "lingui extract --clean",
"i18n:compile": "lingui compile --typescript",
"i18n": "pnpm i18n:extract && pnpm i18n:compile",
"prebuild": "pnpm run i18n",
"build": "vite build -- ignore additional npm args",
"dev:ui": "vite --port 5173",
"dev:src": "vite build --watch",
"dev:catalogs": "pnpm run i18n:extract --debounce 250 --watch > /dev/null",
"dev:messages": "pnpm run i18n:compile --debounce 500 --watch"
}
}

View File

@ -0,0 +1,37 @@
import './style.css'
import '#/locales/setup'
import { i18n } from '@lingui/core'
import { I18nProvider } from '@lingui/react'
import { QueryClient, QueryClientProvider } from '@tanstack/react-query'
import { RouterProvider, createRouter } from '@tanstack/react-router'
import { StrictMode } from 'react'
import { createRoot } from 'react-dom/client'
import { Provider as ToastProvider } from '#/components/Toast'
import { Provider as LocaleProvider } from '#/locales'
import { routeTree } from '#/routeTree.gen'
const qc = new QueryClient()
const router = createRouter({ routeTree })
// Register the router instance for type safety
declare module '@tanstack/react-router' {
interface Register {
router: typeof router
}
}
createRoot(document.getElementById('root')!).render(
<StrictMode>
<I18nProvider i18n={i18n}>
<LocaleProvider>
<QueryClientProvider client={qc}>
<ToastProvider>
<RouterProvider router={router} />
</ToastProvider>
</QueryClientProvider>
</LocaleProvider>
</I18nProvider>
</StrictMode>,
)

View File

@ -0,0 +1,225 @@
import {
API_ENDPOINT_PREFIX,
ApiEndpoints,
CSRF_COOKIE_NAME,
CSRF_HEADER_NAME,
} from '@atproto/oauth-provider-api'
import { readCookie } from '../util/cookies.ts'
import {
JsonClient,
JsonErrorPayload,
JsonErrorResponse,
} from './json-client.ts'
export type { Options } from './json-client.ts'
export class Api extends JsonClient<ApiEndpoints> {
constructor() {
const baseUrl = new URL(API_ENDPOINT_PREFIX, window.origin).toString()
super(baseUrl, () => ({
[CSRF_HEADER_NAME]: readCookie(CSRF_COOKIE_NAME),
}))
}
// Override the parent's parseError method to handle expected error responses
// and transform them into instances of the corresponding error classes.
public static override parseError(
json: unknown,
): undefined | JsonErrorResponse {
// @NOTE Most specific errors first !
if (SecondAuthenticationFactorRequiredError.is(json)) {
return new SecondAuthenticationFactorRequiredError(json)
}
if (InvalidCredentialsError.is(json)) {
return new InvalidCredentialsError(json)
}
if (InvalidInviteCodeError.is(json)) {
return new InvalidInviteCodeError(json)
}
if (HandleUnavailableError.is(json)) {
return new HandleUnavailableError(json)
}
if (EmailTakenError.is(json)) {
return new EmailTakenError(json)
}
if (RequestExpiredError.is(json)) {
return new RequestExpiredError(json)
}
if (UnknownRequestUriError.is(json)) {
return new UnknownRequestUriError(json)
}
if (InvalidRequestError.is(json)) {
return new InvalidRequestError(json)
}
if (AccessDeniedError.is(json)) {
return new AccessDeniedError(json)
}
return super.parseError(json)
}
}
export type AccessDeniedPayload = JsonErrorPayload<'access_denied'>
export class AccessDeniedError<
P extends AccessDeniedPayload = AccessDeniedPayload,
> extends JsonErrorResponse<P> {
constructor(
payload: P,
message = payload.error_description || 'Access denied',
) {
super(payload, message)
}
static is(json: unknown): json is AccessDeniedPayload {
return super.is(json) && json.error === 'access_denied'
}
}
export type InvalidRequestPayload = JsonErrorPayload<'invalid_request'>
export class InvalidRequestError<
P extends InvalidRequestPayload = InvalidRequestPayload,
> extends JsonErrorResponse<P> {
constructor(
payload: P,
message = payload.error_description || 'Invalid request',
) {
super(payload, message)
}
static is(json: unknown): json is InvalidRequestPayload {
return super.is(json) && json.error === 'invalid_request'
}
}
export type InvalidInviteCodePayload = InvalidRequestPayload & {
error_description: `This invite code is invalid.${string}`
}
export class InvalidInviteCodeError<
P extends InvalidInviteCodePayload = InvalidInviteCodePayload,
> extends InvalidRequestError<P> {
constructor(payload: P) {
super(payload)
}
static is(json: unknown): json is InvalidInviteCodePayload {
return (
super.is(json) &&
json.error_description != null &&
json.error_description.startsWith('This invite code is invalid.')
)
}
}
export type RequestExpiredPayload = AccessDeniedPayload & {
error_description: 'This request has expired'
}
export class RequestExpiredError<
P extends RequestExpiredPayload = RequestExpiredPayload,
> extends AccessDeniedError<P> {
static is(json: unknown): json is RequestExpiredPayload {
return (
super.is(json) && json.error_description === 'This request has expired'
)
}
}
export type InvalidCredentialsPayload = InvalidRequestPayload & {
error_description: 'Invalid identifier or password'
}
export class InvalidCredentialsError<
P extends InvalidCredentialsPayload = InvalidCredentialsPayload,
> extends InvalidRequestError<P> {
static is(json: unknown): json is InvalidCredentialsPayload {
return (
super.is(json) &&
json.error_description === 'Invalid identifier or password'
)
}
}
export type UnknownRequestPayload = InvalidRequestPayload & {
error_description: 'Unknown request_uri'
}
export class UnknownRequestUriError<
P extends UnknownRequestPayload = UnknownRequestPayload,
> extends InvalidRequestError<P> {
static is(json: unknown): json is UnknownRequestPayload {
return super.is(json) && json.error_description === 'Unknown request_uri'
}
}
export type EmailTakenPayload = InvalidRequestPayload & {
error_description: 'Email already taken'
}
export class EmailTakenError<
P extends EmailTakenPayload = EmailTakenPayload,
> extends InvalidRequestError<P> {
static is(json: unknown): json is EmailTakenPayload {
return super.is(json) && json.error_description === 'Email already taken'
}
}
export type HandleUnavailablePayload =
JsonErrorPayload<'handle_unavailable'> & {
reason: 'syntax' | 'domain' | 'slur' | 'taken'
}
export class HandleUnavailableError<
P extends HandleUnavailablePayload = HandleUnavailablePayload,
> extends JsonErrorResponse<P> {
constructor(
payload: P,
message = payload.error_description || 'That handle cannot be used',
) {
super(payload, message)
}
get reason() {
return this.payload.reason
}
static is(json: unknown): json is HandleUnavailablePayload {
return (
super.is(json) &&
json.error === 'handle_unavailable' &&
'reason' in json &&
(json.reason === 'syntax' ||
json.reason === 'domain' ||
json.reason === 'slur' ||
json.reason === 'taken')
)
}
}
export type SecondAuthenticationFactorRequiredPayload =
JsonErrorPayload<'second_authentication_factor_required'> & {
type: 'emailOtp'
hint: string
}
export class SecondAuthenticationFactorRequiredError<
P extends
SecondAuthenticationFactorRequiredPayload = SecondAuthenticationFactorRequiredPayload,
> extends JsonErrorResponse<P> {
constructor(
payload: P,
message = payload.error_description ||
`${payload.type} authentication factor required (hint: ${payload.hint})`,
) {
super(payload, message)
}
get type() {
return this.payload.type
}
get hint() {
return this.payload.hint
}
static is(json: unknown): json is SecondAuthenticationFactorRequiredPayload {
return (
super.is(json) &&
json.error === 'second_authentication_factor_required' &&
'type' in json &&
json.type === 'emailOtp' &&
'hint' in json &&
typeof json.hint === 'string'
)
}
}

View File

@ -0,0 +1,10 @@
import { useMemo } from 'react'
import { Api } from '#/api/api'
export type * from '@atproto/oauth-provider-api'
export * from '#/api/api'
export * from '#/api/json-client'
export function useApi() {
return useMemo(() => new Api(), [])
}

View File

@ -0,0 +1,135 @@
// Using a type import to avoid bundling this lib
import type { Json } from '@atproto-labs/fetch'
export { type Json }
type Awaitable<T> = T | PromiseLike<T>
export type Options = {
signal?: AbortSignal
}
export type EndpointPath = `/${string}`
export type EndpointDefinition =
| {
method: 'POST'
input: Json
output: Json | void
}
| {
method: 'GET'
params?: Record<string, string | undefined>
output: Json | void
}
export class JsonClient<
Endpoints extends { [Path: EndpointPath]: EndpointDefinition },
> {
constructor(
protected readonly baseUrl: string,
protected readonly getHeaders: () => Awaitable<
Record<string, string | undefined>
>,
) {}
public async fetch<Path extends EndpointPath & keyof Endpoints>(
method: Endpoints[Path]['method'],
path: Path,
input: Endpoints[Path] extends { method: 'GET' }
? Endpoints[Path]['params']
: Endpoints[Path] extends { method: 'POST' }
? Endpoints[Path]['input']
: undefined,
options?: Options,
): Promise<Endpoints[Path]['output']> {
const url = new URL(`${this.baseUrl}${path}`)
if (method === 'GET') {
if (input) {
for (const [key, value] of Object.entries(input)) {
url.searchParams.set(key, value)
}
}
}
const body = method === 'POST' ? JSON.stringify(input) : undefined
const headers = Object.entries(await this.getHeaders.call(null))
.filter((entry): entry is [string, string] => entry[1] != null)
.map(([k, v]) => [k.toLowerCase(), v] as [string, string])
const response = await fetch(url, {
method,
headers:
body && !headers.some(([k]) => k === 'content-type')
? headers.concat([['content-type', 'application/json']])
: headers,
mode: 'same-origin',
body,
signal: options?.signal,
})
if (response.status === 204) {
return undefined
}
const responseType = response.headers.get('content-type')
if (responseType !== 'application/json') {
await response.body?.cancel()
throw new Error(`Invalid content type "${responseType}"`, {
cause: response,
})
}
const json = await response.json()
if (response.ok) return json as Endpoints[Path]['output']
else throw this.parseError(response, json)
}
protected parseError(response: Response, json: Json): Error {
const Class = this.constructor as typeof JsonClient
const error = Class.parseError(json)
if (error) return error
return new Error('Invalid JSON response', { cause: response })
}
public static parseError(json: unknown): undefined | JsonErrorResponse {
if (JsonErrorResponse.is(json)) {
return new JsonErrorResponse(json)
}
}
}
export type JsonErrorPayload<E extends string = string> = {
error: E
error_description?: string
}
export class JsonErrorResponse<
P extends JsonErrorPayload = JsonErrorPayload,
> extends Error {
constructor(
public readonly payload: P,
message = payload.error_description,
) {
super(message || `Error "${payload.error}"`)
}
get error(): string {
return this.payload.error
}
get description(): string | undefined {
return this.payload.error_description
}
static is(json: unknown): json is JsonErrorPayload {
return (
json != null &&
typeof json === 'object' &&
typeof json['error'] === 'string' &&
(json['error_description'] === undefined ||
typeof json['error_description'] === 'string')
)
}
}

View File

@ -0,0 +1,115 @@
import { msg } from '@lingui/core/macro'
import { useLingui } from '@lingui/react'
import { Trans } from '@lingui/react/macro'
import { DotsHorizontalIcon } from '@radix-ui/react-icons'
import * as Popover from '@radix-ui/react-popover'
import { clsx } from 'clsx'
import { Avatar } from '#/components/Avatar'
import { Button } from '#/components/Button'
import { Link } from '#/components/Link'
import { useCurrentSession } from '#/data/useCurrentSession'
import { useDeviceSessionsQuery } from '#/data/useDeviceSessionsQuery'
import { useSignOutMutation } from '#/data/useSignOutMutation'
import { getAccountName } from '#/util/getAccountName'
import { sanitizeHandle } from '#/util/sanitizeHandle'
export function AccountSelector() {
const { _ } = useLingui()
const { data } = useDeviceSessionsQuery()
const { account: currentAccount } = useCurrentSession()
const { mutate: signOut } = useSignOutMutation()
return (
<Popover.Root>
<Popover.Trigger asChild>
<button
className={clsx([
'flex items-center space-x-2 truncate rounded-lg border py-1 pl-1 pr-3',
'bg-contrast-0 dark:bg-contrast-25 border-contrast-50 dark:border-contrast-100',
'hover:bg-contrast-25 dark:hover:bg-contrast-50 hover:border-contrast-100 dark:hover:border-contrast-200',
])}
aria-label={_(msg`Select an account`)}
style={{ maxWidth: 220 }}
>
<div>
<Avatar
size={36}
src={currentAccount.picture}
displayName={currentAccount.name}
/>
</div>
<div className="flex-1 truncate text-left">
<p className="text-text-default truncate text-sm font-bold leading-tight">
{getAccountName(currentAccount)}
</p>
<p className="text-text-light truncate text-sm leading-tight">
{sanitizeHandle(currentAccount.preferred_username)}
</p>
</div>
<div className="pl-4">
<DotsHorizontalIcon width={20} />
</div>
</button>
</Popover.Trigger>
<Popover.Portal>
<Popover.Content
side="top"
align="end"
className="PopoverContent w-full"
sideOffset={5}
style={{ width: 320 }}
>
<div className="bg-contrast-0 dark:bg-contrast-25 border-contrast-25 dark:border-contrast-50 shadow-contrast-900/15 dark:shadow-contrast-0/60 relative rounded-lg border shadow-xl">
<div className="flex flex-col overflow-hidden rounded-lg">
{data.map(({ account }, i) => (
<Link
key={account.sub}
to="/account/$sub"
params={account}
className={clsx([
'flex items-center space-x-2 py-2 pl-2 pr-4',
'hover:bg-contrast-25 dark:hover:bg-contrast-50 focus:bg-contrast-25 dark:focus:bg-contrast-50',
i !== 0 &&
'border-contrast-25 dark:border-contrast-50 border-t',
])}
>
<Avatar
size={36}
src={account.picture}
displayName={account.name}
/>
<div className="flex-1 space-x-1 truncate text-left">
<p className="text-text-default truncate text-sm font-bold leading-snug">
{getAccountName(account)}
</p>
<p className="text-text-light truncate text-sm leading-snug">
{sanitizeHandle(account.preferred_username)}
</p>
</div>
<div className="flex-shrink">
<Button
size="sm"
color="secondary"
onClick={(e) => {
// technically invalid markup to have a button inside a link :/
// prevent click from bubbling up to the Link
e.stopPropagation()
e.preventDefault()
signOut({ sub: account.sub })
}}
>
<Button.Text>
<Trans>Sign out</Trans>
</Button.Text>
</Button>
</div>
</Link>
))}
</div>
</div>
</Popover.Content>
</Popover.Portal>
</Popover.Root>
)
}

View File

@ -0,0 +1,92 @@
import {
CircleBackslashIcon,
ExclamationTriangleIcon,
InfoCircledIcon,
QuestionMarkCircledIcon,
} from '@radix-ui/react-icons'
import { clsx } from 'clsx'
import { ReactNode } from 'react'
type Variant = 'tip' | 'info' | 'warning' | 'error'
const icons: Record<Variant, typeof QuestionMarkCircledIcon> = {
tip: QuestionMarkCircledIcon,
info: InfoCircledIcon,
warning: ExclamationTriangleIcon,
error: CircleBackslashIcon,
}
const borderColors: Record<Variant, string> = {
tip: '',
info: '',
warning: 'border-warning-500 dark:border-warning-700',
error: 'border-error-500 dark:border-error-400',
}
const iconColors: Record<Variant, string> = {
tip: 'text-success-600 dark:text-success-500',
info: 'text-primary-500',
warning: 'text-warning-600 dark:text-warning-500',
error: 'text-error-500 dark:text-error-400',
}
export function Card({
children,
variant,
}: {
children: ReactNode
variant?: Variant
}) {
const borderColor = variant ? borderColors[variant] : ''
return (
<div
className={clsx([
'border-contrast-25 dark:border-contrast-50 shadow-contrast-500/20 dark:shadow-contrast-0/50 flex items-start space-x-3 rounded-md border py-3 pl-3 pr-4 shadow-lg',
borderColor,
])}
>
{children}
</div>
)
}
export function Icon({ variant }: { variant?: Variant }) {
const Icon = variant ? icons[variant] : icons.info
const color = variant ? iconColors[variant] : iconColors.info
return (
<div className={clsx(['pt-0.5', color])}>
<Icon width={20} height={20} />
</div>
)
}
export function Content({ children }: { children: ReactNode }) {
return <div className="flex-grow space-y-1">{children}</div>
}
export function Title({ children }: { children: ReactNode }) {
return <h3 className="text-lg font-semibold leading-snug">{children}</h3>
}
export function Text({ children }: { children: ReactNode }) {
return <p className="text-md text-text-light">{children}</p>
}
export function Default({
variant,
title,
text,
}: {
variant?: Variant
title?: string
text: string
}) {
return (
<Card variant={variant}>
<Icon variant={variant} />
<Content>
{title && <Title>{title}</Title>}
<Text>{text}</Text>
</Content>
</Card>
)
}

View File

@ -0,0 +1,41 @@
import { msg } from '@lingui/core/macro'
import { useLingui } from '@lingui/react'
import { PersonIcon } from '@radix-ui/react-icons'
export function Avatar({
src,
size = 40,
displayName,
}: {
src: string | undefined
size?: number
displayName: string | undefined
}) {
const { _ } = useLingui()
return (
<div
className="align-center relative flex justify-center overflow-hidden rounded-full"
style={{
width: size,
height: size,
}}
>
{src ? (
<img src={src} alt={_(msg`User avatar`)} className="absolute inset-0" />
) : displayName ? (
<p
className="absolute uppercase"
style={{
fontSize: size / 2 + 'px',
lineHeight: size + 'px',
}}
>
{displayName.replace(/[^A-z0-0]/g, '').slice(0, 1)}
</p>
) : (
<PersonIcon width={size * (1 / 2)} />
)}
<div className="border-contrast-100 absolute inset-0 rounded-full border-2" />
</div>
)
}

View File

@ -0,0 +1,68 @@
import { clsx } from 'clsx'
import { useMemo } from 'react'
type ButtonVariantProps = {
color?: 'primary' | 'secondary'
size?: 'sm' | 'md' | 'lg'
}
type ButtonStateProps = {
disabled?: boolean
}
export function Button({
children,
color = 'primary',
size = 'md',
...rest
}: React.ButtonHTMLAttributes<HTMLButtonElement> &
ButtonVariantProps &
ButtonStateProps & {
children: React.ReactNode
}) {
const cn = useButtonStyles({ color, size, disabled: rest.disabled })
return (
<button
{...rest}
aria-disabled={rest.disabled ? 'true' : 'false'}
className={clsx(cn, rest.className)}
>
{children}
</button>
)
}
function Text({ children }: { children: React.ReactNode }) {
return <span>{children}</span>
}
Button.Text = Text
export type ButtonStyleProps = ButtonVariantProps & ButtonStateProps
export function useButtonStyles({ color, size, disabled }: ButtonStyleProps) {
return useMemo(() => {
return clsx([
'flex-1 flex items-center justify-center text-center rounded-md font-medium',
size === 'sm' && ['px-3 h-7 space-x-1', 'text-sm'],
size === 'md' && ['px-5 h-10 space-x-1', 'text-md'],
size === 'lg' && ['px-6 h-12 space-x-2', 'text-md'],
color === 'primary' && [
disabled
? ['bg-primary-400 text-primary-100', 'cursor-not-allowed']
: [
'bg-primary text-primary-contrast',
'focus:outline-none focus:shadow-sm focus:shadow-primary-700/30',
],
],
color === 'secondary' && [
disabled
? ['bg-contrast-300 text-white/50', 'cursor-not-allowed']
: [
'bg-contrast-500 dark:bg-contrast-300 text-white',
'hover:bg-contrast-600 focus:bg-contrast-600 dark:hover:bg-contrast-400 dark:focus:bg-contrast-400',
'focus:outline-none focus:shadow-sm focus:shadow-contrast-700/30',
],
],
])
}, [])
}

View File

@ -0,0 +1,25 @@
import { clsx } from 'clsx'
import { ReactNode } from 'react'
export function ContentCard({
children,
size = 'narrow',
}: {
children: ReactNode
size?: 'full' | 'narrow'
}) {
const maxWidth = size === 'full' ? 600 : 400
return (
<div
className={clsx([
'mx-auto rounded-lg border p-5 shadow-xl md:p-7 dark:shadow-2xl',
'border-contrast-25 dark:border-contrast-50 shadow-contrast-500/20 dark:shadow-contrast-0/50',
])}
style={{
maxWidth,
}}
>
{children}
</div>
)
}

View File

@ -0,0 +1,56 @@
import * as Dialog from '@radix-ui/react-dialog'
import { Cross2Icon } from '@radix-ui/react-icons'
import { clsx } from 'clsx'
import { AriaRole, ReactNode } from 'react'
export const Root = Dialog.Root
export const Trigger = Dialog.Trigger
export const Title = Dialog.Title
export const Description = Dialog.Description
export function Outer({ children }: { children: ReactNode }) {
return (
<Dialog.Portal>
<Dialog.Overlay className="DialogOverlay bg-contrast-900/30 dark:bg-contrast-0/60 fixed inset-0" />
{children}
</Dialog.Portal>
)
}
export function Inner({
children,
role,
className,
}: {
children: ReactNode
role?: AriaRole
className?: string
}) {
return (
<Dialog.Content
role={role}
className={clsx([
'DialogContent',
'max-w-[600px] rounded-xl p-5 shadow-xl',
'bg-contrast-0 dark:bg-contrast-25 shadow-contrast-975/15 dark:shadow-contrast-0/60',
className,
])}
>
{children}
</Dialog.Content>
)
}
export function Close() {
return (
<Dialog.Close
className={clsx([
'absolute right-3 top-3 flex h-8 w-8 items-center justify-center rounded-full border transition-colors focus:outline-0',
'bg-contrast-0 dark:bg-contrast-25 border-contrast-25 dark:border-contrast-50',
'hover:bg-contrast-25 dark:hover:bg-contrast-50 hover:border-contrast-50 dark:hover:border-contrast-100',
])}
>
<Cross2Icon className="text-text-light hover:text-text-default focus:text-text-default" />
</Dialog.Close>
)
}

View File

@ -0,0 +1,3 @@
export function Divider() {
return <div className="border-border-default w-full border-t" />
}

View File

@ -0,0 +1,30 @@
import { Trans } from '@lingui/react/macro'
import { ErrorComponentProps } from '@tanstack/react-router'
export function ErrorScreen({
title,
description,
}: {
title?: string
description: string
}) {
return (
<main className="bg-contrast-25 min-h-screen px-4 pt-16 md:px-6">
<div
className="mx-auto w-full"
style={{ maxWidth: 600, minHeight: '100vh' }}
>
<div role="alert">
<h1 className="text-3xl font-bold">
{title || <Trans>Whoops! An error occurred.</Trans>}
</h1>
<p>{description}</p>
</div>
</div>
</main>
)
}
export function RouterErrorComponent({ error }: ErrorComponentProps) {
return <ErrorScreen description={error.message} />
}

View File

@ -0,0 +1,35 @@
import { InlineLink } from '#/components/Link'
import { LocaleSelector } from '#/components/LocaleSelector'
import { useCustomizationData } from '#/data/useCustomizationData'
import { useLocale } from '#/locales'
import { Locale, locales } from '#/locales/locales'
export function Footer() {
const { locale, setLocale, localizeString } = useLocale()
const { links } = useCustomizationData()
return (
<footer className="h-15 bg-contrast-25 dark:bg-contrast-50 fixed inset-x-0 bottom-0 flex items-center justify-between px-4 md:px-6">
<div className="flex flex-wrap">
{links?.map((link) => (
<InlineLink
href={link.href}
className="text-text-light mr-4 text-sm"
key={link.href}
>
{localizeString(link.title)}
</InlineLink>
))}
</div>
<LocaleSelector
items={Object.entries(locales).map(([code, l]) => ({
label: l.flag + ' ' + l.name,
value: code,
}))}
value={locale}
onSelect={(value) => setLocale(value as Locale)}
/>
</footer>
)
}

View File

@ -0,0 +1,24 @@
import { clsx } from 'clsx'
export function Outer({ children }: { children: React.ReactNode }) {
return <main className="px-4 md:px-6">{children}</main>
}
export function Center({
children,
className,
style = {},
}: {
children: React.ReactNode
className?: string
style?: React.CSSProperties
}) {
return (
<div
className={clsx(['mx-auto w-full py-10', className])}
style={{ maxWidth: 600, ...style }}
>
{children}
</div>
)
}

View File

@ -0,0 +1,61 @@
import { Link as RouterLink, LinkComponentProps } from '@tanstack/react-router'
import { clsx } from 'clsx'
import { ButtonStyleProps, useButtonStyles } from '#/components/Button'
export type LinkProps = LinkComponentProps & {
label?: string
}
export function Link({ children, label, ...rest }: LinkProps) {
return (
<RouterLink {...rest} aria-label={label || rest.href || rest.to}>
{children}
</RouterLink>
)
}
export function InlineLink({ children, className, ...rest }: LinkProps) {
return (
<Link
{...rest}
className={clsx([
'text-primary-500',
'hover:underline',
'focus:underline focus:outline-none',
className,
])}
>
{children}
</Link>
)
}
export function staticClick(
onClick: (e: React.MouseEvent) => void,
): Partial<LinkComponentProps> {
return {
to: '.',
onClick(e) {
e.preventDefault()
onClick(e)
},
}
}
Link.staticClick = staticClick
InlineLink.staticClick = staticClick
export function ButtonLink({
children,
color = 'primary',
size = 'md',
disabled,
...rest
}: LinkProps & ButtonStyleProps) {
const cn = useButtonStyles({ color, size, disabled })
return (
<Link {...rest} className={cn}>
{children}
</Link>
)
}

View File

@ -0,0 +1,35 @@
const sizes = {
sm: 20,
md: 28,
lg: 36,
}
export function Loader({
fill = 'var(--color-primary)',
size: sizeName = 'md',
width,
}: {
fill?: string
size?: 'sm' | 'md' | 'lg'
width?: number
}) {
const size = sizes[sizeName] || width
return (
<div
className="align-center relative justify-center"
style={{ width: size, height: size }}
>
<div className="loader-animation">
<svg fill="none" viewBox="0 0 24 24" width={size} height={size}>
<path
fill={fill || 'var(--color-primary)'}
fillRule="evenodd"
clipRule="evenodd"
d="M12 5a7 7 0 0 0-5.218 11.666A1 1 0 0 1 5.292 18a9 9 0 1 1 13.416 0 1 1 0 1 1-1.49-1.334A7 7 0 0 0 12 5Z"
/>
</svg>
</div>
</div>
)
}

View File

@ -0,0 +1,37 @@
import { ChevronDownIcon } from '@radix-ui/react-icons'
import { clsx } from 'clsx'
export function LocaleSelector({
items,
value,
onSelect,
}: {
items: {
label: string
value: string
}[]
value: string
onSelect: (value: string) => void
}) {
return (
<div className="relative">
<select
className={clsx([
'bg-contrast-25 text-text-default border-contrast-100 cursor-pointer rounded-full border py-1.5 pl-2 pr-8 text-sm font-semibold focus:shadow-sm',
'hover:bg-contrast-0 focus:bg-contrast-0 dark:hover:bg-contrast-0',
'focus:bg-contrast-0 dark:focus:bg-contrast-0 focus:outline-none',
])}
onChange={(e) => onSelect(e.target.value)}
value={value}
>
{items.map((item) => (
<option key={item.value} value={item.value}>
{item.label}
</option>
))}
</select>
<ChevronDownIcon className="pointer-events-none absolute bottom-0 right-2 top-0 my-auto h-5 w-5" />
</div>
)
}

View File

@ -0,0 +1,33 @@
import { useLingui } from '@lingui/react/macro'
import { AccountSelector } from '#/components/AccountSelector'
import { Link } from '#/components/Link'
import { useCustomizationData } from '#/data/useCustomizationData'
export function Nav() {
const { t } = useLingui()
const { logo, name } = useCustomizationData()
return (
<>
<nav className="bg-contrast-0 dark:bg-contrast-25 border-contrast-100 h-15 fixed inset-x-0 top-0 flex items-center justify-between border-b px-4 md:px-6">
{logo ? (
<Link to="/account">
<div style={{ width: 120, height: 30 }}>
<img
src={logo}
alt={name || t`Logo`}
className="h-full w-full object-contain object-left"
/>
</div>
</Link>
) : (
<div />
)}
<AccountSelector />
</nav>
{/* Spacer */}
<div className="h-15" />
</>
)
}

View File

@ -0,0 +1,73 @@
import { msg } from '@lingui/core/macro'
import { useLingui } from '@lingui/react'
import { useState } from 'react'
import { Button } from '#/components/Button'
import * as Dialog from '#/components/Dialog'
export function Prompt({
children,
title,
description,
confirmCTA,
cancelCTA,
onConfirm,
onCancel,
}: {
children: React.ReactNode
title: string
description?: string
confirmCTA?: string
cancelCTA?: string
onConfirm?: () => void
onCancel?: () => void
}) {
const { _ } = useLingui()
const [open, setOpen] = useState(false)
const handleOnConfirm = () => {
setOpen(false)
onConfirm?.()
}
const handleOnCancel = () => {
setOpen(false)
onCancel?.()
}
return (
<Dialog.Root open={open} onOpenChange={setOpen}>
<Dialog.Trigger asChild>{children}</Dialog.Trigger>
<Dialog.Outer>
<Dialog.Inner role="alertdialog" className="max-w-[400px]!">
<Dialog.Title className="text-xl font-semibold leading-snug">
{title}
</Dialog.Title>
{description && (
<Dialog.Description className="text-text-light pt-1 leading-snug">
{description}
</Dialog.Description>
)}
<div className="flex flex-wrap-reverse items-center gap-2 pt-4">
<Button
className="w-full min-w-[150px]"
color="secondary"
onClick={handleOnCancel}
>
{cancelCTA || _(msg`Cancel`)}
</Button>
{confirmCTA && (
<Button
className="w-full min-w-[150px]"
color="primary"
onClick={handleOnConfirm}
>
{confirmCTA}
</Button>
)}
</div>
<Dialog.Close />
</Dialog.Inner>
</Dialog.Outer>
</Dialog.Root>
)
}

View File

@ -0,0 +1,132 @@
import { Cross2Icon } from '@radix-ui/react-icons'
import * as ToastBase from '@radix-ui/react-toast'
import { clsx } from 'clsx'
import {
ReactNode,
createContext,
useCallback,
useContext,
useMemo,
useState,
} from 'react'
type Variant = 'success' | 'warning' | 'error'
type Toast = {
id: string
variant: Variant
title: string
duration?: number
dissmissable?: boolean
}
type Context = {
show(toast: Omit<Toast, 'id'>): void
}
const Context = createContext<Context>({
show: () => {},
})
const borderColors: Record<Variant, string> = {
success: 'border-success-200 dark:border-success-900',
warning: 'border-warning-200 dark:border-warning-900',
error: 'border-error-200 dark:border-error-900',
}
const bgColors: Record<Variant, string> = {
success: 'bg-success-50 dark:bg-success-975',
warning: 'bg-warning-100 dark:bg-warning-975',
error: 'bg-error-50 dark:bg-error-975',
}
const titleColors: Record<Variant, string> = {
success: 'text-success-900 dark:text-success-400',
warning: 'text-warning-800 dark:text-warning-400',
error: 'text-error-800 dark:text-error-50',
}
const getRandomId = () => {
const randomId = Math.random().toString(36).substring(2, 8)
return randomId
}
export function Provider({ children }: { children: ReactNode }) {
const [toasts, setToasts] = useState<Toast[]>([])
const show = useCallback<Context['show']>(
(toast) => {
setToasts((prev) => [
...prev,
{
...toast,
id: getRandomId(),
},
])
},
[setToasts],
)
const ctx = useMemo(
() => ({
show,
}),
[show],
)
return (
<ToastBase.Provider swipeDirection="up">
<Context.Provider value={ctx}>
{children}
{toasts.map((toast) => (
<ToastBase.Root
key={toast.id}
duration={toast.duration}
onOpenChange={(open) => {
if (!open) {
setTimeout(() => {
setToasts((prev) => prev.filter((t) => t.id !== toast.id))
}, 1e3)
}
}}
className={clsx(['ToastRoot', 'py-1'])}
>
<div
className={clsx([
'relative rounded-full border py-3 pl-6 pr-8 shadow-lg',
'shadow-contrast-900/15 dark:shadow-contrast-0/60',
borderColors[toast.variant],
bgColors[toast.variant],
])}
>
<ToastBase.Title
className={clsx([
'text-sm font-semibold leading-snug',
titleColors[toast.variant],
])}
>
{toast.title}
</ToastBase.Title>
{toast.dissmissable && (
<ToastBase.Close
className={clsx([
'absolute bottom-0 right-2 top-0 my-auto flex h-6 w-6 items-center justify-center rounded-full border transition-colors focus:outline-0',
'border-contrast-975/30',
'hover:bg-contrast-975/30 hover:border-contrast-975/60',
])}
>
<Cross2Icon className="text-text-light hover:text-text-default focus:text-text-default" />
</ToastBase.Close>
)}
</div>
</ToastBase.Root>
))}
</Context.Provider>
<ToastBase.Viewport className="fixed left-6 right-6 top-0 mx-auto max-w-[400px] pt-8" />
</ToastBase.Provider>
)
}
export function useToast() {
return useContext(Context)
}

View File

@ -0,0 +1,41 @@
import { clsx } from 'clsx'
export type CheckboxProps = React.InputHTMLAttributes<HTMLInputElement> & {
name: string
value: string
invalid?: boolean
disabled?: boolean
}
export function Checkbox({ disabled, invalid, ...rest }: CheckboxProps) {
return (
<input
id={`checkbox-${rest.name}`}
type="checkbox"
{...rest}
className={clsx([
'block h-5 w-5 rounded-md border-2 focus:shadow-sm focus:outline-none',
'border-contrast-200 focus:border-primary-500 focus:bg-contrast-25 dark:focus:bg-contrast-50 focus:shadow-primary-600/30',
invalid &&
'border-error-300 text-error-900 placeholder-error-300 focus:border-error-500',
disabled && 'bg-contrast-50 text-contrast-500 cursor-not-allowed',
])}
/>
)
}
function Label({
children,
name,
}: {
children: React.ReactNode
name: string
}) {
return (
<label htmlFor={`checkbox-${name}`} className="text-sm">
{children}
</label>
)
}
Checkbox.Label = Label

View File

@ -0,0 +1,25 @@
import { StandardSchemaV1Issue } from '@tanstack/react-form'
import { ReactNode } from 'react'
export function Errors({
errors,
}: {
errors: (StandardSchemaV1Issue | undefined)[]
}) {
if (errors.length === 0) return null
return (
<ul className="space-y-1">
{(errors.filter(Boolean) as StandardSchemaV1Issue[]).map((error, i) => (
<Error key={i}>{error.message}</Error>
))}
</ul>
)
}
export function Error({ children }: { children: ReactNode }) {
return (
<li className="text-error-900 dark:text-error-25 bg-error-100 dark:bg-error-800 space-y-1 rounded-md px-2 py-1 text-sm">
{children}
</li>
)
}

View File

@ -0,0 +1,16 @@
import { ReactNode } from 'react'
export function Fieldset({
children,
label,
}: {
children: ReactNode
label: string
}) {
return (
<fieldset className="space-y-3">
<legend className="hidden">{label}</legend>
{children}
</fieldset>
)
}

View File

@ -0,0 +1,12 @@
import { clsx } from 'clsx'
import { ReactNode } from 'react'
export function Item({
children,
className,
}: {
children: ReactNode
className?: string
}) {
return <div className={clsx('space-y-2', className)}>{children}</div>
}

View File

@ -0,0 +1,24 @@
import { clsx } from 'clsx'
import { ReactNode } from 'react'
export function Label({
children,
name,
hidden,
}: {
children: ReactNode
name: string
hidden?: boolean
}) {
return (
<label
htmlFor={`field-${name}`}
className={clsx([
'text-text-light block text-sm font-medium',
hidden && 'sr-only',
])}
>
{children}
</label>
)
}

View File

@ -0,0 +1,26 @@
import { clsx } from 'clsx'
import { InputHTMLAttributes } from 'react'
export type TextProps = InputHTMLAttributes<HTMLInputElement> & {
name: string
value: string
invalid?: boolean
disabled?: boolean
}
export function Text({ disabled, invalid, ...rest }: TextProps) {
return (
<input
{...rest}
id={`field-${rest.name}`}
disabled={disabled}
className={clsx([
'text-md block w-full rounded-md border-2 px-4 py-2.5 focus:shadow-sm focus:outline-none',
'border-contrast-200 focus:border-primary focus:bg-contrast-25 dark:focus:bg-contrast-50 focus:shadow-primary-600/30',
invalid &&
'border-error focus:border-error text-error placeholder-error focus:text-inherit',
disabled && 'bg-contrast-50 text-contrast-500 cursor-not-allowed',
])}
/>
)
}

View File

@ -0,0 +1,6 @@
export * from '#/components/forms/Fieldset'
export * from '#/components/forms/Item'
export * from '#/components/forms/Label'
export * from '#/components/forms/Text'
export * from '#/components/forms/Errors'
export * from '#/components/forms/Checkbox'

View File

@ -0,0 +1,94 @@
export function Palette() {
return (
<>
<div className="flex items-center justify-center">
<div className="bg-contrast-0 p-10" />
<div className="bg-contrast-25 p-10" />
<div className="bg-contrast-50 p-10" />
<div className="bg-contrast-100 p-10" />
<div className="bg-contrast-200 p-10" />
<div className="bg-contrast-300 p-10" />
<div className="bg-contrast-400 p-10" />
<div className="bg-contrast-500 p-10" />
<div className="bg-contrast-600 p-10" />
<div className="bg-contrast-700 p-10" />
<div className="bg-contrast-800 p-10" />
<div className="bg-contrast-900 p-10" />
<div className="bg-contrast-950 p-10" />
<div className="bg-contrast-975 p-10" />
<div className="bg-contrast-1000 p-10" />
</div>
<div className="flex items-center justify-center">
<div className="bg-primary-25 p-10" />
<div className="bg-primary-50 p-10" />
<div className="bg-primary-100 p-10" />
<div className="bg-primary-200 p-10" />
<div className="bg-primary-300 p-10" />
<div className="bg-primary-400 p-10" />
<div className="bg-primary-500 p-10" />
<div className="bg-primary-600 p-10" />
<div className="bg-primary-700 p-10" />
<div className="bg-primary-800 p-10" />
<div className="bg-primary-900 p-10" />
<div className="bg-primary-950 p-10" />
<div className="bg-primary-975 p-10" />
</div>
<div className="flex items-center justify-center">
<div className="bg-error-25 p-10" />
<div className="bg-error-50 p-10" />
<div className="bg-error-100 p-10" />
<div className="bg-error-200 p-10" />
<div className="bg-error-300 p-10" />
<div className="bg-error-400 p-10" />
<div className="bg-error-500 p-10" />
<div className="bg-error-600 p-10" />
<div className="bg-error-700 p-10" />
<div className="bg-error-800 p-10" />
<div className="bg-error-900 p-10" />
<div className="bg-error-950 p-10" />
<div className="bg-error-975 p-10" />
</div>
<div className="flex items-center justify-center">
<div className="bg-warning-25 p-10" />
<div className="bg-warning-50 p-10" />
<div className="bg-warning-100 p-10" />
<div className="bg-warning-200 p-10" />
<div className="bg-warning-300 p-10" />
<div className="bg-warning-400 p-10" />
<div className="bg-warning-500 p-10" />
<div className="bg-warning-600 p-10" />
<div className="bg-warning-700 p-10" />
<div className="bg-warning-800 p-10" />
<div className="bg-warning-900 p-10" />
<div className="bg-warning-950 p-10" />
<div className="bg-warning-975 p-10" />
</div>
<div className="flex items-center justify-center">
<div className="bg-success-25 p-10" />
<div className="bg-success-50 p-10" />
<div className="bg-success-100 p-10" />
<div className="bg-success-200 p-10" />
<div className="bg-success-300 p-10" />
<div className="bg-success-400 p-10" />
<div className="bg-success-500 p-10" />
<div className="bg-success-600 p-10" />
<div className="bg-success-700 p-10" />
<div className="bg-success-800 p-10" />
<div className="bg-success-900 p-10" />
<div className="bg-success-950 p-10" />
<div className="bg-success-975 p-10" />
</div>
<div className="flex items-center justify-center">
<div className="bg-primary text-primary-contrast p-10">Az</div>
<div className="bg-error text-error-contrast p-10">Az</div>
<div className="bg-warning text-warning-contrast p-10">Az</div>
<div className="bg-success text-success-contrast p-10">Az</div>
</div>
</>
)
}

View File

@ -0,0 +1,23 @@
import { useQuery } from '@tanstack/react-query'
import { ActiveAccountSession, useApi } from '#/api'
export type UseAccountSessionsQueryInput = {
sub: string
}
export const accountSessionsQueryKey = ({
sub,
}: UseAccountSessionsQueryInput) => ['account-sessions', sub] as const
export function useAccountSessionsQuery(input: UseAccountSessionsQueryInput) {
const api = useApi()
return useQuery<ActiveAccountSession[]>({
refetchOnWindowFocus: 'always',
staleTime: 15e3, // 15s
queryKey: accountSessionsQueryKey(input),
queryFn: async (options) => {
return api.fetch('GET', '/account-sessions', input, options)
},
})
}

View File

@ -0,0 +1,44 @@
import { useLingui } from '@lingui/react/macro'
import type { OAuthClientMetadata } from '@atproto/oauth-types'
export function useClientName({
clientId,
clientMetadata,
clientTrusted = false,
}: {
clientId: string
clientMetadata?: OAuthClientMetadata
clientTrusted?: boolean
}): string {
const { t } = useLingui()
if (clientTrusted && clientMetadata?.client_name) {
return clientMetadata.client_name
}
// @NOTE: not using isOAuthClientIdLoopback & isOAuthClientIdDiscoverable from
// @atproto/oauth-types here because 1) we don't need to validate here and 2)
// we prefer not to import un-necessary code to improve bundle size.
if (clientId.startsWith('http://')) {
return t`A local app`
}
if (clientId.startsWith('https://')) {
try {
const url = new URL(clientId)
if (
url.protocol === 'https:' &&
url.pathname === '/oauth-client-metadata.json' &&
!url.port &&
!url.search
) {
return url.hostname
}
} catch {
// ignore
}
}
return clientId
}

View File

@ -0,0 +1,16 @@
import { useDeviceSessionsQuery } from '#/data/useDeviceSessionsQuery'
import { Route as AccountRoute } from '#/routes/account/_appLayout/$sub'
export function useCurrentSession() {
const { data: sessions } = useDeviceSessionsQuery()
const { sub } = AccountRoute.useParams()
const current = sessions?.find(({ account }) => account.sub === sub)
if (!current) {
throw new Error(
`No current account available. Are you sure you're using this hook in the right context?`,
)
}
return current
}

View File

@ -0,0 +1,5 @@
import { useHydrationData } from './useHydrationData'
export function useCustomizationData() {
return useHydrationData('__customizationData')
}

View File

@ -0,0 +1,46 @@
import { useQuery, useQueryClient } from '@tanstack/react-query'
import { useCallback } from 'react'
import { ActiveDeviceSession, useApi } from '#/api'
import { upsert } from '#/util/upsert'
import { useHydrationData } from './useHydrationData'
export const useDeviceSessionsQueryKey = ['device-sessions'] as const
export type UseAccountsQueryResponse = ActiveDeviceSession[]
/**
* All accounts logged in on _this device_.
*/
export function useDeviceSessionsQuery() {
const api = useApi()
const initialData = useHydrationData('__deviceSessions')
return useQuery<ActiveDeviceSession[]>({
initialData: [...initialData],
refetchOnWindowFocus: 'always',
staleTime: 15e3, // 15s
queryKey: useDeviceSessionsQueryKey,
queryFn: async (options) => {
return api.fetch('GET', '/device-sessions', undefined, options)
},
})
}
export function useUpsertDeviceAccount() {
const qc = useQueryClient()
return useCallback(
(newSession: ActiveDeviceSession) => {
return qc.setQueryData<ActiveDeviceSession[]>(
useDeviceSessionsQueryKey,
(data) =>
upsert(
data,
newSession,
(a) => a.account.sub === newSession.account.sub,
),
)
},
[qc, ...useDeviceSessionsQueryKey],
)
}

View File

@ -0,0 +1,31 @@
import { useLingui } from '@lingui/react/macro'
export function useFriendlyClientId({
clientId,
clientTrusted = false,
}: {
clientId: string
clientTrusted?: boolean
}): string {
const { t } = useLingui()
if (clientId.startsWith('http://')) {
return t`loopback`
}
if (clientId.startsWith('https://')) {
try {
const url = new URL(clientId)
if (clientTrusted) {
return url.hostname
}
if (url.pathname === '/oauth-client-metadata.json' && !url.port) {
return url.hostname
}
} catch {
// ignore
}
}
return clientId
}

View File

@ -0,0 +1,6 @@
import { useDeviceSessionsQuery } from '#/data/useDeviceSessionsQuery'
export function useHasAccounts() {
const { data: accounts } = useDeviceSessionsQuery()
return accounts?.length > 0
}

View File

@ -0,0 +1,9 @@
import type { HydrationData } from '../hydration-data.d.ts'
const hydrationData = window as typeof window & HydrationData['account-page']
export function useHydrationData<T extends keyof HydrationData['account-page']>(
key: T,
): HydrationData['account-page'][T] {
return hydrationData[key]
}

View File

@ -0,0 +1,21 @@
import { useQuery } from '@tanstack/react-query'
import { ActiveOAuthSession, useApi } from '#/api'
export type OAuthSessionsQueryInput = {
sub: string
}
export const oauthSessionsQueryKey = ({ sub }: OAuthSessionsQueryInput) =>
['oauth-sessions', sub] as const
export function useOAuthSessionsQuery(input: OAuthSessionsQueryInput) {
const api = useApi()
return useQuery<ActiveOAuthSession[]>({
refetchOnWindowFocus: 'always',
staleTime: 15e3, // 15s
queryKey: oauthSessionsQueryKey(input),
queryFn: async (options) => {
return await api.fetch('GET', '/oauth-sessions', input, options)
},
})
}

View File

@ -0,0 +1,14 @@
import { useMutation } from '@tanstack/react-query'
import { ConfirmResetPasswordInput, useApi } from '#/api'
export type PasswordConfirmMutationInput = ConfirmResetPasswordInput
export function usePasswordConfirmMutation() {
const api = useApi()
return useMutation({
async mutationFn(input: ConfirmResetPasswordInput) {
await api.fetch('POST', '/reset-password-confirm', input)
},
})
}

View File

@ -0,0 +1,19 @@
import { useMutation } from '@tanstack/react-query'
import { InitiatePasswordResetInput, useApi } from '#/api'
import { useLocale } from '#/locales'
export type PasswordResetMutationInput = Omit<
InitiatePasswordResetInput,
'locale'
>
export function usePasswordResetMutation() {
const api = useApi()
const { locale } = useLocale()
return useMutation({
async mutationFn(input: PasswordResetMutationInput) {
await api.fetch('POST', '/reset-password-request', { ...input, locale })
},
})
}

View File

@ -0,0 +1,19 @@
import { useMutation, useQueryClient } from '@tanstack/react-query'
import { RevokeAccountSessionInput, useApi } from '#/api'
import { accountSessionsQueryKey } from '#/data/useAccountSessionsQuery'
import { useDeviceSessionsQueryKey } from '#/data/useDeviceSessionsQuery'
export function useRevokeAccountSessionMutation() {
const api = useApi()
const qc = useQueryClient()
return useMutation({
async mutationFn(input: RevokeAccountSessionInput) {
return api.fetch('POST', '/revoke-account-session', input)
},
onSuccess(_, input) {
qc.invalidateQueries({ queryKey: accountSessionsQueryKey(input) })
qc.invalidateQueries({ queryKey: useDeviceSessionsQueryKey })
},
})
}

View File

@ -0,0 +1,20 @@
import { useMutation, useQueryClient } from '@tanstack/react-query'
import { RevokeOAuthSessionInput, useApi } from '#/api'
import { oauthSessionsQueryKey } from './useOAuthSessionsQuery'
export function useRevokeOAuthSessionMutation() {
const api = useApi()
const qc = useQueryClient()
return useMutation({
async mutationFn(input: RevokeOAuthSessionInput) {
await api.fetch('POST', '/revoke-oauth-session', input)
},
onError(error, input) {
qc.invalidateQueries({ queryKey: oauthSessionsQueryKey(input) })
},
onSuccess(_, input) {
qc.invalidateQueries({ queryKey: oauthSessionsQueryKey(input) })
},
})
}

View File

@ -0,0 +1,26 @@
import { useMutation } from '@tanstack/react-query'
import { SignInInput, useApi } from '#/api'
import { useUpsertDeviceAccount } from '#/data/useDeviceSessionsQuery'
import { useLocale } from '#/locales'
export type SignInMutationInput = Omit<SignInInput, 'locale'>
export function useSignInMutation() {
const api = useApi()
const { locale } = useLocale()
const upsertDeviceAccount = useUpsertDeviceAccount()
return useMutation({
async mutationFn(input: SignInMutationInput) {
const res = await api.fetch('POST', '/sign-in', { ...input, locale })
upsertDeviceAccount({
account: res.account,
loginRequired: false,
})
return res
},
})
}

View File

@ -0,0 +1,22 @@
import { useMutation, useQueryClient } from '@tanstack/react-query'
import { SignOutInput, useApi } from '#/api'
import { accountSessionsQueryKey } from '#/data/useAccountSessionsQuery'
import { useDeviceSessionsQueryKey } from '#/data/useDeviceSessionsQuery'
export function useSignOutMutation() {
const api = useApi()
const qc = useQueryClient()
return useMutation({
async mutationFn(input: SignOutInput) {
return api.fetch('POST', '/sign-out', input)
},
onSuccess(_, input) {
qc.invalidateQueries({ queryKey: useDeviceSessionsQueryKey })
const subs = Array.isArray(input.sub) ? input.sub : [input.sub]
for (const sub of subs) {
qc.invalidateQueries({ queryKey: accountSessionsQueryKey({ sub }) })
}
},
})
}

View File

@ -0,0 +1,17 @@
import {
ActiveDeviceSession,
CustomizationData,
} from '@atproto/oauth-provider-api'
export type HydrationData = {
'account-page': {
/**
* needed by `useCustomizationData.ts`
*/
__customizationData: CustomizationData
/**
* needed by `useDeviceSessionsQuery.ts`
*/
__deviceSessions: readonly ActiveDeviceSession[]
}
}

View File

@ -0,0 +1,13 @@
import { i18n } from '@lingui/core'
import * as en from '#/locales/en/messages'
import { Locale } from './locales'
export async function activateLocale(locale: Locale) {
const { messages } = await import(`./${locale}/messages.ts`).catch((e) => {
console.error('Error loading locale', e)
return en
})
i18n.load(locale, messages)
i18n.activate(locale)
}

View File

@ -0,0 +1,296 @@
msgid ""
msgstr ""
"POT-Creation-Date: 2025-03-19 13:49-0500\n"
"MIME-Version: 1.0\n"
"Content-Type: text/plain; charset=utf-8\n"
"Content-Transfer-Encoding: 8bit\n"
"X-Generator: @lingui/cli\n"
"Language: an\n"
"Project-Id-Version: \n"
"Report-Msgid-Bugs-To: \n"
"PO-Revision-Date: \n"
"Last-Translator: \n"
"Language-Team: \n"
"Plural-Forms: \n"
#: src/routes/account/_minimalLayout/sign-in.tsx:155
msgid "@handle or email"
msgstr ""
#: src/routes/account/_minimalLayout/sign-in.tsx:49
msgid "← Back to accounts"
msgstr ""
#: src/data/useClientName.ts:24
msgid "A local app"
msgstr ""
#: src/routes/account/_minimalLayout/reset-password.tsx:65
msgid "A new password is required"
msgstr ""
#: src/routes/account/_minimalLayout/index.tsx:64
msgid "Accounts"
msgstr ""
#: src/routes/account/_minimalLayout/sign-in.tsx:125
#: src/routes/account/_minimalLayout/reset-password.tsx:51
#: src/routes/account/_minimalLayout/reset-password.tsx:78
msgid "An error occurred, please try again."
msgstr ""
#: src/routes/account/_appLayout/$sub.tsx:270
msgid "Are you sure you want to remove this device?"
msgstr ""
#: src/routes/account/_appLayout/$sub.tsx:176
msgid "Are you sure you want to revoke access? This application won't be able to access your account anymore."
msgstr ""
#: src/routes/account/_minimalLayout/reset-password.tsx:122
#: src/routes/account/_minimalLayout/reset-password.tsx:195
msgid "Back to sign in"
msgstr ""
#: src/components/Prompt.tsx:56
msgid "Cancel"
msgstr ""
#: src/routes/account/_minimalLayout/reset-password.tsx:319
msgid "Click here to send a new code to your email."
msgstr ""
#: src/routes/account/_minimalLayout/sign-in.tsx:193
#: src/routes/account/_minimalLayout/reset-password.tsx:222
msgid "Code"
msgstr ""
#: src/routes/account/_minimalLayout/reset-password.tsx:88
msgid "Code was resent"
msgstr ""
#: src/routes/account/_appLayout/$sub.tsx:52
msgid "Connected apps"
msgstr ""
#: src/routes/account/_minimalLayout/sign-in.tsx:143
msgid "Credentials"
msgstr ""
#: src/routes/account/_minimalLayout/reset-password.tsx:314
msgid "Don't see the email? <0>Try sending again.</0>"
msgstr ""
#: src/routes/account/_minimalLayout/reset-password.tsx:149
msgid "Email"
msgstr ""
#: src/routes/account/_minimalLayout/reset-password.tsx:62
msgid "Email code is required"
msgstr ""
#: src/routes/account/_minimalLayout/reset-password.tsx:42
msgid "Email is required"
msgstr ""
#: src/routes/account/_minimalLayout/reset-password.tsx:264
msgid "Enter a new password"
msgstr ""
#: src/routes/account/_minimalLayout/reset-password.tsx:154
msgid "Enter your email"
msgstr ""
#: src/routes/account/_minimalLayout/reset-password.tsx:132
msgid "Enter your email to receive a reset code."
msgstr ""
#: src/routes/account/_appLayout/$sub.tsx:60
msgid "Failed to load connected apps"
msgstr ""
#: src/routes/account/_appLayout/$sub.tsx:91
msgid "Failed to load devices"
msgstr ""
#: src/routes/account/_appLayout/$sub.tsx:222
msgid "Failed to remove device"
msgstr ""
#: src/routes/account/_minimalLayout/reset-password.tsx:94
msgid "Failed to resend code"
msgstr ""
#: src/routes/account/_appLayout/$sub.tsx:147
msgid "Failed to sign out"
msgstr ""
#: src/routes/account/_minimalLayout/sign-in.tsx:235
msgid "Forgot password?"
msgstr ""
#: src/routes/account/_minimalLayout/reset-password.tsx:186
msgid "Get reset code"
msgstr ""
#: src/routes/account/_appLayout/$sub.tsx:42
msgid "Home"
msgstr ""
#: src/routes/account/_minimalLayout/sign-in.tsx:150
msgid "Identifier"
msgstr ""
#: src/routes/account/_minimalLayout/reset-password.tsx:41
msgid "Invalid email"
msgstr ""
#: src/routes/account/_minimalLayout/sign-in.tsx:83
msgid "Invalid handle"
msgstr ""
#: src/routes/account/_minimalLayout/sign-in.tsx:123
msgid "Invalid identifier or password."
msgstr ""
#: src/routes/account/_appLayout/$sub.tsx:77
msgid "It appears that you havent used this account to sign in to any apps yet."
msgstr ""
#: src/routes/account/_minimalLayout.tsx:21
#: src/components/Nav.tsx:18
msgid "Logo"
msgstr ""
#: src/routes/account/_appLayout/$sub.tsx:107
msgid "Looks like you aren't logged in on any other devices."
msgstr ""
#: src/data/useFriendlyClientId.ts:13
msgid "loopback"
msgstr ""
#: src/routes/account/_appLayout/$sub.tsx:83
msgid "My devices"
msgstr ""
#: src/routes/account/_appLayout/$sub.tsx:75
msgid "No connected apps"
msgstr ""
#: src/routes/account/_appLayout/$sub.tsx:106
msgid "No devices"
msgstr ""
#: src/routes/account/_minimalLayout/sign-in.tsx:170
#: src/routes/account/_minimalLayout/sign-in.tsx:176
#: src/routes/account/_minimalLayout/reset-password.tsx:257
msgid "Password"
msgstr ""
#: src/routes/account/_minimalLayout/sign-in.tsx:88
msgid "Password is required"
msgstr ""
#: src/routes/account/_minimalLayout/reset-password.tsx:68
msgid "Password must be at least {MIN_PASSWORD_LENGTH} characters"
msgstr ""
#: src/routes/account/_appLayout/$sub.tsx:271
#: src/routes/account/_appLayout/$sub.tsx:276
msgid "Remove"
msgstr ""
#: src/routes/account/_appLayout/$sub.tsx:269
msgid "Remove this device"
msgstr ""
#: src/routes/account/_minimalLayout/reset-password.tsx:129
#: src/routes/account/_minimalLayout/reset-password.tsx:308
msgid "Reset password"
msgstr ""
#: src/routes/account/_appLayout/$sub.tsx:172
msgid "Revoke access to {clientName}"
msgstr ""
#: src/routes/account/_appLayout/$sub.tsx:173
msgid "Revoke access to this application"
msgstr ""
#: src/components/AccountSelector.tsx:32
msgid "Select an account"
msgstr ""
#: src/routes/account/_minimalLayout/index.tsx:67
msgid "Select the account you would like to manage."
msgstr ""
#: src/routes/account/_minimalLayout/sign-in.tsx:134
#: src/routes/account/_minimalLayout/sign-in.tsx:226
msgid "Sign in"
msgstr ""
#: src/routes/account/_minimalLayout/index.tsx:107
msgid "Sign in with another account"
msgstr ""
#: src/routes/account/_appLayout/$sub.tsx:178
#: src/routes/account/_appLayout/$sub.tsx:183
#: src/components/AccountSelector.tsx:103
msgid "Sign out"
msgstr ""
#: src/routes/account/_minimalLayout/index.tsx:39
msgid "Something went wrong"
msgstr ""
#: src/routes/account/_minimalLayout/reset-password.tsx:115
msgid "Success!"
msgstr ""
#: src/routes/account/_appLayout/$sub.tsx:216
msgid "Successfully removed device"
msgstr ""
#: src/routes/account/_appLayout/$sub.tsx:141
msgid "Successfully signed out"
msgstr ""
#: src/routes/account/_appLayout/$sub.tsx:263
msgid "This device"
msgstr ""
#: src/routes/account/_appLayout/$sub.tsx:247
msgid "Unknown user agent"
msgstr ""
#: src/components/Avatar.tsx:24
msgid "User avatar"
msgstr ""
#. placeholder {0}: getAccountName(account)
#: src/routes/account/_minimalLayout/index.tsx:83
msgid "View and manage account for {0}"
msgstr ""
#: src/routes/account/_minimalLayout/index.tsx:41
msgid "We weren't able to load your accounts. Please refresh the page to try again."
msgstr ""
#: src/components/ErrorScreen.tsx:19
msgid "Whoops! An error occurred."
msgstr ""
#: src/routes/account/_minimalLayout/sign-in.tsx:198
#: src/routes/account/_minimalLayout/reset-password.tsx:228
msgid "XXXXX-XXXXX"
msgstr ""
#: src/routes/account/_appLayout/$sub.tsx:47
msgid "Your account"
msgstr ""
#: src/routes/account/_minimalLayout/reset-password.tsx:118
msgid "Your password has been reset."
msgstr ""

View File

@ -0,0 +1,296 @@
msgid ""
msgstr ""
"POT-Creation-Date: 2025-03-19 13:49-0500\n"
"MIME-Version: 1.0\n"
"Content-Type: text/plain; charset=utf-8\n"
"Content-Transfer-Encoding: 8bit\n"
"X-Generator: @lingui/cli\n"
"Language: ast\n"
"Project-Id-Version: \n"
"Report-Msgid-Bugs-To: \n"
"PO-Revision-Date: \n"
"Last-Translator: \n"
"Language-Team: \n"
"Plural-Forms: \n"
#: src/routes/account/_minimalLayout/sign-in.tsx:155
msgid "@handle or email"
msgstr ""
#: src/routes/account/_minimalLayout/sign-in.tsx:49
msgid "← Back to accounts"
msgstr ""
#: src/data/useClientName.ts:24
msgid "A local app"
msgstr ""
#: src/routes/account/_minimalLayout/reset-password.tsx:65
msgid "A new password is required"
msgstr ""
#: src/routes/account/_minimalLayout/index.tsx:64
msgid "Accounts"
msgstr ""
#: src/routes/account/_minimalLayout/sign-in.tsx:125
#: src/routes/account/_minimalLayout/reset-password.tsx:51
#: src/routes/account/_minimalLayout/reset-password.tsx:78
msgid "An error occurred, please try again."
msgstr ""
#: src/routes/account/_appLayout/$sub.tsx:270
msgid "Are you sure you want to remove this device?"
msgstr ""
#: src/routes/account/_appLayout/$sub.tsx:176
msgid "Are you sure you want to revoke access? This application won't be able to access your account anymore."
msgstr ""
#: src/routes/account/_minimalLayout/reset-password.tsx:122
#: src/routes/account/_minimalLayout/reset-password.tsx:195
msgid "Back to sign in"
msgstr ""
#: src/components/Prompt.tsx:56
msgid "Cancel"
msgstr ""
#: src/routes/account/_minimalLayout/reset-password.tsx:319
msgid "Click here to send a new code to your email."
msgstr ""
#: src/routes/account/_minimalLayout/sign-in.tsx:193
#: src/routes/account/_minimalLayout/reset-password.tsx:222
msgid "Code"
msgstr ""
#: src/routes/account/_minimalLayout/reset-password.tsx:88
msgid "Code was resent"
msgstr ""
#: src/routes/account/_appLayout/$sub.tsx:52
msgid "Connected apps"
msgstr ""
#: src/routes/account/_minimalLayout/sign-in.tsx:143
msgid "Credentials"
msgstr ""
#: src/routes/account/_minimalLayout/reset-password.tsx:314
msgid "Don't see the email? <0>Try sending again.</0>"
msgstr ""
#: src/routes/account/_minimalLayout/reset-password.tsx:149
msgid "Email"
msgstr ""
#: src/routes/account/_minimalLayout/reset-password.tsx:62
msgid "Email code is required"
msgstr ""
#: src/routes/account/_minimalLayout/reset-password.tsx:42
msgid "Email is required"
msgstr ""
#: src/routes/account/_minimalLayout/reset-password.tsx:264
msgid "Enter a new password"
msgstr ""
#: src/routes/account/_minimalLayout/reset-password.tsx:154
msgid "Enter your email"
msgstr ""
#: src/routes/account/_minimalLayout/reset-password.tsx:132
msgid "Enter your email to receive a reset code."
msgstr ""
#: src/routes/account/_appLayout/$sub.tsx:60
msgid "Failed to load connected apps"
msgstr ""
#: src/routes/account/_appLayout/$sub.tsx:91
msgid "Failed to load devices"
msgstr ""
#: src/routes/account/_appLayout/$sub.tsx:222
msgid "Failed to remove device"
msgstr ""
#: src/routes/account/_minimalLayout/reset-password.tsx:94
msgid "Failed to resend code"
msgstr ""
#: src/routes/account/_appLayout/$sub.tsx:147
msgid "Failed to sign out"
msgstr ""
#: src/routes/account/_minimalLayout/sign-in.tsx:235
msgid "Forgot password?"
msgstr ""
#: src/routes/account/_minimalLayout/reset-password.tsx:186
msgid "Get reset code"
msgstr ""
#: src/routes/account/_appLayout/$sub.tsx:42
msgid "Home"
msgstr ""
#: src/routes/account/_minimalLayout/sign-in.tsx:150
msgid "Identifier"
msgstr ""
#: src/routes/account/_minimalLayout/reset-password.tsx:41
msgid "Invalid email"
msgstr ""
#: src/routes/account/_minimalLayout/sign-in.tsx:83
msgid "Invalid handle"
msgstr ""
#: src/routes/account/_minimalLayout/sign-in.tsx:123
msgid "Invalid identifier or password."
msgstr ""
#: src/routes/account/_appLayout/$sub.tsx:77
msgid "It appears that you havent used this account to sign in to any apps yet."
msgstr ""
#: src/routes/account/_minimalLayout.tsx:21
#: src/components/Nav.tsx:18
msgid "Logo"
msgstr ""
#: src/routes/account/_appLayout/$sub.tsx:107
msgid "Looks like you aren't logged in on any other devices."
msgstr ""
#: src/data/useFriendlyClientId.ts:13
msgid "loopback"
msgstr ""
#: src/routes/account/_appLayout/$sub.tsx:83
msgid "My devices"
msgstr ""
#: src/routes/account/_appLayout/$sub.tsx:75
msgid "No connected apps"
msgstr ""
#: src/routes/account/_appLayout/$sub.tsx:106
msgid "No devices"
msgstr ""
#: src/routes/account/_minimalLayout/sign-in.tsx:170
#: src/routes/account/_minimalLayout/sign-in.tsx:176
#: src/routes/account/_minimalLayout/reset-password.tsx:257
msgid "Password"
msgstr ""
#: src/routes/account/_minimalLayout/sign-in.tsx:88
msgid "Password is required"
msgstr ""
#: src/routes/account/_minimalLayout/reset-password.tsx:68
msgid "Password must be at least {MIN_PASSWORD_LENGTH} characters"
msgstr ""
#: src/routes/account/_appLayout/$sub.tsx:271
#: src/routes/account/_appLayout/$sub.tsx:276
msgid "Remove"
msgstr ""
#: src/routes/account/_appLayout/$sub.tsx:269
msgid "Remove this device"
msgstr ""
#: src/routes/account/_minimalLayout/reset-password.tsx:129
#: src/routes/account/_minimalLayout/reset-password.tsx:308
msgid "Reset password"
msgstr ""
#: src/routes/account/_appLayout/$sub.tsx:172
msgid "Revoke access to {clientName}"
msgstr ""
#: src/routes/account/_appLayout/$sub.tsx:173
msgid "Revoke access to this application"
msgstr ""
#: src/components/AccountSelector.tsx:32
msgid "Select an account"
msgstr ""
#: src/routes/account/_minimalLayout/index.tsx:67
msgid "Select the account you would like to manage."
msgstr ""
#: src/routes/account/_minimalLayout/sign-in.tsx:134
#: src/routes/account/_minimalLayout/sign-in.tsx:226
msgid "Sign in"
msgstr ""
#: src/routes/account/_minimalLayout/index.tsx:107
msgid "Sign in with another account"
msgstr ""
#: src/routes/account/_appLayout/$sub.tsx:178
#: src/routes/account/_appLayout/$sub.tsx:183
#: src/components/AccountSelector.tsx:103
msgid "Sign out"
msgstr ""
#: src/routes/account/_minimalLayout/index.tsx:39
msgid "Something went wrong"
msgstr ""
#: src/routes/account/_minimalLayout/reset-password.tsx:115
msgid "Success!"
msgstr ""
#: src/routes/account/_appLayout/$sub.tsx:216
msgid "Successfully removed device"
msgstr ""
#: src/routes/account/_appLayout/$sub.tsx:141
msgid "Successfully signed out"
msgstr ""
#: src/routes/account/_appLayout/$sub.tsx:263
msgid "This device"
msgstr ""
#: src/routes/account/_appLayout/$sub.tsx:247
msgid "Unknown user agent"
msgstr ""
#: src/components/Avatar.tsx:24
msgid "User avatar"
msgstr ""
#. placeholder {0}: getAccountName(account)
#: src/routes/account/_minimalLayout/index.tsx:83
msgid "View and manage account for {0}"
msgstr ""
#: src/routes/account/_minimalLayout/index.tsx:41
msgid "We weren't able to load your accounts. Please refresh the page to try again."
msgstr ""
#: src/components/ErrorScreen.tsx:19
msgid "Whoops! An error occurred."
msgstr ""
#: src/routes/account/_minimalLayout/sign-in.tsx:198
#: src/routes/account/_minimalLayout/reset-password.tsx:228
msgid "XXXXX-XXXXX"
msgstr ""
#: src/routes/account/_appLayout/$sub.tsx:47
msgid "Your account"
msgstr ""
#: src/routes/account/_minimalLayout/reset-password.tsx:118
msgid "Your password has been reset."
msgstr ""

View File

@ -0,0 +1,296 @@
msgid ""
msgstr ""
"POT-Creation-Date: 2025-03-19 13:49-0500\n"
"MIME-Version: 1.0\n"
"Content-Type: text/plain; charset=utf-8\n"
"Content-Transfer-Encoding: 8bit\n"
"X-Generator: @lingui/cli\n"
"Language: ca\n"
"Project-Id-Version: \n"
"Report-Msgid-Bugs-To: \n"
"PO-Revision-Date: \n"
"Last-Translator: \n"
"Language-Team: \n"
"Plural-Forms: \n"
#: src/routes/account/_minimalLayout/sign-in.tsx:155
msgid "@handle or email"
msgstr ""
#: src/routes/account/_minimalLayout/sign-in.tsx:49
msgid "← Back to accounts"
msgstr ""
#: src/data/useClientName.ts:24
msgid "A local app"
msgstr ""
#: src/routes/account/_minimalLayout/reset-password.tsx:65
msgid "A new password is required"
msgstr ""
#: src/routes/account/_minimalLayout/index.tsx:64
msgid "Accounts"
msgstr ""
#: src/routes/account/_minimalLayout/sign-in.tsx:125
#: src/routes/account/_minimalLayout/reset-password.tsx:51
#: src/routes/account/_minimalLayout/reset-password.tsx:78
msgid "An error occurred, please try again."
msgstr ""
#: src/routes/account/_appLayout/$sub.tsx:270
msgid "Are you sure you want to remove this device?"
msgstr ""
#: src/routes/account/_appLayout/$sub.tsx:176
msgid "Are you sure you want to revoke access? This application won't be able to access your account anymore."
msgstr ""
#: src/routes/account/_minimalLayout/reset-password.tsx:122
#: src/routes/account/_minimalLayout/reset-password.tsx:195
msgid "Back to sign in"
msgstr ""
#: src/components/Prompt.tsx:56
msgid "Cancel"
msgstr ""
#: src/routes/account/_minimalLayout/reset-password.tsx:319
msgid "Click here to send a new code to your email."
msgstr ""
#: src/routes/account/_minimalLayout/sign-in.tsx:193
#: src/routes/account/_minimalLayout/reset-password.tsx:222
msgid "Code"
msgstr ""
#: src/routes/account/_minimalLayout/reset-password.tsx:88
msgid "Code was resent"
msgstr ""
#: src/routes/account/_appLayout/$sub.tsx:52
msgid "Connected apps"
msgstr ""
#: src/routes/account/_minimalLayout/sign-in.tsx:143
msgid "Credentials"
msgstr ""
#: src/routes/account/_minimalLayout/reset-password.tsx:314
msgid "Don't see the email? <0>Try sending again.</0>"
msgstr ""
#: src/routes/account/_minimalLayout/reset-password.tsx:149
msgid "Email"
msgstr ""
#: src/routes/account/_minimalLayout/reset-password.tsx:62
msgid "Email code is required"
msgstr ""
#: src/routes/account/_minimalLayout/reset-password.tsx:42
msgid "Email is required"
msgstr ""
#: src/routes/account/_minimalLayout/reset-password.tsx:264
msgid "Enter a new password"
msgstr ""
#: src/routes/account/_minimalLayout/reset-password.tsx:154
msgid "Enter your email"
msgstr ""
#: src/routes/account/_minimalLayout/reset-password.tsx:132
msgid "Enter your email to receive a reset code."
msgstr ""
#: src/routes/account/_appLayout/$sub.tsx:60
msgid "Failed to load connected apps"
msgstr ""
#: src/routes/account/_appLayout/$sub.tsx:91
msgid "Failed to load devices"
msgstr ""
#: src/routes/account/_appLayout/$sub.tsx:222
msgid "Failed to remove device"
msgstr ""
#: src/routes/account/_minimalLayout/reset-password.tsx:94
msgid "Failed to resend code"
msgstr ""
#: src/routes/account/_appLayout/$sub.tsx:147
msgid "Failed to sign out"
msgstr ""
#: src/routes/account/_minimalLayout/sign-in.tsx:235
msgid "Forgot password?"
msgstr ""
#: src/routes/account/_minimalLayout/reset-password.tsx:186
msgid "Get reset code"
msgstr ""
#: src/routes/account/_appLayout/$sub.tsx:42
msgid "Home"
msgstr ""
#: src/routes/account/_minimalLayout/sign-in.tsx:150
msgid "Identifier"
msgstr ""
#: src/routes/account/_minimalLayout/reset-password.tsx:41
msgid "Invalid email"
msgstr ""
#: src/routes/account/_minimalLayout/sign-in.tsx:83
msgid "Invalid handle"
msgstr ""
#: src/routes/account/_minimalLayout/sign-in.tsx:123
msgid "Invalid identifier or password."
msgstr ""
#: src/routes/account/_appLayout/$sub.tsx:77
msgid "It appears that you havent used this account to sign in to any apps yet."
msgstr ""
#: src/routes/account/_minimalLayout.tsx:21
#: src/components/Nav.tsx:18
msgid "Logo"
msgstr ""
#: src/routes/account/_appLayout/$sub.tsx:107
msgid "Looks like you aren't logged in on any other devices."
msgstr ""
#: src/data/useFriendlyClientId.ts:13
msgid "loopback"
msgstr ""
#: src/routes/account/_appLayout/$sub.tsx:83
msgid "My devices"
msgstr ""
#: src/routes/account/_appLayout/$sub.tsx:75
msgid "No connected apps"
msgstr ""
#: src/routes/account/_appLayout/$sub.tsx:106
msgid "No devices"
msgstr ""
#: src/routes/account/_minimalLayout/sign-in.tsx:170
#: src/routes/account/_minimalLayout/sign-in.tsx:176
#: src/routes/account/_minimalLayout/reset-password.tsx:257
msgid "Password"
msgstr ""
#: src/routes/account/_minimalLayout/sign-in.tsx:88
msgid "Password is required"
msgstr ""
#: src/routes/account/_minimalLayout/reset-password.tsx:68
msgid "Password must be at least {MIN_PASSWORD_LENGTH} characters"
msgstr ""
#: src/routes/account/_appLayout/$sub.tsx:271
#: src/routes/account/_appLayout/$sub.tsx:276
msgid "Remove"
msgstr ""
#: src/routes/account/_appLayout/$sub.tsx:269
msgid "Remove this device"
msgstr ""
#: src/routes/account/_minimalLayout/reset-password.tsx:129
#: src/routes/account/_minimalLayout/reset-password.tsx:308
msgid "Reset password"
msgstr ""
#: src/routes/account/_appLayout/$sub.tsx:172
msgid "Revoke access to {clientName}"
msgstr ""
#: src/routes/account/_appLayout/$sub.tsx:173
msgid "Revoke access to this application"
msgstr ""
#: src/components/AccountSelector.tsx:32
msgid "Select an account"
msgstr ""
#: src/routes/account/_minimalLayout/index.tsx:67
msgid "Select the account you would like to manage."
msgstr ""
#: src/routes/account/_minimalLayout/sign-in.tsx:134
#: src/routes/account/_minimalLayout/sign-in.tsx:226
msgid "Sign in"
msgstr ""
#: src/routes/account/_minimalLayout/index.tsx:107
msgid "Sign in with another account"
msgstr ""
#: src/routes/account/_appLayout/$sub.tsx:178
#: src/routes/account/_appLayout/$sub.tsx:183
#: src/components/AccountSelector.tsx:103
msgid "Sign out"
msgstr ""
#: src/routes/account/_minimalLayout/index.tsx:39
msgid "Something went wrong"
msgstr ""
#: src/routes/account/_minimalLayout/reset-password.tsx:115
msgid "Success!"
msgstr ""
#: src/routes/account/_appLayout/$sub.tsx:216
msgid "Successfully removed device"
msgstr ""
#: src/routes/account/_appLayout/$sub.tsx:141
msgid "Successfully signed out"
msgstr ""
#: src/routes/account/_appLayout/$sub.tsx:263
msgid "This device"
msgstr ""
#: src/routes/account/_appLayout/$sub.tsx:247
msgid "Unknown user agent"
msgstr ""
#: src/components/Avatar.tsx:24
msgid "User avatar"
msgstr ""
#. placeholder {0}: getAccountName(account)
#: src/routes/account/_minimalLayout/index.tsx:83
msgid "View and manage account for {0}"
msgstr ""
#: src/routes/account/_minimalLayout/index.tsx:41
msgid "We weren't able to load your accounts. Please refresh the page to try again."
msgstr ""
#: src/components/ErrorScreen.tsx:19
msgid "Whoops! An error occurred."
msgstr ""
#: src/routes/account/_minimalLayout/sign-in.tsx:198
#: src/routes/account/_minimalLayout/reset-password.tsx:228
msgid "XXXXX-XXXXX"
msgstr ""
#: src/routes/account/_appLayout/$sub.tsx:47
msgid "Your account"
msgstr ""
#: src/routes/account/_minimalLayout/reset-password.tsx:118
msgid "Your password has been reset."
msgstr ""

View File

@ -0,0 +1,296 @@
msgid ""
msgstr ""
"POT-Creation-Date: 2025-03-19 13:49-0500\n"
"MIME-Version: 1.0\n"
"Content-Type: text/plain; charset=utf-8\n"
"Content-Transfer-Encoding: 8bit\n"
"X-Generator: @lingui/cli\n"
"Language: da\n"
"Project-Id-Version: \n"
"Report-Msgid-Bugs-To: \n"
"PO-Revision-Date: \n"
"Last-Translator: \n"
"Language-Team: \n"
"Plural-Forms: \n"
#: src/routes/account/_minimalLayout/sign-in.tsx:155
msgid "@handle or email"
msgstr ""
#: src/routes/account/_minimalLayout/sign-in.tsx:49
msgid "← Back to accounts"
msgstr ""
#: src/data/useClientName.ts:24
msgid "A local app"
msgstr ""
#: src/routes/account/_minimalLayout/reset-password.tsx:65
msgid "A new password is required"
msgstr ""
#: src/routes/account/_minimalLayout/index.tsx:64
msgid "Accounts"
msgstr ""
#: src/routes/account/_minimalLayout/sign-in.tsx:125
#: src/routes/account/_minimalLayout/reset-password.tsx:51
#: src/routes/account/_minimalLayout/reset-password.tsx:78
msgid "An error occurred, please try again."
msgstr ""
#: src/routes/account/_appLayout/$sub.tsx:270
msgid "Are you sure you want to remove this device?"
msgstr ""
#: src/routes/account/_appLayout/$sub.tsx:176
msgid "Are you sure you want to revoke access? This application won't be able to access your account anymore."
msgstr ""
#: src/routes/account/_minimalLayout/reset-password.tsx:122
#: src/routes/account/_minimalLayout/reset-password.tsx:195
msgid "Back to sign in"
msgstr ""
#: src/components/Prompt.tsx:56
msgid "Cancel"
msgstr ""
#: src/routes/account/_minimalLayout/reset-password.tsx:319
msgid "Click here to send a new code to your email."
msgstr ""
#: src/routes/account/_minimalLayout/sign-in.tsx:193
#: src/routes/account/_minimalLayout/reset-password.tsx:222
msgid "Code"
msgstr ""
#: src/routes/account/_minimalLayout/reset-password.tsx:88
msgid "Code was resent"
msgstr ""
#: src/routes/account/_appLayout/$sub.tsx:52
msgid "Connected apps"
msgstr ""
#: src/routes/account/_minimalLayout/sign-in.tsx:143
msgid "Credentials"
msgstr ""
#: src/routes/account/_minimalLayout/reset-password.tsx:314
msgid "Don't see the email? <0>Try sending again.</0>"
msgstr ""
#: src/routes/account/_minimalLayout/reset-password.tsx:149
msgid "Email"
msgstr ""
#: src/routes/account/_minimalLayout/reset-password.tsx:62
msgid "Email code is required"
msgstr ""
#: src/routes/account/_minimalLayout/reset-password.tsx:42
msgid "Email is required"
msgstr ""
#: src/routes/account/_minimalLayout/reset-password.tsx:264
msgid "Enter a new password"
msgstr ""
#: src/routes/account/_minimalLayout/reset-password.tsx:154
msgid "Enter your email"
msgstr ""
#: src/routes/account/_minimalLayout/reset-password.tsx:132
msgid "Enter your email to receive a reset code."
msgstr ""
#: src/routes/account/_appLayout/$sub.tsx:60
msgid "Failed to load connected apps"
msgstr ""
#: src/routes/account/_appLayout/$sub.tsx:91
msgid "Failed to load devices"
msgstr ""
#: src/routes/account/_appLayout/$sub.tsx:222
msgid "Failed to remove device"
msgstr ""
#: src/routes/account/_minimalLayout/reset-password.tsx:94
msgid "Failed to resend code"
msgstr ""
#: src/routes/account/_appLayout/$sub.tsx:147
msgid "Failed to sign out"
msgstr ""
#: src/routes/account/_minimalLayout/sign-in.tsx:235
msgid "Forgot password?"
msgstr ""
#: src/routes/account/_minimalLayout/reset-password.tsx:186
msgid "Get reset code"
msgstr ""
#: src/routes/account/_appLayout/$sub.tsx:42
msgid "Home"
msgstr ""
#: src/routes/account/_minimalLayout/sign-in.tsx:150
msgid "Identifier"
msgstr ""
#: src/routes/account/_minimalLayout/reset-password.tsx:41
msgid "Invalid email"
msgstr ""
#: src/routes/account/_minimalLayout/sign-in.tsx:83
msgid "Invalid handle"
msgstr ""
#: src/routes/account/_minimalLayout/sign-in.tsx:123
msgid "Invalid identifier or password."
msgstr ""
#: src/routes/account/_appLayout/$sub.tsx:77
msgid "It appears that you havent used this account to sign in to any apps yet."
msgstr ""
#: src/routes/account/_minimalLayout.tsx:21
#: src/components/Nav.tsx:18
msgid "Logo"
msgstr ""
#: src/routes/account/_appLayout/$sub.tsx:107
msgid "Looks like you aren't logged in on any other devices."
msgstr ""
#: src/data/useFriendlyClientId.ts:13
msgid "loopback"
msgstr ""
#: src/routes/account/_appLayout/$sub.tsx:83
msgid "My devices"
msgstr ""
#: src/routes/account/_appLayout/$sub.tsx:75
msgid "No connected apps"
msgstr ""
#: src/routes/account/_appLayout/$sub.tsx:106
msgid "No devices"
msgstr ""
#: src/routes/account/_minimalLayout/sign-in.tsx:170
#: src/routes/account/_minimalLayout/sign-in.tsx:176
#: src/routes/account/_minimalLayout/reset-password.tsx:257
msgid "Password"
msgstr ""
#: src/routes/account/_minimalLayout/sign-in.tsx:88
msgid "Password is required"
msgstr ""
#: src/routes/account/_minimalLayout/reset-password.tsx:68
msgid "Password must be at least {MIN_PASSWORD_LENGTH} characters"
msgstr ""
#: src/routes/account/_appLayout/$sub.tsx:271
#: src/routes/account/_appLayout/$sub.tsx:276
msgid "Remove"
msgstr ""
#: src/routes/account/_appLayout/$sub.tsx:269
msgid "Remove this device"
msgstr ""
#: src/routes/account/_minimalLayout/reset-password.tsx:129
#: src/routes/account/_minimalLayout/reset-password.tsx:308
msgid "Reset password"
msgstr ""
#: src/routes/account/_appLayout/$sub.tsx:172
msgid "Revoke access to {clientName}"
msgstr ""
#: src/routes/account/_appLayout/$sub.tsx:173
msgid "Revoke access to this application"
msgstr ""
#: src/components/AccountSelector.tsx:32
msgid "Select an account"
msgstr ""
#: src/routes/account/_minimalLayout/index.tsx:67
msgid "Select the account you would like to manage."
msgstr ""
#: src/routes/account/_minimalLayout/sign-in.tsx:134
#: src/routes/account/_minimalLayout/sign-in.tsx:226
msgid "Sign in"
msgstr ""
#: src/routes/account/_minimalLayout/index.tsx:107
msgid "Sign in with another account"
msgstr ""
#: src/routes/account/_appLayout/$sub.tsx:178
#: src/routes/account/_appLayout/$sub.tsx:183
#: src/components/AccountSelector.tsx:103
msgid "Sign out"
msgstr ""
#: src/routes/account/_minimalLayout/index.tsx:39
msgid "Something went wrong"
msgstr ""
#: src/routes/account/_minimalLayout/reset-password.tsx:115
msgid "Success!"
msgstr ""
#: src/routes/account/_appLayout/$sub.tsx:216
msgid "Successfully removed device"
msgstr ""
#: src/routes/account/_appLayout/$sub.tsx:141
msgid "Successfully signed out"
msgstr ""
#: src/routes/account/_appLayout/$sub.tsx:263
msgid "This device"
msgstr ""
#: src/routes/account/_appLayout/$sub.tsx:247
msgid "Unknown user agent"
msgstr ""
#: src/components/Avatar.tsx:24
msgid "User avatar"
msgstr ""
#. placeholder {0}: getAccountName(account)
#: src/routes/account/_minimalLayout/index.tsx:83
msgid "View and manage account for {0}"
msgstr ""
#: src/routes/account/_minimalLayout/index.tsx:41
msgid "We weren't able to load your accounts. Please refresh the page to try again."
msgstr ""
#: src/components/ErrorScreen.tsx:19
msgid "Whoops! An error occurred."
msgstr ""
#: src/routes/account/_minimalLayout/sign-in.tsx:198
#: src/routes/account/_minimalLayout/reset-password.tsx:228
msgid "XXXXX-XXXXX"
msgstr ""
#: src/routes/account/_appLayout/$sub.tsx:47
msgid "Your account"
msgstr ""
#: src/routes/account/_minimalLayout/reset-password.tsx:118
msgid "Your password has been reset."
msgstr ""

View File

@ -0,0 +1,296 @@
msgid ""
msgstr ""
"POT-Creation-Date: 2025-03-19 13:49-0500\n"
"MIME-Version: 1.0\n"
"Content-Type: text/plain; charset=utf-8\n"
"Content-Transfer-Encoding: 8bit\n"
"X-Generator: @lingui/cli\n"
"Language: de\n"
"Project-Id-Version: \n"
"Report-Msgid-Bugs-To: \n"
"PO-Revision-Date: \n"
"Last-Translator: \n"
"Language-Team: \n"
"Plural-Forms: \n"
#: src/routes/account/_minimalLayout/sign-in.tsx:155
msgid "@handle or email"
msgstr ""
#: src/routes/account/_minimalLayout/sign-in.tsx:49
msgid "← Back to accounts"
msgstr ""
#: src/data/useClientName.ts:24
msgid "A local app"
msgstr ""
#: src/routes/account/_minimalLayout/reset-password.tsx:65
msgid "A new password is required"
msgstr ""
#: src/routes/account/_minimalLayout/index.tsx:64
msgid "Accounts"
msgstr ""
#: src/routes/account/_minimalLayout/sign-in.tsx:125
#: src/routes/account/_minimalLayout/reset-password.tsx:51
#: src/routes/account/_minimalLayout/reset-password.tsx:78
msgid "An error occurred, please try again."
msgstr ""
#: src/routes/account/_appLayout/$sub.tsx:270
msgid "Are you sure you want to remove this device?"
msgstr ""
#: src/routes/account/_appLayout/$sub.tsx:176
msgid "Are you sure you want to revoke access? This application won't be able to access your account anymore."
msgstr ""
#: src/routes/account/_minimalLayout/reset-password.tsx:122
#: src/routes/account/_minimalLayout/reset-password.tsx:195
msgid "Back to sign in"
msgstr ""
#: src/components/Prompt.tsx:56
msgid "Cancel"
msgstr ""
#: src/routes/account/_minimalLayout/reset-password.tsx:319
msgid "Click here to send a new code to your email."
msgstr ""
#: src/routes/account/_minimalLayout/sign-in.tsx:193
#: src/routes/account/_minimalLayout/reset-password.tsx:222
msgid "Code"
msgstr ""
#: src/routes/account/_minimalLayout/reset-password.tsx:88
msgid "Code was resent"
msgstr ""
#: src/routes/account/_appLayout/$sub.tsx:52
msgid "Connected apps"
msgstr ""
#: src/routes/account/_minimalLayout/sign-in.tsx:143
msgid "Credentials"
msgstr ""
#: src/routes/account/_minimalLayout/reset-password.tsx:314
msgid "Don't see the email? <0>Try sending again.</0>"
msgstr ""
#: src/routes/account/_minimalLayout/reset-password.tsx:149
msgid "Email"
msgstr ""
#: src/routes/account/_minimalLayout/reset-password.tsx:62
msgid "Email code is required"
msgstr ""
#: src/routes/account/_minimalLayout/reset-password.tsx:42
msgid "Email is required"
msgstr ""
#: src/routes/account/_minimalLayout/reset-password.tsx:264
msgid "Enter a new password"
msgstr ""
#: src/routes/account/_minimalLayout/reset-password.tsx:154
msgid "Enter your email"
msgstr ""
#: src/routes/account/_minimalLayout/reset-password.tsx:132
msgid "Enter your email to receive a reset code."
msgstr ""
#: src/routes/account/_appLayout/$sub.tsx:60
msgid "Failed to load connected apps"
msgstr ""
#: src/routes/account/_appLayout/$sub.tsx:91
msgid "Failed to load devices"
msgstr ""
#: src/routes/account/_appLayout/$sub.tsx:222
msgid "Failed to remove device"
msgstr ""
#: src/routes/account/_minimalLayout/reset-password.tsx:94
msgid "Failed to resend code"
msgstr ""
#: src/routes/account/_appLayout/$sub.tsx:147
msgid "Failed to sign out"
msgstr ""
#: src/routes/account/_minimalLayout/sign-in.tsx:235
msgid "Forgot password?"
msgstr ""
#: src/routes/account/_minimalLayout/reset-password.tsx:186
msgid "Get reset code"
msgstr ""
#: src/routes/account/_appLayout/$sub.tsx:42
msgid "Home"
msgstr ""
#: src/routes/account/_minimalLayout/sign-in.tsx:150
msgid "Identifier"
msgstr ""
#: src/routes/account/_minimalLayout/reset-password.tsx:41
msgid "Invalid email"
msgstr ""
#: src/routes/account/_minimalLayout/sign-in.tsx:83
msgid "Invalid handle"
msgstr ""
#: src/routes/account/_minimalLayout/sign-in.tsx:123
msgid "Invalid identifier or password."
msgstr ""
#: src/routes/account/_appLayout/$sub.tsx:77
msgid "It appears that you havent used this account to sign in to any apps yet."
msgstr ""
#: src/routes/account/_minimalLayout.tsx:21
#: src/components/Nav.tsx:18
msgid "Logo"
msgstr ""
#: src/routes/account/_appLayout/$sub.tsx:107
msgid "Looks like you aren't logged in on any other devices."
msgstr ""
#: src/data/useFriendlyClientId.ts:13
msgid "loopback"
msgstr ""
#: src/routes/account/_appLayout/$sub.tsx:83
msgid "My devices"
msgstr ""
#: src/routes/account/_appLayout/$sub.tsx:75
msgid "No connected apps"
msgstr ""
#: src/routes/account/_appLayout/$sub.tsx:106
msgid "No devices"
msgstr ""
#: src/routes/account/_minimalLayout/sign-in.tsx:170
#: src/routes/account/_minimalLayout/sign-in.tsx:176
#: src/routes/account/_minimalLayout/reset-password.tsx:257
msgid "Password"
msgstr ""
#: src/routes/account/_minimalLayout/sign-in.tsx:88
msgid "Password is required"
msgstr ""
#: src/routes/account/_minimalLayout/reset-password.tsx:68
msgid "Password must be at least {MIN_PASSWORD_LENGTH} characters"
msgstr ""
#: src/routes/account/_appLayout/$sub.tsx:271
#: src/routes/account/_appLayout/$sub.tsx:276
msgid "Remove"
msgstr ""
#: src/routes/account/_appLayout/$sub.tsx:269
msgid "Remove this device"
msgstr ""
#: src/routes/account/_minimalLayout/reset-password.tsx:129
#: src/routes/account/_minimalLayout/reset-password.tsx:308
msgid "Reset password"
msgstr ""
#: src/routes/account/_appLayout/$sub.tsx:172
msgid "Revoke access to {clientName}"
msgstr ""
#: src/routes/account/_appLayout/$sub.tsx:173
msgid "Revoke access to this application"
msgstr ""
#: src/components/AccountSelector.tsx:32
msgid "Select an account"
msgstr ""
#: src/routes/account/_minimalLayout/index.tsx:67
msgid "Select the account you would like to manage."
msgstr ""
#: src/routes/account/_minimalLayout/sign-in.tsx:134
#: src/routes/account/_minimalLayout/sign-in.tsx:226
msgid "Sign in"
msgstr ""
#: src/routes/account/_minimalLayout/index.tsx:107
msgid "Sign in with another account"
msgstr ""
#: src/routes/account/_appLayout/$sub.tsx:178
#: src/routes/account/_appLayout/$sub.tsx:183
#: src/components/AccountSelector.tsx:103
msgid "Sign out"
msgstr ""
#: src/routes/account/_minimalLayout/index.tsx:39
msgid "Something went wrong"
msgstr ""
#: src/routes/account/_minimalLayout/reset-password.tsx:115
msgid "Success!"
msgstr ""
#: src/routes/account/_appLayout/$sub.tsx:216
msgid "Successfully removed device"
msgstr ""
#: src/routes/account/_appLayout/$sub.tsx:141
msgid "Successfully signed out"
msgstr ""
#: src/routes/account/_appLayout/$sub.tsx:263
msgid "This device"
msgstr ""
#: src/routes/account/_appLayout/$sub.tsx:247
msgid "Unknown user agent"
msgstr ""
#: src/components/Avatar.tsx:24
msgid "User avatar"
msgstr ""
#. placeholder {0}: getAccountName(account)
#: src/routes/account/_minimalLayout/index.tsx:83
msgid "View and manage account for {0}"
msgstr ""
#: src/routes/account/_minimalLayout/index.tsx:41
msgid "We weren't able to load your accounts. Please refresh the page to try again."
msgstr ""
#: src/components/ErrorScreen.tsx:19
msgid "Whoops! An error occurred."
msgstr ""
#: src/routes/account/_minimalLayout/sign-in.tsx:198
#: src/routes/account/_minimalLayout/reset-password.tsx:228
msgid "XXXXX-XXXXX"
msgstr ""
#: src/routes/account/_appLayout/$sub.tsx:47
msgid "Your account"
msgstr ""
#: src/routes/account/_minimalLayout/reset-password.tsx:118
msgid "Your password has been reset."
msgstr ""

View File

@ -0,0 +1,296 @@
msgid ""
msgstr ""
"POT-Creation-Date: 2025-03-19 13:49-0500\n"
"MIME-Version: 1.0\n"
"Content-Type: text/plain; charset=utf-8\n"
"Content-Transfer-Encoding: 8bit\n"
"X-Generator: @lingui/cli\n"
"Language: el\n"
"Project-Id-Version: \n"
"Report-Msgid-Bugs-To: \n"
"PO-Revision-Date: \n"
"Last-Translator: \n"
"Language-Team: \n"
"Plural-Forms: \n"
#: src/routes/account/_minimalLayout/sign-in.tsx:155
msgid "@handle or email"
msgstr ""
#: src/routes/account/_minimalLayout/sign-in.tsx:49
msgid "← Back to accounts"
msgstr ""
#: src/data/useClientName.ts:24
msgid "A local app"
msgstr ""
#: src/routes/account/_minimalLayout/reset-password.tsx:65
msgid "A new password is required"
msgstr ""
#: src/routes/account/_minimalLayout/index.tsx:64
msgid "Accounts"
msgstr ""
#: src/routes/account/_minimalLayout/sign-in.tsx:125
#: src/routes/account/_minimalLayout/reset-password.tsx:51
#: src/routes/account/_minimalLayout/reset-password.tsx:78
msgid "An error occurred, please try again."
msgstr ""
#: src/routes/account/_appLayout/$sub.tsx:270
msgid "Are you sure you want to remove this device?"
msgstr ""
#: src/routes/account/_appLayout/$sub.tsx:176
msgid "Are you sure you want to revoke access? This application won't be able to access your account anymore."
msgstr ""
#: src/routes/account/_minimalLayout/reset-password.tsx:122
#: src/routes/account/_minimalLayout/reset-password.tsx:195
msgid "Back to sign in"
msgstr ""
#: src/components/Prompt.tsx:56
msgid "Cancel"
msgstr ""
#: src/routes/account/_minimalLayout/reset-password.tsx:319
msgid "Click here to send a new code to your email."
msgstr ""
#: src/routes/account/_minimalLayout/sign-in.tsx:193
#: src/routes/account/_minimalLayout/reset-password.tsx:222
msgid "Code"
msgstr ""
#: src/routes/account/_minimalLayout/reset-password.tsx:88
msgid "Code was resent"
msgstr ""
#: src/routes/account/_appLayout/$sub.tsx:52
msgid "Connected apps"
msgstr ""
#: src/routes/account/_minimalLayout/sign-in.tsx:143
msgid "Credentials"
msgstr ""
#: src/routes/account/_minimalLayout/reset-password.tsx:314
msgid "Don't see the email? <0>Try sending again.</0>"
msgstr ""
#: src/routes/account/_minimalLayout/reset-password.tsx:149
msgid "Email"
msgstr ""
#: src/routes/account/_minimalLayout/reset-password.tsx:62
msgid "Email code is required"
msgstr ""
#: src/routes/account/_minimalLayout/reset-password.tsx:42
msgid "Email is required"
msgstr ""
#: src/routes/account/_minimalLayout/reset-password.tsx:264
msgid "Enter a new password"
msgstr ""
#: src/routes/account/_minimalLayout/reset-password.tsx:154
msgid "Enter your email"
msgstr ""
#: src/routes/account/_minimalLayout/reset-password.tsx:132
msgid "Enter your email to receive a reset code."
msgstr ""
#: src/routes/account/_appLayout/$sub.tsx:60
msgid "Failed to load connected apps"
msgstr ""
#: src/routes/account/_appLayout/$sub.tsx:91
msgid "Failed to load devices"
msgstr ""
#: src/routes/account/_appLayout/$sub.tsx:222
msgid "Failed to remove device"
msgstr ""
#: src/routes/account/_minimalLayout/reset-password.tsx:94
msgid "Failed to resend code"
msgstr ""
#: src/routes/account/_appLayout/$sub.tsx:147
msgid "Failed to sign out"
msgstr ""
#: src/routes/account/_minimalLayout/sign-in.tsx:235
msgid "Forgot password?"
msgstr ""
#: src/routes/account/_minimalLayout/reset-password.tsx:186
msgid "Get reset code"
msgstr ""
#: src/routes/account/_appLayout/$sub.tsx:42
msgid "Home"
msgstr ""
#: src/routes/account/_minimalLayout/sign-in.tsx:150
msgid "Identifier"
msgstr ""
#: src/routes/account/_minimalLayout/reset-password.tsx:41
msgid "Invalid email"
msgstr ""
#: src/routes/account/_minimalLayout/sign-in.tsx:83
msgid "Invalid handle"
msgstr ""
#: src/routes/account/_minimalLayout/sign-in.tsx:123
msgid "Invalid identifier or password."
msgstr ""
#: src/routes/account/_appLayout/$sub.tsx:77
msgid "It appears that you havent used this account to sign in to any apps yet."
msgstr ""
#: src/routes/account/_minimalLayout.tsx:21
#: src/components/Nav.tsx:18
msgid "Logo"
msgstr ""
#: src/routes/account/_appLayout/$sub.tsx:107
msgid "Looks like you aren't logged in on any other devices."
msgstr ""
#: src/data/useFriendlyClientId.ts:13
msgid "loopback"
msgstr ""
#: src/routes/account/_appLayout/$sub.tsx:83
msgid "My devices"
msgstr ""
#: src/routes/account/_appLayout/$sub.tsx:75
msgid "No connected apps"
msgstr ""
#: src/routes/account/_appLayout/$sub.tsx:106
msgid "No devices"
msgstr ""
#: src/routes/account/_minimalLayout/sign-in.tsx:170
#: src/routes/account/_minimalLayout/sign-in.tsx:176
#: src/routes/account/_minimalLayout/reset-password.tsx:257
msgid "Password"
msgstr ""
#: src/routes/account/_minimalLayout/sign-in.tsx:88
msgid "Password is required"
msgstr ""
#: src/routes/account/_minimalLayout/reset-password.tsx:68
msgid "Password must be at least {MIN_PASSWORD_LENGTH} characters"
msgstr ""
#: src/routes/account/_appLayout/$sub.tsx:271
#: src/routes/account/_appLayout/$sub.tsx:276
msgid "Remove"
msgstr ""
#: src/routes/account/_appLayout/$sub.tsx:269
msgid "Remove this device"
msgstr ""
#: src/routes/account/_minimalLayout/reset-password.tsx:129
#: src/routes/account/_minimalLayout/reset-password.tsx:308
msgid "Reset password"
msgstr ""
#: src/routes/account/_appLayout/$sub.tsx:172
msgid "Revoke access to {clientName}"
msgstr ""
#: src/routes/account/_appLayout/$sub.tsx:173
msgid "Revoke access to this application"
msgstr ""
#: src/components/AccountSelector.tsx:32
msgid "Select an account"
msgstr ""
#: src/routes/account/_minimalLayout/index.tsx:67
msgid "Select the account you would like to manage."
msgstr ""
#: src/routes/account/_minimalLayout/sign-in.tsx:134
#: src/routes/account/_minimalLayout/sign-in.tsx:226
msgid "Sign in"
msgstr ""
#: src/routes/account/_minimalLayout/index.tsx:107
msgid "Sign in with another account"
msgstr ""
#: src/routes/account/_appLayout/$sub.tsx:178
#: src/routes/account/_appLayout/$sub.tsx:183
#: src/components/AccountSelector.tsx:103
msgid "Sign out"
msgstr ""
#: src/routes/account/_minimalLayout/index.tsx:39
msgid "Something went wrong"
msgstr ""
#: src/routes/account/_minimalLayout/reset-password.tsx:115
msgid "Success!"
msgstr ""
#: src/routes/account/_appLayout/$sub.tsx:216
msgid "Successfully removed device"
msgstr ""
#: src/routes/account/_appLayout/$sub.tsx:141
msgid "Successfully signed out"
msgstr ""
#: src/routes/account/_appLayout/$sub.tsx:263
msgid "This device"
msgstr ""
#: src/routes/account/_appLayout/$sub.tsx:247
msgid "Unknown user agent"
msgstr ""
#: src/components/Avatar.tsx:24
msgid "User avatar"
msgstr ""
#. placeholder {0}: getAccountName(account)
#: src/routes/account/_minimalLayout/index.tsx:83
msgid "View and manage account for {0}"
msgstr ""
#: src/routes/account/_minimalLayout/index.tsx:41
msgid "We weren't able to load your accounts. Please refresh the page to try again."
msgstr ""
#: src/components/ErrorScreen.tsx:19
msgid "Whoops! An error occurred."
msgstr ""
#: src/routes/account/_minimalLayout/sign-in.tsx:198
#: src/routes/account/_minimalLayout/reset-password.tsx:228
msgid "XXXXX-XXXXX"
msgstr ""
#: src/routes/account/_appLayout/$sub.tsx:47
msgid "Your account"
msgstr ""
#: src/routes/account/_minimalLayout/reset-password.tsx:118
msgid "Your password has been reset."
msgstr ""

View File

@ -0,0 +1,296 @@
msgid ""
msgstr ""
"POT-Creation-Date: 2025-03-19 13:49-0500\n"
"MIME-Version: 1.0\n"
"Content-Type: text/plain; charset=utf-8\n"
"Content-Transfer-Encoding: 8bit\n"
"X-Generator: @lingui/cli\n"
"Language: en-GB\n"
"Project-Id-Version: \n"
"Report-Msgid-Bugs-To: \n"
"PO-Revision-Date: \n"
"Last-Translator: \n"
"Language-Team: \n"
"Plural-Forms: \n"
#: src/routes/account/_minimalLayout/sign-in.tsx:155
msgid "@handle or email"
msgstr ""
#: src/routes/account/_minimalLayout/sign-in.tsx:49
msgid "← Back to accounts"
msgstr ""
#: src/data/useClientName.ts:24
msgid "A local app"
msgstr ""
#: src/routes/account/_minimalLayout/reset-password.tsx:65
msgid "A new password is required"
msgstr ""
#: src/routes/account/_minimalLayout/index.tsx:64
msgid "Accounts"
msgstr ""
#: src/routes/account/_minimalLayout/sign-in.tsx:125
#: src/routes/account/_minimalLayout/reset-password.tsx:51
#: src/routes/account/_minimalLayout/reset-password.tsx:78
msgid "An error occurred, please try again."
msgstr ""
#: src/routes/account/_appLayout/$sub.tsx:270
msgid "Are you sure you want to remove this device?"
msgstr ""
#: src/routes/account/_appLayout/$sub.tsx:176
msgid "Are you sure you want to revoke access? This application won't be able to access your account anymore."
msgstr ""
#: src/routes/account/_minimalLayout/reset-password.tsx:122
#: src/routes/account/_minimalLayout/reset-password.tsx:195
msgid "Back to sign in"
msgstr ""
#: src/components/Prompt.tsx:56
msgid "Cancel"
msgstr ""
#: src/routes/account/_minimalLayout/reset-password.tsx:319
msgid "Click here to send a new code to your email."
msgstr ""
#: src/routes/account/_minimalLayout/sign-in.tsx:193
#: src/routes/account/_minimalLayout/reset-password.tsx:222
msgid "Code"
msgstr ""
#: src/routes/account/_minimalLayout/reset-password.tsx:88
msgid "Code was resent"
msgstr ""
#: src/routes/account/_appLayout/$sub.tsx:52
msgid "Connected apps"
msgstr ""
#: src/routes/account/_minimalLayout/sign-in.tsx:143
msgid "Credentials"
msgstr ""
#: src/routes/account/_minimalLayout/reset-password.tsx:314
msgid "Don't see the email? <0>Try sending again.</0>"
msgstr ""
#: src/routes/account/_minimalLayout/reset-password.tsx:149
msgid "Email"
msgstr ""
#: src/routes/account/_minimalLayout/reset-password.tsx:62
msgid "Email code is required"
msgstr ""
#: src/routes/account/_minimalLayout/reset-password.tsx:42
msgid "Email is required"
msgstr ""
#: src/routes/account/_minimalLayout/reset-password.tsx:264
msgid "Enter a new password"
msgstr ""
#: src/routes/account/_minimalLayout/reset-password.tsx:154
msgid "Enter your email"
msgstr ""
#: src/routes/account/_minimalLayout/reset-password.tsx:132
msgid "Enter your email to receive a reset code."
msgstr ""
#: src/routes/account/_appLayout/$sub.tsx:60
msgid "Failed to load connected apps"
msgstr ""
#: src/routes/account/_appLayout/$sub.tsx:91
msgid "Failed to load devices"
msgstr ""
#: src/routes/account/_appLayout/$sub.tsx:222
msgid "Failed to remove device"
msgstr ""
#: src/routes/account/_minimalLayout/reset-password.tsx:94
msgid "Failed to resend code"
msgstr ""
#: src/routes/account/_appLayout/$sub.tsx:147
msgid "Failed to sign out"
msgstr ""
#: src/routes/account/_minimalLayout/sign-in.tsx:235
msgid "Forgot password?"
msgstr ""
#: src/routes/account/_minimalLayout/reset-password.tsx:186
msgid "Get reset code"
msgstr ""
#: src/routes/account/_appLayout/$sub.tsx:42
msgid "Home"
msgstr ""
#: src/routes/account/_minimalLayout/sign-in.tsx:150
msgid "Identifier"
msgstr ""
#: src/routes/account/_minimalLayout/reset-password.tsx:41
msgid "Invalid email"
msgstr ""
#: src/routes/account/_minimalLayout/sign-in.tsx:83
msgid "Invalid handle"
msgstr ""
#: src/routes/account/_minimalLayout/sign-in.tsx:123
msgid "Invalid identifier or password."
msgstr ""
#: src/routes/account/_appLayout/$sub.tsx:77
msgid "It appears that you havent used this account to sign in to any apps yet."
msgstr ""
#: src/routes/account/_minimalLayout.tsx:21
#: src/components/Nav.tsx:18
msgid "Logo"
msgstr ""
#: src/routes/account/_appLayout/$sub.tsx:107
msgid "Looks like you aren't logged in on any other devices."
msgstr ""
#: src/data/useFriendlyClientId.ts:13
msgid "loopback"
msgstr ""
#: src/routes/account/_appLayout/$sub.tsx:83
msgid "My devices"
msgstr ""
#: src/routes/account/_appLayout/$sub.tsx:75
msgid "No connected apps"
msgstr ""
#: src/routes/account/_appLayout/$sub.tsx:106
msgid "No devices"
msgstr ""
#: src/routes/account/_minimalLayout/sign-in.tsx:170
#: src/routes/account/_minimalLayout/sign-in.tsx:176
#: src/routes/account/_minimalLayout/reset-password.tsx:257
msgid "Password"
msgstr ""
#: src/routes/account/_minimalLayout/sign-in.tsx:88
msgid "Password is required"
msgstr ""
#: src/routes/account/_minimalLayout/reset-password.tsx:68
msgid "Password must be at least {MIN_PASSWORD_LENGTH} characters"
msgstr ""
#: src/routes/account/_appLayout/$sub.tsx:271
#: src/routes/account/_appLayout/$sub.tsx:276
msgid "Remove"
msgstr ""
#: src/routes/account/_appLayout/$sub.tsx:269
msgid "Remove this device"
msgstr ""
#: src/routes/account/_minimalLayout/reset-password.tsx:129
#: src/routes/account/_minimalLayout/reset-password.tsx:308
msgid "Reset password"
msgstr ""
#: src/routes/account/_appLayout/$sub.tsx:172
msgid "Revoke access to {clientName}"
msgstr ""
#: src/routes/account/_appLayout/$sub.tsx:173
msgid "Revoke access to this application"
msgstr ""
#: src/components/AccountSelector.tsx:32
msgid "Select an account"
msgstr ""
#: src/routes/account/_minimalLayout/index.tsx:67
msgid "Select the account you would like to manage."
msgstr ""
#: src/routes/account/_minimalLayout/sign-in.tsx:134
#: src/routes/account/_minimalLayout/sign-in.tsx:226
msgid "Sign in"
msgstr ""
#: src/routes/account/_minimalLayout/index.tsx:107
msgid "Sign in with another account"
msgstr ""
#: src/routes/account/_appLayout/$sub.tsx:178
#: src/routes/account/_appLayout/$sub.tsx:183
#: src/components/AccountSelector.tsx:103
msgid "Sign out"
msgstr ""
#: src/routes/account/_minimalLayout/index.tsx:39
msgid "Something went wrong"
msgstr ""
#: src/routes/account/_minimalLayout/reset-password.tsx:115
msgid "Success!"
msgstr ""
#: src/routes/account/_appLayout/$sub.tsx:216
msgid "Successfully removed device"
msgstr ""
#: src/routes/account/_appLayout/$sub.tsx:141
msgid "Successfully signed out"
msgstr ""
#: src/routes/account/_appLayout/$sub.tsx:263
msgid "This device"
msgstr ""
#: src/routes/account/_appLayout/$sub.tsx:247
msgid "Unknown user agent"
msgstr ""
#: src/components/Avatar.tsx:24
msgid "User avatar"
msgstr ""
#. placeholder {0}: getAccountName(account)
#: src/routes/account/_minimalLayout/index.tsx:83
msgid "View and manage account for {0}"
msgstr ""
#: src/routes/account/_minimalLayout/index.tsx:41
msgid "We weren't able to load your accounts. Please refresh the page to try again."
msgstr ""
#: src/components/ErrorScreen.tsx:19
msgid "Whoops! An error occurred."
msgstr ""
#: src/routes/account/_minimalLayout/sign-in.tsx:198
#: src/routes/account/_minimalLayout/reset-password.tsx:228
msgid "XXXXX-XXXXX"
msgstr ""
#: src/routes/account/_appLayout/$sub.tsx:47
msgid "Your account"
msgstr ""
#: src/routes/account/_minimalLayout/reset-password.tsx:118
msgid "Your password has been reset."
msgstr ""

View File

@ -0,0 +1,296 @@
msgid ""
msgstr ""
"POT-Creation-Date: 2025-03-19 13:49-0500\n"
"MIME-Version: 1.0\n"
"Content-Type: text/plain; charset=utf-8\n"
"Content-Transfer-Encoding: 8bit\n"
"X-Generator: @lingui/cli\n"
"Language: en\n"
"Project-Id-Version: \n"
"Report-Msgid-Bugs-To: \n"
"PO-Revision-Date: \n"
"Last-Translator: \n"
"Language-Team: \n"
"Plural-Forms: \n"
#: src/routes/account/_minimalLayout/sign-in.tsx:155
msgid "@handle or email"
msgstr "@handle or email"
#: src/routes/account/_minimalLayout/sign-in.tsx:49
msgid "← Back to accounts"
msgstr "← Back to accounts"
#: src/data/useClientName.ts:24
msgid "A local app"
msgstr "A local app"
#: src/routes/account/_minimalLayout/reset-password.tsx:65
msgid "A new password is required"
msgstr "A new password is required"
#: src/routes/account/_minimalLayout/index.tsx:64
msgid "Accounts"
msgstr "Accounts"
#: src/routes/account/_minimalLayout/sign-in.tsx:125
#: src/routes/account/_minimalLayout/reset-password.tsx:51
#: src/routes/account/_minimalLayout/reset-password.tsx:78
msgid "An error occurred, please try again."
msgstr "An error occurred, please try again."
#: src/routes/account/_appLayout/$sub.tsx:270
msgid "Are you sure you want to remove this device?"
msgstr "Are you sure you want to remove this device?"
#: src/routes/account/_appLayout/$sub.tsx:176
msgid "Are you sure you want to revoke access? This application won't be able to access your account anymore."
msgstr "Are you sure you want to revoke access? This application won't be able to access your account anymore."
#: src/routes/account/_minimalLayout/reset-password.tsx:122
#: src/routes/account/_minimalLayout/reset-password.tsx:195
msgid "Back to sign in"
msgstr "Back to sign in"
#: src/components/Prompt.tsx:56
msgid "Cancel"
msgstr "Cancel"
#: src/routes/account/_minimalLayout/reset-password.tsx:319
msgid "Click here to send a new code to your email."
msgstr "Click here to send a new code to your email."
#: src/routes/account/_minimalLayout/sign-in.tsx:193
#: src/routes/account/_minimalLayout/reset-password.tsx:222
msgid "Code"
msgstr "Code"
#: src/routes/account/_minimalLayout/reset-password.tsx:88
msgid "Code was resent"
msgstr "Code was resent"
#: src/routes/account/_appLayout/$sub.tsx:52
msgid "Connected apps"
msgstr "Connected apps"
#: src/routes/account/_minimalLayout/sign-in.tsx:143
msgid "Credentials"
msgstr "Credentials"
#: src/routes/account/_minimalLayout/reset-password.tsx:314
msgid "Don't see the email? <0>Try sending again.</0>"
msgstr "Don't see the email? <0>Try sending again.</0>"
#: src/routes/account/_minimalLayout/reset-password.tsx:149
msgid "Email"
msgstr "Email"
#: src/routes/account/_minimalLayout/reset-password.tsx:62
msgid "Email code is required"
msgstr "Email code is required"
#: src/routes/account/_minimalLayout/reset-password.tsx:42
msgid "Email is required"
msgstr "Email is required"
#: src/routes/account/_minimalLayout/reset-password.tsx:264
msgid "Enter a new password"
msgstr "Enter a new password"
#: src/routes/account/_minimalLayout/reset-password.tsx:154
msgid "Enter your email"
msgstr "Enter your email"
#: src/routes/account/_minimalLayout/reset-password.tsx:132
msgid "Enter your email to receive a reset code."
msgstr "Enter your email to receive a reset code."
#: src/routes/account/_appLayout/$sub.tsx:60
msgid "Failed to load connected apps"
msgstr "Failed to load connected apps"
#: src/routes/account/_appLayout/$sub.tsx:91
msgid "Failed to load devices"
msgstr "Failed to load devices"
#: src/routes/account/_appLayout/$sub.tsx:222
msgid "Failed to remove device"
msgstr "Failed to remove device"
#: src/routes/account/_minimalLayout/reset-password.tsx:94
msgid "Failed to resend code"
msgstr "Failed to resend code"
#: src/routes/account/_appLayout/$sub.tsx:147
msgid "Failed to sign out"
msgstr "Failed to sign out"
#: src/routes/account/_minimalLayout/sign-in.tsx:235
msgid "Forgot password?"
msgstr "Forgot password?"
#: src/routes/account/_minimalLayout/reset-password.tsx:186
msgid "Get reset code"
msgstr "Get reset code"
#: src/routes/account/_appLayout/$sub.tsx:42
msgid "Home"
msgstr "Home"
#: src/routes/account/_minimalLayout/sign-in.tsx:150
msgid "Identifier"
msgstr "Identifier"
#: src/routes/account/_minimalLayout/reset-password.tsx:41
msgid "Invalid email"
msgstr "Invalid email"
#: src/routes/account/_minimalLayout/sign-in.tsx:83
msgid "Invalid handle"
msgstr "Invalid handle"
#: src/routes/account/_minimalLayout/sign-in.tsx:123
msgid "Invalid identifier or password."
msgstr "Invalid identifier or password."
#: src/routes/account/_appLayout/$sub.tsx:77
msgid "It appears that you havent used this account to sign in to any apps yet."
msgstr "It appears that you havent used this account to sign in to any apps yet."
#: src/routes/account/_minimalLayout.tsx:21
#: src/components/Nav.tsx:18
msgid "Logo"
msgstr "Logo"
#: src/routes/account/_appLayout/$sub.tsx:107
msgid "Looks like you aren't logged in on any other devices."
msgstr "Looks like you aren't logged in on any other devices."
#: src/data/useFriendlyClientId.ts:13
msgid "loopback"
msgstr "loopback"
#: src/routes/account/_appLayout/$sub.tsx:83
msgid "My devices"
msgstr "My devices"
#: src/routes/account/_appLayout/$sub.tsx:75
msgid "No connected apps"
msgstr "No connected apps"
#: src/routes/account/_appLayout/$sub.tsx:106
msgid "No devices"
msgstr "No devices"
#: src/routes/account/_minimalLayout/sign-in.tsx:170
#: src/routes/account/_minimalLayout/sign-in.tsx:176
#: src/routes/account/_minimalLayout/reset-password.tsx:257
msgid "Password"
msgstr "Password"
#: src/routes/account/_minimalLayout/sign-in.tsx:88
msgid "Password is required"
msgstr "Password is required"
#: src/routes/account/_minimalLayout/reset-password.tsx:68
msgid "Password must be at least {MIN_PASSWORD_LENGTH} characters"
msgstr "Password must be at least {MIN_PASSWORD_LENGTH} characters"
#: src/routes/account/_appLayout/$sub.tsx:271
#: src/routes/account/_appLayout/$sub.tsx:276
msgid "Remove"
msgstr "Remove"
#: src/routes/account/_appLayout/$sub.tsx:269
msgid "Remove this device"
msgstr "Remove this device"
#: src/routes/account/_minimalLayout/reset-password.tsx:129
#: src/routes/account/_minimalLayout/reset-password.tsx:308
msgid "Reset password"
msgstr "Reset password"
#: src/routes/account/_appLayout/$sub.tsx:172
msgid "Revoke access to {clientName}"
msgstr "Revoke access to {clientName}"
#: src/routes/account/_appLayout/$sub.tsx:173
msgid "Revoke access to this application"
msgstr "Revoke access to this application"
#: src/components/AccountSelector.tsx:32
msgid "Select an account"
msgstr "Select an account"
#: src/routes/account/_minimalLayout/index.tsx:67
msgid "Select the account you would like to manage."
msgstr "Select the account you would like to manage."
#: src/routes/account/_minimalLayout/sign-in.tsx:134
#: src/routes/account/_minimalLayout/sign-in.tsx:226
msgid "Sign in"
msgstr "Sign in"
#: src/routes/account/_minimalLayout/index.tsx:107
msgid "Sign in with another account"
msgstr "Sign in with another account"
#: src/routes/account/_appLayout/$sub.tsx:178
#: src/routes/account/_appLayout/$sub.tsx:183
#: src/components/AccountSelector.tsx:103
msgid "Sign out"
msgstr "Sign out"
#: src/routes/account/_minimalLayout/index.tsx:39
msgid "Something went wrong"
msgstr "Something went wrong"
#: src/routes/account/_minimalLayout/reset-password.tsx:115
msgid "Success!"
msgstr "Success!"
#: src/routes/account/_appLayout/$sub.tsx:216
msgid "Successfully removed device"
msgstr "Successfully removed device"
#: src/routes/account/_appLayout/$sub.tsx:141
msgid "Successfully signed out"
msgstr "Successfully signed out"
#: src/routes/account/_appLayout/$sub.tsx:263
msgid "This device"
msgstr "This device"
#: src/routes/account/_appLayout/$sub.tsx:247
msgid "Unknown user agent"
msgstr "Unknown user agent"
#: src/components/Avatar.tsx:24
msgid "User avatar"
msgstr "User avatar"
#. placeholder {0}: getAccountName(account)
#: src/routes/account/_minimalLayout/index.tsx:83
msgid "View and manage account for {0}"
msgstr "View and manage account for {0}"
#: src/routes/account/_minimalLayout/index.tsx:41
msgid "We weren't able to load your accounts. Please refresh the page to try again."
msgstr "We weren't able to load your accounts. Please refresh the page to try again."
#: src/components/ErrorScreen.tsx:19
msgid "Whoops! An error occurred."
msgstr "Whoops! An error occurred."
#: src/routes/account/_minimalLayout/sign-in.tsx:198
#: src/routes/account/_minimalLayout/reset-password.tsx:228
msgid "XXXXX-XXXXX"
msgstr "XXXXX-XXXXX"
#: src/routes/account/_appLayout/$sub.tsx:47
msgid "Your account"
msgstr "Your account"
#: src/routes/account/_minimalLayout/reset-password.tsx:118
msgid "Your password has been reset."
msgstr "Your password has been reset."

View File

@ -0,0 +1,296 @@
msgid ""
msgstr ""
"POT-Creation-Date: 2025-03-19 13:49-0500\n"
"MIME-Version: 1.0\n"
"Content-Type: text/plain; charset=utf-8\n"
"Content-Transfer-Encoding: 8bit\n"
"X-Generator: @lingui/cli\n"
"Language: es\n"
"Project-Id-Version: \n"
"Report-Msgid-Bugs-To: \n"
"PO-Revision-Date: \n"
"Last-Translator: \n"
"Language-Team: \n"
"Plural-Forms: \n"
#: src/routes/account/_minimalLayout/sign-in.tsx:155
msgid "@handle or email"
msgstr ""
#: src/routes/account/_minimalLayout/sign-in.tsx:49
msgid "← Back to accounts"
msgstr ""
#: src/data/useClientName.ts:24
msgid "A local app"
msgstr ""
#: src/routes/account/_minimalLayout/reset-password.tsx:65
msgid "A new password is required"
msgstr ""
#: src/routes/account/_minimalLayout/index.tsx:64
msgid "Accounts"
msgstr ""
#: src/routes/account/_minimalLayout/sign-in.tsx:125
#: src/routes/account/_minimalLayout/reset-password.tsx:51
#: src/routes/account/_minimalLayout/reset-password.tsx:78
msgid "An error occurred, please try again."
msgstr ""
#: src/routes/account/_appLayout/$sub.tsx:270
msgid "Are you sure you want to remove this device?"
msgstr ""
#: src/routes/account/_appLayout/$sub.tsx:176
msgid "Are you sure you want to revoke access? This application won't be able to access your account anymore."
msgstr ""
#: src/routes/account/_minimalLayout/reset-password.tsx:122
#: src/routes/account/_minimalLayout/reset-password.tsx:195
msgid "Back to sign in"
msgstr ""
#: src/components/Prompt.tsx:56
msgid "Cancel"
msgstr ""
#: src/routes/account/_minimalLayout/reset-password.tsx:319
msgid "Click here to send a new code to your email."
msgstr ""
#: src/routes/account/_minimalLayout/sign-in.tsx:193
#: src/routes/account/_minimalLayout/reset-password.tsx:222
msgid "Code"
msgstr ""
#: src/routes/account/_minimalLayout/reset-password.tsx:88
msgid "Code was resent"
msgstr ""
#: src/routes/account/_appLayout/$sub.tsx:52
msgid "Connected apps"
msgstr ""
#: src/routes/account/_minimalLayout/sign-in.tsx:143
msgid "Credentials"
msgstr ""
#: src/routes/account/_minimalLayout/reset-password.tsx:314
msgid "Don't see the email? <0>Try sending again.</0>"
msgstr ""
#: src/routes/account/_minimalLayout/reset-password.tsx:149
msgid "Email"
msgstr ""
#: src/routes/account/_minimalLayout/reset-password.tsx:62
msgid "Email code is required"
msgstr ""
#: src/routes/account/_minimalLayout/reset-password.tsx:42
msgid "Email is required"
msgstr ""
#: src/routes/account/_minimalLayout/reset-password.tsx:264
msgid "Enter a new password"
msgstr ""
#: src/routes/account/_minimalLayout/reset-password.tsx:154
msgid "Enter your email"
msgstr ""
#: src/routes/account/_minimalLayout/reset-password.tsx:132
msgid "Enter your email to receive a reset code."
msgstr ""
#: src/routes/account/_appLayout/$sub.tsx:60
msgid "Failed to load connected apps"
msgstr ""
#: src/routes/account/_appLayout/$sub.tsx:91
msgid "Failed to load devices"
msgstr ""
#: src/routes/account/_appLayout/$sub.tsx:222
msgid "Failed to remove device"
msgstr ""
#: src/routes/account/_minimalLayout/reset-password.tsx:94
msgid "Failed to resend code"
msgstr ""
#: src/routes/account/_appLayout/$sub.tsx:147
msgid "Failed to sign out"
msgstr ""
#: src/routes/account/_minimalLayout/sign-in.tsx:235
msgid "Forgot password?"
msgstr ""
#: src/routes/account/_minimalLayout/reset-password.tsx:186
msgid "Get reset code"
msgstr ""
#: src/routes/account/_appLayout/$sub.tsx:42
msgid "Home"
msgstr ""
#: src/routes/account/_minimalLayout/sign-in.tsx:150
msgid "Identifier"
msgstr ""
#: src/routes/account/_minimalLayout/reset-password.tsx:41
msgid "Invalid email"
msgstr ""
#: src/routes/account/_minimalLayout/sign-in.tsx:83
msgid "Invalid handle"
msgstr ""
#: src/routes/account/_minimalLayout/sign-in.tsx:123
msgid "Invalid identifier or password."
msgstr ""
#: src/routes/account/_appLayout/$sub.tsx:77
msgid "It appears that you havent used this account to sign in to any apps yet."
msgstr ""
#: src/routes/account/_minimalLayout.tsx:21
#: src/components/Nav.tsx:18
msgid "Logo"
msgstr ""
#: src/routes/account/_appLayout/$sub.tsx:107
msgid "Looks like you aren't logged in on any other devices."
msgstr ""
#: src/data/useFriendlyClientId.ts:13
msgid "loopback"
msgstr ""
#: src/routes/account/_appLayout/$sub.tsx:83
msgid "My devices"
msgstr ""
#: src/routes/account/_appLayout/$sub.tsx:75
msgid "No connected apps"
msgstr ""
#: src/routes/account/_appLayout/$sub.tsx:106
msgid "No devices"
msgstr ""
#: src/routes/account/_minimalLayout/sign-in.tsx:170
#: src/routes/account/_minimalLayout/sign-in.tsx:176
#: src/routes/account/_minimalLayout/reset-password.tsx:257
msgid "Password"
msgstr ""
#: src/routes/account/_minimalLayout/sign-in.tsx:88
msgid "Password is required"
msgstr ""
#: src/routes/account/_minimalLayout/reset-password.tsx:68
msgid "Password must be at least {MIN_PASSWORD_LENGTH} characters"
msgstr ""
#: src/routes/account/_appLayout/$sub.tsx:271
#: src/routes/account/_appLayout/$sub.tsx:276
msgid "Remove"
msgstr ""
#: src/routes/account/_appLayout/$sub.tsx:269
msgid "Remove this device"
msgstr ""
#: src/routes/account/_minimalLayout/reset-password.tsx:129
#: src/routes/account/_minimalLayout/reset-password.tsx:308
msgid "Reset password"
msgstr ""
#: src/routes/account/_appLayout/$sub.tsx:172
msgid "Revoke access to {clientName}"
msgstr ""
#: src/routes/account/_appLayout/$sub.tsx:173
msgid "Revoke access to this application"
msgstr ""
#: src/components/AccountSelector.tsx:32
msgid "Select an account"
msgstr ""
#: src/routes/account/_minimalLayout/index.tsx:67
msgid "Select the account you would like to manage."
msgstr ""
#: src/routes/account/_minimalLayout/sign-in.tsx:134
#: src/routes/account/_minimalLayout/sign-in.tsx:226
msgid "Sign in"
msgstr ""
#: src/routes/account/_minimalLayout/index.tsx:107
msgid "Sign in with another account"
msgstr ""
#: src/routes/account/_appLayout/$sub.tsx:178
#: src/routes/account/_appLayout/$sub.tsx:183
#: src/components/AccountSelector.tsx:103
msgid "Sign out"
msgstr ""
#: src/routes/account/_minimalLayout/index.tsx:39
msgid "Something went wrong"
msgstr ""
#: src/routes/account/_minimalLayout/reset-password.tsx:115
msgid "Success!"
msgstr ""
#: src/routes/account/_appLayout/$sub.tsx:216
msgid "Successfully removed device"
msgstr ""
#: src/routes/account/_appLayout/$sub.tsx:141
msgid "Successfully signed out"
msgstr ""
#: src/routes/account/_appLayout/$sub.tsx:263
msgid "This device"
msgstr ""
#: src/routes/account/_appLayout/$sub.tsx:247
msgid "Unknown user agent"
msgstr ""
#: src/components/Avatar.tsx:24
msgid "User avatar"
msgstr ""
#. placeholder {0}: getAccountName(account)
#: src/routes/account/_minimalLayout/index.tsx:83
msgid "View and manage account for {0}"
msgstr ""
#: src/routes/account/_minimalLayout/index.tsx:41
msgid "We weren't able to load your accounts. Please refresh the page to try again."
msgstr ""
#: src/components/ErrorScreen.tsx:19
msgid "Whoops! An error occurred."
msgstr ""
#: src/routes/account/_minimalLayout/sign-in.tsx:198
#: src/routes/account/_minimalLayout/reset-password.tsx:228
msgid "XXXXX-XXXXX"
msgstr ""
#: src/routes/account/_appLayout/$sub.tsx:47
msgid "Your account"
msgstr ""
#: src/routes/account/_minimalLayout/reset-password.tsx:118
msgid "Your password has been reset."
msgstr ""

View File

@ -0,0 +1,296 @@
msgid ""
msgstr ""
"POT-Creation-Date: 2025-03-19 13:49-0500\n"
"MIME-Version: 1.0\n"
"Content-Type: text/plain; charset=utf-8\n"
"Content-Transfer-Encoding: 8bit\n"
"X-Generator: @lingui/cli\n"
"Language: eu\n"
"Project-Id-Version: \n"
"Report-Msgid-Bugs-To: \n"
"PO-Revision-Date: \n"
"Last-Translator: \n"
"Language-Team: \n"
"Plural-Forms: \n"
#: src/routes/account/_minimalLayout/sign-in.tsx:155
msgid "@handle or email"
msgstr ""
#: src/routes/account/_minimalLayout/sign-in.tsx:49
msgid "← Back to accounts"
msgstr ""
#: src/data/useClientName.ts:24
msgid "A local app"
msgstr ""
#: src/routes/account/_minimalLayout/reset-password.tsx:65
msgid "A new password is required"
msgstr ""
#: src/routes/account/_minimalLayout/index.tsx:64
msgid "Accounts"
msgstr ""
#: src/routes/account/_minimalLayout/sign-in.tsx:125
#: src/routes/account/_minimalLayout/reset-password.tsx:51
#: src/routes/account/_minimalLayout/reset-password.tsx:78
msgid "An error occurred, please try again."
msgstr ""
#: src/routes/account/_appLayout/$sub.tsx:270
msgid "Are you sure you want to remove this device?"
msgstr ""
#: src/routes/account/_appLayout/$sub.tsx:176
msgid "Are you sure you want to revoke access? This application won't be able to access your account anymore."
msgstr ""
#: src/routes/account/_minimalLayout/reset-password.tsx:122
#: src/routes/account/_minimalLayout/reset-password.tsx:195
msgid "Back to sign in"
msgstr ""
#: src/components/Prompt.tsx:56
msgid "Cancel"
msgstr ""
#: src/routes/account/_minimalLayout/reset-password.tsx:319
msgid "Click here to send a new code to your email."
msgstr ""
#: src/routes/account/_minimalLayout/sign-in.tsx:193
#: src/routes/account/_minimalLayout/reset-password.tsx:222
msgid "Code"
msgstr ""
#: src/routes/account/_minimalLayout/reset-password.tsx:88
msgid "Code was resent"
msgstr ""
#: src/routes/account/_appLayout/$sub.tsx:52
msgid "Connected apps"
msgstr ""
#: src/routes/account/_minimalLayout/sign-in.tsx:143
msgid "Credentials"
msgstr ""
#: src/routes/account/_minimalLayout/reset-password.tsx:314
msgid "Don't see the email? <0>Try sending again.</0>"
msgstr ""
#: src/routes/account/_minimalLayout/reset-password.tsx:149
msgid "Email"
msgstr ""
#: src/routes/account/_minimalLayout/reset-password.tsx:62
msgid "Email code is required"
msgstr ""
#: src/routes/account/_minimalLayout/reset-password.tsx:42
msgid "Email is required"
msgstr ""
#: src/routes/account/_minimalLayout/reset-password.tsx:264
msgid "Enter a new password"
msgstr ""
#: src/routes/account/_minimalLayout/reset-password.tsx:154
msgid "Enter your email"
msgstr ""
#: src/routes/account/_minimalLayout/reset-password.tsx:132
msgid "Enter your email to receive a reset code."
msgstr ""
#: src/routes/account/_appLayout/$sub.tsx:60
msgid "Failed to load connected apps"
msgstr ""
#: src/routes/account/_appLayout/$sub.tsx:91
msgid "Failed to load devices"
msgstr ""
#: src/routes/account/_appLayout/$sub.tsx:222
msgid "Failed to remove device"
msgstr ""
#: src/routes/account/_minimalLayout/reset-password.tsx:94
msgid "Failed to resend code"
msgstr ""
#: src/routes/account/_appLayout/$sub.tsx:147
msgid "Failed to sign out"
msgstr ""
#: src/routes/account/_minimalLayout/sign-in.tsx:235
msgid "Forgot password?"
msgstr ""
#: src/routes/account/_minimalLayout/reset-password.tsx:186
msgid "Get reset code"
msgstr ""
#: src/routes/account/_appLayout/$sub.tsx:42
msgid "Home"
msgstr ""
#: src/routes/account/_minimalLayout/sign-in.tsx:150
msgid "Identifier"
msgstr ""
#: src/routes/account/_minimalLayout/reset-password.tsx:41
msgid "Invalid email"
msgstr ""
#: src/routes/account/_minimalLayout/sign-in.tsx:83
msgid "Invalid handle"
msgstr ""
#: src/routes/account/_minimalLayout/sign-in.tsx:123
msgid "Invalid identifier or password."
msgstr ""
#: src/routes/account/_appLayout/$sub.tsx:77
msgid "It appears that you havent used this account to sign in to any apps yet."
msgstr ""
#: src/routes/account/_minimalLayout.tsx:21
#: src/components/Nav.tsx:18
msgid "Logo"
msgstr ""
#: src/routes/account/_appLayout/$sub.tsx:107
msgid "Looks like you aren't logged in on any other devices."
msgstr ""
#: src/data/useFriendlyClientId.ts:13
msgid "loopback"
msgstr ""
#: src/routes/account/_appLayout/$sub.tsx:83
msgid "My devices"
msgstr ""
#: src/routes/account/_appLayout/$sub.tsx:75
msgid "No connected apps"
msgstr ""
#: src/routes/account/_appLayout/$sub.tsx:106
msgid "No devices"
msgstr ""
#: src/routes/account/_minimalLayout/sign-in.tsx:170
#: src/routes/account/_minimalLayout/sign-in.tsx:176
#: src/routes/account/_minimalLayout/reset-password.tsx:257
msgid "Password"
msgstr ""
#: src/routes/account/_minimalLayout/sign-in.tsx:88
msgid "Password is required"
msgstr ""
#: src/routes/account/_minimalLayout/reset-password.tsx:68
msgid "Password must be at least {MIN_PASSWORD_LENGTH} characters"
msgstr ""
#: src/routes/account/_appLayout/$sub.tsx:271
#: src/routes/account/_appLayout/$sub.tsx:276
msgid "Remove"
msgstr ""
#: src/routes/account/_appLayout/$sub.tsx:269
msgid "Remove this device"
msgstr ""
#: src/routes/account/_minimalLayout/reset-password.tsx:129
#: src/routes/account/_minimalLayout/reset-password.tsx:308
msgid "Reset password"
msgstr ""
#: src/routes/account/_appLayout/$sub.tsx:172
msgid "Revoke access to {clientName}"
msgstr ""
#: src/routes/account/_appLayout/$sub.tsx:173
msgid "Revoke access to this application"
msgstr ""
#: src/components/AccountSelector.tsx:32
msgid "Select an account"
msgstr ""
#: src/routes/account/_minimalLayout/index.tsx:67
msgid "Select the account you would like to manage."
msgstr ""
#: src/routes/account/_minimalLayout/sign-in.tsx:134
#: src/routes/account/_minimalLayout/sign-in.tsx:226
msgid "Sign in"
msgstr ""
#: src/routes/account/_minimalLayout/index.tsx:107
msgid "Sign in with another account"
msgstr ""
#: src/routes/account/_appLayout/$sub.tsx:178
#: src/routes/account/_appLayout/$sub.tsx:183
#: src/components/AccountSelector.tsx:103
msgid "Sign out"
msgstr ""
#: src/routes/account/_minimalLayout/index.tsx:39
msgid "Something went wrong"
msgstr ""
#: src/routes/account/_minimalLayout/reset-password.tsx:115
msgid "Success!"
msgstr ""
#: src/routes/account/_appLayout/$sub.tsx:216
msgid "Successfully removed device"
msgstr ""
#: src/routes/account/_appLayout/$sub.tsx:141
msgid "Successfully signed out"
msgstr ""
#: src/routes/account/_appLayout/$sub.tsx:263
msgid "This device"
msgstr ""
#: src/routes/account/_appLayout/$sub.tsx:247
msgid "Unknown user agent"
msgstr ""
#: src/components/Avatar.tsx:24
msgid "User avatar"
msgstr ""
#. placeholder {0}: getAccountName(account)
#: src/routes/account/_minimalLayout/index.tsx:83
msgid "View and manage account for {0}"
msgstr ""
#: src/routes/account/_minimalLayout/index.tsx:41
msgid "We weren't able to load your accounts. Please refresh the page to try again."
msgstr ""
#: src/components/ErrorScreen.tsx:19
msgid "Whoops! An error occurred."
msgstr ""
#: src/routes/account/_minimalLayout/sign-in.tsx:198
#: src/routes/account/_minimalLayout/reset-password.tsx:228
msgid "XXXXX-XXXXX"
msgstr ""
#: src/routes/account/_appLayout/$sub.tsx:47
msgid "Your account"
msgstr ""
#: src/routes/account/_minimalLayout/reset-password.tsx:118
msgid "Your password has been reset."
msgstr ""

View File

@ -0,0 +1,296 @@
msgid ""
msgstr ""
"POT-Creation-Date: 2025-03-19 13:49-0500\n"
"MIME-Version: 1.0\n"
"Content-Type: text/plain; charset=utf-8\n"
"Content-Transfer-Encoding: 8bit\n"
"X-Generator: @lingui/cli\n"
"Language: fi\n"
"Project-Id-Version: \n"
"Report-Msgid-Bugs-To: \n"
"PO-Revision-Date: \n"
"Last-Translator: \n"
"Language-Team: \n"
"Plural-Forms: \n"
#: src/routes/account/_minimalLayout/sign-in.tsx:155
msgid "@handle or email"
msgstr ""
#: src/routes/account/_minimalLayout/sign-in.tsx:49
msgid "← Back to accounts"
msgstr ""
#: src/data/useClientName.ts:24
msgid "A local app"
msgstr ""
#: src/routes/account/_minimalLayout/reset-password.tsx:65
msgid "A new password is required"
msgstr ""
#: src/routes/account/_minimalLayout/index.tsx:64
msgid "Accounts"
msgstr ""
#: src/routes/account/_minimalLayout/sign-in.tsx:125
#: src/routes/account/_minimalLayout/reset-password.tsx:51
#: src/routes/account/_minimalLayout/reset-password.tsx:78
msgid "An error occurred, please try again."
msgstr ""
#: src/routes/account/_appLayout/$sub.tsx:270
msgid "Are you sure you want to remove this device?"
msgstr ""
#: src/routes/account/_appLayout/$sub.tsx:176
msgid "Are you sure you want to revoke access? This application won't be able to access your account anymore."
msgstr ""
#: src/routes/account/_minimalLayout/reset-password.tsx:122
#: src/routes/account/_minimalLayout/reset-password.tsx:195
msgid "Back to sign in"
msgstr ""
#: src/components/Prompt.tsx:56
msgid "Cancel"
msgstr ""
#: src/routes/account/_minimalLayout/reset-password.tsx:319
msgid "Click here to send a new code to your email."
msgstr ""
#: src/routes/account/_minimalLayout/sign-in.tsx:193
#: src/routes/account/_minimalLayout/reset-password.tsx:222
msgid "Code"
msgstr ""
#: src/routes/account/_minimalLayout/reset-password.tsx:88
msgid "Code was resent"
msgstr ""
#: src/routes/account/_appLayout/$sub.tsx:52
msgid "Connected apps"
msgstr ""
#: src/routes/account/_minimalLayout/sign-in.tsx:143
msgid "Credentials"
msgstr ""
#: src/routes/account/_minimalLayout/reset-password.tsx:314
msgid "Don't see the email? <0>Try sending again.</0>"
msgstr ""
#: src/routes/account/_minimalLayout/reset-password.tsx:149
msgid "Email"
msgstr ""
#: src/routes/account/_minimalLayout/reset-password.tsx:62
msgid "Email code is required"
msgstr ""
#: src/routes/account/_minimalLayout/reset-password.tsx:42
msgid "Email is required"
msgstr ""
#: src/routes/account/_minimalLayout/reset-password.tsx:264
msgid "Enter a new password"
msgstr ""
#: src/routes/account/_minimalLayout/reset-password.tsx:154
msgid "Enter your email"
msgstr ""
#: src/routes/account/_minimalLayout/reset-password.tsx:132
msgid "Enter your email to receive a reset code."
msgstr ""
#: src/routes/account/_appLayout/$sub.tsx:60
msgid "Failed to load connected apps"
msgstr ""
#: src/routes/account/_appLayout/$sub.tsx:91
msgid "Failed to load devices"
msgstr ""
#: src/routes/account/_appLayout/$sub.tsx:222
msgid "Failed to remove device"
msgstr ""
#: src/routes/account/_minimalLayout/reset-password.tsx:94
msgid "Failed to resend code"
msgstr ""
#: src/routes/account/_appLayout/$sub.tsx:147
msgid "Failed to sign out"
msgstr ""
#: src/routes/account/_minimalLayout/sign-in.tsx:235
msgid "Forgot password?"
msgstr ""
#: src/routes/account/_minimalLayout/reset-password.tsx:186
msgid "Get reset code"
msgstr ""
#: src/routes/account/_appLayout/$sub.tsx:42
msgid "Home"
msgstr ""
#: src/routes/account/_minimalLayout/sign-in.tsx:150
msgid "Identifier"
msgstr ""
#: src/routes/account/_minimalLayout/reset-password.tsx:41
msgid "Invalid email"
msgstr ""
#: src/routes/account/_minimalLayout/sign-in.tsx:83
msgid "Invalid handle"
msgstr ""
#: src/routes/account/_minimalLayout/sign-in.tsx:123
msgid "Invalid identifier or password."
msgstr ""
#: src/routes/account/_appLayout/$sub.tsx:77
msgid "It appears that you havent used this account to sign in to any apps yet."
msgstr ""
#: src/routes/account/_minimalLayout.tsx:21
#: src/components/Nav.tsx:18
msgid "Logo"
msgstr ""
#: src/routes/account/_appLayout/$sub.tsx:107
msgid "Looks like you aren't logged in on any other devices."
msgstr ""
#: src/data/useFriendlyClientId.ts:13
msgid "loopback"
msgstr ""
#: src/routes/account/_appLayout/$sub.tsx:83
msgid "My devices"
msgstr ""
#: src/routes/account/_appLayout/$sub.tsx:75
msgid "No connected apps"
msgstr ""
#: src/routes/account/_appLayout/$sub.tsx:106
msgid "No devices"
msgstr ""
#: src/routes/account/_minimalLayout/sign-in.tsx:170
#: src/routes/account/_minimalLayout/sign-in.tsx:176
#: src/routes/account/_minimalLayout/reset-password.tsx:257
msgid "Password"
msgstr ""
#: src/routes/account/_minimalLayout/sign-in.tsx:88
msgid "Password is required"
msgstr ""
#: src/routes/account/_minimalLayout/reset-password.tsx:68
msgid "Password must be at least {MIN_PASSWORD_LENGTH} characters"
msgstr ""
#: src/routes/account/_appLayout/$sub.tsx:271
#: src/routes/account/_appLayout/$sub.tsx:276
msgid "Remove"
msgstr ""
#: src/routes/account/_appLayout/$sub.tsx:269
msgid "Remove this device"
msgstr ""
#: src/routes/account/_minimalLayout/reset-password.tsx:129
#: src/routes/account/_minimalLayout/reset-password.tsx:308
msgid "Reset password"
msgstr ""
#: src/routes/account/_appLayout/$sub.tsx:172
msgid "Revoke access to {clientName}"
msgstr ""
#: src/routes/account/_appLayout/$sub.tsx:173
msgid "Revoke access to this application"
msgstr ""
#: src/components/AccountSelector.tsx:32
msgid "Select an account"
msgstr ""
#: src/routes/account/_minimalLayout/index.tsx:67
msgid "Select the account you would like to manage."
msgstr ""
#: src/routes/account/_minimalLayout/sign-in.tsx:134
#: src/routes/account/_minimalLayout/sign-in.tsx:226
msgid "Sign in"
msgstr ""
#: src/routes/account/_minimalLayout/index.tsx:107
msgid "Sign in with another account"
msgstr ""
#: src/routes/account/_appLayout/$sub.tsx:178
#: src/routes/account/_appLayout/$sub.tsx:183
#: src/components/AccountSelector.tsx:103
msgid "Sign out"
msgstr ""
#: src/routes/account/_minimalLayout/index.tsx:39
msgid "Something went wrong"
msgstr ""
#: src/routes/account/_minimalLayout/reset-password.tsx:115
msgid "Success!"
msgstr ""
#: src/routes/account/_appLayout/$sub.tsx:216
msgid "Successfully removed device"
msgstr ""
#: src/routes/account/_appLayout/$sub.tsx:141
msgid "Successfully signed out"
msgstr ""
#: src/routes/account/_appLayout/$sub.tsx:263
msgid "This device"
msgstr ""
#: src/routes/account/_appLayout/$sub.tsx:247
msgid "Unknown user agent"
msgstr ""
#: src/components/Avatar.tsx:24
msgid "User avatar"
msgstr ""
#. placeholder {0}: getAccountName(account)
#: src/routes/account/_minimalLayout/index.tsx:83
msgid "View and manage account for {0}"
msgstr ""
#: src/routes/account/_minimalLayout/index.tsx:41
msgid "We weren't able to load your accounts. Please refresh the page to try again."
msgstr ""
#: src/components/ErrorScreen.tsx:19
msgid "Whoops! An error occurred."
msgstr ""
#: src/routes/account/_minimalLayout/sign-in.tsx:198
#: src/routes/account/_minimalLayout/reset-password.tsx:228
msgid "XXXXX-XXXXX"
msgstr ""
#: src/routes/account/_appLayout/$sub.tsx:47
msgid "Your account"
msgstr ""
#: src/routes/account/_minimalLayout/reset-password.tsx:118
msgid "Your password has been reset."
msgstr ""

View File

@ -0,0 +1,296 @@
msgid ""
msgstr ""
"POT-Creation-Date: 2025-03-19 13:49-0500\n"
"MIME-Version: 1.0\n"
"Content-Type: text/plain; charset=utf-8\n"
"Content-Transfer-Encoding: 8bit\n"
"X-Generator: @lingui/cli\n"
"Language: fr\n"
"Project-Id-Version: \n"
"Report-Msgid-Bugs-To: \n"
"PO-Revision-Date: \n"
"Last-Translator: \n"
"Language-Team: \n"
"Plural-Forms: \n"
#: src/routes/account/_minimalLayout/sign-in.tsx:155
msgid "@handle or email"
msgstr "@identifiant ou adresse email"
#: src/routes/account/_minimalLayout/sign-in.tsx:49
msgid "← Back to accounts"
msgstr "← Retour à la liste des comptes"
#: src/data/useClientName.ts:24
msgid "A local app"
msgstr "Une application anonyme"
#: src/routes/account/_minimalLayout/reset-password.tsx:65
msgid "A new password is required"
msgstr "Un nouveau mot de passe est requis"
#: src/routes/account/_minimalLayout/index.tsx:64
msgid "Accounts"
msgstr "Utilisateurs"
#: src/routes/account/_minimalLayout/sign-in.tsx:125
#: src/routes/account/_minimalLayout/reset-password.tsx:51
#: src/routes/account/_minimalLayout/reset-password.tsx:78
msgid "An error occurred, please try again."
msgstr "Une erreur s'est produite, veuillez réessayer."
#: src/routes/account/_appLayout/$sub.tsx:270
msgid "Are you sure you want to remove this device?"
msgstr "Êtes-vous sûr de vouloir déconnecter cet appareil ?"
#: src/routes/account/_appLayout/$sub.tsx:176
msgid "Are you sure you want to revoke access? This application won't be able to access your account anymore."
msgstr "Êtes-vous sûr de vouloir supprimer l'accès? Cette application ne sera plus en mesure d'accéder à vos informations."
#: src/routes/account/_minimalLayout/reset-password.tsx:122
#: src/routes/account/_minimalLayout/reset-password.tsx:195
msgid "Back to sign in"
msgstr "Retour vers la page de connexion"
#: src/components/Prompt.tsx:56
msgid "Cancel"
msgstr "Annuler"
#: src/routes/account/_minimalLayout/reset-password.tsx:319
msgid "Click here to send a new code to your email."
msgstr "Cliquez ici pour envoyer un nouveau code dans votre boîte mail."
#: src/routes/account/_minimalLayout/sign-in.tsx:193
#: src/routes/account/_minimalLayout/reset-password.tsx:222
msgid "Code"
msgstr "Code"
#: src/routes/account/_minimalLayout/reset-password.tsx:88
msgid "Code was resent"
msgstr "Un nouveau code a été envoyé"
#: src/routes/account/_appLayout/$sub.tsx:52
msgid "Connected apps"
msgstr "Applications connectées"
#: src/routes/account/_minimalLayout/sign-in.tsx:143
msgid "Credentials"
msgstr "Identifiants"
#: src/routes/account/_minimalLayout/reset-password.tsx:314
msgid "Don't see the email? <0>Try sending again.</0>"
msgstr "Vous ne voyez pas l'email ? <0>Essayez d'envoyer à nouveau.</0>"
#: src/routes/account/_minimalLayout/reset-password.tsx:149
msgid "Email"
msgstr "Email"
#: src/routes/account/_minimalLayout/reset-password.tsx:62
msgid "Email code is required"
msgstr "Veuillez entrer le code de vérification envoyé par email"
#: src/routes/account/_minimalLayout/reset-password.tsx:42
msgid "Email is required"
msgstr "L'adresse email est requise"
#: src/routes/account/_minimalLayout/reset-password.tsx:264
msgid "Enter a new password"
msgstr "Entrez un nouveau mot de passe"
#: src/routes/account/_minimalLayout/reset-password.tsx:154
msgid "Enter your email"
msgstr "Entrez votre adresse email"
#: src/routes/account/_minimalLayout/reset-password.tsx:132
msgid "Enter your email to receive a reset code."
msgstr "Entrez votre adresse email pour recevoir un code de réinitialisation."
#: src/routes/account/_appLayout/$sub.tsx:60
msgid "Failed to load connected apps"
msgstr "Échec du chargement des applications connectées"
#: src/routes/account/_appLayout/$sub.tsx:91
msgid "Failed to load devices"
msgstr "Échec du chargement des appareils"
#: src/routes/account/_appLayout/$sub.tsx:222
msgid "Failed to remove device"
msgstr "Échec de la suppression de l'appareil"
#: src/routes/account/_minimalLayout/reset-password.tsx:94
msgid "Failed to resend code"
msgstr "Échec de l'envoi du code par email"
#: src/routes/account/_appLayout/$sub.tsx:147
msgid "Failed to sign out"
msgstr "Échec de la déconnexion"
#: src/routes/account/_minimalLayout/sign-in.tsx:235
msgid "Forgot password?"
msgstr "Mot de passe oublié ?"
#: src/routes/account/_minimalLayout/reset-password.tsx:186
msgid "Get reset code"
msgstr "Obtenir le code de réinitialisation"
#: src/routes/account/_appLayout/$sub.tsx:42
msgid "Home"
msgstr "Accueil"
#: src/routes/account/_minimalLayout/sign-in.tsx:150
msgid "Identifier"
msgstr "Identifiant"
#: src/routes/account/_minimalLayout/reset-password.tsx:41
msgid "Invalid email"
msgstr "Adresse email invalide"
#: src/routes/account/_minimalLayout/sign-in.tsx:83
msgid "Invalid handle"
msgstr "Identifiant invalide"
#: src/routes/account/_minimalLayout/sign-in.tsx:123
msgid "Invalid identifier or password."
msgstr "Identifiant ou mot de passe invalide."
#: src/routes/account/_appLayout/$sub.tsx:77
msgid "It appears that you havent used this account to sign in to any apps yet."
msgstr "Il semble que vous n'ayez pas encore utilisé ce compte pour vous connecter à une application."
#: src/routes/account/_minimalLayout.tsx:21
#: src/components/Nav.tsx:18
msgid "Logo"
msgstr "Logo"
#: src/routes/account/_appLayout/$sub.tsx:107
msgid "Looks like you aren't logged in on any other devices."
msgstr "Votre compte n'est utilisé sur aucun autre appareil."
#: src/data/useFriendlyClientId.ts:13
msgid "loopback"
msgstr ""
#: src/routes/account/_appLayout/$sub.tsx:83
msgid "My devices"
msgstr "Mes appareils"
#: src/routes/account/_appLayout/$sub.tsx:75
msgid "No connected apps"
msgstr "Aucune application connectée"
#: src/routes/account/_appLayout/$sub.tsx:106
msgid "No devices"
msgstr "Aucun appareil"
#: src/routes/account/_minimalLayout/sign-in.tsx:170
#: src/routes/account/_minimalLayout/sign-in.tsx:176
#: src/routes/account/_minimalLayout/reset-password.tsx:257
msgid "Password"
msgstr "Mot de passe"
#: src/routes/account/_minimalLayout/sign-in.tsx:88
msgid "Password is required"
msgstr "Le mot de passe est requis"
#: src/routes/account/_minimalLayout/reset-password.tsx:68
msgid "Password must be at least {MIN_PASSWORD_LENGTH} characters"
msgstr "Le mot de passe doit contenir au moins {MIN_PASSWORD_LENGTH} caractères"
#: src/routes/account/_appLayout/$sub.tsx:271
#: src/routes/account/_appLayout/$sub.tsx:276
msgid "Remove"
msgstr "Déconnecter"
#: src/routes/account/_appLayout/$sub.tsx:269
msgid "Remove this device"
msgstr "Déconnecter cet appareil"
#: src/routes/account/_minimalLayout/reset-password.tsx:129
#: src/routes/account/_minimalLayout/reset-password.tsx:308
msgid "Reset password"
msgstr "Réinitialiser le mot de passe"
#: src/routes/account/_appLayout/$sub.tsx:172
msgid "Revoke access to {clientName}"
msgstr ""
#: src/routes/account/_appLayout/$sub.tsx:173
msgid "Revoke access to this application"
msgstr "Révoquer l'accès à cette application"
#: src/components/AccountSelector.tsx:32
msgid "Select an account"
msgstr "Sélectionner un compte"
#: src/routes/account/_minimalLayout/index.tsx:67
msgid "Select the account you would like to manage."
msgstr "Sélectionnez le compte que vous souhaitez gérer."
#: src/routes/account/_minimalLayout/sign-in.tsx:134
#: src/routes/account/_minimalLayout/sign-in.tsx:226
msgid "Sign in"
msgstr "Se connecter"
#: src/routes/account/_minimalLayout/index.tsx:107
msgid "Sign in with another account"
msgstr "Se connecter avec un autre compte"
#: src/routes/account/_appLayout/$sub.tsx:178
#: src/routes/account/_appLayout/$sub.tsx:183
#: src/components/AccountSelector.tsx:103
msgid "Sign out"
msgstr "Déconnecter"
#: src/routes/account/_minimalLayout/index.tsx:39
msgid "Something went wrong"
msgstr "Une erreur s'est produite"
#: src/routes/account/_minimalLayout/reset-password.tsx:115
msgid "Success!"
msgstr "Succès !"
#: src/routes/account/_appLayout/$sub.tsx:216
msgid "Successfully removed device"
msgstr "Déconnexion de l'appareil réussie"
#: src/routes/account/_appLayout/$sub.tsx:141
msgid "Successfully signed out"
msgstr "Déconnexion réussie"
#: src/routes/account/_appLayout/$sub.tsx:263
msgid "This device"
msgstr "Cet appareil"
#: src/routes/account/_appLayout/$sub.tsx:247
msgid "Unknown user agent"
msgstr "Appareil inconnu"
#: src/components/Avatar.tsx:24
msgid "User avatar"
msgstr "Avatar de l'utilisateur"
#. placeholder {0}: getAccountName(account)
#: src/routes/account/_minimalLayout/index.tsx:83
msgid "View and manage account for {0}"
msgstr "Afficher et gérer le compte de {0}"
#: src/routes/account/_minimalLayout/index.tsx:41
msgid "We weren't able to load your accounts. Please refresh the page to try again."
msgstr "Nous n'avons pas pu charger vos comptes. Veuillez réactualiser la page pour réessayer."
#: src/components/ErrorScreen.tsx:19
msgid "Whoops! An error occurred."
msgstr "Oups ! Une erreur s'est produite."
#: src/routes/account/_minimalLayout/sign-in.tsx:198
#: src/routes/account/_minimalLayout/reset-password.tsx:228
msgid "XXXXX-XXXXX"
msgstr "XXXXX-XXXXX"
#: src/routes/account/_appLayout/$sub.tsx:47
msgid "Your account"
msgstr "Votre compte"
#: src/routes/account/_minimalLayout/reset-password.tsx:118
msgid "Your password has been reset."
msgstr "Votre mot de passe a été réinitialisé."

Some files were not shown because too many files have changed in this diff Show More