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": [
|
"project": [
|
||||||
"tsconfig.json",
|
"tsconfig.json",
|
||||||
"packages/oauth/*/tsconfig.json",
|
"packages/oauth/*/tsconfig.json",
|
||||||
|
"packages/oauth/*/tsconfig.src.json",
|
||||||
"packages/internal/*/tsconfig.json",
|
"packages/internal/*/tsconfig.json",
|
||||||
"packages/*/tsconfig.json"
|
"packages/*/tsconfig.json"
|
||||||
]
|
]
|
||||||
|
2
.github/workflows/repo.yaml
vendored
2
.github/workflows/repo.yaml
vendored
@ -35,7 +35,7 @@ jobs:
|
|||||||
path: |
|
path: |
|
||||||
packages/*/dist
|
packages/*/dist
|
||||||
packages/*/*/dist
|
packages/*/*/dist
|
||||||
packages/oauth/oauth-provider-ui/src/locales/*/messages.ts
|
packages/oauth/*/src/locales/*/messages.ts
|
||||||
retention-days: 1
|
retention-days: 1
|
||||||
test:
|
test:
|
||||||
name: Test
|
name: Test
|
||||||
|
@ -14,4 +14,6 @@ packages/pds/src/lexicon
|
|||||||
packages/ozone/src/lexicon
|
packages/ozone/src/lexicon
|
||||||
|
|
||||||
# Automatically generated by lingui
|
# 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,
|
"tabWidth": 2,
|
||||||
"semi": false,
|
"semi": false,
|
||||||
"singleQuote": true,
|
"singleQuote": true,
|
||||||
|
"plugins": ["prettier-plugin-tailwindcss"],
|
||||||
"overrides": [
|
"overrides": [
|
||||||
{
|
{
|
||||||
"files": "*.hbs",
|
"files": "*.hbs",
|
||||||
|
@ -20,8 +20,8 @@
|
|||||||
"verify:types": "tsc --build tsconfig.json",
|
"verify:types": "tsc --build tsconfig.json",
|
||||||
"format": "pnpm lint:fix && pnpm style:fix",
|
"format": "pnpm lint:fix && pnpm style:fix",
|
||||||
"precodegen": "pnpm run --recursive --stream --filter '@atproto/lex-cli...' build --force",
|
"precodegen": "pnpm run --recursive --stream --filter '@atproto/lex-cli...' build --force",
|
||||||
"codegen": "pnpm run --recursive --stream --parallel codegen",
|
"codegen": "pnpm run --sort --recursive --stream --parallel codegen",
|
||||||
"build": "pnpm run --recursive --stream '/^(build|build:.+)$/'",
|
"build": "pnpm run --sort --recursive --stream '/^(build|build:.+)$/'",
|
||||||
"dev": "NODE_ENV=development pnpm run --recursive --parallel --stream '/^(dev|dev:.+)$/'",
|
"dev": "NODE_ENV=development pnpm run --recursive --parallel --stream '/^(dev|dev:.+)$/'",
|
||||||
"dev:tsc": "tsc --build tsconfig.json --preserveWatchOutput --watch",
|
"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",
|
"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",
|
"pino-pretty": "^9.1.0",
|
||||||
"prettier": "^3.2.5",
|
"prettier": "^3.2.5",
|
||||||
"prettier-config-standard": "^7.0.0",
|
"prettier-config-standard": "^7.0.0",
|
||||||
"typescript": "^5.6.3"
|
"prettier-plugin-tailwindcss": "^0.6.11",
|
||||||
|
"typescript": "^5.8.2"
|
||||||
},
|
},
|
||||||
"workspaces": {
|
"workspaces": {
|
||||||
"packages": [
|
"packages": [
|
||||||
|
@ -17,8 +17,8 @@
|
|||||||
"bin": "dist/bin.js",
|
"bin": "dist/bin.js",
|
||||||
"scripts": {
|
"scripts": {
|
||||||
"build": "tsc --build tsconfig.build.json",
|
"build": "tsc --build tsconfig.build.json",
|
||||||
"start": "../dev-infra/with-test-redis-and-db.sh node 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 --watch dist/bin.js"
|
"dev": "../dev-infra/with-test-redis-and-db.sh node --enable-source-maps --watch dist/bin.js"
|
||||||
},
|
},
|
||||||
"engines": {
|
"engines": {
|
||||||
"node": ">=18.7.0"
|
"node": ">=18.7.0"
|
||||||
|
@ -49,17 +49,13 @@ export class TestPds {
|
|||||||
inviteRequired: false,
|
inviteRequired: false,
|
||||||
disableSsrfProtection: true,
|
disableSsrfProtection: true,
|
||||||
serviceName: 'Development PDS',
|
serviceName: 'Development PDS',
|
||||||
brandColor: '#8338ec',
|
primaryColor: '#f0828d',
|
||||||
errorColor: '#ff006e',
|
errorColor: '#a5414b',
|
||||||
// Purposefully not setting warningColor to ensure that not all branding
|
|
||||||
// colors are required from a config perspective.
|
|
||||||
warningColor: undefined,
|
|
||||||
successColor: '#02c39a',
|
|
||||||
logoUrl:
|
logoUrl:
|
||||||
// Using a "data:" instead of a real URL to avoid making CORS requests in dev.
|
// Using a "data:" instead of a real URL to avoid making CORS requests in dev.
|
||||||
// License: https://uxwing.com/license/
|
// License: https://uxwing.com/license/
|
||||||
// Source: https://uxwing.com/bee-icon/
|
// 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/',
|
homeUrl: 'https://bsky.social/',
|
||||||
termsOfServiceUrl: 'https://bsky.social/about/support/tos',
|
termsOfServiceUrl: 'https://bsky.social/about/support/tos',
|
||||||
privacyPolicyUrl: 'https://bsky.social/about/support/privacy-policy',
|
privacyPolicyUrl: 'https://bsky.social/about/support/privacy-policy',
|
||||||
|
@ -1,7 +1,7 @@
|
|||||||
import { createHash } from 'node:crypto'
|
import { createHash } from 'node:crypto'
|
||||||
import { extname } from 'node:path'
|
import { extname } from 'node:path'
|
||||||
import mime from 'mime'
|
import mime from 'mime'
|
||||||
import { Plugin } from 'rollup'
|
import type { Plugin } from 'rollup'
|
||||||
|
|
||||||
type AssetItem = {
|
type AssetItem = {
|
||||||
type: 'asset'
|
type: 'asset'
|
||||||
@ -26,7 +26,7 @@ export type ManifestItem = AssetItem | ChunkItem
|
|||||||
|
|
||||||
export type Manifest = Record<string, ManifestItem>
|
export type Manifest = Record<string, ManifestItem>
|
||||||
|
|
||||||
export default function bundleManifest({
|
export function bundleManifest({
|
||||||
name = 'bundle-manifest.json',
|
name = 'bundle-manifest.json',
|
||||||
data = false,
|
data = false,
|
||||||
}: {
|
}: {
|
||||||
|
@ -7,7 +7,7 @@ const { default: nodeResolve } = require('@rollup/plugin-node-resolve')
|
|||||||
const { default: swc } = require('@rollup/plugin-swc')
|
const { default: swc } = require('@rollup/plugin-swc')
|
||||||
const { defineConfig } = require('rollup')
|
const { defineConfig } = require('rollup')
|
||||||
const {
|
const {
|
||||||
default: manifest,
|
bundleManifest,
|
||||||
} = require('@atproto-labs/rollup-plugin-bundle-manifest')
|
} = require('@atproto-labs/rollup-plugin-bundle-manifest')
|
||||||
const postcss = ((m) => m.default || m)(require('rollup-plugin-postcss'))
|
const postcss = ((m) => m.default || m)(require('rollup-plugin-postcss'))
|
||||||
const serve = ((m) => m.default || m)(require('rollup-plugin-serve'))
|
const serve = ((m) => m.default || m)(require('rollup-plugin-serve'))
|
||||||
@ -106,7 +106,7 @@ module.exports = defineConfig((commandLineArguments) => {
|
|||||||
</html>
|
</html>
|
||||||
`,
|
`,
|
||||||
}),
|
}),
|
||||||
manifest({ name: 'files.json', data: true }),
|
bundleManifest({ name: 'files.json', data: true }),
|
||||||
|
|
||||||
commandLineArguments.watch &&
|
commandLineArguments.watch &&
|
||||||
serve({
|
serve({
|
||||||
|
@ -35,9 +35,9 @@ export function AuthForm({ atpSignIn, oauthSignIn, signUpUrl }: AuthFormProps) {
|
|||||||
// Tailwind css tabs
|
// Tailwind css tabs
|
||||||
return (
|
return (
|
||||||
<div className="p-4">
|
<div className="p-4">
|
||||||
<div className="flex my-4">
|
<div className="my-4 flex">
|
||||||
<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 === 'oauth' ? 'bg-blue-700' : ''
|
method === 'oauth' ? 'bg-blue-700' : ''
|
||||||
}`}
|
}`}
|
||||||
onClick={() => oauthSignIn && setMethod('oauth')}
|
onClick={() => oauthSignIn && setMethod('oauth')}
|
||||||
@ -47,7 +47,7 @@ export function AuthForm({ atpSignIn, oauthSignIn, signUpUrl }: AuthFormProps) {
|
|||||||
</button>
|
</button>
|
||||||
|
|
||||||
<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' : ''
|
method === 'credential' ? 'bg-blue-700' : ''
|
||||||
}`}
|
}`}
|
||||||
onClick={() => atpSignIn && setMethod('credential')}
|
onClick={() => atpSignIn && setMethod('credential')}
|
||||||
|
@ -24,7 +24,7 @@ export function CredentialSignInForm({
|
|||||||
const [password, setPassword] = useState('')
|
const [password, setPassword] = useState('')
|
||||||
const [service, setService] = useState('http://localhost:2583')
|
const [service, setService] = useState('http://localhost:2583')
|
||||||
|
|
||||||
// TODO: add auth factor support ?
|
// @TODO: add auth factor support ?
|
||||||
|
|
||||||
const onSubmit = useCallback(
|
const onSubmit = useCallback(
|
||||||
async (e: FormEvent<HTMLFormElement>) => {
|
async (e: FormEvent<HTMLFormElement>) => {
|
||||||
@ -49,14 +49,14 @@ export function CredentialSignInForm({
|
|||||||
)
|
)
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<form {...props} className="max-w-lg w-full" onSubmit={onSubmit}>
|
<form {...props} 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">
|
<fieldset className="rounded-md border border-solid border-slate-200 text-neutral-700 dark:border-slate-700 dark:text-neutral-100">
|
||||||
<div className="relative p-1 flex flex-col flex-wrap items-center justify-stretch">
|
<div className="relative flex flex-col flex-wrap items-center justify-stretch p-1">
|
||||||
<input
|
<input
|
||||||
id="identifier"
|
id="identifier"
|
||||||
name="identifier"
|
name="identifier"
|
||||||
type="text"
|
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"
|
placeholder="@handle or email"
|
||||||
aria-label="@handle or email"
|
aria-label="@handle or email"
|
||||||
required
|
required
|
||||||
@ -69,7 +69,7 @@ export function CredentialSignInForm({
|
|||||||
id="password"
|
id="password"
|
||||||
name="password"
|
name="password"
|
||||||
type="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"
|
placeholder="Password"
|
||||||
aria-label="Password"
|
aria-label="Password"
|
||||||
required
|
required
|
||||||
@ -82,7 +82,7 @@ export function CredentialSignInForm({
|
|||||||
id="service"
|
id="service"
|
||||||
name="service"
|
name="service"
|
||||||
type="text"
|
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"
|
placeholder="Service"
|
||||||
aria-label="Service"
|
aria-label="Service"
|
||||||
required
|
required
|
||||||
@ -94,7 +94,7 @@ export function CredentialSignInForm({
|
|||||||
<button
|
<button
|
||||||
type="submit"
|
type="submit"
|
||||||
disabled={loading}
|
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
|
Login
|
||||||
</button>
|
</button>
|
||||||
|
@ -61,15 +61,15 @@ export function OAuthSignInForm({
|
|||||||
return (
|
return (
|
||||||
<form
|
<form
|
||||||
{...props}
|
{...props}
|
||||||
className={`${className || ''} max-w-lg w-full`}
|
className={`${className || ''} w-full max-w-lg`}
|
||||||
onSubmit={onSubmit}
|
onSubmit={onSubmit}
|
||||||
>
|
>
|
||||||
<fieldset className="rounded-md border border-solid border-slate-200 dark:border-slate-700 text-neutral-700 dark:text-neutral-100">
|
<fieldset className="rounded-md border border-solid border-slate-200 text-neutral-700 dark:border-slate-700 dark:text-neutral-100">
|
||||||
<div className="relative p-1 flex flex-wrap items-center justify-stretch">
|
<div className="relative flex flex-wrap items-center justify-stretch p-1">
|
||||||
<input
|
<input
|
||||||
name="value"
|
name="value"
|
||||||
type="text"
|
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"
|
placeholder="@handle, DID or PDS url"
|
||||||
aria-label="@handle, DID or PDS url"
|
aria-label="@handle, DID or PDS url"
|
||||||
required
|
required
|
||||||
@ -80,7 +80,7 @@ export function OAuthSignInForm({
|
|||||||
<button
|
<button
|
||||||
type="submit"
|
type="submit"
|
||||||
disabled={loading}
|
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
|
Login
|
||||||
</button>
|
</button>
|
||||||
@ -93,7 +93,7 @@ export function OAuthSignInForm({
|
|||||||
type="button"
|
type="button"
|
||||||
onClick={() => signIn(signUpUrl)}
|
onClick={() => signIn(signUpUrl)}
|
||||||
disabled={loading}
|
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
|
Sign up
|
||||||
</button>
|
</button>
|
||||||
|
@ -142,7 +142,7 @@ export class BrowserOAuthClient extends OAuthClient implements Disposable {
|
|||||||
database.getProtectedResourceMetadataCache(),
|
database.getProtectedResourceMetadataCache(),
|
||||||
})
|
})
|
||||||
|
|
||||||
// TODO: replace with AsyncDisposableStack once they are standardized
|
// @TODO replace with AsyncDisposableStack once they are standardized
|
||||||
const ac = new AbortController()
|
const ac = new AbortController()
|
||||||
const { signal } = ac
|
const { signal } = ac
|
||||||
this[Symbol.dispose] = () => ac.abort()
|
this[Symbol.dispose] = () => ac.abort()
|
||||||
@ -293,7 +293,7 @@ export class BrowserOAuthClient extends OAuthClient implements Disposable {
|
|||||||
}
|
}
|
||||||
|
|
||||||
const cancel = () => {
|
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.
|
// callback (e.g. in the popup) to revoke the session or credentials.
|
||||||
|
|
||||||
reject(new Error(options?.signal?.aborted ? 'Aborted' : 'Timeout'))
|
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
|
// credentials from the authorization server (e.g. because some migration
|
||||||
// occurred). Any ways, there is no point in keeping the session.
|
// occurred). Any ways, there is no point in keeping the session.
|
||||||
if (isInvalidTokenResponse(finalResponse)) {
|
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
|
// session as "expired" in the session store, allowing the user to trigger
|
||||||
// a new login (using login_hint)?
|
// a new login (using login_hint)?
|
||||||
await this.sessionGetter.delStored(
|
await this.sessionGetter.delStored(
|
||||||
|
@ -25,6 +25,7 @@
|
|||||||
}
|
}
|
||||||
},
|
},
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
|
"@atproto/jwk": "workspace:*",
|
||||||
"@atproto/oauth-types": "workspace:*"
|
"@atproto/oauth-types": "workspace:*"
|
||||||
},
|
},
|
||||||
"devDependencies": {
|
"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 } from './types.js'
|
||||||
import type { LinkDefinition, ScopeDetail, Session } from './types.js'
|
|
||||||
|
|
||||||
// These are the types of the variables that are injected into the HTML by the
|
// These are the types of the variables that are injected into the HTML by the
|
||||||
// backend. They are used to configure the frontend.
|
// backend. They are used to configure the frontend.
|
||||||
|
|
||||||
export type AvailableLocales = readonly string[]
|
|
||||||
|
|
||||||
export type CustomizationData = {
|
export type CustomizationData = {
|
||||||
// Functional customization
|
// Functional customization
|
||||||
hcaptchaSiteKey?: string
|
hcaptchaSiteKey?: string
|
||||||
@ -17,19 +14,3 @@ export type CustomizationData = {
|
|||||||
logo?: string
|
logo?: string
|
||||||
links?: LinkDefinition[]
|
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 './backend-types.js'
|
||||||
export type * from './types.js'
|
export type * from './types.js'
|
||||||
|
|
||||||
|
export * from './contants.js'
|
||||||
|
@ -18,9 +18,12 @@ export type Session = {
|
|||||||
consentRequired: boolean
|
consentRequired: boolean
|
||||||
}
|
}
|
||||||
|
|
||||||
export type LocalizedString =
|
export type MultiLangString = { en: string } & Record<
|
||||||
| string
|
string,
|
||||||
| ({ en: string } & Record<string, string | undefined>)
|
string | undefined
|
||||||
|
>
|
||||||
|
|
||||||
|
export type LocalizedString = string | MultiLangString
|
||||||
|
|
||||||
export type LinkDefinition = {
|
export type LinkDefinition = {
|
||||||
title: LocalizedString
|
title: LocalizedString
|
||||||
@ -30,5 +33,13 @@ export type LinkDefinition = {
|
|||||||
|
|
||||||
export type ScopeDetail = {
|
export type ScopeDetail = {
|
||||||
scope: string
|
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