Account management page (#3659)
--------- Co-authored-by: Eric Bailey <git@esb.lol>
This commit is contained in:
parent
8b98fec885
commit
371e04aad2
5
.changeset/blue-moles-remain.md
Normal file
5
.changeset/blue-moles-remain.md
Normal file
@ -0,0 +1,5 @@
|
||||
---
|
||||
"@atproto/oauth-provider": patch
|
||||
---
|
||||
|
||||
Security fix: Properly validate JWT `exp` claim when it is zero.
|
5
.changeset/brave-candles-lie.md
Normal file
5
.changeset/brave-candles-lie.md
Normal 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).
|
5
.changeset/dirty-games-dream.md
Normal file
5
.changeset/dirty-games-dream.md
Normal file
@ -0,0 +1,5 @@
|
||||
---
|
||||
"@atproto/oauth-provider": minor
|
||||
---
|
||||
|
||||
Remove unused `getAuthorizationDetails` hook
|
5
.changeset/empty-badgers-sort.md
Normal file
5
.changeset/empty-badgers-sort.md
Normal file
@ -0,0 +1,5 @@
|
||||
---
|
||||
"@atproto/oauth-provider": minor
|
||||
---
|
||||
|
||||
Change name of `onSignupAttempt` hook to `onSignUpAttempt`
|
5
.changeset/forty-fishes-kiss.md
Normal file
5
.changeset/forty-fishes-kiss.md
Normal file
@ -0,0 +1,5 @@
|
||||
---
|
||||
"@atproto/oauth-types": patch
|
||||
---
|
||||
|
||||
Add `OAuthAuthenticationErrorResponse`
|
5
.changeset/grumpy-actors-end.md
Normal file
5
.changeset/grumpy-actors-end.md
Normal file
@ -0,0 +1,5 @@
|
||||
---
|
||||
"@atproto-labs/rollup-plugin-bundle-manifest": minor
|
||||
---
|
||||
|
||||
Export plugin as named export
|
5
.changeset/healthy-ads-crash.md
Normal file
5
.changeset/healthy-ads-crash.md
Normal file
@ -0,0 +1,5 @@
|
||||
---
|
||||
"@atproto/oauth-provider-api": minor
|
||||
---
|
||||
|
||||
Various adaptations
|
5
.changeset/neat-rocks-compete.md
Normal file
5
.changeset/neat-rocks-compete.md
Normal file
@ -0,0 +1,5 @@
|
||||
---
|
||||
"@atproto/oauth-provider": minor
|
||||
---
|
||||
|
||||
Store & verify new authorization requests against previously approved scopes for the same client
|
5
.changeset/old-cycles-sit.md
Normal file
5
.changeset/old-cycles-sit.md
Normal file
@ -0,0 +1,5 @@
|
||||
---
|
||||
"@atproto/oauth-provider": minor
|
||||
---
|
||||
|
||||
Split oauth endpoints & authorization page routes from `OAuthProvider`
|
5
.changeset/red-lemons-provide.md
Normal file
5
.changeset/red-lemons-provide.md
Normal file
@ -0,0 +1,5 @@
|
||||
---
|
||||
"@atproto/oauth-provider": patch
|
||||
---
|
||||
|
||||
Always log to console in dev mode
|
5
.changeset/smart-seahorses-lie.md
Normal file
5
.changeset/smart-seahorses-lie.md
Normal file
@ -0,0 +1,5 @@
|
||||
---
|
||||
"@atproto/pds": patch
|
||||
---
|
||||
|
||||
Add account management page for oauth sessions
|
5
.changeset/soft-readers-hide.md
Normal file
5
.changeset/soft-readers-hide.md
Normal 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
|
5
.changeset/spotty-baboons-clap.md
Normal file
5
.changeset/spotty-baboons-clap.md
Normal file
@ -0,0 +1,5 @@
|
||||
---
|
||||
"@atproto/oauth-provider": minor
|
||||
---
|
||||
|
||||
Change "brand" color to "primary"
|
6
.changeset/ten-dryers-behave.md
Normal file
6
.changeset/ten-dryers-behave.md
Normal file
@ -0,0 +1,6 @@
|
||||
---
|
||||
"@atproto/oauth-provider-frontend": minor
|
||||
"@atproto/oauth-provider-ui": minor
|
||||
---
|
||||
|
||||
New build system
|
5
.changeset/ten-ears-check.md
Normal file
5
.changeset/ten-ears-check.md
Normal file
@ -0,0 +1,5 @@
|
||||
---
|
||||
"@atproto/oauth-provider": patch
|
||||
---
|
||||
|
||||
Do not return invalid authorization response errors
|
5
.changeset/twelve-bobcats-confess.md
Normal file
5
.changeset/twelve-bobcats-confess.md
Normal file
@ -0,0 +1,5 @@
|
||||
---
|
||||
"@atproto/oauth-provider": minor
|
||||
---
|
||||
|
||||
Remove instrospection endpoint
|
5
.changeset/twenty-shirts-unite.md
Normal file
5
.changeset/twenty-shirts-unite.md
Normal file
@ -0,0 +1,5 @@
|
||||
---
|
||||
"@atproto/oauth-provider": patch
|
||||
---
|
||||
|
||||
Apply time mitigation strategy on the sensitive part of the operation only.
|
@ -87,6 +87,7 @@
|
||||
"project": [
|
||||
"tsconfig.json",
|
||||
"packages/oauth/*/tsconfig.json",
|
||||
"packages/oauth/*/tsconfig.src.json",
|
||||
"packages/internal/*/tsconfig.json",
|
||||
"packages/*/tsconfig.json"
|
||||
]
|
||||
|
2
.github/workflows/repo.yaml
vendored
2
.github/workflows/repo.yaml
vendored
@ -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
|
||||
|
@ -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
|
||||
|
||||
|
@ -3,6 +3,7 @@
|
||||
"tabWidth": 2,
|
||||
"semi": false,
|
||||
"singleQuote": true,
|
||||
"plugins": ["prettier-plugin-tailwindcss"],
|
||||
"overrides": [
|
||||
{
|
||||
"files": "*.hbs",
|
||||
|
@ -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": [
|
||||
|
@ -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"
|
||||
|
@ -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',
|
||||
|
@ -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,
|
||||
}: {
|
||||
|
@ -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({
|
||||
|
@ -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')}
|
||||
|
@ -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>
|
||||
|
@ -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>
|
||||
|
@ -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'))
|
||||
|
@ -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(
|
||||
|
@ -25,6 +25,7 @@
|
||||
}
|
||||
},
|
||||
"dependencies": {
|
||||
"@atproto/jwk": "workspace:*",
|
||||
"@atproto/oauth-types": "workspace:*"
|
||||
},
|
||||
"devDependencies": {
|
||||
|
207
packages/oauth/oauth-provider-api/src/api-endpoints.ts
Normal file
207
packages/oauth/oauth-provider-api/src/api-endpoints.ts
Normal 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
|
||||
}
|
@ -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
|
||||
}
|
@ -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[]
|
||||
}
|
||||
|
4
packages/oauth/oauth-provider-api/src/contants.ts
Normal file
4
packages/oauth/oauth-provider-api/src/contants.ts
Normal 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'
|
@ -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'
|
||||
|
@ -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`
|
||||
|
3
packages/oauth/oauth-provider-frontend/.gitignore
vendored
Normal file
3
packages/oauth/oauth-provider-frontend/.gitignore
vendored
Normal file
@ -0,0 +1,3 @@
|
||||
src/locales/*/*.ts
|
||||
.swc
|
||||
dist
|
58
packages/oauth/oauth-provider-frontend/.linguirc
Normal file
58
packages/oauth/oauth-provider-frontend/.linguirc
Normal 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"
|
||||
}
|
352
packages/oauth/oauth-provider-frontend/account.html
Normal file
352
packages/oauth/oauth-provider-frontend/account.html
Normal 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>
|
11
packages/oauth/oauth-provider-frontend/index.html
Normal file
11
packages/oauth/oauth-provider-frontend/index.html
Normal 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>
|
71
packages/oauth/oauth-provider-frontend/package.json
Normal file
71
packages/oauth/oauth-provider-frontend/package.json
Normal 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"
|
||||
}
|
||||
}
|
37
packages/oauth/oauth-provider-frontend/src/account-page.tsx
Normal file
37
packages/oauth/oauth-provider-frontend/src/account-page.tsx
Normal 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>,
|
||||
)
|
225
packages/oauth/oauth-provider-frontend/src/api/api.ts
Normal file
225
packages/oauth/oauth-provider-frontend/src/api/api.ts
Normal 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'
|
||||
)
|
||||
}
|
||||
}
|
10
packages/oauth/oauth-provider-frontend/src/api/index.ts
Normal file
10
packages/oauth/oauth-provider-frontend/src/api/index.ts
Normal 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(), [])
|
||||
}
|
135
packages/oauth/oauth-provider-frontend/src/api/json-client.ts
Normal file
135
packages/oauth/oauth-provider-frontend/src/api/json-client.ts
Normal 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')
|
||||
)
|
||||
}
|
||||
}
|
@ -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>
|
||||
)
|
||||
}
|
@ -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>
|
||||
)
|
||||
}
|
@ -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>
|
||||
)
|
||||
}
|
@ -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',
|
||||
],
|
||||
],
|
||||
])
|
||||
}, [])
|
||||
}
|
@ -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>
|
||||
)
|
||||
}
|
@ -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>
|
||||
)
|
||||
}
|
@ -0,0 +1,3 @@
|
||||
export function Divider() {
|
||||
return <div className="border-border-default w-full border-t" />
|
||||
}
|
@ -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} />
|
||||
}
|
@ -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>
|
||||
)
|
||||
}
|
@ -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>
|
||||
)
|
||||
}
|
@ -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>
|
||||
)
|
||||
}
|
@ -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>
|
||||
)
|
||||
}
|
@ -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>
|
||||
)
|
||||
}
|
@ -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" />
|
||||
</>
|
||||
)
|
||||
}
|
@ -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>
|
||||
)
|
||||
}
|
132
packages/oauth/oauth-provider-frontend/src/components/Toast.tsx
Normal file
132
packages/oauth/oauth-provider-frontend/src/components/Toast.tsx
Normal 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)
|
||||
}
|
@ -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
|
@ -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>
|
||||
)
|
||||
}
|
@ -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>
|
||||
)
|
||||
}
|
@ -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>
|
||||
}
|
@ -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>
|
||||
)
|
||||
}
|
@ -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',
|
||||
])}
|
||||
/>
|
||||
)
|
||||
}
|
@ -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'
|
@ -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>
|
||||
</>
|
||||
)
|
||||
}
|
@ -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)
|
||||
},
|
||||
})
|
||||
}
|
@ -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
|
||||
}
|
@ -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
|
||||
}
|
@ -0,0 +1,5 @@
|
||||
import { useHydrationData } from './useHydrationData'
|
||||
|
||||
export function useCustomizationData() {
|
||||
return useHydrationData('__customizationData')
|
||||
}
|
@ -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],
|
||||
)
|
||||
}
|
@ -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
|
||||
}
|
@ -0,0 +1,6 @@
|
||||
import { useDeviceSessionsQuery } from '#/data/useDeviceSessionsQuery'
|
||||
|
||||
export function useHasAccounts() {
|
||||
const { data: accounts } = useDeviceSessionsQuery()
|
||||
return accounts?.length > 0
|
||||
}
|
@ -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]
|
||||
}
|
@ -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)
|
||||
},
|
||||
})
|
||||
}
|
@ -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)
|
||||
},
|
||||
})
|
||||
}
|
@ -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 })
|
||||
},
|
||||
})
|
||||
}
|
@ -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 })
|
||||
},
|
||||
})
|
||||
}
|
@ -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) })
|
||||
},
|
||||
})
|
||||
}
|
@ -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
|
||||
},
|
||||
})
|
||||
}
|
@ -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 }) })
|
||||
}
|
||||
},
|
||||
})
|
||||
}
|
17
packages/oauth/oauth-provider-frontend/src/hydration-data.d.ts
vendored
Normal file
17
packages/oauth/oauth-provider-frontend/src/hydration-data.d.ts
vendored
Normal 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[]
|
||||
}
|
||||
}
|
@ -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)
|
||||
}
|
@ -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 haven’t 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 ""
|
@ -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 haven’t 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 ""
|
@ -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 haven’t 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 ""
|
@ -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 haven’t 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 ""
|
@ -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 haven’t 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 ""
|
@ -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 haven’t 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 ""
|
@ -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 haven’t 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 ""
|
@ -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 haven’t used this account to sign in to any apps yet."
|
||||
msgstr "It appears that you haven’t 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."
|
@ -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 haven’t 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 ""
|
@ -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 haven’t 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 ""
|
@ -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 haven’t 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 ""
|
@ -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 haven’t 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
Loading…
x
Reference in New Issue
Block a user