From cfd12f3fab71a326093c25ec66ebdddb0657639e Mon Sep 17 00:00:00 2001 From: Hailey Date: Mon, 1 Apr 2024 22:45:08 -0700 Subject: [PATCH 01/54] rm old login code --- src/screens/Login/LoginForm.tsx | 186 +++----------------------------- 1 file changed, 12 insertions(+), 174 deletions(-) diff --git a/src/screens/Login/LoginForm.tsx b/src/screens/Login/LoginForm.tsx index 6bf215ee56..157ea52939 100644 --- a/src/screens/Login/LoginForm.tsx +++ b/src/screens/Login/LoginForm.tsx @@ -53,8 +53,6 @@ export const LoginForm = ({ const {track} = useAnalytics() const t = useTheme() const [isProcessing, setIsProcessing] = useState(false) - const [identifier, setIdentifier] = useState(initialHandle) - const [password, setPassword] = useState('') const passwordInputRef = useRef(null) const {_} = useLingui() const {login} = useSessionApi() @@ -64,69 +62,8 @@ export const LoginForm = ({ track('Signin:PressedSelectService') }, [track]) - const onPressNext = async () => { - if (isProcessing) return - Keyboard.dismiss() - LayoutAnimation.configureNext(LayoutAnimation.Presets.easeInEaseOut) - setError('') - setIsProcessing(true) - - try { - // try to guess the handle if the user just gave their own username - let fullIdent = identifier - if ( - !identifier.includes('@') && // not an email - !identifier.includes('.') && // not a domain - serviceDescription && - serviceDescription.availableUserDomains.length > 0 - ) { - let matched = false - for (const domain of serviceDescription.availableUserDomains) { - if (fullIdent.endsWith(domain)) { - matched = true - } - } - if (!matched) { - fullIdent = createFullHandle( - identifier, - serviceDescription.availableUserDomains[0], - ) - } - } + const onPressNext = async () => {} - // TODO remove double login - await login( - { - service: serviceUrl, - identifier: fullIdent, - password, - }, - 'LoginForm', - ) - } catch (e: any) { - const errMsg = e.toString() - LayoutAnimation.configureNext(LayoutAnimation.Presets.easeInEaseOut) - setIsProcessing(false) - if (errMsg.includes('Authentication Required')) { - logger.debug('Failed to login due to invalid credentials', { - error: errMsg, - }) - setError(_(msg`Invalid username or password`)) - } else if (isNetworkError(e)) { - logger.warn('Failed to login due to network error', {error: errMsg}) - setError( - _( - msg`Unable to contact your service. Please check your Internet connection.`, - ), - ) - } else { - logger.warn('Failed to login', {error: errMsg}) - setError(cleanError(errMsg)) - } - } - } - - const isReady = !!serviceDescription && !!identifier && !!password return ( Sign in}> @@ -139,84 +76,8 @@ export const LoginForm = ({ onOpenDialog={onPressSelectService} /> - - - Account - - - - - { - passwordInputRef.current?.focus() - }} - blurOnSubmit={false} // prevents flickering due to onSubmitEditing going to next field - value={identifier} - onChangeText={str => - setIdentifier((str || '').toLowerCase().trim()) - } - editable={!isProcessing} - accessibilityHint={_( - msg`Input the username or email address you used at signup`, - )} - /> - - - - - - - - - - + - - {!serviceDescription && error ? ( - - ) : !serviceDescription ? ( - <> - - - Connecting... - - - ) : isReady ? ( - - ) : undefined} + ) From 9128cb16905682feef525e1f9723f72751dac103 Mon Sep 17 00:00:00 2001 From: Hailey Date: Mon, 1 Apr 2024 23:56:43 -0700 Subject: [PATCH 02/54] rm some more code and add some new code --- src/screens/Login/LoginForm.tsx | 24 +++++++++++++++++++++++- 1 file changed, 23 insertions(+), 1 deletion(-) diff --git a/src/screens/Login/LoginForm.tsx b/src/screens/Login/LoginForm.tsx index 157ea52939..e090e080c3 100644 --- a/src/screens/Login/LoginForm.tsx +++ b/src/screens/Login/LoginForm.tsx @@ -6,6 +6,7 @@ import { TextInput, View, } from 'react-native' +import * as Browser from 'expo-web-browser' import {ComAtprotoServerDescribeServer} from '@atproto/api' import {msg, Trans} from '@lingui/macro' import {useLingui} from '@lingui/react' @@ -57,12 +58,33 @@ export const LoginForm = ({ const {_} = useLingui() const {login} = useSessionApi() + // This improves speed at which the browser presents itself on Android + React.useEffect(() => { + Browser.warmUpAsync() + }, []) + const onPressSelectService = React.useCallback(() => { Keyboard.dismiss() track('Signin:PressedSelectService') }, [track]) - const onPressNext = async () => {} + const onPressNext = async () => { + const authSession = await Browser.openAuthSessionAsync( + 'https://bsky.app/login', // Replace this with the PDS auth url + 'bsky://login', // Replace this as well with the appropriate link + { + // Similar to how Google auth works. Sessions will be remembered so that we can + // usually proceed without needing credentials + preferEphemeralSession: true, + }, + ) + + if (authSession.type !== 'success') { + return + } + + // Handle session storage here + } return ( Sign in}> From 103d441f081328bcbe4b4e3e8e505998fdc88e52 Mon Sep 17 00:00:00 2001 From: Hailey Date: Tue, 2 Apr 2024 15:49:39 -0700 Subject: [PATCH 03/54] remove some more unnecessary stuff --- src/screens/Login/ForgotPasswordForm.tsx | 184 --------------------- src/screens/Login/LoginForm.tsx | 46 ++---- src/screens/Login/PasswordUpdatedForm.tsx | 50 ------ src/screens/Login/SetNewPasswordForm.tsx | 192 ---------------------- src/screens/Login/index.tsx | 55 +------ 5 files changed, 15 insertions(+), 512 deletions(-) delete mode 100644 src/screens/Login/ForgotPasswordForm.tsx delete mode 100644 src/screens/Login/PasswordUpdatedForm.tsx delete mode 100644 src/screens/Login/SetNewPasswordForm.tsx diff --git a/src/screens/Login/ForgotPasswordForm.tsx b/src/screens/Login/ForgotPasswordForm.tsx deleted file mode 100644 index 580452e75b..0000000000 --- a/src/screens/Login/ForgotPasswordForm.tsx +++ /dev/null @@ -1,184 +0,0 @@ -import React, {useEffect, useState} from 'react' -import {ActivityIndicator, Keyboard, View} from 'react-native' -import {ComAtprotoServerDescribeServer} from '@atproto/api' -import {BskyAgent} from '@atproto/api' -import {msg, Trans} from '@lingui/macro' -import {useLingui} from '@lingui/react' -import * as EmailValidator from 'email-validator' - -import {useAnalytics} from '#/lib/analytics/analytics' -import {isNetworkError} from '#/lib/strings/errors' -import {cleanError} from '#/lib/strings/errors' -import {logger} from '#/logger' -import {atoms as a, useTheme} from '#/alf' -import {Button, ButtonText} from '#/components/Button' -import {FormError} from '#/components/forms/FormError' -import {HostingProvider} from '#/components/forms/HostingProvider' -import * as TextField from '#/components/forms/TextField' -import {At_Stroke2_Corner0_Rounded as At} from '#/components/icons/At' -import {Text} from '#/components/Typography' -import {FormContainer} from './FormContainer' - -type ServiceDescription = ComAtprotoServerDescribeServer.OutputSchema - -export const ForgotPasswordForm = ({ - error, - serviceUrl, - serviceDescription, - setError, - setServiceUrl, - onPressBack, - onEmailSent, -}: { - error: string - serviceUrl: string - serviceDescription: ServiceDescription | undefined - setError: (v: string) => void - setServiceUrl: (v: string) => void - onPressBack: () => void - onEmailSent: () => void -}) => { - const t = useTheme() - const [isProcessing, setIsProcessing] = useState(false) - const [email, setEmail] = useState('') - const {screen} = useAnalytics() - const {_} = useLingui() - - useEffect(() => { - screen('Signin:ForgotPassword') - }, [screen]) - - const onPressSelectService = React.useCallback(() => { - Keyboard.dismiss() - }, []) - - const onPressNext = async () => { - if (!EmailValidator.validate(email)) { - return setError(_(msg`Your email appears to be invalid.`)) - } - - setError('') - setIsProcessing(true) - - try { - const agent = new BskyAgent({service: serviceUrl}) - await agent.com.atproto.server.requestPasswordReset({email}) - onEmailSent() - } catch (e: any) { - const errMsg = e.toString() - logger.warn('Failed to request password reset', {error: e}) - setIsProcessing(false) - if (isNetworkError(e)) { - setError( - _( - msg`Unable to contact your service. Please check your Internet connection.`, - ), - ) - } else { - setError(cleanError(errMsg)) - } - } - } - - return ( - Reset password}> - - - Hosting provider - - - - - - Email address - - - - - - - - - - Enter the email you used to create your account. We'll send you a - "reset code" so you can set a new password. - - - - - - - - - {!serviceDescription || isProcessing ? ( - - ) : ( - - )} - {!serviceDescription || isProcessing ? ( - - Processing... - - ) : undefined} - - - - - - ) -} diff --git a/src/screens/Login/LoginForm.tsx b/src/screens/Login/LoginForm.tsx index e090e080c3..919591eb5e 100644 --- a/src/screens/Login/LoginForm.tsx +++ b/src/screens/Login/LoginForm.tsx @@ -1,31 +1,17 @@ -import React, {useRef, useState} from 'react' -import { - ActivityIndicator, - Keyboard, - LayoutAnimation, - TextInput, - View, -} from 'react-native' +import React from 'react' +import {Keyboard, View} from 'react-native' import * as Browser from 'expo-web-browser' import {ComAtprotoServerDescribeServer} from '@atproto/api' import {msg, Trans} from '@lingui/macro' import {useLingui} from '@lingui/react' import {useAnalytics} from '#/lib/analytics/analytics' -import {isNetworkError} from '#/lib/strings/errors' -import {cleanError} from '#/lib/strings/errors' -import {createFullHandle} from '#/lib/strings/handles' -import {logger} from '#/logger' -import {useSessionApi} from '#/state/session' -import {atoms as a, useTheme} from '#/alf' -import {Button, ButtonIcon, ButtonText} from '#/components/Button' +import {isAndroid} from 'platform/detection' +import {atoms as a} from '#/alf' +import {Button, ButtonText} from '#/components/Button' import {FormError} from '#/components/forms/FormError' import {HostingProvider} from '#/components/forms/HostingProvider' import * as TextField from '#/components/forms/TextField' -import {At_Stroke2_Corner0_Rounded as At} from '#/components/icons/At' -import {Lock_Stroke2_Corner0_Rounded as Lock} from '#/components/icons/Lock' -import {Loader} from '#/components/Loader' -import {Text} from '#/components/Typography' import {FormContainer} from './FormContainer' type ServiceDescription = ComAtprotoServerDescribeServer.OutputSchema @@ -34,17 +20,14 @@ export const LoginForm = ({ error, serviceUrl, serviceDescription, - initialHandle, setError, setServiceUrl, onPressRetryConnect, onPressBack, - onPressForgotPassword, }: { error: string serviceUrl: string serviceDescription: ServiceDescription | undefined - initialHandle: string setError: (v: string) => void setServiceUrl: (v: string) => void onPressRetryConnect: () => void @@ -52,15 +35,13 @@ export const LoginForm = ({ onPressForgotPassword: () => void }) => { const {track} = useAnalytics() - const t = useTheme() - const [isProcessing, setIsProcessing] = useState(false) - const passwordInputRef = useRef(null) const {_} = useLingui() - const {login} = useSessionApi() // This improves speed at which the browser presents itself on Android React.useEffect(() => { - Browser.warmUpAsync() + if (isAndroid) { + Browser.warmUpAsync() + } }, []) const onPressSelectService = React.useCallback(() => { @@ -73,9 +54,7 @@ export const LoginForm = ({ 'https://bsky.app/login', // Replace this with the PDS auth url 'bsky://login', // Replace this as well with the appropriate link { - // Similar to how Google auth works. Sessions will be remembered so that we can - // usually proceed without needing credentials - preferEphemeralSession: true, + windowFeatures: {}, }, ) @@ -86,6 +65,8 @@ export const LoginForm = ({ // Handle session storage here } + console.log(serviceDescription) + return ( Sign in}> @@ -115,9 +96,10 @@ export const LoginForm = ({ variant="solid" color="primary" size="medium" - onPress={onPressBack}> + onPress={onPressNext} + disabled={!serviceDescription}> - Login + Sign In diff --git a/src/screens/Login/PasswordUpdatedForm.tsx b/src/screens/Login/PasswordUpdatedForm.tsx deleted file mode 100644 index 5407f3f1e3..0000000000 --- a/src/screens/Login/PasswordUpdatedForm.tsx +++ /dev/null @@ -1,50 +0,0 @@ -import React, {useEffect} from 'react' -import {View} from 'react-native' -import {msg, Trans} from '@lingui/macro' -import {useLingui} from '@lingui/react' - -import {useAnalytics} from '#/lib/analytics/analytics' -import {atoms as a, useBreakpoints} from '#/alf' -import {Button, ButtonText} from '#/components/Button' -import {Text} from '#/components/Typography' -import {FormContainer} from './FormContainer' - -export const PasswordUpdatedForm = ({ - onPressNext, -}: { - onPressNext: () => void -}) => { - const {screen} = useAnalytics() - const {_} = useLingui() - const {gtMobile} = useBreakpoints() - - useEffect(() => { - screen('Signin:PasswordUpdatedForm') - }, [screen]) - - return ( - - - Password updated! - - - You can now sign in with your new password. - - - - - - ) -} diff --git a/src/screens/Login/SetNewPasswordForm.tsx b/src/screens/Login/SetNewPasswordForm.tsx deleted file mode 100644 index e7b4886550..0000000000 --- a/src/screens/Login/SetNewPasswordForm.tsx +++ /dev/null @@ -1,192 +0,0 @@ -import React, {useEffect, useState} from 'react' -import {ActivityIndicator, View} from 'react-native' -import {BskyAgent} from '@atproto/api' -import {msg, Trans} from '@lingui/macro' -import {useLingui} from '@lingui/react' - -import {useAnalytics} from '#/lib/analytics/analytics' -import {isNetworkError} from '#/lib/strings/errors' -import {cleanError} from '#/lib/strings/errors' -import {checkAndFormatResetCode} from '#/lib/strings/password' -import {logger} from '#/logger' -import {atoms as a, useTheme} from '#/alf' -import {Button, ButtonText} from '#/components/Button' -import {FormError} from '#/components/forms/FormError' -import * as TextField from '#/components/forms/TextField' -import {Lock_Stroke2_Corner0_Rounded as Lock} from '#/components/icons/Lock' -import {Ticket_Stroke2_Corner0_Rounded as Ticket} from '#/components/icons/Ticket' -import {Text} from '#/components/Typography' -import {FormContainer} from './FormContainer' - -export const SetNewPasswordForm = ({ - error, - serviceUrl, - setError, - onPressBack, - onPasswordSet, -}: { - error: string - serviceUrl: string - setError: (v: string) => void - onPressBack: () => void - onPasswordSet: () => void -}) => { - const {screen} = useAnalytics() - const {_} = useLingui() - const t = useTheme() - - useEffect(() => { - screen('Signin:SetNewPasswordForm') - }, [screen]) - - const [isProcessing, setIsProcessing] = useState(false) - const [resetCode, setResetCode] = useState('') - const [password, setPassword] = useState('') - - const onPressNext = async () => { - // Check that the code is correct. We do this again just incase the user enters the code after their pw and we - // don't get to call onBlur first - const formattedCode = checkAndFormatResetCode(resetCode) - // TODO Better password strength check - if (!formattedCode || !password) { - setError( - _( - msg`You have entered an invalid code. It should look like XXXXX-XXXXX.`, - ), - ) - return - } - - setError('') - setIsProcessing(true) - - try { - const agent = new BskyAgent({service: serviceUrl}) - await agent.com.atproto.server.resetPassword({ - token: formattedCode, - password, - }) - onPasswordSet() - } catch (e: any) { - const errMsg = e.toString() - logger.warn('Failed to set new password', {error: e}) - setIsProcessing(false) - if (isNetworkError(e)) { - setError( - _( - msg`Unable to contact your service. Please check your Internet connection.`, - ), - ) - } else { - setError(cleanError(errMsg)) - } - } - } - - const onBlur = () => { - const formattedCode = checkAndFormatResetCode(resetCode) - if (!formattedCode) { - setError( - _( - msg`You have entered an invalid code. It should look like XXXXX-XXXXX.`, - ), - ) - return - } - setResetCode(formattedCode) - } - - return ( - Set new password}> - - - You will receive an email with a "reset code." Enter that code here, - then enter your new password. - - - - - Reset code - - - setError('')} - onBlur={onBlur} - editable={!isProcessing} - accessibilityHint={_( - msg`Input code sent to your email for password reset`, - )} - /> - - - - - New password - - - - - - - - - - - - {isProcessing ? ( - - ) : ( - - )} - {isProcessing ? ( - - Updating... - - ) : undefined} - - - ) -} diff --git a/src/screens/Login/index.tsx b/src/screens/Login/index.tsx index 1fce63d298..42b355a730 100644 --- a/src/screens/Login/index.tsx +++ b/src/screens/Login/index.tsx @@ -4,17 +4,13 @@ import {LayoutAnimationConfig} from 'react-native-reanimated' import {msg} from '@lingui/macro' import {useLingui} from '@lingui/react' -import {useAnalytics} from '#/lib/analytics/analytics' import {DEFAULT_SERVICE} from '#/lib/constants' import {logger} from '#/logger' import {useServiceQuery} from '#/state/queries/service' import {SessionAccount, useSession} from '#/state/session' import {useLoggedOutView} from '#/state/shell/logged-out' import {LoggedOutLayout} from '#/view/com/util/layouts/LoggedOutLayout' -import {ForgotPasswordForm} from '#/screens/Login/ForgotPasswordForm' import {LoginForm} from '#/screens/Login/LoginForm' -import {PasswordUpdatedForm} from '#/screens/Login/PasswordUpdatedForm' -import {SetNewPasswordForm} from '#/screens/Login/SetNewPasswordForm' import {atoms as a} from '#/alf' import {ChooseAccountForm} from './ChooseAccountForm' import {ScreenTransition} from './ScreenTransition' @@ -22,16 +18,12 @@ import {ScreenTransition} from './ScreenTransition' enum Forms { Login, ChooseAccount, - ForgotPassword, - SetNewPassword, - PasswordUpdated, } export const Login = ({onPressBack}: {onPressBack: () => void}) => { const {_} = useLingui() const {accounts} = useSession() - const {track} = useAnalytics() const {requestedAccountSwitchTo} = useLoggedOutView() const requestedAccount = accounts.find( acc => acc.did === requestedAccountSwitchTo, @@ -41,9 +33,6 @@ export const Login = ({onPressBack}: {onPressBack: () => void}) => { const [serviceUrl, setServiceUrl] = React.useState( requestedAccount?.service || DEFAULT_SERVICE, ) - const [initialHandle, setInitialHandle] = React.useState( - requestedAccount?.handle || '', - ) const [currentForm, setCurrentForm] = React.useState( requestedAccount ? Forms.Login @@ -62,7 +51,7 @@ export const Login = ({onPressBack}: {onPressBack: () => void}) => { if (account?.service) { setServiceUrl(account.service) } - setInitialHandle(account?.handle || '') + // TODO set the service URL. We really need to fix this though in general setCurrentForm(Forms.Login) } @@ -86,11 +75,6 @@ export const Login = ({onPressBack}: {onPressBack: () => void}) => { } }, [serviceError, serviceUrl, _]) - const onPressForgotPassword = () => { - track('Signin:PressedForgotPassword') - setCurrentForm(Forms.ForgotPassword) - } - let content = null let title = '' let description = '' @@ -104,13 +88,11 @@ export const Login = ({onPressBack}: {onPressBack: () => void}) => { error={error} serviceUrl={serviceUrl} serviceDescription={serviceDescription} - initialHandle={initialHandle} setError={setError} setServiceUrl={setServiceUrl} onPressBack={() => accounts.length ? gotoForm(Forms.ChooseAccount) : onPressBack() } - onPressForgotPassword={onPressForgotPassword} onPressRetryConnect={refetchService} /> ) @@ -125,41 +107,6 @@ export const Login = ({onPressBack}: {onPressBack: () => void}) => { /> ) break - case Forms.ForgotPassword: - title = _(msg`Forgot Password`) - description = _(msg`Let's get your password reset!`) - content = ( - gotoForm(Forms.Login)} - onEmailSent={() => gotoForm(Forms.SetNewPassword)} - /> - ) - break - case Forms.SetNewPassword: - title = _(msg`Forgot Password`) - description = _(msg`Let's get your password reset!`) - content = ( - gotoForm(Forms.ForgotPassword)} - onPasswordSet={() => gotoForm(Forms.PasswordUpdated)} - /> - ) - break - case Forms.PasswordUpdated: - title = _(msg`Password updated`) - description = _(msg`You can now sign in with your new password.`) - content = ( - gotoForm(Forms.Login)} /> - ) - break } return ( From 6856b7665624262e0ceeb5b6f87b8844f33b95b5 Mon Sep 17 00:00:00 2001 From: Hailey Date: Tue, 2 Apr 2024 15:54:56 -0700 Subject: [PATCH 04/54] add auth session browser for native --- src/screens/Login/LoginForm.tsx | 25 ++++--------------------- src/screens/Login/hooks/useLogin.ts | 25 +++++++++++++++++++++++++ src/screens/Login/hooks/useLogin.web.ts | 0 3 files changed, 29 insertions(+), 21 deletions(-) create mode 100644 src/screens/Login/hooks/useLogin.ts create mode 100644 src/screens/Login/hooks/useLogin.web.ts diff --git a/src/screens/Login/LoginForm.tsx b/src/screens/Login/LoginForm.tsx index 919591eb5e..237e160d61 100644 --- a/src/screens/Login/LoginForm.tsx +++ b/src/screens/Login/LoginForm.tsx @@ -7,6 +7,7 @@ import {useLingui} from '@lingui/react' import {useAnalytics} from '#/lib/analytics/analytics' import {isAndroid} from 'platform/detection' +import {useLogin} from '#/screens/Login/hooks/useLogin' import {atoms as a} from '#/alf' import {Button, ButtonText} from '#/components/Button' import {FormError} from '#/components/forms/FormError' @@ -32,10 +33,10 @@ export const LoginForm = ({ setServiceUrl: (v: string) => void onPressRetryConnect: () => void onPressBack: () => void - onPressForgotPassword: () => void }) => { const {track} = useAnalytics() const {_} = useLingui() + const {openAuthSession} = useLogin(serviceUrl) // This improves speed at which the browser presents itself on Android React.useEffect(() => { @@ -49,24 +50,6 @@ export const LoginForm = ({ track('Signin:PressedSelectService') }, [track]) - const onPressNext = async () => { - const authSession = await Browser.openAuthSessionAsync( - 'https://bsky.app/login', // Replace this with the PDS auth url - 'bsky://login', // Replace this as well with the appropriate link - { - windowFeatures: {}, - }, - ) - - if (authSession.type !== 'success') { - return - } - - // Handle session storage here - } - - console.log(serviceDescription) - return ( Sign in}> @@ -80,7 +63,7 @@ export const LoginForm = ({ /> - + + + ) +} + +function AccountItem({ + account, + onSelect, + isCurrentAccount, +}: { + account: SessionAccount + onSelect: (account: SessionAccount) => void + isCurrentAccount: boolean +}) { + const t = useTheme() + const {_} = useLingui() + const {data: profile} = useProfileQuery({did: account.did}) + + const onPress = React.useCallback(() => { + onSelect(account) + }, [account, onSelect]) + + return ( + + ) +} diff --git a/src/components/Button.tsx b/src/components/Button.tsx index 67c33fa0c6..ece1ad6b0f 100644 --- a/src/components/Button.tsx +++ b/src/components/Button.tsx @@ -14,7 +14,6 @@ import { import LinearGradient from 'react-native-linear-gradient' import {Trans} from '@lingui/macro' -import {logger} from '#/logger' import {android, atoms as a, flatten, tokens, useTheme} from '#/alf' import {Props as SVGIconProps} from '#/components/icons/common' import {normalizeTextStyles} from '#/components/Typography' @@ -405,51 +404,20 @@ export function Button({ )} - - {/* @ts-ignore */} - {typeof children === 'string' || children?.type === Trans ? ( - /* @ts-ignore */ - {children} - ) : typeof children === 'function' ? ( - children(context) - ) : ( - children - )} - + {/* @ts-ignore */} + {typeof children === 'string' || children?.type === Trans ? ( + /* @ts-ignore */ + {children} + ) : typeof children === 'function' ? ( + children(context) + ) : ( + children + )} ) } -export class ButtonTextErrorBoundary extends React.Component< - React.PropsWithChildren<{}>, - {hasError: boolean; error: Error | undefined} -> { - public state = { - hasError: false, - error: undefined, - } - - public static getDerivedStateFromError(error: Error) { - return {hasError: true, error} - } - - public componentDidCatch(error: Error, errorInfo: React.ErrorInfo) { - logger.error('ButtonTextErrorBoundary caught an error', { - message: error.message, - errorInfo, - }) - } - - public render() { - if (this.state.hasError) { - return ERROR - } - - return this.props.children - } -} - export function useSharedButtonTextStyles() { const t = useTheme() const {color, variant, disabled, size} = useButtonContext() diff --git a/src/components/Error.tsx b/src/components/Error.tsx index 7df166c3fc..91b33f48e0 100644 --- a/src/components/Error.tsx +++ b/src/components/Error.tsx @@ -1,9 +1,9 @@ import React from 'react' import {View} from 'react-native' -import {useNavigation} from '@react-navigation/core' -import {StackActions} from '@react-navigation/native' import {msg, Trans} from '@lingui/macro' import {useLingui} from '@lingui/react' +import {useNavigation} from '@react-navigation/core' +import {StackActions} from '@react-navigation/native' import {NavigationProp} from 'lib/routes/types' import {CenteredView} from 'view/com/util/Views' diff --git a/src/components/LikedByList.tsx b/src/components/LikedByList.tsx index bd12136394..239a7044f6 100644 --- a/src/components/LikedByList.tsx +++ b/src/components/LikedByList.tsx @@ -1,47 +1,54 @@ import React from 'react' -import {View} from 'react-native' import {AppBskyFeedGetLikes as GetLikes} from '@atproto/api' -import {Trans} from '@lingui/macro' +import {msg} from '@lingui/macro' +import {useLingui} from '@lingui/react' import {logger} from '#/logger' -import {List} from '#/view/com/util/List' -import {ProfileCardWithFollowBtn} from '#/view/com/profile/ProfileCard' -import {useResolveUriQuery} from '#/state/queries/resolve-uri' import {useLikedByQuery} from '#/state/queries/post-liked-by' +import {useResolveUriQuery} from '#/state/queries/resolve-uri' import {useInitialNumToRender} from 'lib/hooks/useInitialNumToRender' -import {ListFooter} from '#/components/Lists' +import {cleanError} from 'lib/strings/errors' +import {ProfileCardWithFollowBtn} from '#/view/com/profile/ProfileCard' +import {List} from '#/view/com/util/List' +import {ListFooter, ListMaybePlaceholder} from '#/components/Lists' -import {atoms as a, useTheme} from '#/alf' -import {Loader} from '#/components/Loader' -import {Text} from '#/components/Typography' +function renderItem({item}: {item: GetLikes.Like}) { + return +} + +function keyExtractor(item: GetLikes.Like) { + return item.actor.did +} export function LikedByList({uri}: {uri: string}) { - const t = useTheme() + const {_} = useLingui() + const initialNumToRender = useInitialNumToRender() const [isPTRing, setIsPTRing] = React.useState(false) + const { data: resolvedUri, error: resolveError, - isFetching: isFetchingResolvedUri, + isLoading: isUriLoading, } = useResolveUriQuery(uri) const { data, - isFetching, - isFetched, - isRefetching, + isLoading: isLikedByLoading, + isFetchingNextPage, hasNextPage, fetchNextPage, - isError, error: likedByError, refetch, } = useLikedByQuery(resolvedUri?.uri) + + const error = resolveError || likedByError + const isError = !!resolveError || !!likedByError + const likes = React.useMemo(() => { if (data?.pages) { return data.pages.flatMap(page => page.likes) } return [] }, [data]) - const initialNumToRender = useInitialNumToRender() - const error = resolveError || likedByError const onRefresh = React.useCallback(async () => { setIsPTRing(true) @@ -54,56 +61,47 @@ export function LikedByList({uri}: {uri: string}) { }, [refetch, setIsPTRing]) const onEndReached = React.useCallback(async () => { - if (isFetching || !hasNextPage || isError) return + if (isFetchingNextPage || !hasNextPage || isError) return try { await fetchNextPage() } catch (err) { logger.error('Failed to load more likes', {message: err}) } - }, [isFetching, hasNextPage, isError, fetchNextPage]) - - const renderItem = React.useCallback(({item}: {item: GetLikes.Like}) => { - return ( - - ) - }, []) + }, [isFetchingNextPage, hasNextPage, isError, fetchNextPage]) - if (isFetchingResolvedUri || !isFetched) { + if (likes.length < 1) { return ( - - - + ) } - return likes.length ? ( + return ( item.actor.did} + renderItem={renderItem} + keyExtractor={keyExtractor} refreshing={isPTRing} onRefresh={onRefresh} onEndReached={onEndReached} - onEndReachedThreshold={3} - renderItem={renderItem} - initialNumToRender={initialNumToRender} - ListFooterComponent={() => ( + ListFooterComponent={ - )} + } + onEndReachedThreshold={3} + initialNumToRender={initialNumToRender} + windowSize={11} /> - ) : ( - - - - - Nobody has liked this yet. Maybe you should be the first! - - - - ) } diff --git a/src/components/Link.tsx b/src/components/Link.tsx index 7d0e833329..65a015ba3a 100644 --- a/src/components/Link.tsx +++ b/src/components/Link.tsx @@ -1,23 +1,24 @@ import React from 'react' import {GestureResponderEvent} from 'react-native' -import {useLinkProps, StackActions} from '@react-navigation/native' import {sanitizeUrl} from '@braintree/sanitize-url' +import {StackActions, useLinkProps} from '@react-navigation/native' -import {useInteractionState} from '#/components/hooks/useInteractionState' -import {isWeb} from '#/platform/detection' -import {useTheme, web, flatten, TextStyleProp, atoms as a} from '#/alf' -import {Button, ButtonProps} from '#/components/Button' import {AllNavigatorParams} from '#/lib/routes/types' +import {shareUrl} from '#/lib/sharing' import { convertBskyAppUrlIfNeeded, isExternalUrl, linkRequiresWarning, } from '#/lib/strings/url-helpers' +import {isNative, isWeb} from '#/platform/detection' import {useModalControls} from '#/state/modals' -import {router} from '#/routes' -import {Text, TextProps} from '#/components/Typography' -import {useOpenLink} from 'state/preferences/in-app-browser' +import {useOpenLink} from '#/state/preferences/in-app-browser' import {useNavigationDeduped} from 'lib/hooks/useNavigationDeduped' +import {atoms as a, flatten, TextStyleProp, useTheme, web} from '#/alf' +import {Button, ButtonProps} from '#/components/Button' +import {useInteractionState} from '#/components/hooks/useInteractionState' +import {Text, TextProps} from '#/components/Typography' +import {router} from '#/routes' /** * Only available within a `Link`, since that inherits from `Button`. @@ -60,6 +61,11 @@ type BaseLinkProps = Pick< * Web-only attribute. Sets `download` attr on web. */ download?: string + + /** + * Native-only attribute. If true, will open the share sheet on long press. + */ + shareOnLongPress?: boolean } export function useLink({ @@ -68,6 +74,7 @@ export function useLink({ action = 'push', disableMismatchWarning, onPress: outerOnPress, + shareOnLongPress, }: BaseLinkProps & { displayText: string }) { @@ -157,10 +164,34 @@ export function useLink({ ], ) + const handleLongPress = React.useCallback(() => { + const requiresWarning = Boolean( + !disableMismatchWarning && + displayText && + isExternal && + linkRequiresWarning(href, displayText), + ) + + if (requiresWarning) { + openModal({ + name: 'link-warning', + text: displayText, + href: href, + share: true, + }) + } else { + shareUrl(href) + } + }, [disableMismatchWarning, displayText, href, isExternal, openModal]) + + const onLongPress = + isNative && isExternal && shareOnLongPress ? handleLongPress : undefined + return { isExternal, href, onPress, + onLongPress, } } @@ -219,7 +250,7 @@ export type InlineLinkProps = React.PropsWithChildren< BaseLinkProps & TextStyleProp & Pick > -export function InlineLink({ +export function InlineLinkText({ children, to, action = 'push', @@ -229,16 +260,18 @@ export function InlineLink({ download, selectable, label, + shareOnLongPress, ...rest }: InlineLinkProps) { const t = useTheme() const stringChildren = typeof children === 'string' - const {href, isExternal, onPress} = useLink({ + const {href, isExternal, onPress, onLongPress} = useLink({ to, displayText: stringChildren ? children : '', action, disableMismatchWarning, onPress: outerOnPress, + shareOnLongPress, }) const { state: hovered, @@ -270,6 +303,7 @@ export function InlineLink({ ]} role="link" onPress={download ? undefined : onPress} + onLongPress={onLongPress} onPressIn={onPressIn} onPressOut={onPressOut} onFocus={onFocus} diff --git a/src/components/Lists.tsx b/src/components/Lists.tsx index d3e0720286..605626fef2 100644 --- a/src/components/Lists.tsx +++ b/src/components/Lists.tsx @@ -1,25 +1,23 @@ import React from 'react' -import {atoms as a, useBreakpoints, useTheme} from '#/alf' import {View} from 'react-native' +import {msg, Trans} from '@lingui/macro' import {useLingui} from '@lingui/react' -import {Trans, msg} from '@lingui/macro' -import {CenteredView} from 'view/com/util/Views' -import {Loader} from '#/components/Loader' import {cleanError} from 'lib/strings/errors' +import {CenteredView} from 'view/com/util/Views' +import {atoms as a, useBreakpoints, useTheme} from '#/alf' import {Button} from '#/components/Button' -import {Text} from '#/components/Typography' import {Error} from '#/components/Error' +import {Loader} from '#/components/Loader' +import {Text} from '#/components/Typography' export function ListFooter({ - isFetching, - isError, + isFetchingNextPage, error, onRetry, height, }: { - isFetching?: boolean - isError?: boolean + isFetchingNextPage?: boolean error?: string onRetry?: () => Promise height?: number @@ -36,32 +34,26 @@ export function ListFooter({ t.atoms.border_contrast_low, {height: height ?? 180, paddingTop: 30}, ]}> - {isFetching ? ( + {isFetchingNextPage ? ( ) : ( - + )} ) } function ListFooterMaybeError({ - isError, error, onRetry, }: { - isError?: boolean error?: string onRetry?: () => Promise }) { const t = useTheme() const {_} = useLingui() - if (!isError) return null + if (!error) return null return ( @@ -128,7 +120,7 @@ export function ListHeaderDesktop({ export function ListMaybePlaceholder({ isLoading, - isEmpty, + noEmpty, isError, emptyTitle, emptyMessage, @@ -138,7 +130,7 @@ export function ListMaybePlaceholder({ onRetry, }: { isLoading: boolean - isEmpty?: boolean + noEmpty?: boolean isError?: boolean emptyTitle?: string emptyMessage?: string @@ -151,16 +143,6 @@ export function ListMaybePlaceholder({ const {_} = useLingui() const {gtMobile, gtTablet} = useBreakpoints() - if (!isLoading && isError) { - return ( - - ) - } - if (isLoading) { return ( + ) + } + + if (!noEmpty) { return ( ) } + + return null } diff --git a/src/components/Loader.tsx b/src/components/Loader.tsx index b9f399f953..e0b3be6373 100644 --- a/src/components/Loader.tsx +++ b/src/components/Loader.tsx @@ -1,13 +1,13 @@ import React from 'react' import Animated, { Easing, - useSharedValue, useAnimatedStyle, + useSharedValue, withRepeat, withTiming, } from 'react-native-reanimated' -import {atoms as a, useTheme, flatten} from '#/alf' +import {atoms as a, flatten, useTheme} from '#/alf' import {Props, useCommonSVGProps} from '#/components/icons/common' import {Loader_Stroke2_Corner0_Rounded as Icon} from '#/components/icons/Loader' diff --git a/src/components/Loader.web.tsx b/src/components/Loader.web.tsx new file mode 100644 index 0000000000..d8182673f6 --- /dev/null +++ b/src/components/Loader.web.tsx @@ -0,0 +1,34 @@ +import React from 'react' +import {View} from 'react-native' + +import {atoms as a, flatten, useTheme} from '#/alf' +import {Props, useCommonSVGProps} from '#/components/icons/common' +import {Loader_Stroke2_Corner0_Rounded as Icon} from '#/components/icons/Loader' + +export function Loader(props: Props) { + const t = useTheme() + const common = useCommonSVGProps(props) + + return ( + + {/* css rotation animation - /bskyweb/templates/base.html */} +
+ +
+
+ ) +} diff --git a/src/components/Prompt.tsx b/src/components/Prompt.tsx index b81b207075..000d2a3cd5 100644 --- a/src/components/Prompt.tsx +++ b/src/components/Prompt.tsx @@ -3,11 +3,10 @@ import {View} from 'react-native' import {msg} from '@lingui/macro' import {useLingui} from '@lingui/react' -import {useTheme, atoms as a, useBreakpoints} from '#/alf' -import {Text} from '#/components/Typography' +import {atoms as a, useBreakpoints, useTheme} from '#/alf' import {Button, ButtonColor, ButtonText} from '#/components/Button' - import * as Dialog from '#/components/Dialog' +import {Text} from '#/components/Typography' export {useDialogControl as usePromptControl} from '#/components/Dialog' @@ -52,7 +51,7 @@ export function Outer({ ) } -export function Title({children}: React.PropsWithChildren<{}>) { +export function TitleText({children}: React.PropsWithChildren<{}>) { const {titleId} = React.useContext(Context) return ( @@ -61,7 +60,7 @@ export function Title({children}: React.PropsWithChildren<{}>) { ) } -export function Description({children}: React.PropsWithChildren<{}>) { +export function DescriptionText({children}: React.PropsWithChildren<{}>) { const t = useTheme() const {descriptionId} = React.useContext(Context) return ( @@ -80,7 +79,7 @@ export function Actions({children}: React.PropsWithChildren<{}>) { ) { return ( - {title} - {description} + {title} + {description} {segment.text} - , + , ) } else if (link && AppBskyRichtextFacet.validateLink(link).success) { if (disableLinks) { els.push(toShortUrl(segment.text)) } else { els.push( - + dataSet={WORD_WRAP} + shareOnLongPress> {toShortUrl(segment.text)} - , + , ) } } else if ( diff --git a/src/components/Typography.tsx b/src/components/Typography.tsx index f8b3ad1bd8..31dd931c6a 100644 --- a/src/components/Typography.tsx +++ b/src/components/Typography.tsx @@ -1,14 +1,9 @@ import React from 'react' -import { - Text as RNText, - StyleProp, - TextStyle, - TextProps as RNTextProps, -} from 'react-native' -import {UITextView} from 'react-native-ui-text-view' +import {StyleProp, TextProps as RNTextProps, TextStyle} from 'react-native' +import {UITextView} from 'react-native-uitextview' -import {useTheme, atoms, web, flatten} from '#/alf' -import {isIOS, isNative} from '#/platform/detection' +import {isNative} from '#/platform/detection' +import {atoms, flatten, useTheme, web} from '#/alf' export type TextProps = RNTextProps & { /** @@ -61,11 +56,8 @@ export function normalizeTextStyles(styles: StyleProp) { export function Text({style, selectable, ...rest}: TextProps) { const t = useTheme() const s = normalizeTextStyles([atoms.text_sm, t.atoms.text, flatten(style)]) - return selectable && isIOS ? ( - - ) : ( - - ) + + return } export function createHeadingElement({level}: {level: number}) { diff --git a/src/components/dialogs/BirthDateSettings.tsx b/src/components/dialogs/BirthDateSettings.tsx index 4a3e96e56d..d831c6002a 100644 --- a/src/components/dialogs/BirthDateSettings.tsx +++ b/src/components/dialogs/BirthDateSettings.tsx @@ -1,23 +1,23 @@ import React from 'react' -import {useLingui} from '@lingui/react' -import {Trans, msg} from '@lingui/macro' import {View} from 'react-native' +import {msg, Trans} from '@lingui/macro' +import {useLingui} from '@lingui/react' -import * as Dialog from '#/components/Dialog' -import {Text} from '../Typography' -import {DateInput} from '#/view/com/util/forms/DateInput' +import {cleanError} from '#/lib/strings/errors' import {logger} from '#/logger' +import {isIOS, isWeb} from '#/platform/detection' import { usePreferencesQuery, - usePreferencesSetBirthDateMutation, UsePreferencesQueryResponse, + usePreferencesSetBirthDateMutation, } from '#/state/queries/preferences' -import {Button, ButtonIcon, ButtonText} from '../Button' -import {atoms as a, useTheme} from '#/alf' import {ErrorMessage} from '#/view/com/util/error/ErrorMessage' -import {cleanError} from '#/lib/strings/errors' -import {isIOS, isWeb} from '#/platform/detection' +import {DateInput} from '#/view/com/util/forms/DateInput' +import {atoms as a, useTheme} from '#/alf' +import * as Dialog from '#/components/Dialog' import {Loader} from '#/components/Loader' +import {Button, ButtonIcon, ButtonText} from '../Button' +import {Text} from '../Typography' export function BirthDateSettingsDialog({ control, diff --git a/src/components/dialogs/MutedWords.tsx b/src/components/dialogs/MutedWords.tsx index 46f319adfe..0eced11e3d 100644 --- a/src/components/dialogs/MutedWords.tsx +++ b/src/components/dialogs/MutedWords.tsx @@ -1,37 +1,36 @@ import React from 'react' import {Keyboard, View} from 'react-native' +import {AppBskyActorDefs, sanitizeMutedWordValue} from '@atproto/api' import {msg, Trans} from '@lingui/macro' import {useLingui} from '@lingui/react' -import {AppBskyActorDefs, sanitizeMutedWordValue} from '@atproto/api' +import {logger} from '#/logger' +import {isNative} from '#/platform/detection' import { usePreferencesQuery, - useUpsertMutedWordsMutation, useRemoveMutedWordMutation, + useUpsertMutedWordsMutation, } from '#/state/queries/preferences' -import {isNative} from '#/platform/detection' import { atoms as a, - useTheme, + native, useBreakpoints, + useTheme, ViewStyleProp, web, - native, } from '#/alf' -import {Text} from '#/components/Typography' import {Button, ButtonIcon, ButtonText} from '#/components/Button' -import {PlusLarge_Stroke2_Corner0_Rounded as Plus} from '#/components/icons/Plus' -import {TimesLarge_Stroke2_Corner0_Rounded as X} from '#/components/icons/Times' +import * as Dialog from '#/components/Dialog' +import {useGlobalDialogsControlContext} from '#/components/dialogs/Context' +import {Divider} from '#/components/Divider' +import * as Toggle from '#/components/forms/Toggle' import {Hashtag_Stroke2_Corner0_Rounded as Hashtag} from '#/components/icons/Hashtag' import {PageText_Stroke2_Corner0_Rounded as PageText} from '#/components/icons/PageText' -import {Divider} from '#/components/Divider' +import {PlusLarge_Stroke2_Corner0_Rounded as Plus} from '#/components/icons/Plus' +import {TimesLarge_Stroke2_Corner0_Rounded as X} from '#/components/icons/Times' import {Loader} from '#/components/Loader' -import {logger} from '#/logger' -import * as Dialog from '#/components/Dialog' -import * as Toggle from '#/components/forms/Toggle' import * as Prompt from '#/components/Prompt' - -import {useGlobalDialogsControlContext} from '#/components/dialogs/Context' +import {Text} from '#/components/Typography' export function MutedWordsDialog() { const {mutedWordsDialogControl: control} = useGlobalDialogsControlContext() @@ -130,9 +129,9 @@ function MutedWordsInner({}: {control: Dialog.DialogOuterProps['control']}) { - + Mute in text & tags - + @@ -145,9 +144,9 @@ function MutedWordsInner({}: {control: Dialog.DialogOuterProps['control']}) { - + Mute in tags only - + diff --git a/src/components/dialogs/SwitchAccount.tsx b/src/components/dialogs/SwitchAccount.tsx new file mode 100644 index 0000000000..645113d4af --- /dev/null +++ b/src/components/dialogs/SwitchAccount.tsx @@ -0,0 +1,61 @@ +import React, {useCallback} from 'react' +import {View} from 'react-native' +import {msg, Trans} from '@lingui/macro' +import {useLingui} from '@lingui/react' + +import {useAccountSwitcher} from '#/lib/hooks/useAccountSwitcher' +import {type SessionAccount, useSession} from '#/state/session' +import {useLoggedOutViewControls} from '#/state/shell/logged-out' +import {useCloseAllActiveElements} from '#/state/util' +import {atoms as a} from '#/alf' +import * as Dialog from '#/components/Dialog' +import {AccountList} from '../AccountList' +import {Text} from '../Typography' + +export function SwitchAccountDialog({ + control, +}: { + control: Dialog.DialogControlProps +}) { + const {_} = useLingui() + const {currentAccount} = useSession() + const {onPressSwitchAccount} = useAccountSwitcher() + const {setShowLoggedOut} = useLoggedOutViewControls() + const closeAllActiveElements = useCloseAllActiveElements() + + const onSelectAccount = useCallback( + (account: SessionAccount) => { + if (account.did === currentAccount?.did) { + control.close() + } else { + onPressSwitchAccount(account, 'SwitchAccount') + } + }, + [currentAccount, control, onPressSwitchAccount], + ) + + const onPressAddAccount = useCallback(() => { + setShowLoggedOut(true) + closeAllActiveElements() + }, [setShowLoggedOut, closeAllActiveElements]) + + return ( + + + + + + + Switch Account + + + + + + + ) +} diff --git a/src/components/forms/DateField/index.android.tsx b/src/components/forms/DateField/index.android.tsx index 700d15e6d6..1830ca4bfd 100644 --- a/src/components/forms/DateField/index.android.tsx +++ b/src/components/forms/DateField/index.android.tsx @@ -8,7 +8,7 @@ import * as TextField from '#/components/forms/TextField' import {DateFieldButton} from './index.shared' export * as utils from '#/components/forms/DateField/utils' -export const Label = TextField.Label +export const LabelText = TextField.LabelText export function DateField({ value, diff --git a/src/components/forms/DateField/index.tsx b/src/components/forms/DateField/index.tsx index 5662bb5941..e231ac5baf 100644 --- a/src/components/forms/DateField/index.tsx +++ b/src/components/forms/DateField/index.tsx @@ -13,7 +13,7 @@ import * as TextField from '#/components/forms/TextField' import {DateFieldButton} from './index.shared' export * as utils from '#/components/forms/DateField/utils' -export const Label = TextField.Label +export const LabelText = TextField.LabelText /** * Date-only input. Accepts a date in the format YYYY-MM-DD, and reports date diff --git a/src/components/forms/DateField/index.web.tsx b/src/components/forms/DateField/index.web.tsx index 982d32711a..b764620e33 100644 --- a/src/components/forms/DateField/index.web.tsx +++ b/src/components/forms/DateField/index.web.tsx @@ -9,7 +9,7 @@ import * as TextField from '#/components/forms/TextField' import {CalendarDays_Stroke2_Corner0_Rounded as CalendarDays} from '#/components/icons/CalendarDays' export * as utils from '#/components/forms/DateField/utils' -export const Label = TextField.Label +export const LabelText = TextField.LabelText const InputBase = React.forwardRef( ({style, ...props}, ref) => { diff --git a/src/components/forms/TextField.tsx b/src/components/forms/TextField.tsx index 0bdeca6458..73a660ea6c 100644 --- a/src/components/forms/TextField.tsx +++ b/src/components/forms/TextField.tsx @@ -225,7 +225,7 @@ export function createInput(Component: typeof TextInput) { export const Input = createInput(TextInput) -export function Label({ +export function LabelText({ nativeID, children, }: React.PropsWithChildren<{nativeID?: string}>) { @@ -288,7 +288,7 @@ export function Icon({icon: Comp}: {icon: React.ComponentType}) { ) } -export function Suffix({ +export function SuffixText({ children, label, accessibilityHint, diff --git a/src/components/forms/Toggle.tsx b/src/components/forms/Toggle.tsx index 7a4b5ac959..7285e5faca 100644 --- a/src/components/forms/Toggle.tsx +++ b/src/components/forms/Toggle.tsx @@ -3,16 +3,16 @@ import {Pressable, View, ViewStyle} from 'react-native' import {HITSLOP_10} from 'lib/constants' import { - useTheme, atoms as a, - native, flatten, - ViewStyleProp, + native, TextStyleProp, + useTheme, + ViewStyleProp, } from '#/alf' -import {Text} from '#/components/Typography' import {useInteractionState} from '#/components/hooks/useInteractionState' import {CheckThick_Stroke2_Corner0_Rounded as Checkmark} from '#/components/icons/Check' +import {Text} from '#/components/Typography' export type ItemState = { name: string @@ -234,7 +234,7 @@ export function Item({ ) } -export function Label({ +export function LabelText({ children, style, }: React.PropsWithChildren) { diff --git a/src/components/moderation/LabelPreference.tsx b/src/components/moderation/LabelPreference.tsx index 7d4bd9c321..990e736228 100644 --- a/src/components/moderation/LabelPreference.tsx +++ b/src/components/moderation/LabelPreference.tsx @@ -1,22 +1,21 @@ import React from 'react' import {View} from 'react-native' import {InterpretedLabelValueDefinition, LabelPreference} from '@atproto/api' -import {useLingui} from '@lingui/react' import {msg, Trans} from '@lingui/macro' +import {useLingui} from '@lingui/react' import {useGlobalLabelStrings} from '#/lib/moderation/useGlobalLabelStrings' +import {useLabelBehaviorDescription} from '#/lib/moderation/useLabelBehaviorDescription' +import {getLabelStrings} from '#/lib/moderation/useLabelInfo' import { usePreferencesQuery, usePreferencesSetContentLabelMutation, } from '#/state/queries/preferences' -import {useLabelBehaviorDescription} from '#/lib/moderation/useLabelBehaviorDescription' -import {getLabelStrings} from '#/lib/moderation/useLabelInfo' - -import {useTheme, atoms as a, useBreakpoints} from '#/alf' +import {atoms as a, useBreakpoints, useTheme} from '#/alf' +import * as ToggleButton from '#/components/forms/ToggleButton' +import {InlineLinkText} from '#/components/Link' import {Text} from '#/components/Typography' -import {InlineLink} from '#/components/Link' import {CircleInfo_Stroke2_Corner0_Rounded as CircleInfo} from '../icons/CircleInfo' -import * as ToggleButton from '#/components/forms/ToggleButton' export function Outer({children}: React.PropsWithChildren<{}>) { return ( @@ -244,9 +243,9 @@ export function LabelerLabelPreference({ ) : isGlobalLabel ? ( Configured in{' '} - + moderation settings - + . ) : null} diff --git a/src/components/moderation/LabelsOnMeDialog.tsx b/src/components/moderation/LabelsOnMeDialog.tsx index 6eddbc7ceb..95e3d242b9 100644 --- a/src/components/moderation/LabelsOnMeDialog.tsx +++ b/src/components/moderation/LabelsOnMeDialog.tsx @@ -1,20 +1,19 @@ import React from 'react' import {View} from 'react-native' +import {ComAtprotoLabelDefs, ComAtprotoModerationDefs} from '@atproto/api' import {msg, Trans} from '@lingui/macro' import {useLingui} from '@lingui/react' -import {ComAtprotoLabelDefs, ComAtprotoModerationDefs} from '@atproto/api' import {useLabelInfo} from '#/lib/moderation/useLabelInfo' import {makeProfileLink} from '#/lib/routes/links' import {sanitizeHandle} from '#/lib/strings/handles' import {getAgent} from '#/state/session' - +import * as Toast from '#/view/com/util/Toast' import {atoms as a, useBreakpoints, useTheme} from '#/alf' -import {Text} from '#/components/Typography' -import * as Dialog from '#/components/Dialog' import {Button, ButtonText} from '#/components/Button' -import {InlineLink} from '#/components/Link' -import * as Toast from '#/view/com/util/Toast' +import * as Dialog from '#/components/Dialog' +import {InlineLinkText} from '#/components/Link' +import {Text} from '#/components/Typography' import {Divider} from '../Divider' export {useDialogControl as useLabelsOnMeDialogControl} from '#/components/Dialog' @@ -145,13 +144,13 @@ function Label({ Source:{' '} - control.close()}> {labeler ? sanitizeHandle(labeler.creator.handle, '@') : label.src} - + @@ -204,14 +203,14 @@ function AppealForm({ This appeal will be sent to{' '} - control.close()} style={[a.text_md, a.leading_snug]}> {labeler ? sanitizeHandle(labeler.creator.handle, '@') : label.src} - + . diff --git a/src/components/moderation/ModerationDetailsDialog.tsx b/src/components/moderation/ModerationDetailsDialog.tsx index da490cb43e..da57de4df3 100644 --- a/src/components/moderation/ModerationDetailsDialog.tsx +++ b/src/components/moderation/ModerationDetailsDialog.tsx @@ -1,19 +1,18 @@ import React from 'react' import {View} from 'react-native' +import {ModerationCause} from '@atproto/api' import {msg, Trans} from '@lingui/macro' import {useLingui} from '@lingui/react' -import {ModerationCause} from '@atproto/api' -import {listUriToHref} from '#/lib/strings/url-helpers' import {useModerationCauseDescription} from '#/lib/moderation/useModerationCauseDescription' import {makeProfileLink} from '#/lib/routes/links' - +import {listUriToHref} from '#/lib/strings/url-helpers' import {isNative} from '#/platform/detection' -import {useTheme, atoms as a} from '#/alf' -import {Text} from '#/components/Typography' +import {atoms as a, useTheme} from '#/alf' import * as Dialog from '#/components/Dialog' -import {InlineLink} from '#/components/Link' import {Divider} from '#/components/Divider' +import {InlineLinkText} from '#/components/Link' +import {Text} from '#/components/Typography' export {useDialogControl as useModerationDetailsDialogControl} from '#/components/Dialog' @@ -55,9 +54,9 @@ function ModerationDetailsDialogInner({ description = ( This user is included in the{' '} - + {list.name} - {' '} + {' '} list which you have blocked. ) @@ -84,9 +83,9 @@ function ModerationDetailsDialogInner({ description = ( This user is included in the{' '} - + {list.name} - {' '} + {' '} list which you have muted. ) @@ -127,12 +126,12 @@ function ModerationDetailsDialogInner({ {modcause.source.type === 'user' ? ( the author ) : ( - control.close()} style={a.text_md}> {desc.source} - + )} . diff --git a/src/components/moderation/ScreenHider.tsx b/src/components/moderation/ScreenHider.tsx index 4e3a9680f5..0d316bc885 100644 --- a/src/components/moderation/ScreenHider.tsx +++ b/src/components/moderation/ScreenHider.tsx @@ -1,27 +1,26 @@ import React from 'react' import { - TouchableWithoutFeedback, StyleProp, + TouchableWithoutFeedback, View, ViewStyle, } from 'react-native' -import {useNavigation} from '@react-navigation/native' import {ModerationUI} from '@atproto/api' -import {Trans, msg} from '@lingui/macro' +import {msg, Trans} from '@lingui/macro' import {useLingui} from '@lingui/react' +import {useNavigation} from '@react-navigation/native' +import {useModerationCauseDescription} from '#/lib/moderation/useModerationCauseDescription' import {useWebMediaQueries} from 'lib/hooks/useWebMediaQueries' import {NavigationProp} from 'lib/routes/types' -import {useModerationCauseDescription} from '#/lib/moderation/useModerationCauseDescription' - -import {useTheme, atoms as a} from '#/alf' import {CenteredView} from '#/view/com/util/Views' -import {Text} from '#/components/Typography' +import {atoms as a, useTheme} from '#/alf' import {Button, ButtonText} from '#/components/Button' import { ModerationDetailsDialog, useModerationDetailsDialogControl, } from '#/components/moderation/ModerationDetailsDialog' +import {Text} from '#/components/Typography' export function ScreenHider({ testID, @@ -125,7 +124,15 @@ export function ScreenHider({ accessibilityRole="button" accessibilityLabel={_(msg`Learn more about this warning`)} accessibilityHint=""> - + Learn More diff --git a/src/lib/app-info.ts b/src/lib/app-info.ts index 3f026d3fe6..3071e031b3 100644 --- a/src/lib/app-info.ts +++ b/src/lib/app-info.ts @@ -1,5 +1,9 @@ import VersionNumber from 'react-native-version-number' -import * as Updates from 'expo-updates' -export const updateChannel = Updates.channel -export const appVersion = `${VersionNumber.appVersion} (${VersionNumber.buildVersion})` +export const IS_DEV = process.env.EXPO_PUBLIC_ENV === 'development' +export const IS_TESTFLIGHT = process.env.EXPO_PUBLIC_ENV === 'testflight' + +const UPDATES_CHANNEL = IS_TESTFLIGHT ? 'testflight' : 'production' +export const appVersion = `${VersionNumber.appVersion} (${ + VersionNumber.buildVersion +}, ${IS_DEV ? 'development' : UPDATES_CHANNEL})` diff --git a/src/lib/hooks/useOTAUpdates.ts b/src/lib/hooks/useOTAUpdates.ts new file mode 100644 index 0000000000..181f0b2c66 --- /dev/null +++ b/src/lib/hooks/useOTAUpdates.ts @@ -0,0 +1,142 @@ +import React from 'react' +import {Alert, AppState, AppStateStatus} from 'react-native' +import app from 'react-native-version-number' +import { + checkForUpdateAsync, + fetchUpdateAsync, + isEnabled, + reloadAsync, + setExtraParamAsync, + useUpdates, +} from 'expo-updates' + +import {logger} from '#/logger' +import {IS_TESTFLIGHT} from 'lib/app-info' +import {isIOS} from 'platform/detection' + +const MINIMUM_MINIMIZE_TIME = 15 * 60e3 + +async function setExtraParams() { + await setExtraParamAsync( + isIOS ? 'ios-build-number' : 'android-build-number', + // Hilariously, `buildVersion` is not actually a string on Android even though the TS type says it is. + // This just ensures it gets passed as a string + `${app.buildVersion}`, + ) + await setExtraParamAsync( + 'channel', + IS_TESTFLIGHT ? 'testflight' : 'production', + ) +} + +export function useOTAUpdates() { + const appState = React.useRef('active') + const lastMinimize = React.useRef(0) + const ranInitialCheck = React.useRef(false) + const timeout = React.useRef() + const {isUpdatePending} = useUpdates() + + const setCheckTimeout = React.useCallback(() => { + timeout.current = setTimeout(async () => { + try { + await setExtraParams() + + logger.debug('Checking for update...') + const res = await checkForUpdateAsync() + + if (res.isAvailable) { + logger.debug('Attempting to fetch update...') + await fetchUpdateAsync() + } else { + logger.debug('No update available.') + } + } catch (e) { + logger.warn('OTA Update Error', {error: `${e}`}) + } + }, 10e3) + }, []) + + const onIsTestFlight = React.useCallback(() => { + setTimeout(async () => { + try { + await setExtraParams() + + const res = await checkForUpdateAsync() + if (res.isAvailable) { + await fetchUpdateAsync() + + Alert.alert( + 'Update Available', + 'A new version of the app is available. Relaunch now?', + [ + { + text: 'No', + style: 'cancel', + }, + { + text: 'Relaunch', + style: 'default', + onPress: async () => { + await reloadAsync() + }, + }, + ], + ) + } + } catch (e: any) { + // No need to handle + } + }, 3e3) + }, []) + + React.useEffect(() => { + // For Testflight users, we can prompt the user to update immediately whenever there's an available update. This + // is suspect however with the Apple App Store guidelines, so we don't want to prompt production users to update + // immediately. + if (IS_TESTFLIGHT) { + onIsTestFlight() + return + } else if (!isEnabled || __DEV__ || ranInitialCheck.current) { + // Development client shouldn't check for updates at all, so we skip that here. + return + } + + setCheckTimeout() + ranInitialCheck.current = true + }, [onIsTestFlight, setCheckTimeout]) + + // After the app has been minimized for 30 minutes, we want to either A. install an update if one has become available + // or B check for an update again. + React.useEffect(() => { + if (!isEnabled) return + + const subscription = AppState.addEventListener( + 'change', + async nextAppState => { + if ( + appState.current.match(/inactive|background/) && + nextAppState === 'active' + ) { + // If it's been 15 minutes since the last "minimize", we should feel comfortable updating the client since + // chances are that there isn't anything important going on in the current session. + if (lastMinimize.current <= Date.now() - MINIMUM_MINIMIZE_TIME) { + if (isUpdatePending) { + await reloadAsync() + } else { + setCheckTimeout() + } + } + } else { + lastMinimize.current = Date.now() + } + + appState.current = nextAppState + }, + ) + + return () => { + clearTimeout(timeout.current) + subscription.remove() + } + }, [isUpdatePending, setCheckTimeout]) +} diff --git a/src/lib/icons.tsx b/src/lib/icons.tsx index 7ae88806f7..93b45ea3a9 100644 --- a/src/lib/icons.tsx +++ b/src/lib/icons.tsx @@ -1,6 +1,6 @@ import React from 'react' import {StyleProp, TextStyle, ViewStyle} from 'react-native' -import Svg, {Path, Rect, Line, Ellipse} from 'react-native-svg' +import Svg, {Ellipse, Line, Path, Rect} from 'react-native-svg' export function GridIcon({ style, @@ -141,8 +141,8 @@ export function MagnifyingGlassIcon2({ width={size || 24} height={size || 24} style={style}> - - + + ) } @@ -167,14 +167,14 @@ export function MagnifyingGlassIcon2Solid({ style={style}> - - + + ) } diff --git a/src/lib/moderation/useGlobalLabelStrings.ts b/src/lib/moderation/useGlobalLabelStrings.ts index 1c5a482314..4f41c62b10 100644 --- a/src/lib/moderation/useGlobalLabelStrings.ts +++ b/src/lib/moderation/useGlobalLabelStrings.ts @@ -1,6 +1,6 @@ +import {useMemo} from 'react' import {msg} from '@lingui/macro' import {useLingui} from '@lingui/react' -import {useMemo} from 'react' export type GlobalLabelStrings = Record< string, @@ -31,7 +31,7 @@ export function useGlobalLabelStrings(): GlobalLabelStrings { ), }, porn: { - name: _(msg`Pornography`), + name: _(msg`Adult Content`), description: _(msg`Explicit sexual images.`), }, sexual: { diff --git a/src/lib/moderation/useReportOptions.ts b/src/lib/moderation/useReportOptions.ts index e001705943..a22386b991 100644 --- a/src/lib/moderation/useReportOptions.ts +++ b/src/lib/moderation/useReportOptions.ts @@ -1,7 +1,7 @@ -import {msg} from '@lingui/macro' -import {useLingui} from '@lingui/react' import {useMemo} from 'react' import {ComAtprotoModerationDefs} from '@atproto/api' +import {msg} from '@lingui/macro' +import {useLingui} from '@lingui/react' export interface ReportOption { reason: string @@ -68,7 +68,7 @@ export function useReportOptions(): ReportOptions { { reason: ComAtprotoModerationDefs.REASONSEXUAL, title: _(msg`Unwanted Sexual Content`), - description: _(msg`Nudity or pornography not labeled as such`), + description: _(msg`Nudity or adult content not labeled as such`), }, ...common, ], diff --git a/src/lib/notifications/notifications.ts b/src/lib/notifications/notifications.ts index e811f690ed..0f628f4288 100644 --- a/src/lib/notifications/notifications.ts +++ b/src/lib/notifications/notifications.ts @@ -1,12 +1,14 @@ +import {useEffect} from 'react' import * as Notifications from 'expo-notifications' import {QueryClient} from '@tanstack/react-query' -import {resetToTab} from '../../Navigation' -import {devicePlatform, isIOS} from 'platform/detection' -import {track} from 'lib/analytics/analytics' + import {logger} from '#/logger' import {RQKEY as RQKEY_NOTIFS} from '#/state/queries/notifications/feed' import {truncateAndInvalidate} from '#/state/queries/util' -import {SessionAccount, getAgent} from '#/state/session' +import {getAgent, SessionAccount} from '#/state/session' +import {track} from 'lib/analytics/analytics' +import {devicePlatform, isIOS} from 'platform/detection' +import {resetToTab} from '../../Navigation' import {logEvent} from '../statsig/statsig' const SERVICE_DID = (serviceUrl?: string) => @@ -80,53 +82,63 @@ export function registerTokenChangeHandler( } } -export function init(queryClient: QueryClient) { - // handle notifications that are received, both in the foreground or background - // NOTE: currently just here for debug logging - Notifications.addNotificationReceivedListener(event => { - logger.debug( - 'Notifications: received', - {event}, - logger.DebugContext.notifications, - ) - if (event.request.trigger.type === 'push') { - // handle payload-based deeplinks - let payload - if (isIOS) { - payload = event.request.trigger.payload - } else { - // TODO: handle android payload deeplink +export function useNotificationsListener(queryClient: QueryClient) { + useEffect(() => { + // handle notifications that are received, both in the foreground or background + // NOTE: currently just here for debug logging + const sub1 = Notifications.addNotificationReceivedListener(event => { + logger.debug( + 'Notifications: received', + {event}, + logger.DebugContext.notifications, + ) + if (event.request.trigger.type === 'push') { + // handle payload-based deeplinks + let payload + if (isIOS) { + payload = event.request.trigger.payload + } else { + // TODO: handle android payload deeplink + } + if (payload) { + logger.debug( + 'Notifications: received payload', + payload, + logger.DebugContext.notifications, + ) + // TODO: deeplink notif here + } } - if (payload) { + }) + + // handle notifications that are tapped on + const sub2 = Notifications.addNotificationResponseReceivedListener( + response => { logger.debug( - 'Notifications: received payload', - payload, + 'Notifications: response received', + { + actionIdentifier: response.actionIdentifier, + }, logger.DebugContext.notifications, ) - // TODO: deeplink notif here - } - } - }) - - // handle notifications that are tapped on - Notifications.addNotificationResponseReceivedListener(response => { - logger.debug( - 'Notifications: response received', - { - actionIdentifier: response.actionIdentifier, + if ( + response.actionIdentifier === Notifications.DEFAULT_ACTION_IDENTIFIER + ) { + logger.debug( + 'User pressed a notification, opening notifications tab', + {}, + logger.DebugContext.notifications, + ) + track('Notificatons:OpenApp') + logEvent('notifications:openApp', {}) + truncateAndInvalidate(queryClient, RQKEY_NOTIFS()) + resetToTab('NotificationsTab') // open notifications tab + } }, - logger.DebugContext.notifications, ) - if (response.actionIdentifier === Notifications.DEFAULT_ACTION_IDENTIFIER) { - logger.debug( - 'User pressed a notification, opening notifications tab', - {}, - logger.DebugContext.notifications, - ) - track('Notificatons:OpenApp') - logEvent('notifications:openApp', {}) - truncateAndInvalidate(queryClient, RQKEY_NOTIFS()) - resetToTab('NotificationsTab') // open notifications tab + return () => { + sub1.remove() + sub2.remove() } - }) + }, [queryClient]) } diff --git a/src/lib/react-query.ts b/src/lib/react-query.ts deleted file mode 100644 index d6cd3c54b2..0000000000 --- a/src/lib/react-query.ts +++ /dev/null @@ -1,70 +0,0 @@ -import {AppState, AppStateStatus} from 'react-native' -import {QueryClient, focusManager} from '@tanstack/react-query' -import {createAsyncStoragePersister} from '@tanstack/query-async-storage-persister' -import AsyncStorage from '@react-native-async-storage/async-storage' -import {PersistQueryClientProviderProps} from '@tanstack/react-query-persist-client' - -import {isNative} from '#/platform/detection' - -// any query keys in this array will be persisted to AsyncStorage -const STORED_CACHE_QUERY_KEYS = ['labelers-detailed-info'] - -focusManager.setEventListener(onFocus => { - if (isNative) { - const subscription = AppState.addEventListener( - 'change', - (status: AppStateStatus) => { - focusManager.setFocused(status === 'active') - }, - ) - - return () => subscription.remove() - } else if (typeof window !== 'undefined' && window.addEventListener) { - // these handlers are a bit redundant but focus catches when the browser window - // is blurred/focused while visibilitychange seems to only handle when the - // window minimizes (both of them catch tab changes) - // there's no harm to redundant fires because refetchOnWindowFocus is only - // used with queries that employ stale data times - const handler = () => onFocus() - window.addEventListener('focus', handler, false) - window.addEventListener('visibilitychange', handler, false) - return () => { - window.removeEventListener('visibilitychange', handler) - window.removeEventListener('focus', handler) - } - } -}) - -export const queryClient = new QueryClient({ - defaultOptions: { - queries: { - // NOTE - // refetchOnWindowFocus breaks some UIs (like feeds) - // so we only selectively want to enable this - // -prf - refetchOnWindowFocus: false, - // Structural sharing between responses makes it impossible to rely on - // "first seen" timestamps on objects to determine if they're fresh. - // Disable this optimization so that we can rely on "first seen" timestamps. - structuralSharing: false, - // We don't want to retry queries by default, because in most cases we - // want to fail early and show a response to the user. There are - // exceptions, and those can be made on a per-query basis. For others, we - // should give users controls to retry. - retry: false, - }, - }, -}) - -export const asyncStoragePersister = createAsyncStoragePersister({ - storage: AsyncStorage, - key: 'queryCache', -}) - -export const dehydrateOptions: PersistQueryClientProviderProps['persistOptions']['dehydrateOptions'] = - { - shouldDehydrateMutation: (_: any) => false, - shouldDehydrateQuery: query => { - return STORED_CACHE_QUERY_KEYS.includes(String(query.queryKey[0])) - }, - } diff --git a/src/lib/react-query.tsx b/src/lib/react-query.tsx new file mode 100644 index 0000000000..be507216aa --- /dev/null +++ b/src/lib/react-query.tsx @@ -0,0 +1,124 @@ +import React, {useRef, useState} from 'react' +import {AppState, AppStateStatus} from 'react-native' +import AsyncStorage from '@react-native-async-storage/async-storage' +import {createAsyncStoragePersister} from '@tanstack/query-async-storage-persister' +import {focusManager, QueryClient} from '@tanstack/react-query' +import { + PersistQueryClientProvider, + PersistQueryClientProviderProps, +} from '@tanstack/react-query-persist-client' + +import {isNative} from '#/platform/detection' + +// any query keys in this array will be persisted to AsyncStorage +export const labelersDetailedInfoQueryKeyRoot = 'labelers-detailed-info' +const STORED_CACHE_QUERY_KEY_ROOTS = [labelersDetailedInfoQueryKeyRoot] + +focusManager.setEventListener(onFocus => { + if (isNative) { + const subscription = AppState.addEventListener( + 'change', + (status: AppStateStatus) => { + focusManager.setFocused(status === 'active') + }, + ) + + return () => subscription.remove() + } else if (typeof window !== 'undefined' && window.addEventListener) { + // these handlers are a bit redundant but focus catches when the browser window + // is blurred/focused while visibilitychange seems to only handle when the + // window minimizes (both of them catch tab changes) + // there's no harm to redundant fires because refetchOnWindowFocus is only + // used with queries that employ stale data times + const handler = () => onFocus() + window.addEventListener('focus', handler, false) + window.addEventListener('visibilitychange', handler, false) + return () => { + window.removeEventListener('visibilitychange', handler) + window.removeEventListener('focus', handler) + } + } +}) + +const createQueryClient = () => + new QueryClient({ + defaultOptions: { + queries: { + // NOTE + // refetchOnWindowFocus breaks some UIs (like feeds) + // so we only selectively want to enable this + // -prf + refetchOnWindowFocus: false, + // Structural sharing between responses makes it impossible to rely on + // "first seen" timestamps on objects to determine if they're fresh. + // Disable this optimization so that we can rely on "first seen" timestamps. + structuralSharing: false, + // We don't want to retry queries by default, because in most cases we + // want to fail early and show a response to the user. There are + // exceptions, and those can be made on a per-query basis. For others, we + // should give users controls to retry. + retry: false, + }, + }, + }) + +const dehydrateOptions: PersistQueryClientProviderProps['persistOptions']['dehydrateOptions'] = + { + shouldDehydrateMutation: (_: any) => false, + shouldDehydrateQuery: query => { + return STORED_CACHE_QUERY_KEY_ROOTS.includes(String(query.queryKey[0])) + }, + } + +export function QueryProvider({ + children, + currentDid, +}: { + children: React.ReactNode + currentDid: string | undefined +}) { + return ( + + {children} + + ) +} + +function QueryProviderInner({ + children, + currentDid, +}: { + children: React.ReactNode + currentDid: string | undefined +}) { + const initialDid = useRef(currentDid) + if (currentDid !== initialDid.current) { + throw Error( + 'Something is very wrong. Expected did to be stable due to key above.', + ) + } + // We create the query client here so that it's scoped to a specific DID. + // Do not move the query client creation outside of this component. + const [queryClient, _setQueryClient] = useState(() => createQueryClient()) + const [persistOptions, _setPersistOptions] = useState(() => { + const asyncPersister = createAsyncStoragePersister({ + storage: AsyncStorage, + key: 'queryClient-' + (currentDid ?? 'logged-out'), + }) + return { + persister: asyncPersister, + dehydrateOptions, + } + }) + return ( + + {children} + + ) +} diff --git a/src/lib/sharing.ts b/src/lib/sharing.ts index 9f402f8737..c50a2734a3 100644 --- a/src/lib/sharing.ts +++ b/src/lib/sharing.ts @@ -1,8 +1,9 @@ -import {isIOS, isAndroid} from 'platform/detection' +import {Share} from 'react-native' // import * as Sharing from 'expo-sharing' import Clipboard from '@react-native-clipboard/clipboard' -import * as Toast from '../view/com/util/Toast' -import {Share} from 'react-native' + +import {isAndroid, isIOS} from 'platform/detection' +import * as Toast from '#/view/com/util/Toast' /** * This function shares a URL using the native Share API if available, or copies it to the clipboard diff --git a/src/locale/helpers.ts b/src/locale/helpers.ts index d07b95d93e..24ab678934 100644 --- a/src/locale/helpers.ts +++ b/src/locale/helpers.ts @@ -1,7 +1,8 @@ import {AppBskyFeedDefs, AppBskyFeedPost} from '@atproto/api' +import * as bcp47Match from 'bcp-47-match' import lande from 'lande' + import {hasProp} from 'lib/type-guards' -import * as bcp47Match from 'bcp-47-match' import { AppLanguage, LANGUAGES_MAP_CODE2, @@ -118,6 +119,8 @@ export function sanitizeAppLanguageSetting(appLanguage: string): AppLanguage { switch (lang) { case 'en': return AppLanguage.en + case 'ca': + return AppLanguage.ca case 'de': return AppLanguage.de case 'es': @@ -126,24 +129,28 @@ export function sanitizeAppLanguageSetting(appLanguage: string): AppLanguage { return AppLanguage.fi case 'fr': return AppLanguage.fr + case 'ga': + return AppLanguage.ga case 'hi': return AppLanguage.hi case 'id': return AppLanguage.id + case 'it': + return AppLanguage.it case 'ja': return AppLanguage.ja case 'ko': return AppLanguage.ko case 'pt-BR': return AppLanguage.pt_BR + case 'tr': + return AppLanguage.tr case 'uk': return AppLanguage.uk - case 'ca': - return AppLanguage.ca case 'zh-CN': return AppLanguage.zh_CN - case 'it': - return AppLanguage.it + case 'zh-TW': + return AppLanguage.zh_TW default: continue } diff --git a/src/locale/i18n.ts b/src/locale/i18n.ts index a1e950947b..725332de01 100644 --- a/src/locale/i18n.ts +++ b/src/locale/i18n.ts @@ -1,30 +1,36 @@ import {useEffect} from 'react' import {i18n} from '@lingui/core' -import {useLanguagePrefs} from '#/state/preferences' -import {messages as messagesEn} from '#/locale/locales/en/messages' +import {sanitizeAppLanguageSetting} from '#/locale/helpers' +import {AppLanguage} from '#/locale/languages' +import {messages as messagesCa} from '#/locale/locales/ca/messages' import {messages as messagesDe} from '#/locale/locales/de/messages' -import {messages as messagesId} from '#/locale/locales/id/messages' +import {messages as messagesEn} from '#/locale/locales/en/messages' import {messages as messagesEs} from '#/locale/locales/es/messages' import {messages as messagesFi} from '#/locale/locales/fi/messages' import {messages as messagesFr} from '#/locale/locales/fr/messages' +import {messages as messagesGa} from '#/locale/locales/ga/messages' import {messages as messagesHi} from '#/locale/locales/hi/messages' +import {messages as messagesId} from '#/locale/locales/id/messages' +import {messages as messagesIt} from '#/locale/locales/it/messages' import {messages as messagesJa} from '#/locale/locales/ja/messages' import {messages as messagesKo} from '#/locale/locales/ko/messages' import {messages as messagesPt_BR} from '#/locale/locales/pt-BR/messages' +import {messages as messagesTr} from '#/locale/locales/tr/messages' import {messages as messagesUk} from '#/locale/locales/uk/messages' -import {messages as messagesCa} from '#/locale/locales/ca/messages' import {messages as messagesZh_CN} from '#/locale/locales/zh-CN/messages' -import {messages as messagesIt} from '#/locale/locales/it/messages' - -import {sanitizeAppLanguageSetting} from '#/locale/helpers' -import {AppLanguage} from '#/locale/languages' +import {messages as messagesZh_TW} from '#/locale/locales/zh-TW/messages' +import {useLanguagePrefs} from '#/state/preferences' /** * We do a dynamic import of just the catalog that we need */ export async function dynamicActivate(locale: AppLanguage) { switch (locale) { + case AppLanguage.ca: { + i18n.loadAndActivate({locale, messages: messagesCa}) + break + } case AppLanguage.de: { i18n.loadAndActivate({locale, messages: messagesDe}) break @@ -41,6 +47,10 @@ export async function dynamicActivate(locale: AppLanguage) { i18n.loadAndActivate({locale, messages: messagesFr}) break } + case AppLanguage.ga: { + i18n.loadAndActivate({locale, messages: messagesGa}) + break + } case AppLanguage.hi: { i18n.loadAndActivate({locale, messages: messagesHi}) break @@ -49,6 +59,10 @@ export async function dynamicActivate(locale: AppLanguage) { i18n.loadAndActivate({locale, messages: messagesId}) break } + case AppLanguage.it: { + i18n.loadAndActivate({locale, messages: messagesIt}) + break + } case AppLanguage.ja: { i18n.loadAndActivate({locale, messages: messagesJa}) break @@ -61,20 +75,20 @@ export async function dynamicActivate(locale: AppLanguage) { i18n.loadAndActivate({locale, messages: messagesPt_BR}) break } - case AppLanguage.uk: { - i18n.loadAndActivate({locale, messages: messagesUk}) + case AppLanguage.tr: { + i18n.loadAndActivate({locale, messages: messagesTr}) break } - case AppLanguage.ca: { - i18n.loadAndActivate({locale, messages: messagesCa}) + case AppLanguage.uk: { + i18n.loadAndActivate({locale, messages: messagesUk}) break } case AppLanguage.zh_CN: { i18n.loadAndActivate({locale, messages: messagesZh_CN}) break } - case AppLanguage.it: { - i18n.loadAndActivate({locale, messages: messagesIt}) + case AppLanguage.zh_TW: { + i18n.loadAndActivate({locale, messages: messagesZh_TW}) break } default: { diff --git a/src/locale/i18n.web.ts b/src/locale/i18n.web.ts index 334b2586e5..87c3c590e9 100644 --- a/src/locale/i18n.web.ts +++ b/src/locale/i18n.web.ts @@ -1,9 +1,9 @@ import {useEffect} from 'react' import {i18n} from '@lingui/core' -import {useLanguagePrefs} from '#/state/preferences' import {sanitizeAppLanguageSetting} from '#/locale/helpers' import {AppLanguage} from '#/locale/languages' +import {useLanguagePrefs} from '#/state/preferences' /** * We do a dynamic import of just the catalog that we need @@ -12,6 +12,10 @@ export async function dynamicActivate(locale: AppLanguage) { let mod: any switch (locale) { + case AppLanguage.ca: { + mod = await import(`./locales/ca/messages`) + break + } case AppLanguage.de: { mod = await import(`./locales/de/messages`) break @@ -28,6 +32,10 @@ export async function dynamicActivate(locale: AppLanguage) { mod = await import(`./locales/fr/messages`) break } + case AppLanguage.ga: { + mod = await import(`./locales/ga/messages`) + break + } case AppLanguage.hi: { mod = await import(`./locales/hi/messages`) break @@ -36,6 +44,10 @@ export async function dynamicActivate(locale: AppLanguage) { mod = await import(`./locales/id/messages`) break } + case AppLanguage.it: { + mod = await import(`./locales/it/messages`) + break + } case AppLanguage.ja: { mod = await import(`./locales/ja/messages`) break @@ -48,20 +60,20 @@ export async function dynamicActivate(locale: AppLanguage) { mod = await import(`./locales/pt-BR/messages`) break } - case AppLanguage.uk: { - mod = await import(`./locales/uk/messages`) + case AppLanguage.tr: { + mod = await import(`./locales/tr/messages`) break } - case AppLanguage.ca: { - mod = await import(`./locales/ca/messages`) + case AppLanguage.uk: { + mod = await import(`./locales/uk/messages`) break } case AppLanguage.zh_CN: { mod = await import(`./locales/zh-CN/messages`) break } - case AppLanguage.it: { - mod = await import(`./locales/it/messages`) + case AppLanguage.zh_TW: { + mod = await import(`./locales/zh-TW/messages`) break } default: { diff --git a/src/locale/languages.ts b/src/locale/languages.ts index 1cbe8fa830..626c00f389 100644 --- a/src/locale/languages.ts +++ b/src/locale/languages.ts @@ -6,19 +6,22 @@ interface Language { export enum AppLanguage { en = 'en', + ca = 'ca', de = 'de', es = 'es', fi = 'fi', fr = 'fr', + ga = 'ga', hi = 'hi', id = 'id', + it = 'it', ja = 'ja', ko = 'ko', pt_BR = 'pt-BR', + tr = 'tr', uk = 'uk', - ca = 'ca', zh_CN = 'zh-CN', - it = 'it', + zh_TW = 'zh-TW', } interface AppLanguageConfig { @@ -28,19 +31,22 @@ interface AppLanguageConfig { export const APP_LANGUAGES: AppLanguageConfig[] = [ {code2: AppLanguage.en, name: 'English'}, + {code2: AppLanguage.ca, name: 'Català – Catalan'}, {code2: AppLanguage.de, name: 'Deutsch – German'}, {code2: AppLanguage.es, name: 'Español – Spanish'}, {code2: AppLanguage.fi, name: 'Suomi – Finnish'}, {code2: AppLanguage.fr, name: 'Français – French'}, + {code2: AppLanguage.ga, name: 'Gaeilge – Irish'}, {code2: AppLanguage.hi, name: 'हिंदी – Hindi'}, {code2: AppLanguage.id, name: 'Bahasa Indonesia – Indonesian'}, + {code2: AppLanguage.it, name: 'Italiano – Italian'}, {code2: AppLanguage.ja, name: '日本語 – Japanese'}, {code2: AppLanguage.ko, name: '한국어 – Korean'}, {code2: AppLanguage.pt_BR, name: 'Português (BR) – Portuguese (BR)'}, + {code2: AppLanguage.tr, name: 'Türkçe – Turkish'}, {code2: AppLanguage.uk, name: 'Українська – Ukrainian'}, - {code2: AppLanguage.ca, name: 'Català – Catalan'}, - {code2: AppLanguage.zh_CN, name: '简体中文(中国) – Chinese (Simplified)'}, - {code2: AppLanguage.it, name: 'Italiano - Italian'}, + {code2: AppLanguage.zh_CN, name: '简体中文(中国)– Chinese (Simplified)'}, + {code2: AppLanguage.zh_TW, name: '繁體中文(臺灣)– Chinese (Traditional)'}, ] export const LANGUAGES: Language[] = [ diff --git a/src/locale/locales/ca/messages.po b/src/locale/locales/ca/messages.po index 9f723fd71f..4d5da97cf6 100644 --- a/src/locale/locales/ca/messages.po +++ b/src/locale/locales/ca/messages.po @@ -64,7 +64,7 @@ msgstr "<0/> membres" #: src/view/shell/Drawer.tsx:97 msgid "<0>{0} following" -msgstr "" +msgstr "<0>{0} seguint" #: src/screens/Profile/Header/Metrics.tsx:46 msgid "<0>{following} <1>following" @@ -80,7 +80,7 @@ msgstr "<0>Segueix alguns<1>usuaris<2>recomanats" #: src/view/com/auth/onboarding/WelcomeDesktop.tsx:21 msgid "<0>Welcome to<1>Bluesky" -msgstr "<0>Benvingut a<1>Bluesky" +msgstr "<0>Us donem la benvinguda a<1>Bluesky" #: src/screens/Profile/Header/Handle.tsx:42 msgid "⚠Invalid Handle" @@ -92,7 +92,7 @@ msgstr "⚠Identificador invàlid" #: src/lib/hooks/useOTAUpdate.ts:16 #~ msgid "A new version of the app is available. Please update to continue using the app." -#~ msgstr "Hi ha una nova versió d'aquesta aplicació. Actualitza-la per continuar." +#~ msgstr "Hi ha una nova versió d'aquesta aplicació. Actualitza-la per a continuar." #: src/view/com/util/ViewHeader.tsx:89 #: src/view/screens/Search/Search.tsx:648 @@ -110,7 +110,7 @@ msgstr "Accessibilitat" #: src/components/moderation/LabelsOnMe.tsx:42 msgid "account" -msgstr "" +msgstr "compte" #: src/view/com/auth/login/LoginForm.tsx:169 #: src/view/screens/Settings/index.tsx:327 @@ -124,7 +124,7 @@ msgstr "Compte bloquejat" #: src/view/com/profile/ProfileMenu.tsx:153 msgid "Account followed" -msgstr "" +msgstr "Compte seguit" #: src/view/com/profile/ProfileMenu.tsx:113 msgid "Account muted" @@ -154,7 +154,7 @@ msgstr "Compte desbloquejat" #: src/view/com/profile/ProfileMenu.tsx:166 msgid "Account unfollowed" -msgstr "" +msgstr "Compte no seguit" #: src/view/com/profile/ProfileMenu.tsx:102 msgid "Account unmuted" @@ -212,11 +212,11 @@ msgstr "Afegeix una targeta a l'enllaç:" #: src/components/dialogs/MutedWords.tsx:158 msgid "Add mute word for configured settings" -msgstr "" +msgstr "Afegeix paraula silenciada a la configuració" #: src/components/dialogs/MutedWords.tsx:87 msgid "Add muted words and tags" -msgstr "" +msgstr "Afegeix les paraules i etiquetes silenciades" #: src/view/com/modals/ChangeHandle.tsx:417 msgid "Add the following DNS record to your domain:" @@ -246,7 +246,7 @@ msgstr "Afegit als meus canals" #: src/view/screens/PreferencesFollowingFeed.tsx:173 msgid "Adjust the number of likes a reply must have to be shown in your feed." -msgstr "Ajusta el nombre de m'agrades que hagi de tenir una resposta per aparèixer al teu canal." +msgstr "Ajusta el nombre de m'agrades que hagi de tenir una resposta per a aparèixer al teu canal." #: src/screens/Onboarding/StepModeration/AdultContentEnabledPref.tsx:117 #: src/view/com/modals/SelfLabel.tsx:75 @@ -259,7 +259,7 @@ msgstr "Contingut per a adults" #: src/components/moderation/ModerationLabelPref.tsx:114 msgid "Adult content is disabled." -msgstr "" +msgstr "El contingut per adults està deshabilitat." #: src/screens/Moderation/index.tsx:377 #: src/view/screens/Settings/index.tsx:684 @@ -301,14 +301,14 @@ msgstr "S'ha enviat un correu a la teva adreça prèvia, {0}. Inclou un codi de #: src/lib/moderation/useReportOptions.ts:26 msgid "An issue not included in these options" -msgstr "" +msgstr "Un problema que no està inclòs en aquestes opcions" #: src/view/com/profile/FollowButton.tsx:35 #: src/view/com/profile/FollowButton.tsx:45 #: src/view/com/profile/ProfileHeaderSuggestedFollows.tsx:188 #: src/view/com/profile/ProfileHeaderSuggestedFollows.tsx:198 msgid "An issue occurred, please try again." -msgstr "Hi ha hagut un problema, prova-ho de nou" +msgstr "Hi ha hagut un problema, prova-ho de nou." #: src/view/com/notifications/FeedItem.tsx:240 #: src/view/com/threadgate/WhoCanReply.tsx:178 @@ -321,7 +321,7 @@ msgstr "Animals" #: src/lib/moderation/useReportOptions.ts:31 msgid "Anti-Social Behavior" -msgstr "" +msgstr "Comportament antisocial" #: src/view/screens/LanguageSettings.tsx:95 msgid "App Language" @@ -337,7 +337,7 @@ msgstr "La contrasenya de l'aplicació només pot estar formada per lletres, nú #: src/view/com/modals/AddAppPasswords.tsx:99 msgid "App Password names must be at least 4 characters long." -msgstr "La contrasenya de l'aplicació ha de ser d'almenys 4 caràcters" +msgstr "La contrasenya de l'aplicació ha de ser d'almenys 4 caràcters." #: src/view/screens/Settings/index.tsx:695 msgid "App password settings" @@ -356,11 +356,11 @@ msgstr "Contrasenyes de l'aplicació" #: src/components/moderation/LabelsOnMeDialog.tsx:134 #: src/components/moderation/LabelsOnMeDialog.tsx:137 msgid "Appeal" -msgstr "" +msgstr "Apel·la" #: src/components/moderation/LabelsOnMeDialog.tsx:202 msgid "Appeal \"{0}\" label" -msgstr "" +msgstr "Apel·la \"{0}\" etiqueta" #: src/view/com/util/forms/PostDropdownBtn.tsx:337 #: src/view/com/util/forms/PostDropdownBtn.tsx:346 @@ -376,7 +376,7 @@ msgstr "" #: src/components/moderation/LabelsOnMeDialog.tsx:193 msgid "Appeal submitted." -msgstr "" +msgstr "Apel·lació enviada." #: src/view/com/util/moderation/LabelInfo.tsx:52 #~ msgid "Appeal this decision" @@ -396,7 +396,7 @@ msgstr "Confirmes que vols eliminar la contrasenya de l'aplicació \"{name}\"?" #: src/view/com/feeds/FeedSourceCard.tsx:280 msgid "Are you sure you want to remove {0} from your feeds?" -msgstr "" +msgstr "Confirmes que vols eliminar {0} dels teus canals?" #: src/view/com/composer/Composer.tsx:508 msgid "Are you sure you'd like to discard this draft?" @@ -459,7 +459,7 @@ msgstr "Aniversari:" #: src/screens/Profile/Header/ProfileHeaderStandard.tsx:278 #: src/view/com/profile/ProfileMenu.tsx:361 msgid "Block" -msgstr "" +msgstr "Bloqueja" #: src/view/com/profile/ProfileMenu.tsx:300 #: src/view/com/profile/ProfileMenu.tsx:307 @@ -468,7 +468,7 @@ msgstr "Bloqueja el compte" #: src/view/com/profile/ProfileMenu.tsx:344 msgid "Block Account?" -msgstr "" +msgstr "Vols bloquejar el compte?" #: src/view/screens/ProfileList.tsx:530 msgid "Block accounts" @@ -515,7 +515,7 @@ msgstr "Publicació bloquejada." #: src/screens/Profile/Sections/Labels.tsx:153 msgid "Blocking does not prevent this labeler from placing labels on your account." -msgstr "" +msgstr "El bloqueig no evita que aquest etiquetador apliqui etiquetes al teu compte." #: src/view/screens/ProfileList.tsx:631 msgid "Blocking is public. Blocked accounts cannot reply in your threads, mention you, or otherwise interact with you." @@ -523,7 +523,7 @@ msgstr "El bloqueig és públic. Els comptes bloquejats no poden respondre els t #: src/view/com/profile/ProfileMenu.tsx:353 msgid "Blocking will not prevent labels from being applied on your account, but it will stop this account from replying in your threads or interacting with you." -msgstr "" +msgstr "Bloquejar no evitarà que s'apliquin etiquetes al teu compte, però no deixarà que aquest compte respongui els teus fils ni interactui amb tu." #: src/view/com/auth/HomeLoggedOutCTA.tsx:97 #: src/view/com/auth/SplashScreen.web.tsx:133 @@ -538,7 +538,7 @@ msgstr "Bluesky" #: src/view/com/auth/server-input/index.tsx:150 msgid "Bluesky is an open network where you can choose your hosting provider. Custom hosting is now available in beta for developers." -msgstr "Bluesky és una xarxa oberta on pots escollir el teu proveïdor d'allotjament. L'allotjament personalitzat està disponible en beta per a desenvolupadors" +msgstr "Bluesky és una xarxa oberta on pots escollir el teu proveïdor d'allotjament. L'allotjament personalitzat està disponible en beta per a desenvolupadors." #: src/view/com/auth/onboarding/WelcomeDesktop.tsx:80 #: src/view/com/auth/onboarding/WelcomeMobile.tsx:82 @@ -569,11 +569,11 @@ msgstr "Bluesky no mostrarà el teu perfil ni les publicacions als usuaris que n #: src/lib/moderation/useLabelBehaviorDescription.ts:53 msgid "Blur images" -msgstr "" +msgstr "Difumina les imatges" #: src/lib/moderation/useLabelBehaviorDescription.ts:51 msgid "Blur images and filter from feeds" -msgstr "" +msgstr "Difumina les imatges i filtra-ho dels canals" #: src/screens/Onboarding/index.tsx:33 msgid "Books" @@ -590,7 +590,7 @@ msgstr "Negocis" #: src/view/com/modals/ServerInput.tsx:115 #~ msgid "Button disabled. Input custom domain to proceed." -#~ msgstr "Botó deshabilitat. Entra el domini personalitzat per continuar." +#~ msgstr "Botó deshabilitat. Entra el domini personalitzat per a continuar." #: src/view/com/profile/ProfileSubpageHeader.tsx:157 msgid "by —" @@ -602,7 +602,7 @@ msgstr "per {0}" #: src/components/LabelingServiceCard/index.tsx:57 msgid "By {0}" -msgstr "" +msgstr "Per {0}" #: src/view/com/profile/ProfileSubpageHeader.tsx:161 msgid "by <0/>" @@ -610,7 +610,7 @@ msgstr "per <0/>" #: src/view/com/auth/create/Policies.tsx:87 msgid "By creating an account you agree to the {els}." -msgstr "" +msgstr "Creant el compte indiques que estàs d'acord amb {els}." #: src/view/com/profile/ProfileSubpageHeader.tsx:159 msgid "by you" @@ -694,7 +694,7 @@ msgstr "Cancel·la la cerca" #: src/view/com/modals/LinkWarning.tsx:88 msgid "Cancels opening the linked website" -msgstr "" +msgstr "Cancel·la obrir la web enllaçada" #: src/view/com/modals/VerifyEmail.tsx:152 msgid "Change" @@ -746,15 +746,15 @@ msgstr "Comprova el meu estat" #: src/view/com/auth/onboarding/RecommendedFeeds.tsx:121 msgid "Check out some recommended feeds. Tap + to add them to your list of pinned feeds." -msgstr "Mira alguns canals recomanats. Prem + per afegir-los als teus canals fixats." +msgstr "Mira alguns canals recomanats. Prem + per a afegir-los als teus canals fixats." #: src/view/com/auth/onboarding/RecommendedFollows.tsx:185 msgid "Check out some recommended users. Follow them to see similar users." -msgstr "Mira alguns usuaris recomanats. Segueix-los per veure altres usuaris similars." +msgstr "Mira alguns usuaris recomanats. Segueix-los per a veure altres usuaris similars." #: src/view/com/modals/DeleteAccount.tsx:169 msgid "Check your inbox for an email with the confirmation code to enter below:" -msgstr "Comprova el teu correu per rebre el codi de confirmació i entra'l aquí sota:" +msgstr "Comprova el teu correu per a rebre el codi de confirmació i entra'l aquí sota:" #: src/view/com/modals/Threadgate.tsx:72 msgid "Choose \"Everybody\" or \"Nobody\"" @@ -808,11 +808,11 @@ msgstr "Esborra la cerca" #: src/view/screens/Settings/index.tsx:869 msgid "Clears all legacy storage data" -msgstr "" +msgstr "Esborra totes les dades antigues emmagatzemades" #: src/view/screens/Settings/index.tsx:881 msgid "Clears all storage data" -msgstr "" +msgstr "Esborra totes les dades emmagatzemades" #: src/view/screens/Support.tsx:40 msgid "click here" @@ -820,11 +820,11 @@ msgstr "clica aquí" #: src/components/TagMenu/index.web.tsx:138 msgid "Click here to open tag menu for {tag}" -msgstr "" +msgstr "Clica aquí per obrir el menú d'etiquetes per {tag}" #: src/components/RichText.tsx:191 msgid "Click here to open tag menu for #{tag}" -msgstr "" +msgstr "Clica aquí per obrir el menú d'etiquetes per #{tag}" #: src/screens/Onboarding/index.tsx:35 msgid "Climate" @@ -863,7 +863,7 @@ msgstr "Tanca el peu de la navegació" #: src/components/Menu/index.tsx:207 #: src/components/TagMenu/index.tsx:262 msgid "Close this dialog" -msgstr "" +msgstr "Tanca aquest diàleg" #: src/view/shell/index.web.tsx:56 msgid "Closes bottom navigation bar" @@ -904,7 +904,7 @@ msgstr "Finalitza el registre i comença a utilitzar el teu compte" #: src/view/com/auth/create/Step3.tsx:73 msgid "Complete the challenge" -msgstr "" +msgstr "Completa la prova" #: src/view/com/composer/Composer.tsx:437 msgid "Compose posts up to {MAX_GRAPHEME_LENGTH} characters in length" @@ -922,7 +922,7 @@ msgstr "Configura els filtres de continguts per la categoria: {0}" #: src/components/moderation/ModerationLabelPref.tsx:116 msgid "Configured in <0>moderation settings." -msgstr "" +msgstr "Configurat a <0>configuració de moderació." #: src/components/Prompt.tsx:152 #: src/components/Prompt.tsx:155 @@ -955,15 +955,15 @@ msgstr "Confirma l'eliminació del compte" #: src/view/com/modals/ContentFilteringSettings.tsx:156 #~ msgid "Confirm your age to enable adult content." -#~ msgstr "Confirma la teva edat per habilitar el contingut per a adults" +#~ msgstr "Confirma la teva edat per a habilitar el contingut per a adults" #: src/screens/Moderation/index.tsx:303 msgid "Confirm your age:" -msgstr "" +msgstr "Confirma la teva edat:" #: src/screens/Moderation/index.tsx:294 msgid "Confirm your birthdate" -msgstr "" +msgstr "Confirma la teva data de naixement" #: src/view/com/modals/ChangeEmail.tsx:157 #: src/view/com/modals/DeleteAccount.tsx:176 @@ -987,11 +987,11 @@ msgstr "Contacta amb suport" #: src/components/moderation/LabelsOnMe.tsx:42 msgid "content" -msgstr "" +msgstr "contingut" #: src/lib/moderation/useGlobalLabelStrings.ts:18 msgid "Content Blocked" -msgstr "" +msgstr "Contingut bloquejat" #: src/view/screens/Moderation.tsx:83 #~ msgid "Content filtering" @@ -1003,7 +1003,7 @@ msgstr "" #: src/screens/Moderation/index.tsx:287 msgid "Content filters" -msgstr "" +msgstr "Filtres de contingut" #: src/view/com/modals/lang-settings/ContentLanguagesSettings.tsx:74 #: src/view/screens/LanguageSettings.tsx:278 @@ -1028,7 +1028,7 @@ msgstr "Advertències del contingut" #: src/components/Menu/index.web.tsx:84 msgid "Context menu backdrop, click to close the menu." -msgstr "" +msgstr "Teló de fons del menú contextual, fes clic per tancar-lo." #: src/screens/Onboarding/StepAlgoFeeds/index.tsx:170 #: src/screens/Onboarding/StepFollowingFeed.tsx:153 @@ -1086,7 +1086,7 @@ msgstr "Copia" #: src/view/com/modals/ChangeHandle.tsx:481 msgid "Copy {0}" -msgstr "" +msgstr "Copia {0}" #: src/view/screens/ProfileList.tsx:388 msgid "Copy link to list" @@ -1148,7 +1148,7 @@ msgstr "Crea un nou compte" #: src/components/ReportDialog/SelectReportOptionView.tsx:94 msgid "Create report for {0}" -msgstr "" +msgstr "Crea un informe per a {0}" #: src/view/screens/AppPasswords.tsx:246 msgid "Created {0}" @@ -1164,7 +1164,7 @@ msgstr "Creat {0}" #: src/view/com/composer/Composer.tsx:468 msgid "Creates a card with a thumbnail. The card links to {url}" -msgstr "Crea una targeta amb una minuatura. La targeta enllaça a {url}" +msgstr "Crea una targeta amb una miniatura. La targeta enllaça a {url}" #: src/screens/Onboarding/index.tsx:29 msgid "Culture" @@ -1186,7 +1186,7 @@ msgstr "Els canals personalitzats fets per la comunitat et porten noves experiè #: src/view/screens/PreferencesExternalEmbeds.tsx:55 msgid "Customize media from external sites." -msgstr "Personalitza el contingut dels llocs externs" +msgstr "Personalitza el contingut dels llocs externs." #: src/view/screens/Settings.tsx:687 #~ msgid "Danger Zone" @@ -1207,7 +1207,7 @@ msgstr "Tema fosc" #: src/view/screens/Settings/index.tsx:841 msgid "Debug Moderation" -msgstr "" +msgstr "Moderació de depuració" #: src/view/screens/Debug.tsx:83 msgid "Debug panel" @@ -1217,7 +1217,7 @@ msgstr "Panell de depuració" #: src/view/screens/AppPasswords.tsx:268 #: src/view/screens/ProfileList.tsx:613 msgid "Delete" -msgstr "" +msgstr "Elimina" #: src/view/screens/Settings/index.tsx:796 msgid "Delete account" @@ -1233,7 +1233,7 @@ msgstr "Elimina la contrasenya d'aplicació" #: src/view/screens/AppPasswords.tsx:263 msgid "Delete app password?" -msgstr "" +msgstr "Vols eliminar la contrasenya d'aplicació?" #: src/view/screens/ProfileList.tsx:415 msgid "Delete List" @@ -1258,7 +1258,7 @@ msgstr "Elimina la publicació" #: src/view/screens/ProfileList.tsx:608 msgid "Delete this list?" -msgstr "" +msgstr "Vols eliminar aquesta llista?" #: src/view/com/util/forms/PostDropdownBtn.tsx:314 msgid "Delete this post?" @@ -1300,7 +1300,7 @@ msgstr "Tènue" #: src/lib/moderation/useLabelBehaviorDescription.ts:68 #: src/screens/Moderation/index.tsx:343 msgid "Disabled" -msgstr "" +msgstr "Deshabilitat" #: src/view/com/composer/Composer.tsx:510 msgid "Discard" @@ -1312,7 +1312,7 @@ msgstr "Descarta" #: src/view/com/composer/Composer.tsx:507 msgid "Discard draft?" -msgstr "" +msgstr "Vols descartar l'esborrany?" #: src/screens/Moderation/index.tsx:520 #: src/screens/Moderation/index.tsx:524 @@ -1342,15 +1342,15 @@ msgstr "Nom mostrat" #: src/view/com/modals/ChangeHandle.tsx:398 msgid "DNS Panel" -msgstr "" +msgstr "Panell de DNS" #: src/lib/moderation/useGlobalLabelStrings.ts:39 msgid "Does not include nudity." -msgstr "" +msgstr "No inclou nuesa." #: src/view/com/modals/ChangeHandle.tsx:482 msgid "Domain Value" -msgstr "" +msgstr "valor del domini" #: src/view/com/modals/ChangeHandle.tsx:489 msgid "Domain verified!" @@ -1395,7 +1395,7 @@ msgstr "Fet{extraText}" #: src/view/com/auth/login/ChooseAccountForm.tsx:46 msgid "Double tap to sign in" -msgstr "Fes doble toc per iniciar la sessió" +msgstr "Fes doble toc per a iniciar la sessió" #: src/view/screens/Settings/index.tsx:755 #~ msgid "Download Bluesky account data (repository)" @@ -1408,47 +1408,47 @@ msgstr "Descarrega el fitxer CAR" #: src/view/com/composer/text-input/TextInput.web.tsx:249 msgid "Drop to add images" -msgstr "Deixa anar per afegir imatges" +msgstr "Deixa anar a afegir imatges" #: src/screens/Onboarding/StepModeration/AdultContentEnabledPref.tsx:120 msgid "Due to Apple policies, adult content can only be enabled on the web after completing sign up." -msgstr "Degut a les polítiques d'Apple, el contingut per a adults només es pot habilitar a la web després de registrar-se" +msgstr "A causa de les polítiques d'Apple, el contingut a adults només es pot habilitar a la web després de registrar-se." #: src/view/com/modals/ChangeHandle.tsx:257 msgid "e.g. alice" -msgstr "" +msgstr "p. ex.jordi" #: src/view/com/modals/EditProfile.tsx:185 msgid "e.g. Alice Roberts" -msgstr "p.ex. Jordi Guix" +msgstr "p. ex.Jordi Guix" #: src/view/com/modals/ChangeHandle.tsx:381 msgid "e.g. alice.com" -msgstr "" +msgstr "p. ex.jordi.com" #: src/view/com/modals/EditProfile.tsx:203 msgid "e.g. Artist, dog-lover, and avid reader." -msgstr "p.ex. Artista, amant dels gossos i amant de la lectura." +msgstr "p. ex.Artista, amant dels gossos i amant de la lectura." #: src/lib/moderation/useGlobalLabelStrings.ts:43 msgid "E.g. artistic nudes." -msgstr "" +msgstr "p. ex.nuesa artística" #: src/view/com/modals/CreateOrEditList.tsx:283 msgid "e.g. Great Posters" -msgstr "p.ex. Gent interessant" +msgstr "p. ex.Gent interessant" #: src/view/com/modals/CreateOrEditList.tsx:284 msgid "e.g. Spammers" -msgstr "p.ex. Spammers" +msgstr "p. ex.Spammers" #: src/view/com/modals/CreateOrEditList.tsx:312 msgid "e.g. The posters who never miss." -msgstr "p.ex. Els que mai fallen" +msgstr "p. ex.Els que mai fallen" #: src/view/com/modals/CreateOrEditList.tsx:313 msgid "e.g. Users that repeatedly reply with ads." -msgstr "p.ex. Usuaris que sempre responen amb anuncis" +msgstr "p. ex.Usuaris que sempre responen amb anuncis" #: src/view/com/modals/InviteCodes.tsx:96 msgid "Each code works once. You'll receive more invite codes periodically." @@ -1462,7 +1462,7 @@ msgstr "Edita" #: src/view/com/util/UserAvatar.tsx:299 #: src/view/com/util/UserBanner.tsx:85 msgid "Edit avatar" -msgstr "" +msgstr "Edita l'avatar" #: src/view/com/composer/photos/Gallery.tsx:144 #: src/view/com/modals/EditImage.tsx:207 @@ -1552,11 +1552,11 @@ msgstr "Habilita només {0}" #: src/screens/Moderation/index.tsx:331 msgid "Enable adult content" -msgstr "" +msgstr "Habilita el contingut per adults" #: src/screens/Onboarding/StepModeration/AdultContentEnabledPref.tsx:94 msgid "Enable Adult Content" -msgstr "Habilita el contingut per a adults" +msgstr "Habilita el contingut per adults" #: src/screens/Onboarding/StepModeration/AdultContentEnabledPref.tsx:78 #: src/screens/Onboarding/StepModeration/AdultContentEnabledPref.tsx:79 @@ -1573,11 +1573,11 @@ msgstr "Habilita reproductors de contingut per" #: src/view/screens/PreferencesFollowingFeed.tsx:147 msgid "Enable this setting to only see replies between people you follow." -msgstr "Activa aquesta opció per veure només les respostes entre els comptes que segueixes." +msgstr "Activa aquesta opció per a veure només les respostes entre els comptes que segueixes." #: src/screens/Moderation/index.tsx:341 msgid "Enabled" -msgstr "" +msgstr "Habilitat" #: src/screens/Profile/Sections/Feed.tsx:84 msgid "End of feed" @@ -1590,7 +1590,7 @@ msgstr "Posa un nom a aquesta contrasenya d'aplicació" #: src/components/dialogs/MutedWords.tsx:100 #: src/components/dialogs/MutedWords.tsx:101 msgid "Enter a word or tag" -msgstr "" +msgstr "Introdueix una lletra o etiqueta" #: src/view/com/modals/VerifyEmail.tsx:105 msgid "Enter Confirmation Code" @@ -1602,7 +1602,7 @@ msgstr "Entra el codi de confirmació" #: src/view/com/modals/ChangePassword.tsx:153 msgid "Enter the code you received to change your password." -msgstr "Introdueix el codi que has rebut per canviar la teva contrasenya." +msgstr "Introdueix el codi que has rebut per a canviar la teva contrasenya." #: src/view/com/modals/ChangeHandle.tsx:371 msgid "Enter the domain you want to use" @@ -1610,7 +1610,7 @@ msgstr "Introdueix el domini que vols utilitzar" #: src/view/com/auth/login/ForgotPasswordForm.tsx:107 msgid "Enter the email you used to create your account. We'll send you a \"reset code\" so you can set a new password." -msgstr "Introdueix el correu que vas fer servir per crear el teu compte. T'enviarem un \"codi de restabliment\" perquè puguis posar una nova contrasenya." +msgstr "Introdueix el correu que vas fer servir per a crear el teu compte. T'enviarem un \"codi de restabliment\" perquè puguis posar una nova contrasenya." #: src/components/dialogs/BirthDateSettings.tsx:108 #: src/view/com/auth/create/Step1.tsx:228 @@ -1643,7 +1643,7 @@ msgstr "Introdueix el teu usuari i contrasenya" #: src/view/com/auth/create/Step3.tsx:67 msgid "Error receiving captcha response." -msgstr "" +msgstr "Erro en rebre la resposta al captcha." #: src/view/screens/Search/Search.tsx:110 msgid "Error:" @@ -1655,11 +1655,11 @@ msgstr "Tothom" #: src/lib/moderation/useReportOptions.ts:66 msgid "Excessive mentions or replies" -msgstr "" +msgstr "Mencions o respostes excessives" #: src/view/com/modals/DeleteAccount.tsx:231 msgid "Exits account deletion process" -msgstr "" +msgstr "Surt del procés d'eliminació del compte" #: src/view/com/modals/ChangeHandle.tsx:150 msgid "Exits handle change process" @@ -1667,7 +1667,7 @@ msgstr "Surt del procés de canvi d'identificador" #: src/view/com/modals/crop-image/CropImage.web.tsx:135 msgid "Exits image cropping process" -msgstr "" +msgstr "Surt del procés de retallar l'imatge" #: src/view/com/lightbox/Lightbox.web.tsx:130 msgid "Exits image view" @@ -1693,11 +1693,11 @@ msgstr "Expandeix o replega la publicació completa a la qual estàs responent" #: src/lib/moderation/useGlobalLabelStrings.ts:47 msgid "Explicit or potentially disturbing media." -msgstr "" +msgstr "Contingut explícit o potencialment pertorbador." #: src/lib/moderation/useGlobalLabelStrings.ts:35 msgid "Explicit sexual images." -msgstr "" +msgstr "Imatges sexuals explícites." #: src/view/screens/Settings/index.tsx:777 msgid "Export my data" @@ -1730,7 +1730,7 @@ msgstr "Configuració del contingut extern" #: src/view/com/modals/AddAppPasswords.tsx:115 #: src/view/com/modals/AddAppPasswords.tsx:119 msgid "Failed to create app password." -msgstr "No s'ha pogut crear la contrasenya d'aplicació" +msgstr "No s'ha pogut crear la contrasenya d'aplicació." #: src/view/com/modals/CreateOrEditList.tsx:206 msgid "Failed to create the list. Check your internet connection and try again." @@ -1747,7 +1747,7 @@ msgstr "Error en carregar els canals recomanats" #: src/view/com/lightbox/Lightbox.tsx:83 msgid "Failed to save image: {0}" -msgstr "" +msgstr "Error en desar la imatge: {0}" #: src/Navigation.tsx:196 msgid "Feed" @@ -1783,7 +1783,7 @@ msgstr "Canals" #: src/view/com/auth/onboarding/RecommendedFeeds.tsx:57 msgid "Feeds are created by users to curate content. Choose some feeds that you find interesting." -msgstr "Els canals són creats pels usuaris per curar contingut. Tria els canals que trobis interessants." +msgstr "Els canals són creats pels usuaris per a curar contingut. Tria els canals que trobis interessants." #: src/view/screens/SavedFeeds.tsx:156 msgid "Feeds are custom algorithms that users build with a little coding expertise. <0/> for more information." @@ -1795,11 +1795,11 @@ msgstr "Els canals també poden ser d'actualitat!" #: src/view/com/modals/ChangeHandle.tsx:482 msgid "File Contents" -msgstr "" +msgstr "Continguts del fitxer" #: src/lib/moderation/useLabelBehaviorDescription.ts:66 msgid "Filter from feeds" -msgstr "" +msgstr "Filtra-ho dels canals" #: src/screens/Onboarding/StepFinished.tsx:151 msgid "Finalizing" @@ -1809,7 +1809,7 @@ msgstr "Finalitzant" #: src/view/com/posts/FollowingEmptyState.tsx:57 #: src/view/com/posts/FollowingEndOfFeed.tsx:58 msgid "Find accounts to follow" -msgstr "Troba comptes per seguir" +msgstr "Troba comptes per a seguir" #: src/view/screens/Search/Search.tsx:441 msgid "Find users on Bluesky" @@ -1825,7 +1825,7 @@ msgstr "Troba comptes similars…" #: src/view/screens/PreferencesFollowingFeed.tsx:111 msgid "Fine-tune the content you see on your Following feed." -msgstr "" +msgstr "Ajusta el contingut que veus al teu canal Seguint." #: src/view/screens/PreferencesHomeFeed.tsx:111 #~ msgid "Fine-tune the content you see on your home screen." @@ -1874,7 +1874,7 @@ msgstr "Segueix {0}" #: src/view/com/profile/ProfileMenu.tsx:242 #: src/view/com/profile/ProfileMenu.tsx:253 msgid "Follow Account" -msgstr "" +msgstr "Segueix el compte" #: src/screens/Onboarding/StepSuggestedAccounts/index.tsx:179 msgid "Follow All" @@ -1886,7 +1886,7 @@ msgstr "Segueix els comptes seleccionats i continua" #: src/view/com/auth/onboarding/RecommendedFollows.tsx:64 msgid "Follow some users to get started. We can recommend you more users based on who you find interesting." -msgstr "Segueix a alguns usuaris per començar. Te'n podem recomanar més basant-nos en els que trobes interessants." +msgstr "Segueix a alguns usuaris per a començar. Te'n podem recomanar més basant-nos en els que trobes interessants." #: src/view/com/profile/ProfileCard.tsx:216 msgid "Followed by {0}" @@ -1926,7 +1926,7 @@ msgstr "Seguint {0}" #: src/view/screens/Settings/index.tsx:553 msgid "Following feed preferences" -msgstr "" +msgstr "Preferències del canal Seguint" #: src/Navigation.tsx:262 #: src/view/com/home/HomeHeaderLayout.web.tsx:50 @@ -1934,7 +1934,7 @@ msgstr "" #: src/view/screens/PreferencesFollowingFeed.tsx:104 #: src/view/screens/Settings/index.tsx:562 msgid "Following Feed Preferences" -msgstr "" +msgstr "Preferències del canal Seguint" #: src/screens/Profile/Header/Handle.tsx:24 msgid "Follows you" @@ -1971,12 +1971,12 @@ msgstr "He oblidat la contrasenya" #: src/lib/moderation/useReportOptions.ts:52 msgid "Frequently Posts Unwanted Content" -msgstr "" +msgstr "Publica contingut no dessitjat freqüentment" #: src/screens/Hashtag.tsx:108 #: src/screens/Hashtag.tsx:148 msgid "From @{sanitizedAuthor}" -msgstr "" +msgstr "De @{sanitizedAuthor}" #: src/view/com/posts/FeedItem.tsx:179 msgctxt "from-feed" @@ -1994,7 +1994,7 @@ msgstr "Comença" #: src/lib/moderation/useReportOptions.ts:37 msgid "Glaring violations of law or terms of service" -msgstr "" +msgstr "Infraccions flagrants de la llei o les condicions del servei" #: src/components/moderation/ScreenHider.tsx:144 #: src/components/moderation/ScreenHider.tsx:153 @@ -2024,16 +2024,16 @@ msgstr "Ves al pas anterior" #: src/view/screens/NotFound.tsx:55 msgid "Go home" -msgstr "" +msgstr "Ves a l'inici" #: src/view/screens/NotFound.tsx:54 msgid "Go Home" -msgstr "" +msgstr "Ves a l'inici" #: src/view/screens/Search/Search.tsx:748 #: src/view/shell/desktop/Search.tsx:263 msgid "Go to @{queryMaybeHandle}" -msgstr "Vés a @{queryMaybeHandle}" +msgstr "Ves a @{queryMaybeHandle}" #: src/view/com/auth/login/ForgotPasswordForm.tsx:189 #: src/view/com/auth/login/ForgotPasswordForm.tsx:218 @@ -2045,7 +2045,7 @@ msgstr "Ves al següent" #: src/lib/moderation/useGlobalLabelStrings.ts:46 msgid "Graphic Media" -msgstr "" +msgstr "Mitjans gràfics" #: src/view/com/modals/ChangeHandle.tsx:265 msgid "Handle" @@ -2053,19 +2053,19 @@ msgstr "Identificador" #: src/lib/moderation/useReportOptions.ts:32 msgid "Harassment, trolling, or intolerance" -msgstr "" +msgstr "Assetjament, troleig o intolerància" #: src/Navigation.tsx:282 msgid "Hashtag" -msgstr "" +msgstr "Etiqueta" #: src/components/RichText.tsx:188 #~ msgid "Hashtag: {tag}" -#~ msgstr "" +#~ msgstr "Etiqueta: {tag}" #: src/components/RichText.tsx:190 msgid "Hashtag: #{tag}" -msgstr "" +msgstr "Etiqueta: #{tag}" #: src/view/com/auth/create/CreateAccount.tsx:208 msgid "Having trouble?" @@ -2086,7 +2086,7 @@ msgstr "Aquí tens alguns canals d'actualitat populars. Pots seguir-ne tants com #: src/screens/Onboarding/StepTopicalFeeds.tsx:80 msgid "Here are some topical feeds based on your interests: {interestsText}. You can choose to follow as many as you like." -msgstr "Aquí tens uns quants canals d'actualitat basats en els teus interesos: {interestsText}. Pots seguir-ne tants com vulguis." +msgstr "Aquí tens uns quants canals d'actualitat basats en els teus interessos: {interestsText}. Pots seguir-ne tants com vulguis." #: src/view/com/modals/AddAppPasswords.tsx:153 msgid "Here is your app password." @@ -2150,15 +2150,15 @@ msgstr "El servidor del canal ha donat una resposta incorrecta. Avisa al propiet #: src/view/com/posts/FeedErrorMessage.tsx:96 msgid "Hmm, we're having trouble finding this feed. It may have been deleted." -msgstr "Tenim problemes per trobar aquest canal. Potser ha estat eliminat." +msgstr "Tenim problemes per a trobar aquest canal. Potser ha estat eliminat." #: src/screens/Moderation/index.tsx:61 msgid "Hmmmm, it seems we're having trouble loading this data. See below for more details. If this issue persists, please contact us." -msgstr "" +msgstr "Tenim problemes per a carregar aquestes dades. Mira a continuació per a veure més detalls. Contacta'ns si aquest problema continua." #: src/screens/Profile/ErrorState.tsx:31 msgid "Hmmmm, we couldn't load that moderation service." -msgstr "" +msgstr "No podem carregar el servei de moderació." #: src/Navigation.tsx:454 #: src/view/shell/bottom-bar/BottomBar.tsx:139 @@ -2177,7 +2177,7 @@ msgstr "Inici" #: src/view/com/modals/ChangeHandle.tsx:421 msgid "Host:" -msgstr "" +msgstr "Allotjament:" #: src/view/com/auth/create/Step1.tsx:75 #: src/view/com/auth/login/ForgotPasswordForm.tsx:120 @@ -2216,23 +2216,23 @@ msgstr "Si no en selecciones cap, és apropiat per a totes les edats." #: src/view/com/auth/create/Policies.tsx:91 msgid "If you are not yet an adult according to the laws of your country, your parent or legal guardian must read these Terms on your behalf." -msgstr "" +msgstr "Si encara no ets un adult segons les lleis del teu país, el teu tutor legal haurà de llegir aquests Termes en el teu lloc." #: src/view/screens/ProfileList.tsx:610 msgid "If you delete this list, you won't be able to recover it." -msgstr "" +msgstr "Si esborres aquesta llista no la podràs recuperar." #: src/view/com/util/forms/PostDropdownBtn.tsx:316 msgid "If you remove this post, you won't be able to recover it." -msgstr "" +msgstr "Si esborres aquesta publicació no la podràs recuperar." #: src/view/com/modals/ChangePassword.tsx:148 msgid "If you want to change your password, we will send you a code to verify that this is your account." -msgstr "Si vols canviar la contrasenya t'enviarem un codi per verificar que aquest compte és teu." +msgstr "Si vols canviar la contrasenya t'enviarem un codi per a verificar que aquest compte és teu." #: src/lib/moderation/useReportOptions.ts:36 msgid "Illegal and Urgent" -msgstr "" +msgstr "Il·legal i urgent" #: src/view/com/util/images/Gallery.tsx:38 msgid "Image" @@ -2249,15 +2249,15 @@ msgstr "Text alternatiu de la imatge" #: src/lib/moderation/useReportOptions.ts:47 msgid "Impersonation or false claims about identity or affiliation" -msgstr "" +msgstr "Suplantació d'identitat o afirmacions falses sobre identitat o afiliació" #: src/view/com/auth/login/SetNewPasswordForm.tsx:138 msgid "Input code sent to your email for password reset" -msgstr "Introdueix el codi que s'ha enviat al teu correu per restablir la contrasenya" +msgstr "Introdueix el codi que s'ha enviat al teu correu per a restablir la contrasenya" #: src/view/com/modals/DeleteAccount.tsx:184 msgid "Input confirmation code for account deletion" -msgstr "Introdueix el codi de confirmació per eliminar el compte" +msgstr "Introdueix el codi de confirmació per a eliminar el compte" #: src/view/com/auth/create/Step1.tsx:177 msgid "Input email for Bluesky account" @@ -2265,7 +2265,7 @@ msgstr "Introdueix el correu del compte de Bluesky" #: src/view/com/auth/create/Step1.tsx:151 msgid "Input invite code to proceed" -msgstr "Introdueix el codi d'invitació per continuar" +msgstr "Introdueix el codi d'invitació per a continuar" #: src/view/com/modals/AddAppPasswords.tsx:180 msgid "Input name for app password" @@ -2277,7 +2277,7 @@ msgstr "Introdueix una nova contrasenya" #: src/view/com/modals/DeleteAccount.tsx:203 msgid "Input password for account deletion" -msgstr "Introdueix la contrasenya per elimiar el compte" +msgstr "Introdueix la contrasenya per a eliminar el compte" #: src/view/com/auth/create/Step2.tsx:196 #~ msgid "Input phone number for SMS verification" @@ -2289,7 +2289,7 @@ msgstr "Introdueix la contrasenya lligada a {identifier}" #: src/view/com/auth/login/LoginForm.tsx:200 msgid "Input the username or email address you used at signup" -msgstr "Introdueix el nom d'usuari o correu que vas utilitzar per registrar-te" +msgstr "Introdueix el nom d'usuari o correu que vas utilitzar per a registrar-te" #: src/view/com/auth/create/Step2.tsx:271 #~ msgid "Input the verification code we have texted to you" @@ -2297,7 +2297,7 @@ msgstr "Introdueix el nom d'usuari o correu que vas utilitzar per registrar-te" #: src/view/com/modals/Waitlist.tsx:90 #~ msgid "Input your email to get on the Bluesky waitlist" -#~ msgstr "Introdueix el teu correu per afegir-te a la llista d'espera de Bluesky" +#~ msgstr "Introdueix el teu correu per a afegir-te a la llista d'espera de Bluesky" #: src/view/com/auth/login/LoginForm.tsx:232 msgid "Input your password" @@ -2305,7 +2305,7 @@ msgstr "Introdueix la teva contrasenya" #: src/view/com/modals/ChangeHandle.tsx:390 msgid "Input your preferred hosting provider" -msgstr "" +msgstr "Introdeix el teu proveïdor d'allotjament preferit" #: src/view/com/auth/create/Step2.tsx:80 msgid "Input your user handle" @@ -2376,35 +2376,35 @@ msgstr "Periodisme" #: src/components/moderation/LabelsOnMe.tsx:59 msgid "label has been placed on this {labelTarget}" -msgstr "" +msgstr "S'ha posat l'etiqueta a aquest {labelTarget}" #: src/components/moderation/ContentHider.tsx:144 msgid "Labeled by {0}." -msgstr "" +msgstr "Etiquetat per {0}." #: src/components/moderation/ContentHider.tsx:142 msgid "Labeled by the author." -msgstr "" +msgstr "Etiquetat per l'autor." #: src/view/screens/Profile.tsx:186 msgid "Labels" -msgstr "" +msgstr "Etiquetes" #: src/screens/Profile/Sections/Labels.tsx:143 msgid "Labels are annotations on users and content. They can be used to hide, warn, and categorize the network." -msgstr "" +msgstr "Les etiquetes son anotacions sobre els usuaris i el contingut. Poden ser utilitzades per a ocultar, advertir i categoritxar la xarxa." #: src/components/moderation/LabelsOnMe.tsx:61 msgid "labels have been placed on this {labelTarget}" -msgstr "" +msgstr "S'han posat etiquetes a aquest {labelTarget}" #: src/components/moderation/LabelsOnMeDialog.tsx:63 msgid "Labels on your account" -msgstr "" +msgstr "Etiquetes al teu compte" #: src/components/moderation/LabelsOnMeDialog.tsx:65 msgid "Labels on your content" -msgstr "" +msgstr "Etiquetes al teu contingut" #: src/view/com/composer/select-language/SelectLangBtn.tsx:104 msgid "Language selection" @@ -2438,7 +2438,7 @@ msgstr "Més informació" #: src/components/moderation/ContentHider.tsx:65 #: src/components/moderation/ContentHider.tsx:128 msgid "Learn more about the moderation applied to this content." -msgstr "" +msgstr "Més informació sobre la moderació que s'ha aplicat a aquest contingut." #: src/components/moderation/PostHider.tsx:85 #: src/components/moderation/ScreenHider.tsx:126 @@ -2451,11 +2451,11 @@ msgstr "Més informació sobre què és públic a Bluesky." #: src/components/moderation/ContentHider.tsx:152 msgid "Learn more." -msgstr "" +msgstr "Més informació." #: src/view/com/modals/lang-settings/ContentLanguagesSettings.tsx:82 msgid "Leave them all unchecked to see any language." -msgstr "Deixa'ls tots sense marcar per veure tots els idiomes." +msgstr "Deixa'ls tots sense marcar per a veure tots els idiomes." #: src/view/com/modals/LinkWarning.tsx:51 msgid "Leaving Bluesky" @@ -2514,7 +2514,7 @@ msgstr "Li ha agradat a {0} {1}" #: src/components/LabelingServiceCard/index.tsx:72 msgid "Liked by {count} {0}" -msgstr "" +msgstr "Li ha agradat a {count} {0}" #: src/screens/Profile/Header/ProfileHeaderLabeler.tsx:277 #: src/screens/Profile/Header/ProfileHeaderLabeler.tsx:291 @@ -2639,15 +2639,15 @@ msgstr "Assegura't que és aquí on vols anar!" #: src/components/dialogs/MutedWords.tsx:83 msgid "Manage your muted words and tags" -msgstr "" +msgstr "Gestiona les teves etiquetes i paraules silenciades" #: src/view/com/auth/create/Step2.tsx:118 msgid "May not be longer than 253 characters" -msgstr "" +msgstr "No pot ser més llarg de 253 caràcters" #: src/view/com/auth/create/Step2.tsx:109 msgid "May only contain letters and numbers" -msgstr "" +msgstr "Només pot tenir lletres i números" #: src/view/screens/Profile.tsx:190 msgid "Media" @@ -2676,7 +2676,7 @@ msgstr "Missatge del servidor: {0}" #: src/lib/moderation/useReportOptions.ts:45 msgid "Misleading Account" -msgstr "" +msgstr "Compte enganyòs" #: src/Navigation.tsx:119 #: src/screens/Moderation/index.tsx:106 @@ -2689,7 +2689,7 @@ msgstr "Moderació" #: src/components/moderation/ModerationDetailsDialog.tsx:113 msgid "Moderation details" -msgstr "" +msgstr "Detalls de la moderació" #: src/view/com/lists/ListCard.tsx:93 #: src/view/com/modals/UserAddRemoveLists.tsx:206 @@ -2729,20 +2729,20 @@ msgstr "Configuració de moderació" #: src/Navigation.tsx:216 msgid "Moderation states" -msgstr "" +msgstr "Estats de moderació" #: src/screens/Moderation/index.tsx:217 msgid "Moderation tools" -msgstr "" +msgstr "Eines de moderació" #: src/components/moderation/ModerationDetailsDialog.tsx:49 #: src/lib/moderation/useModerationCauseDescription.ts:40 msgid "Moderator has chosen to set a general warning on the content." -msgstr "El moderador ha decidit establir un advertiment general sobre el contingut" +msgstr "El moderador ha decidit establir un advertiment general sobre el contingut." #: src/view/com/post-thread/PostThreadItem.tsx:541 msgid "More" -msgstr "" +msgstr "Més" #: src/view/shell/desktop/Feeds.tsx:65 msgid "More feeds" @@ -2762,15 +2762,15 @@ msgstr "Respostes amb més m'agrada primer" #: src/view/com/auth/create/Step2.tsx:122 msgid "Must be at least 3 characters" -msgstr "" +msgstr "Ha de tenir almenys 3 caràcters" #: src/components/TagMenu/index.tsx:249 msgid "Mute" -msgstr "" +msgstr "Silencia" #: src/components/TagMenu/index.web.tsx:105 msgid "Mute {truncatedTag}" -msgstr "" +msgstr "Silencia {truncatedTag}" #: src/view/com/profile/ProfileMenu.tsx:279 #: src/view/com/profile/ProfileMenu.tsx:286 @@ -2783,19 +2783,19 @@ msgstr "Silencia els comptes" #: src/components/TagMenu/index.tsx:209 msgid "Mute all {displayTag} posts" -msgstr "" +msgstr "Silencia totes les publicacions {displayTag}" #: src/components/TagMenu/index.tsx:211 #~ msgid "Mute all {tag} posts" -#~ msgstr "" +#~ msgstr "Silencia totes les publicacions {tag}" #: src/components/dialogs/MutedWords.tsx:149 msgid "Mute in tags only" -msgstr "" +msgstr "Silencia només a les etiquetes" #: src/components/dialogs/MutedWords.tsx:134 msgid "Mute in text & tags" -msgstr "" +msgstr "Silencia a les etiquetes i al text" #: src/view/screens/ProfileList.tsx:461 #: src/view/screens/ProfileList.tsx:624 @@ -2812,11 +2812,11 @@ msgstr "Vols silenciar aquests comptes?" #: src/components/dialogs/MutedWords.tsx:127 msgid "Mute this word in post text and tags" -msgstr "" +msgstr "Silencia aquesta paraula en el text de les publicacions i a les etiquetes" #: src/components/dialogs/MutedWords.tsx:142 msgid "Mute this word in tags only" -msgstr "" +msgstr "Silencia aquesta paraula només a les etiquetes" #: src/view/com/util/forms/PostDropdownBtn.tsx:251 #: src/view/com/util/forms/PostDropdownBtn.tsx:257 @@ -2826,7 +2826,7 @@ msgstr "Silencia el fil de debat" #: src/view/com/util/forms/PostDropdownBtn.tsx:267 #: src/view/com/util/forms/PostDropdownBtn.tsx:269 msgid "Mute words & tags" -msgstr "" +msgstr "Silencia paraules i etiquetes" #: src/view/com/lists/ListCard.tsx:102 msgid "Muted" @@ -2847,11 +2847,11 @@ msgstr "Les publicacions dels comptes silenciats seran eliminats del teu canal i #: src/lib/moderation/useModerationCauseDescription.ts:85 msgid "Muted by \"{0}\"" -msgstr "" +msgstr "Silenciat per \"{0}\"" #: src/screens/Moderation/index.tsx:233 msgid "Muted words & tags" -msgstr "" +msgstr "Paraules i etiquetes silenciades" #: src/view/screens/ProfileList.tsx:621 msgid "Muting is private. Muted accounts can interact with you, but you will not see their posts or receive notifications from them." @@ -2872,7 +2872,7 @@ msgstr "El meu perfil" #: src/view/screens/Settings/index.tsx:596 msgid "My saved feeds" -msgstr "" +msgstr "Els meus canals desats" #: src/view/screens/Settings/index.tsx:602 msgid "My Saved Feeds" @@ -2895,7 +2895,7 @@ msgstr "Es requereix un nom" #: src/lib/moderation/useReportOptions.ts:78 #: src/lib/moderation/useReportOptions.ts:86 msgid "Name or Description Violates Community Standards" -msgstr "" +msgstr "El nom o la descripció infringeixen els estàndards comunitaris" #: src/screens/Onboarding/index.tsx:25 msgid "Nature" @@ -2915,12 +2915,12 @@ msgstr "Navega al teu perfil" #: src/components/ReportDialog/SelectReportOptionView.tsx:124 msgid "Need to report a copyright violation?" -msgstr "" +msgstr "Necessites informar d'una infracció dels drets d'autor?" #: src/view/com/modals/EmbedConsent.tsx:107 #: src/view/com/modals/EmbedConsent.tsx:123 msgid "Never load embeds from {0}" -msgstr "No carreguis mai les incrustacions de {0} " +msgstr "No carreguis mai les incrustacions de {0}" #: src/view/com/auth/onboarding/WelcomeDesktop.tsx:72 #: src/view/com/auth/onboarding/WelcomeMobile.tsx:74 @@ -2933,11 +2933,11 @@ msgstr "No perdis mai accés als teus seguidors i les teves dades." #: src/components/dialogs/MutedWords.tsx:293 #~ msgid "Nevermind" -#~ msgstr "" +#~ msgstr "Tant hi fa" #: src/view/com/modals/ChangeHandle.tsx:520 msgid "Nevermind, create a handle for me" -msgstr "" +msgstr "Tant hi fa, crea'm un identificador" #: src/view/screens/Lists.tsx:76 msgctxt "action" @@ -3034,7 +3034,7 @@ msgstr "Cap descripció" #: src/view/com/modals/ChangeHandle.tsx:406 msgid "No DNS Panel" -msgstr "" +msgstr "No hi ha panell de DNS" #: src/screens/Profile/Header/ProfileHeaderStandard.tsx:111 msgid "No longer following {0}" @@ -3051,7 +3051,7 @@ msgstr "Cap resultat" #: src/components/Lists.tsx:189 msgid "No results found" -msgstr "" +msgstr "No s'han trobat resultats" #: src/view/screens/Feeds.tsx:495 msgid "No results found for \"{query}\"" @@ -3074,11 +3074,11 @@ msgstr "Ningú" #: src/components/LikedByList.tsx:102 #: src/components/LikesDialog.tsx:99 msgid "Nobody has liked this yet. Maybe you should be the first!" -msgstr "" +msgstr "A ningú encara li ha agradat això. Potser hauries de ser el primer!" #: src/lib/moderation/useGlobalLabelStrings.ts:42 msgid "Non-sexual Nudity" -msgstr "" +msgstr "Nuesa no sexual" #: src/view/com/modals/SelfLabel.tsx:135 msgid "Not Applicable." @@ -3097,7 +3097,7 @@ msgstr "Ara mateix no" #: src/view/com/profile/ProfileMenu.tsx:368 #: src/view/com/util/forms/PostDropdownBtn.tsx:342 msgid "Note about sharing" -msgstr "" +msgstr "Nota sobre compartir" #: src/screens/Moderation/index.tsx:542 msgid "Note: Bluesky is an open and public network. This setting only limits the visibility of your content on the Bluesky app and website, and other apps may not respect this setting. Your content may still be shown to logged-out users by other apps and websites." @@ -3119,11 +3119,11 @@ msgstr "Nuesa" #: src/lib/moderation/useReportOptions.ts:71 msgid "Nudity or pornography not labeled as such" -msgstr "" +msgstr "Nuesa o pornografia no etiquetada com a tal" #: src/lib/moderation/useLabelBehaviorDescription.ts:11 msgid "Off" -msgstr "" +msgstr "Apagat" #: src/view/com/util/ErrorBoundary.tsx:49 msgid "Oh no!" @@ -3135,7 +3135,7 @@ msgstr "Ostres! Alguna cosa ha fallat." #: src/screens/Onboarding/StepModeration/AdultContentEnabledPref.tsx:127 msgid "OK" -msgstr "" +msgstr "D'acord" #: src/view/com/auth/login/PasswordUpdatedForm.tsx:41 msgid "Okay" @@ -3159,7 +3159,7 @@ msgstr "Només {0} poden respondre." #: src/components/Lists.tsx:83 msgid "Oops, something went wrong!" -msgstr "" +msgstr "Ostres, alguna cosa ha anat malament!" #: src/components/Lists.tsx:157 #: src/view/screens/AppPasswords.tsx:67 @@ -3173,7 +3173,7 @@ msgstr "Obre" #: src/view/screens/Moderation.tsx:75 #~ msgid "Open content filtering settings" -#~ msgstr "" +#~ msgstr "Obre la configuració del filtre de contingut" #: src/view/com/composer/Composer.tsx:490 #: src/view/com/composer/Composer.tsx:491 @@ -3182,7 +3182,7 @@ msgstr "Obre el selector d'emojis" #: src/view/screens/ProfileFeed.tsx:299 msgid "Open feed options menu" -msgstr "" +msgstr "Obre el menú de les opcions del canal" #: src/view/screens/Settings/index.tsx:734 msgid "Open links with in-app browser" @@ -3190,11 +3190,11 @@ msgstr "Obre els enllaços al navegador de l'aplicació" #: src/screens/Moderation/index.tsx:229 msgid "Open muted words and tags settings" -msgstr "" +msgstr "Obre la configuració de les paraules i etiquetes silenciades" #: src/view/screens/Moderation.tsx:92 #~ msgid "Open muted words settings" -#~ msgstr "" +#~ msgstr "Obre la configuració de les paraules silenciades" #: src/view/com/home/HomeHeaderLayoutMobile.tsx:50 msgid "Open navigation" @@ -3202,7 +3202,7 @@ msgstr "Obre la navegació" #: src/view/com/util/forms/PostDropdownBtn.tsx:183 msgid "Open post options menu" -msgstr "" +msgstr "Obre el menú de les opcions de publicació" #: src/view/screens/Settings/index.tsx:828 #: src/view/screens/Settings/index.tsx:838 @@ -3211,7 +3211,7 @@ msgstr "Obre la pàgina d'historial" #: src/view/screens/Settings/index.tsx:816 msgid "Open system log" -msgstr "" +msgstr "Obre el registre del sistema" #: src/view/com/util/forms/DropdownButton.tsx:154 msgid "Opens {numItems} options" @@ -3219,7 +3219,7 @@ msgstr "Obre {numItems} opcions" #: src/view/screens/Log.tsx:54 msgid "Opens additional details for a debug entry" -msgstr "Obre detalls adicionals per una entrada de depuració" +msgstr "Obre detalls addicionals per una entrada de depuració" #: src/view/com/notifications/FeedItem.tsx:353 msgid "Opens an expanded list of users in this notification" @@ -3243,7 +3243,7 @@ msgstr "Obre la galeria fotogràfica del dispositiu" #: src/view/com/profile/ProfileHeader.tsx:420 #~ msgid "Opens editor for profile display name, avatar, background image, and description" -#~ msgstr "Obre l'editor del perfil per editar el nom, avatar, imatge de fons i descripció" +#~ msgstr "Obre l'editor del perfil per a editar el nom, avatar, imatge de fons i descripció" #: src/view/screens/Settings/index.tsx:669 msgid "Opens external embeds settings" @@ -3252,12 +3252,12 @@ msgstr "Obre la configuració per les incrustacions externes" #: src/view/com/auth/HomeLoggedOutCTA.tsx:56 #: src/view/com/auth/SplashScreen.tsx:70 msgid "Opens flow to create a new Bluesky account" -msgstr "" +msgstr "Obre el procés per a crear un nou compte de Bluesky" #: src/view/com/auth/HomeLoggedOutCTA.tsx:74 #: src/view/com/auth/SplashScreen.tsx:83 msgid "Opens flow to sign into your existing Bluesky account" -msgstr "" +msgstr "Obre el procés per a iniciar sessió a un compte existent de Bluesky" #: src/view/com/profile/ProfileHeader.tsx:575 #~ msgid "Opens followers list" @@ -3277,27 +3277,27 @@ msgstr "Obre la llista de codis d'invitació" #: src/view/screens/Settings/index.tsx:798 msgid "Opens modal for account deletion confirmation. Requires email code" -msgstr "" +msgstr "Obre el modal per a la confirmació de l'eliminació del compte. Requereix codi de correu electrònic" #: src/view/screens/Settings/index.tsx:774 #~ msgid "Opens modal for account deletion confirmation. Requires email code." -#~ msgstr "Obre el modal per confirmar l'eliminació del compte. Requereix un codi de correu" +#~ msgstr "Obre el modal per a confirmar l'eliminació del compte. Requereix un codi de correu" #: src/view/screens/Settings/index.tsx:756 msgid "Opens modal for changing your Bluesky password" -msgstr "" +msgstr "Obre el modal per a canviar la contrasenya de Bluesky" #: src/view/screens/Settings/index.tsx:718 msgid "Opens modal for choosing a new Bluesky handle" -msgstr "" +msgstr "Obre el modal per a triar un nou identificador de Bluesky" #: src/view/screens/Settings/index.tsx:779 msgid "Opens modal for downloading your Bluesky account data (repository)" -msgstr "" +msgstr "Obre el modal per a baixar les dades del vostre compte Bluesky (repositori)" #: src/view/screens/Settings/index.tsx:970 msgid "Opens modal for email verification" -msgstr "" +msgstr "Obre el modal per a verificar el correu" #: src/view/com/modals/ChangeHandle.tsx:281 msgid "Opens modal for using custom domain" @@ -3314,7 +3314,7 @@ msgstr "Obre el formulari de restabliment de la contrasenya" #: src/view/com/home/HomeHeaderLayout.web.tsx:63 #: src/view/screens/Feeds.tsx:356 msgid "Opens screen to edit Saved Feeds" -msgstr "Obre pantalla per editar els canals desats" +msgstr "Obre pantalla per a editar els canals desats" #: src/view/screens/Settings/index.tsx:597 msgid "Opens screen with all saved feeds" @@ -3322,7 +3322,7 @@ msgstr "Obre la pantalla amb tots els canals desats" #: src/view/screens/Settings/index.tsx:696 msgid "Opens the app password settings" -msgstr "" +msgstr "Obre la configuració de les contrasenyes d'aplicació" #: src/view/screens/Settings/index.tsx:676 #~ msgid "Opens the app password settings page" @@ -3330,7 +3330,7 @@ msgstr "" #: src/view/screens/Settings/index.tsx:554 msgid "Opens the Following feed preferences" -msgstr "" +msgstr "Obre les preferències del canal de Seguint" #: src/view/screens/Settings/index.tsx:535 #~ msgid "Opens the home feed preferences" @@ -3338,7 +3338,7 @@ msgstr "" #: src/view/com/modals/LinkWarning.tsx:76 msgid "Opens the linked website" -msgstr "" +msgstr "Obre la web enllaçada" #: src/view/screens/Settings/index.tsx:829 #: src/view/screens/Settings/index.tsx:839 @@ -3359,7 +3359,7 @@ msgstr "Opció {0} de {numItems}" #: src/components/ReportDialog/SubmitView.tsx:162 msgid "Optionally provide additional information below:" -msgstr "" +msgstr "Opcionalment, proporciona informació addicional a continuació:" #: src/view/com/modals/Threadgate.tsx:89 msgid "Or combine these options:" @@ -3367,7 +3367,7 @@ msgstr "O combina aquestes opcions:" #: src/lib/moderation/useReportOptions.ts:25 msgid "Other" -msgstr "" +msgstr "Un altre" #: src/view/com/auth/login/ChooseAccountForm.tsx:147 msgid "Other account" @@ -3402,7 +3402,7 @@ msgstr "Contrasenya" #: src/view/com/modals/ChangePassword.tsx:142 msgid "Password Changed" -msgstr "" +msgstr "Contrasenya canviada" #: src/view/com/auth/login/Login.tsx:157 msgid "Password updated" @@ -3422,11 +3422,11 @@ msgstr "Persones seguint a @{0}" #: src/view/com/lightbox/Lightbox.tsx:66 msgid "Permission to access camera roll is required." -msgstr "Cal permís per accedir al carret de la càmera." +msgstr "Cal permís per a accedir al carret de la càmera." #: src/view/com/lightbox/Lightbox.tsx:72 msgid "Permission to access camera roll was denied. Please enable it in your system settings." -msgstr "S'ha denegat el permís per accedir a la càmera. Activa'l a la configuració del teu sistema." +msgstr "S'ha denegat el permís per a accedir a la càmera. Activa'l a la configuració del teu sistema." #: src/screens/Onboarding/index.tsx:31 msgid "Pets" @@ -3447,7 +3447,7 @@ msgstr "Fixa a l'inici" #: src/view/screens/ProfileFeed.tsx:294 msgid "Pin to Home" -msgstr "" +msgstr "Fixa a l'Inici" #: src/view/screens/SavedFeeds.tsx:88 msgid "Pinned Feeds" @@ -3476,11 +3476,11 @@ msgstr "Tria la teva contrasenya." #: src/view/com/auth/create/state.ts:131 msgid "Please complete the verification captcha." -msgstr "" +msgstr "Completa el captcha de verificació." #: src/view/com/modals/ChangeEmail.tsx:67 msgid "Please confirm your email before changing it. This is a temporary requirement while email-updating tools are added, and it will soon be removed." -msgstr "Confirma el teu correu abans de canviar-lo. Aquest és un requisit temporal mentre no s'afegeixin eines per actualitzar el correu. Aviat no serà necessari," +msgstr "Confirma el teu correu abans de canviar-lo. Aquest és un requisit temporal mentre no s'afegeixin eines per a actualitzar el correu. Aviat no serà necessari." #: src/view/com/modals/AddAppPasswords.tsx:90 msgid "Please enter a name for your app password. All spaces is not allowed." @@ -3496,7 +3496,7 @@ msgstr "Introdueix un nom únic per aquesta contrasenya d'aplicació o fes servi #: src/components/dialogs/MutedWords.tsx:68 msgid "Please enter a valid word, tag, or phrase to mute" -msgstr "" +msgstr "Introdueix una paraula, una etiqueta o una frase vàlida per a silenciar" #: src/view/com/auth/create/state.ts:170 #~ msgid "Please enter the code you received by SMS." @@ -3516,7 +3516,7 @@ msgstr "Introdueix la teva contrasenya també:" #: src/components/moderation/LabelsOnMeDialog.tsx:222 msgid "Please explain why you think this label was incorrectly applied by {0}" -msgstr "" +msgstr "Explica per què creieu que aquesta etiqueta ha estat aplicada incorrectament per {0}" #: src/view/com/modals/AppealLabel.tsx:72 #: src/view/com/modals/AppealLabel.tsx:75 @@ -3544,7 +3544,7 @@ msgstr "Pornografia" #: src/lib/moderation/useGlobalLabelStrings.ts:34 msgid "Pornography" -msgstr "" +msgstr "Pornografia" #: src/view/com/composer/Composer.tsx:366 #: src/view/com/composer/Composer.tsx:374 @@ -3584,12 +3584,12 @@ msgstr "Publicació oculta" #: src/components/moderation/ModerationDetailsDialog.tsx:98 #: src/lib/moderation/useModerationCauseDescription.ts:99 msgid "Post Hidden by Muted Word" -msgstr "" +msgstr "Publicació amagada per una paraula silenciada" #: src/components/moderation/ModerationDetailsDialog.tsx:101 #: src/lib/moderation/useModerationCauseDescription.ts:108 msgid "Post Hidden by You" -msgstr "" +msgstr "Publicació amagada per tu" #: src/view/com/composer/select-language/SelectLangBtn.tsx:87 msgid "Post language" @@ -3606,7 +3606,7 @@ msgstr "Publicació no trobada" #: src/components/TagMenu/index.tsx:253 msgid "posts" -msgstr "" +msgstr "publicacions" #: src/view/screens/Profile.tsx:188 msgid "Posts" @@ -3614,7 +3614,7 @@ msgstr "Publicacions" #: src/components/dialogs/MutedWords.tsx:90 msgid "Posts can be muted based on their text, their tags, or both." -msgstr "" +msgstr "Les publicacions es poder silenciar segons el seu text, etiquetes o ambdues." #: src/view/com/posts/FeedErrorMessage.tsx:64 msgid "Posts hidden" @@ -3626,7 +3626,7 @@ msgstr "Enllaç potencialment enganyós" #: src/components/Lists.tsx:88 msgid "Press to retry" -msgstr "" +msgstr "Prem per a tornar-ho a provar" #: src/view/com/lightbox/Lightbox.web.tsx:150 msgid "Previous image" @@ -3660,7 +3660,7 @@ msgstr "Processant…" #: src/view/screens/DebugMod.tsx:888 #: src/view/screens/Profile.tsx:340 msgid "profile" -msgstr "" +msgstr "perfil" #: src/view/shell/bottom-bar/BottomBar.tsx:251 #: src/view/shell/desktop/LeftNav.tsx:419 @@ -3684,11 +3684,11 @@ msgstr "Públic" #: src/view/screens/ModerationModlists.tsx:61 msgid "Public, shareable lists of users to mute or block in bulk." -msgstr "Llistes d'usuaris per silenciar o bloquejar en massa, públiques i per compartir." +msgstr "Llistes d'usuaris per a silenciar o bloquejar en massa, públiques i per a compartir." #: src/view/screens/Lists.tsx:61 msgid "Public, shareable lists which can drive feeds." -msgstr "Llistes que poden nodrir canals, públiques i per compartir." +msgstr "Llistes que poden nodrir canals, públiques i per a compartir." #: src/view/com/composer/Composer.tsx:351 msgid "Publish post" @@ -3718,7 +3718,7 @@ msgstr "Cita la publicació" #: src/view/screens/PreferencesThreads.tsx:86 msgid "Random (aka \"Poster's Roulette\")" -msgstr "Aleatori (també conegut com \"Poster's Roulette\")" +msgstr "Aleatori (també conegut com a \"Poster's Roulette\")" #: src/view/com/modals/EditImage.tsx:236 msgid "Ratios" @@ -3726,7 +3726,7 @@ msgstr "Proporcions" #: src/view/screens/Search/Search.tsx:776 msgid "Recent Searches" -msgstr "" +msgstr "Cerques recents" #: src/view/com/auth/onboarding/RecommendedFeeds.tsx:116 msgid "Recommended Feeds" @@ -3755,11 +3755,11 @@ msgstr "Elimina el compte" #: src/view/com/util/UserAvatar.tsx:358 msgid "Remove Avatar" -msgstr "" +msgstr "Elimina l'avatar" #: src/view/com/util/UserBanner.tsx:148 msgid "Remove Banner" -msgstr "" +msgstr "Elimina el bàner" #: src/view/com/posts/FeedErrorMessage.tsx:160 msgid "Remove feed" @@ -3767,7 +3767,7 @@ msgstr "Elimina el canal" #: src/view/com/posts/FeedErrorMessage.tsx:201 msgid "Remove feed?" -msgstr "" +msgstr "Vols eliminar el canal?" #: src/view/com/feeds/FeedSourceCard.tsx:173 #: src/view/com/feeds/FeedSourceCard.tsx:233 @@ -3778,7 +3778,7 @@ msgstr "Elimina dels meus canals" #: src/view/com/feeds/FeedSourceCard.tsx:278 msgid "Remove from my feeds?" -msgstr "" +msgstr "Vols eliminar-lo dels teus canals?" #: src/view/com/composer/photos/Gallery.tsx:167 msgid "Remove image" @@ -3790,7 +3790,7 @@ msgstr "Elimina la visualització prèvia de la imatge" #: src/components/dialogs/MutedWords.tsx:330 msgid "Remove mute word from your list" -msgstr "" +msgstr "Elimina la paraula silenciada de la teva llista" #: src/view/com/modals/Repost.tsx:47 msgid "Remove repost" @@ -3798,11 +3798,11 @@ msgstr "Elimina la republicació" #: src/view/com/feeds/FeedSourceCard.tsx:175 #~ msgid "Remove this feed from my feeds?" -#~ msgstr "Vols eliminar aquest canal dels meus canals?" +#~ msgstr "Vols eliminar aquest canal dels teus canals?" #: src/view/com/posts/FeedErrorMessage.tsx:202 msgid "Remove this feed from your saved feeds" -msgstr "" +msgstr "Elimina aquest canal dels meus canals" #: src/view/com/posts/FeedErrorMessage.tsx:132 #~ msgid "Remove this feed from your saved feeds?" @@ -3819,7 +3819,7 @@ msgstr "Eliminat dels meus canals" #: src/view/screens/ProfileFeed.tsx:208 msgid "Removed from your feeds" -msgstr "" +msgstr "Eliminat dels teus canals" #: src/view/com/composer/ExternalEmbed.tsx:71 msgid "Removes default thumbnail from {0}" @@ -3873,23 +3873,23 @@ msgstr "Informa de la publicació" #: src/components/ReportDialog/SelectReportOptionView.tsx:43 msgid "Report this content" -msgstr "" +msgstr "Informa d'aquest contingut" #: src/components/ReportDialog/SelectReportOptionView.tsx:56 msgid "Report this feed" -msgstr "" +msgstr "Informa d'aquest canal" #: src/components/ReportDialog/SelectReportOptionView.tsx:53 msgid "Report this list" -msgstr "" +msgstr "Informa d'aquesta llista" #: src/components/ReportDialog/SelectReportOptionView.tsx:50 msgid "Report this post" -msgstr "" +msgstr "Informa d'aquesta publicació" #: src/components/ReportDialog/SelectReportOptionView.tsx:47 msgid "Report this user" -msgstr "" +msgstr "Informa d'aquest usuari" #: src/view/com/modals/Repost.tsx:43 #: src/view/com/modals/Repost.tsx:48 @@ -4029,12 +4029,12 @@ msgstr "Torna a la pàgina anterior" #: src/view/screens/NotFound.tsx:59 msgid "Returns to home page" -msgstr "" +msgstr "Torna a la pàgina d'inici" #: src/view/screens/NotFound.tsx:58 #: src/view/screens/ProfileFeed.tsx:112 msgid "Returns to previous page" -msgstr "" +msgstr "Torna a la pàgina anterior" #: src/view/shell/desktop/RightNav.tsx:55 #~ msgid "SANDBOX. Posts and accounts are not permanent." @@ -4059,7 +4059,7 @@ msgstr "Desa el text alternatiu" #: src/components/dialogs/BirthDateSettings.tsx:119 msgid "Save birthday" -msgstr "" +msgstr "Desa la data de naixement" #: src/view/com/modals/EditProfile.tsx:232 msgid "Save Changes" @@ -4076,7 +4076,7 @@ msgstr "Desa la imatge retallada" #: src/view/screens/ProfileFeed.tsx:335 #: src/view/screens/ProfileFeed.tsx:341 msgid "Save to my feeds" -msgstr "" +msgstr "Desa-ho als meus canals" #: src/view/screens/SavedFeeds.tsx:122 msgid "Saved Feeds" @@ -4084,11 +4084,11 @@ msgstr "Canals desats" #: src/view/com/lightbox/Lightbox.tsx:81 msgid "Saved to your camera roll." -msgstr "" +msgstr "S'ha desat a la teva galeria d'imatges." #: src/view/screens/ProfileFeed.tsx:212 msgid "Saved to your feeds" -msgstr "" +msgstr "S'ha desat als teus canals." #: src/view/com/modals/EditProfile.tsx:225 msgid "Saves any changes to your profile" @@ -4100,7 +4100,7 @@ msgstr "Desa el canvi d'identificador a {handle}" #: src/view/com/modals/crop-image/CropImage.web.tsx:145 msgid "Saves image crop settings" -msgstr "" +msgstr "Desa la configuració de retall d'imatges" #: src/screens/Onboarding/index.tsx:36 msgid "Science" @@ -4134,19 +4134,19 @@ msgstr "Cerca per \"{query}\"" #: src/components/TagMenu/index.tsx:145 msgid "Search for all posts by @{authorHandle} with tag {displayTag}" -msgstr "" +msgstr "Cerca totes les publicacions de @{authorHandle} amb l'etiqueta {displayTag}" #: src/components/TagMenu/index.tsx:145 #~ msgid "Search for all posts by @{authorHandle} with tag {tag}" -#~ msgstr "" +#~ msgstr "Cerca totes les publicacions de @{authorHandle} amb l'etiqueta {tag}" #: src/components/TagMenu/index.tsx:94 msgid "Search for all posts with tag {displayTag}" -msgstr "" +msgstr "Cerca totes les publicacions amb l'etiqueta {displayTag}" #: src/components/TagMenu/index.tsx:90 #~ msgid "Search for all posts with tag {tag}" -#~ msgstr "" +#~ msgstr "Cerca totes les publicacions amb l'etiqueta {tag}" #: src/view/com/auth/LoggedOut.tsx:104 #: src/view/com/auth/LoggedOut.tsx:105 @@ -4160,27 +4160,27 @@ msgstr "Es requereix un pas de seguretat" #: src/components/TagMenu/index.web.tsx:66 msgid "See {truncatedTag} posts" -msgstr "" +msgstr "Mostra les publicacions amb {truncatedTag}" #: src/components/TagMenu/index.web.tsx:83 msgid "See {truncatedTag} posts by user" -msgstr "" +msgstr "Mostra les publicacions amb {truncatedTag} per usuari" #: src/components/TagMenu/index.tsx:128 msgid "See <0>{displayTag} posts" -msgstr "" +msgstr "Mostra les publicacions amb <0>{displayTag}" #: src/components/TagMenu/index.tsx:187 msgid "See <0>{displayTag} posts by this user" -msgstr "" +msgstr "Mostra les publicacions amb <0>{displayTag} d'aquest usuari" #: src/components/TagMenu/index.tsx:128 #~ msgid "See <0>{tag} posts" -#~ msgstr "" +#~ msgstr "Mostra les publicacions amb <0>{tag}" #: src/components/TagMenu/index.tsx:189 #~ msgid "See <0>{tag} posts by this user" -#~ msgstr "" +#~ msgstr "Mostra les publicacions amb <0>{tag} d'aquest usuari" #: src/view/screens/SavedFeeds.tsx:163 msgid "See this guide" @@ -4204,11 +4204,11 @@ msgstr "Selecciona d'un compte existent" #: src/view/screens/LanguageSettings.tsx:299 msgid "Select languages" -msgstr "" +msgstr "Selecciona els idiomes" #: src/components/ReportDialog/SelectLabelerView.tsx:32 msgid "Select moderator" -msgstr "" +msgstr "Selecciona el moderador" #: src/view/com/util/Selector.tsx:107 msgid "Select option {i} of {numItems}" @@ -4221,11 +4221,11 @@ msgstr "Selecciona el servei" #: src/screens/Onboarding/StepSuggestedAccounts/index.tsx:52 msgid "Select some accounts below to follow" -msgstr "Selecciona alguns d'aquests comptes per seguir-los" +msgstr "Selecciona alguns d'aquests comptes per a seguir-los" #: src/components/ReportDialog/SubmitView.tsx:135 msgid "Select the moderation service(s) to report to" -msgstr "" +msgstr "Selecciona els serveis de moderació als quals voleu informar" #: src/view/com/auth/server-input/index.tsx:82 msgid "Select the service that hosts your data." @@ -4233,7 +4233,7 @@ msgstr "Selecciona el servei que allotja les teves dades." #: src/screens/Onboarding/StepTopicalFeeds.tsx:96 msgid "Select topical feeds to follow from the list below" -msgstr "Selecciona els canals d'actualitat per seguir d'aquesta llista" +msgstr "Selecciona els canals d'actualitat per a seguir d'aquesta llista" #: src/screens/Onboarding/StepModeration/index.tsx:62 msgid "Select what you want to see (or not see), and we’ll handle the rest." @@ -4249,11 +4249,11 @@ msgstr "Selecciona quins idiomes vols que incloguin els canals a què estàs sub #: src/view/screens/LanguageSettings.tsx:98 msgid "Select your app language for the default text to display in the app." -msgstr "" +msgstr "Selecciona l'idioma de l'aplicació perquè el text predeterminat es mostri a l'aplicació." #: src/screens/Onboarding/StepInterests/index.tsx:196 msgid "Select your interests from the options below" -msgstr "Selecciona els teus interesos d'entre aquestes opcions" +msgstr "Selecciona els teus interessos d'entre aquestes opcions" #: src/view/com/auth/create/Step2.tsx:155 #~ msgid "Select your phone's country" @@ -4297,7 +4297,7 @@ msgstr "Envia comentari" #: src/components/ReportDialog/SubmitView.tsx:214 #: src/components/ReportDialog/SubmitView.tsx:218 msgid "Send report" -msgstr "" +msgstr "Envia informe" #: src/view/com/modals/report/SendReportButton.tsx:45 #~ msgid "Send Report" @@ -4305,7 +4305,7 @@ msgstr "" #: src/components/ReportDialog/SelectLabelerView.tsx:46 msgid "Send report to {0}" -msgstr "" +msgstr "Envia informe a {0}" #: src/view/com/modals/DeleteAccount.tsx:133 msgid "Sends email with confirmation code for account deletion" @@ -4327,7 +4327,7 @@ msgstr "Adreça del servidor" #: src/screens/Moderation/index.tsx:306 msgid "Set birthdate" -msgstr "" +msgstr "Estableix la data de naixement" #: src/view/screens/Settings/index.tsx:488 #~ msgid "Set color theme to dark" @@ -4359,27 +4359,27 @@ msgstr "Estableix una contrasenya" #: src/view/screens/PreferencesFollowingFeed.tsx:225 msgid "Set this setting to \"No\" to hide all quote posts from your feed. Reposts will still be visible." -msgstr "Posa \"No\" a aquesta opció per amagar totes les publicacions citades del teu canal. Les republicacions encara seran visibles." +msgstr "Posa \"No\" a aquesta opció per a amagar totes les publicacions citades del teu canal. Les republicacions encara seran visibles." #: src/view/screens/PreferencesFollowingFeed.tsx:122 msgid "Set this setting to \"No\" to hide all replies from your feed." -msgstr "Posa \"No\" a aquesta opció per amagar totes les respostes del teu canal." +msgstr "Posa \"No\" a aquesta opció per a amagar totes les respostes del teu canal." #: src/view/screens/PreferencesFollowingFeed.tsx:191 msgid "Set this setting to \"No\" to hide all reposts from your feed." -msgstr "Posa \"No\" a aquesta opció per amagar totes les republicacions del teu canal." +msgstr "Posa \"No\" a aquesta opció per a amagar totes les republicacions del teu canal." #: src/view/screens/PreferencesThreads.tsx:122 msgid "Set this setting to \"Yes\" to show replies in a threaded view. This is an experimental feature." -msgstr "Posa \"Sí\" a aquesta opció per mostrar les respostes en vista de fil de debat. Aquesta és una opció experimental." +msgstr "Posa \"Sí\" a aquesta opció per a mostrar les respostes en vista de fil de debat. Aquesta és una opció experimental." #: src/view/screens/PreferencesHomeFeed.tsx:261 #~ msgid "Set this setting to \"Yes\" to show samples of your saved feeds in your following feed. This is an experimental feature." -#~ msgstr "Posa \"Sí\" a aquesta opció per mostrar algunes publicacions dels teus canals en el teu canal de seguits. Aquesta és una opció experimental." +#~ msgstr "Posa \"Sí\" a aquesta opció per a mostrar algunes publicacions dels teus canals en el teu canal de seguits. Aquesta és una opció experimental." #: src/view/screens/PreferencesFollowingFeed.tsx:261 msgid "Set this setting to \"Yes\" to show samples of your saved feeds in your Following feed. This is an experimental feature." -msgstr "" +msgstr "Estableix aquesta configuració a \"Sí\" per a mostrar mostres dels teus canals desats al teu canal Seguint. Aquesta és una característica experimental." #: src/screens/Onboarding/Layout.tsx:50 msgid "Set up your account" @@ -4391,43 +4391,43 @@ msgstr "Estableix un nom d'usuari de Bluesky" #: src/view/screens/Settings/index.tsx:507 msgid "Sets color theme to dark" -msgstr "" +msgstr "Estableix el tema a fosc" #: src/view/screens/Settings/index.tsx:500 msgid "Sets color theme to light" -msgstr "" +msgstr "Estableix el tema a clar" #: src/view/screens/Settings/index.tsx:494 msgid "Sets color theme to system setting" -msgstr "" +msgstr "Estableix el tema a la configuració del sistema" #: src/view/screens/Settings/index.tsx:533 msgid "Sets dark theme to the dark theme" -msgstr "" +msgstr "Estableix el tema fosc al tema fosc" #: src/view/screens/Settings/index.tsx:526 msgid "Sets dark theme to the dim theme" -msgstr "" +msgstr "Estableix el tema fosc al tema atenuat" #: src/view/com/auth/login/ForgotPasswordForm.tsx:157 msgid "Sets email for password reset" -msgstr "Estableix un correu per restablir la contrasenya" +msgstr "Estableix un correu per a restablir la contrasenya" #: src/view/com/auth/login/ForgotPasswordForm.tsx:122 msgid "Sets hosting provider for password reset" -msgstr "Estableix un proveïdor d'allotjament per restablir la contrasenya" +msgstr "Estableix un proveïdor d'allotjament per a restablir la contrasenya" #: src/view/com/modals/crop-image/CropImage.web.tsx:123 msgid "Sets image aspect ratio to square" -msgstr "" +msgstr "Estableix la relació d'aspecte de la imatge com a quadrat" #: src/view/com/modals/crop-image/CropImage.web.tsx:113 msgid "Sets image aspect ratio to tall" -msgstr "" +msgstr "Estableix la relació d'aspecte de la imatge com a alta" #: src/view/com/modals/crop-image/CropImage.web.tsx:103 msgid "Sets image aspect ratio to wide" -msgstr "" +msgstr "Estableix la relació d'aspecte de la imatge com a ampla" #: src/view/com/auth/create/Step1.tsx:97 #: src/view/com/auth/login/LoginForm.tsx:154 @@ -4448,7 +4448,7 @@ msgstr "Activitat sexual o nu eròtic." #: src/lib/moderation/useGlobalLabelStrings.ts:38 msgid "Sexually Suggestive" -msgstr "" +msgstr "Suggerent sexualment" #: src/view/com/lightbox/Lightbox.tsx:141 msgctxt "action" @@ -4467,7 +4467,7 @@ msgstr "Comparteix" #: src/view/com/profile/ProfileMenu.tsx:373 #: src/view/com/util/forms/PostDropdownBtn.tsx:347 msgid "Share anyway" -msgstr "" +msgstr "Comparteix de totes maneres" #: src/view/screens/ProfileFeed.tsx:361 #: src/view/screens/ProfileFeed.tsx:363 @@ -4494,11 +4494,11 @@ msgstr "Mostra igualment" #: src/lib/moderation/useLabelBehaviorDescription.ts:27 #: src/lib/moderation/useLabelBehaviorDescription.ts:63 msgid "Show badge" -msgstr "" +msgstr "Mostra la insígnia" #: src/lib/moderation/useLabelBehaviorDescription.ts:61 msgid "Show badge and filter from feeds" -msgstr "" +msgstr "Mostra la insígnia i filtra-ho dels canals" #: src/view/com/modals/EmbedConsent.tsx:87 msgid "Show embeds from {0}" @@ -4548,7 +4548,7 @@ msgstr "Mostra les respostes a Seguint" #: src/screens/Onboarding/StepFollowingFeed.tsx:70 msgid "Show replies in Following feed" -msgstr "Mostrea les respostes al canal Seguint" +msgstr "Mostra les respostes al canal Seguint" #: src/view/screens/PreferencesFollowingFeed.tsx:70 msgid "Show replies with at least {value} {0}" @@ -4573,11 +4573,11 @@ msgstr "Mostra usuaris" #: src/lib/moderation/useLabelBehaviorDescription.ts:58 msgid "Show warning" -msgstr "" +msgstr "Mostra l'advertiment" #: src/lib/moderation/useLabelBehaviorDescription.ts:56 msgid "Show warning and filter from feeds" -msgstr "" +msgstr "Mostra l'advertiment i filtra-ho del canals" #: src/view/com/profile/ProfileHeader.tsx:462 #~ msgid "Shows a list of users similar to this user." @@ -4642,7 +4642,7 @@ msgstr "Registra't" #: src/view/shell/NavSignupCard.tsx:42 msgid "Sign up or sign in to join the conversation" -msgstr "Registra't o inicia sessió per unir-te a la conversa" +msgstr "Registra't o inicia sessió per a unir-te a la conversa" #: src/components/moderation/ScreenHider.tsx:98 #: src/lib/moderation/useGlobalLabelStrings.ts:28 @@ -4655,7 +4655,7 @@ msgstr "S'ha iniciat sessió com a" #: src/view/com/auth/login/ChooseAccountForm.tsx:112 msgid "Signed in as @{0}" -msgstr "Sha iniciat sessió com a @{0}" +msgstr "S'ha iniciat sessió com a @{0}" #: src/view/com/modals/SwitchAccount.tsx:70 msgid "Signs {0} out of Bluesky" @@ -4687,11 +4687,11 @@ msgstr "Desenvolupament de programari" #: src/screens/Moderation/index.tsx:116 #: src/screens/Profile/Sections/Labels.tsx:77 msgid "Something went wrong, please try again." -msgstr "" +msgstr "Alguna cosa ha fallat, torna-ho a provar." #: src/components/Lists.tsx:203 #~ msgid "Something went wrong!" -#~ msgstr "" +#~ msgstr "Alguna cosa ha fallat." #: src/view/com/modals/Waitlist.tsx:51 #~ msgid "Something went wrong. Check your email and try again." @@ -4699,7 +4699,7 @@ msgstr "" #: src/App.native.tsx:71 msgid "Sorry! Your session expired. Please log in again." -msgstr "La teva sessió ha caducat. Torna a inciar-la." +msgstr "La teva sessió ha caducat. Torna a iniciar-la." #: src/view/screens/PreferencesThreads.tsx:69 msgid "Sort Replies" @@ -4711,15 +4711,15 @@ msgstr "Ordena les respostes a la mateixa publicació per:" #: src/components/moderation/LabelsOnMeDialog.tsx:147 msgid "Source:" -msgstr "" +msgstr "Font:" #: src/lib/moderation/useReportOptions.ts:65 msgid "Spam" -msgstr "" +msgstr "Brossa" #: src/lib/moderation/useReportOptions.ts:53 msgid "Spam; excessive mentions or replies" -msgstr "" +msgstr "Brossa; excessives mencions o respostes" #: src/screens/Onboarding/index.tsx:30 msgid "Sports" @@ -4761,11 +4761,11 @@ msgstr "Subscriure's" #: src/screens/Profile/Sections/Labels.tsx:181 msgid "Subscribe to @{0} to use these labels:" -msgstr "" +msgstr "Subscriu-te a @{0} per a utilitzar aquestes etiquetes:" #: src/screens/Profile/Header/ProfileHeaderLabeler.tsx:222 msgid "Subscribe to Labeler" -msgstr "" +msgstr "Subscriu-te a l'Etiquetador" #: src/screens/Onboarding/StepAlgoFeeds/FeedCard.tsx:173 #: src/screens/Onboarding/StepAlgoFeeds/FeedCard.tsx:308 @@ -4774,7 +4774,7 @@ msgstr "Subscriu-te al canal {0}" #: src/screens/Profile/Header/ProfileHeaderLabeler.tsx:185 msgid "Subscribe to this labeler" -msgstr "" +msgstr "Subscriu-te a aquest etiquetador" #: src/view/screens/ProfileList.tsx:586 msgid "Subscribe to this list" @@ -4782,7 +4782,7 @@ msgstr "Subscriure's a la llista" #: src/view/screens/Search/Search.tsx:375 msgid "Suggested Follows" -msgstr "Usuaris suggerits per seguir" +msgstr "Usuaris suggerits per a seguir" #: src/view/com/profile/ProfileHeaderSuggestedFollows.tsx:65 msgid "Suggested for you" @@ -4800,7 +4800,7 @@ msgstr "Suport" #: src/view/com/modals/ProfilePreview.tsx:110 #~ msgid "Swipe up to see more" -#~ msgstr "Llisca cap amunt per veure'n més" +#~ msgstr "Llisca cap amunt per a veure'n més" #: src/view/com/modals/SwitchAccount.tsx:123 msgid "Switch Account" @@ -4826,15 +4826,15 @@ msgstr "Registres del sistema" #: src/components/dialogs/MutedWords.tsx:324 msgid "tag" -msgstr "" +msgstr "etiqueta" #: src/components/TagMenu/index.tsx:78 msgid "Tag menu: {displayTag}" -msgstr "" +msgstr "Menú d'etiquetes: {displayTag}" #: src/components/TagMenu/index.tsx:74 #~ msgid "Tag menu: {tag}" -#~ msgstr "" +#~ msgstr "Menú d'etiquetes: {displayTag}" #: src/view/com/modals/crop-image/CropImage.web.tsx:112 msgid "Tall" @@ -4842,7 +4842,7 @@ msgstr "Alt" #: src/view/com/util/images/AutoSizedImage.tsx:70 msgid "Tap to view fully" -msgstr "Toca per veure-ho completament" +msgstr "Toca per a veure-ho completament" #: src/screens/Onboarding/index.tsx:39 msgid "Tech" @@ -4864,11 +4864,11 @@ msgstr "Condicions del servei" #: src/lib/moderation/useReportOptions.ts:79 #: src/lib/moderation/useReportOptions.ts:87 msgid "Terms used violate community standards" -msgstr "" +msgstr "Els termes utilitzats infringeixen els estàndards de la comunitat" #: src/components/dialogs/MutedWords.tsx:324 msgid "text" -msgstr "" +msgstr "text" #: src/components/moderation/LabelsOnMeDialog.tsx:220 msgid "Text input field" @@ -4876,15 +4876,15 @@ msgstr "Camp d'introducció de text" #: src/components/ReportDialog/SubmitView.tsx:78 msgid "Thank you. Your report has been sent." -msgstr "" +msgstr "Gràcies. El teu informe s'ha enviat." #: src/view/com/modals/ChangeHandle.tsx:466 msgid "That contains the following:" -msgstr "" +msgstr "Això conté els següents:" #: src/view/com/auth/create/CreateAccount.tsx:94 msgid "That handle is already taken." -msgstr "" +msgstr "Aquest identificador ja està agafat." #: src/screens/Profile/Header/ProfileHeaderStandard.tsx:274 #: src/view/com/profile/ProfileMenu.tsx:349 @@ -4893,7 +4893,7 @@ msgstr "El compte podrà interactuar amb tu després del desbloqueig." #: src/components/moderation/ModerationDetailsDialog.tsx:128 msgid "the author" -msgstr "" +msgstr "l'autor" #: src/view/screens/CommunityGuidelines.tsx:36 msgid "The Community Guidelines have been moved to <0/>" @@ -4905,11 +4905,11 @@ msgstr "La política de drets d'autoria ha estat traslladada a <0/>" #: src/components/moderation/LabelsOnMeDialog.tsx:49 msgid "The following labels were applied to your account." -msgstr "" +msgstr "Les següents etiquetes s'han aplicat al teu compte." #: src/components/moderation/LabelsOnMeDialog.tsx:50 msgid "The following labels were applied to your content." -msgstr "" +msgstr "Les següents etiquetes s'han aplicat als teus continguts." #: src/screens/Onboarding/Layout.tsx:60 msgid "The following steps will help customize your Bluesky experience." @@ -4926,32 +4926,32 @@ msgstr "La política de privacitat ha estat traslladada a <0/>" #: src/view/screens/Support.tsx:36 msgid "The support form has been moved. If you need help, please <0/> or visit {HELP_DESK_URL} to get in touch with us." -msgstr "El formulari de suport ha estat traslladat. Si necessites ajuda, <0/> o visita {HELP_DESK_URL} per contactar amb nosaltres." +msgstr "El formulari de suport ha estat traslladat. Si necessites ajuda, <0/> o visita {HELP_DESK_URL} per a contactar amb nosaltres." #: src/view/screens/Support.tsx:36 #~ msgid "The support form has been moved. If you need help, please<0/> or visit {HELP_DESK_URL} to get in touch with us." -#~ msgstr "El formulari de suport ha estat traslladat. Si necessites ajuda, <0/> o visita {HELP_DESK_URL} per contactar amb nosaltres." +#~ msgstr "El formulari de suport ha estat traslladat. Si necessites ajuda, <0/> o visita {HELP_DESK_URL} per a contactar amb nosaltres." #: src/view/screens/TermsOfService.tsx:33 msgid "The Terms of Service have been moved to" -msgstr "Les condicions del servei han estat traslladades a " +msgstr "Les condicions del servei han estat traslladades a" #: src/screens/Onboarding/StepAlgoFeeds/index.tsx:150 msgid "There are many feeds to try:" -msgstr "Hi ha molts canals per provar:" +msgstr "Hi ha molts canals per a provar:" #: src/screens/Profile/Header/ProfileHeaderLabeler.tsx:113 #: src/view/screens/ProfileFeed.tsx:543 msgid "There was an an issue contacting the server, please check your internet connection and try again." -msgstr "Hi ha hagut un problema per contactar amb el servidor, comprova la teva connexió a internet i torna-ho a provar" +msgstr "Hi ha hagut un problema per a contactar amb el servidor, comprova la teva connexió a internet i torna-ho a provar." #: src/view/com/posts/FeedErrorMessage.tsx:138 msgid "There was an an issue removing this feed. Please check your internet connection and try again." -msgstr "Hi ha hagut un problema per eliminar aquest canal, comprova la teva connexió a internet i torna-ho a provar" +msgstr "Hi ha hagut un problema per a eliminar aquest canal, comprova la teva connexió a internet i torna-ho a provar." #: src/view/screens/ProfileFeed.tsx:217 msgid "There was an an issue updating your feeds, please check your internet connection and try again." -msgstr "Hi ha hagut un problema per actualitzar els teus canals, comprova la teva connexió a internet i torna-ho a provar" +msgstr "Hi ha hagut un problema per a actualitzar els teus canals, comprova la teva connexió a internet i torna-ho a provar." #: src/view/screens/ProfileFeed.tsx:244 #: src/view/screens/ProfileList.tsx:275 @@ -4959,35 +4959,35 @@ msgstr "Hi ha hagut un problema per actualitzar els teus canals, comprova la tev #: src/view/screens/SavedFeeds.tsx:231 #: src/view/screens/SavedFeeds.tsx:252 msgid "There was an issue contacting the server" -msgstr "Hi ha hagut un problema per contactar amb el servidor" +msgstr "Hi ha hagut un problema per a contactar amb el servidor" #: src/view/com/auth/onboarding/RecommendedFeedsItem.tsx:57 #: src/view/com/auth/onboarding/RecommendedFeedsItem.tsx:66 #: src/view/com/feeds/FeedSourceCard.tsx:110 #: src/view/com/feeds/FeedSourceCard.tsx:123 msgid "There was an issue contacting your server" -msgstr "Hi ha hagut un problema per contactar amb el teu servidor" +msgstr "Hi ha hagut un problema per a contactar amb el teu servidor" #: src/view/com/notifications/Feed.tsx:117 msgid "There was an issue fetching notifications. Tap here to try again." -msgstr "Hi ha hagut un problema en obtenir les notificacions. Toca aquí per tornar-ho a provar." +msgstr "Hi ha hagut un problema en obtenir les notificacions. Toca aquí per a tornar-ho a provar." #: src/view/com/posts/Feed.tsx:283 msgid "There was an issue fetching posts. Tap here to try again." -msgstr "Hi ha hagut un problema en obtenir les notificacions. Toca aquí per tornar-ho a provar." +msgstr "Hi ha hagut un problema en obtenir les notificacions. Toca aquí per a tornar-ho a provar." #: src/view/com/lists/ListMembers.tsx:172 msgid "There was an issue fetching the list. Tap here to try again." -msgstr "Hi ha hagut un problema en obtenir la llista. Toca aquí per tornar-ho a provar." +msgstr "Hi ha hagut un problema en obtenir la llista. Toca aquí per a tornar-ho a provar." #: src/view/com/feeds/ProfileFeedgens.tsx:148 #: src/view/com/lists/ProfileLists.tsx:155 msgid "There was an issue fetching your lists. Tap here to try again." -msgstr "Hi ha hagut un problema en obtenir les teves llistes. Toca aquí per tornar-ho a provar." +msgstr "Hi ha hagut un problema en obtenir les teves llistes. Toca aquí per a tornar-ho a provar." #: src/components/ReportDialog/SubmitView.tsx:83 msgid "There was an issue sending your report. Please check your internet connection." -msgstr "" +msgstr "S'ha produït un problema en enviar el teu informe. Comprova la teva connexió a Internet." #: src/screens/Onboarding/StepModeration/AdultContentEnabledPref.tsx:65 msgid "There was an issue syncing your preferences with the server" @@ -5043,19 +5043,19 @@ msgstr "Aquesta {screenDescription} ha estat etiquetada:" #: src/components/moderation/ScreenHider.tsx:112 msgid "This account has requested that users sign in to view their profile." -msgstr "Aquest compte ha sol·licitat que els usuaris estiguin registrats per veure el seu perfil." +msgstr "Aquest compte ha sol·licitat que els usuaris estiguin registrats per a veure el seu perfil." #: src/components/moderation/LabelsOnMeDialog.tsx:205 msgid "This appeal will be sent to <0>{0}." -msgstr "" +msgstr "Aquesta apel·lació s'enviarà a <0>{0}." #: src/lib/moderation/useGlobalLabelStrings.ts:19 msgid "This content has been hidden by the moderators." -msgstr "" +msgstr "Aquest contingut ha estat amagat pels moderadors." #: src/lib/moderation/useGlobalLabelStrings.ts:24 msgid "This content has received a general warning from moderators." -msgstr "" +msgstr "Aquest contingut ha rebut una advertència general dels moderadors." #: src/view/com/modals/EmbedConsent.tsx:68 msgid "This content is hosted by {0}. Do you want to enable external media?" @@ -5076,7 +5076,7 @@ msgstr "Aquest contingut no es pot veure sense un compte de Bluesky." #: src/view/screens/Settings/ExportCarDialog.tsx:75 msgid "This feature is in beta. You can read more about repository exports in <0>this blogpost." -msgstr "" +msgstr "Aquesta funció està en versió beta. Podeu obtenir més informació sobre les exportacions de repositoris en <0>aquesta entrada de bloc." #: src/view/com/posts/FeedErrorMessage.tsx:114 msgid "This feed is currently receiving high traffic and is temporarily unavailable. Please try again later." @@ -5090,7 +5090,7 @@ msgstr "Aquest canal està buit!" #: src/view/com/posts/CustomFeedEmptyState.tsx:37 msgid "This feed is empty! You may need to follow more users or tune your language settings." -msgstr "Aquest canal està buit! Necessites seguir més usuaris o modificar la teva configuració d'idiomes" +msgstr "Aquest canal està buit! Necessites seguir més usuaris o modificar la teva configuració d'idiomes." #: src/components/dialogs/BirthDateSettings.tsx:41 msgid "This information is not shared with other users." @@ -5106,11 +5106,11 @@ msgstr "Això és important si mai necessites canviar el teu correu o restablir #: src/components/moderation/ModerationDetailsDialog.tsx:125 msgid "This label was applied by {0}." -msgstr "" +msgstr "Aquesta etiqueta l'ha aplicat {0}." #: src/screens/Profile/Sections/Labels.tsx:168 msgid "This labeler hasn't declared what labels it publishes, and may not be active." -msgstr "" +msgstr "Aquest etiquetador no ha declarat quines etiquetes publica i pot ser que no estigui actiu." #: src/view/com/modals/LinkWarning.tsx:58 msgid "This link is taking you to the following website:" @@ -5122,7 +5122,7 @@ msgstr "Aquesta llista està buida!" #: src/screens/Profile/ErrorState.tsx:40 msgid "This moderation service is unavailable. See below for more details. If this issue persists, contact us." -msgstr "" +msgstr "Aquest servei de moderació no està disponible. Mira a continuació per obtenir més detalls. Si aquest problema persisteix, posa't en contacte amb nosaltres." #: src/view/com/modals/AddAppPasswords.tsx:106 msgid "This name is already in use" @@ -5134,27 +5134,27 @@ msgstr "Aquesta publicació ha estat esborrada." #: src/view/com/util/forms/PostDropdownBtn.tsx:344 msgid "This post is only visible to logged-in users. It won't be visible to people who aren't logged in." -msgstr "" +msgstr "Aquesta publicació només és visible per als usuaris que han iniciat sessió. No serà visible per a les persones que no hagin iniciat sessió." #: src/view/com/util/forms/PostDropdownBtn.tsx:326 msgid "This post will be hidden from feeds." -msgstr "" +msgstr "Aqeusta publicació no es mostrarà als canals." #: src/view/com/profile/ProfileMenu.tsx:370 msgid "This profile is only visible to logged-in users. It won't be visible to people who aren't logged in." -msgstr "" +msgstr "Aquest perfil només és visible per als usuaris que han iniciat sessió. No serà visible per a les persones que no hagin iniciat sessió." #: src/view/com/auth/create/Policies.tsx:46 msgid "This service has not provided terms of service or a privacy policy." -msgstr "" +msgstr "Aquest servei no ha proporcionat termes de servei ni una política de privadesa." #: src/view/com/modals/ChangeHandle.tsx:446 msgid "This should create a domain record at:" -msgstr "" +msgstr "Això hauria de crear un registre de domini a:" #: src/view/com/profile/ProfileFollowers.tsx:95 msgid "This user doesn't have any followers." -msgstr "" +msgstr "Aquest usuari no té cap seguidor." #: src/components/moderation/ModerationDetailsDialog.tsx:73 #: src/lib/moderation/useModerationCauseDescription.ts:68 @@ -5163,7 +5163,7 @@ msgstr "Aquest usuari t'ha bloquejat. No pots veure les seves publicacions." #: src/lib/moderation/useGlobalLabelStrings.ts:30 msgid "This user has requested that their content only be shown to signed-in users." -msgstr "" +msgstr "Aquest usuari ha sol·licitat que el seu contingut només es mostri als usuaris que hagin iniciat la sessió." #: src/view/com/modals/ModerationDetails.tsx:42 #~ msgid "This user is included in the <0/> list which you have blocked." @@ -5175,11 +5175,11 @@ msgstr "" #: src/components/moderation/ModerationDetailsDialog.tsx:56 msgid "This user is included in the <0>{0} list which you have blocked." -msgstr "" +msgstr "Aquest usuari està inclòs a la llista <0>{0} que has bloquejat." #: src/components/moderation/ModerationDetailsDialog.tsx:85 msgid "This user is included in the <0>{0} list which you have muted." -msgstr "" +msgstr "Aquest usuari està inclòs a la llista <0>{0} que has silenciat." #: src/view/com/modals/ModerationDetails.tsx:74 #~ msgid "This user is included the <0/> list which you have muted." @@ -5187,7 +5187,7 @@ msgstr "" #: src/view/com/profile/ProfileFollows.tsx:94 msgid "This user isn't following anyone." -msgstr "" +msgstr "Aquest usuari no segueix a ningú." #: src/view/com/modals/SelfLabel.tsx:137 msgid "This warning is only available for posts with media attached." @@ -5195,7 +5195,7 @@ msgstr "Aquesta advertència només està disponible per publicacions amb contin #: src/components/dialogs/MutedWords.tsx:284 msgid "This will delete {0} from your muted words. You can always add it back later." -msgstr "" +msgstr "Això suprimirà {0} de les teves paraules silenciades. Sempre la pots tornar a afegir més tard." #: src/view/com/util/forms/PostDropdownBtn.tsx:282 #~ msgid "This will hide this post from your feeds." @@ -5203,7 +5203,7 @@ msgstr "" #: src/view/screens/Settings/index.tsx:574 msgid "Thread preferences" -msgstr "" +msgstr "Preferències dels fils de debat" #: src/view/screens/PreferencesThreads.tsx:53 #: src/view/screens/Settings/index.tsx:584 @@ -5220,11 +5220,11 @@ msgstr "Preferències dels fils de debat" #: src/components/ReportDialog/SelectLabelerView.tsx:35 msgid "To whom would you like to send this report?" -msgstr "" +msgstr "A qui vols enviar aquest informe?" #: src/components/dialogs/MutedWords.tsx:113 msgid "Toggle between muted word options." -msgstr "" +msgstr "Commuta entre les opcions de paraules silenciades." #: src/view/com/util/forms/DropdownButton.tsx:246 msgid "Toggle dropdown" @@ -5232,7 +5232,7 @@ msgstr "Commuta el menú desplegable" #: src/screens/Moderation/index.tsx:334 msgid "Toggle to enable or disable adult content" -msgstr "" +msgstr "Communta per a habilitar o deshabilitar el contingut per adults" #: src/view/com/modals/EditImage.tsx:271 msgid "Transformations" @@ -5256,7 +5256,7 @@ msgstr "Torna-ho a provar" #: src/view/com/modals/ChangeHandle.tsx:429 msgid "Type:" -msgstr "" +msgstr "Tipus:" #: src/view/screens/ProfileList.tsx:478 msgid "Un-block list" @@ -5294,7 +5294,7 @@ msgstr "Desbloqueja el compte" #: src/screens/Profile/Header/ProfileHeaderStandard.tsx:272 #: src/view/com/profile/ProfileMenu.tsx:343 msgid "Unblock Account?" -msgstr "" +msgstr "Vols desbloquejar el compte?" #: src/view/com/modals/Repost.tsx:42 #: src/view/com/modals/Repost.tsx:55 @@ -5306,7 +5306,7 @@ msgstr "Desfés la republicació" #: src/view/com/auth/onboarding/RecommendedFollowsItem.tsx:141 #: src/view/com/profile/ProfileHeaderSuggestedFollows.tsx:246 msgid "Unfollow" -msgstr "" +msgstr "Deixa de seguir" #: src/view/com/profile/FollowButton.tsx:60 msgctxt "action" @@ -5320,11 +5320,11 @@ msgstr "Deixa de seguir a {0}" #: src/view/com/profile/ProfileMenu.tsx:241 #: src/view/com/profile/ProfileMenu.tsx:251 msgid "Unfollow Account" -msgstr "" +msgstr "Deixa de seguir el compte" #: src/view/com/auth/create/state.ts:262 msgid "Unfortunately, you do not meet the requirements to create an account." -msgstr "No compleixes les condicions per crear un compte." +msgstr "No compleixes les condicions per a crear un compte." #: src/view/com/util/post-ctrls/PostCtrls.tsx:185 msgid "Unlike" @@ -5332,7 +5332,7 @@ msgstr "Desfés el m'agrada" #: src/view/screens/ProfileFeed.tsx:572 msgid "Unlike this feed" -msgstr "" +msgstr "Desfés el m'agrada a aquest canal" #: src/components/TagMenu/index.tsx:249 #: src/view/screens/ProfileList.tsx:579 @@ -5341,7 +5341,7 @@ msgstr "Deixa de silenciar" #: src/components/TagMenu/index.web.tsx:104 msgid "Unmute {truncatedTag}" -msgstr "" +msgstr "Deixa de silenciar {truncatedTag}" #: src/view/com/profile/ProfileMenu.tsx:278 #: src/view/com/profile/ProfileMenu.tsx:284 @@ -5350,11 +5350,11 @@ msgstr "Deixa de silenciar el compte" #: src/components/TagMenu/index.tsx:208 msgid "Unmute all {displayTag} posts" -msgstr "" +msgstr "Deixa de silenciar totes les publicacions amb {displayTag}" #: src/components/TagMenu/index.tsx:210 #~ msgid "Unmute all {tag} posts" -#~ msgstr "" +#~ msgstr "Deixa de silenciar totes les publicacions amb {tag}" #: src/view/com/util/forms/PostDropdownBtn.tsx:251 #: src/view/com/util/forms/PostDropdownBtn.tsx:256 @@ -5368,7 +5368,7 @@ msgstr "Deixa de fixar" #: src/view/screens/ProfileFeed.tsx:291 msgid "Unpin from home" -msgstr "" +msgstr "Deixa de fixar a l'inici" #: src/view/screens/ProfileList.tsx:444 msgid "Unpin moderation list" @@ -5380,15 +5380,15 @@ msgstr "Desancora la llista de moderació" #: src/screens/Profile/Header/ProfileHeaderLabeler.tsx:220 msgid "Unsubscribe" -msgstr "" +msgstr "Dona't de baixa" #: src/screens/Profile/Header/ProfileHeaderLabeler.tsx:184 msgid "Unsubscribe from this labeler" -msgstr "" +msgstr "Dona't de baixa d'aquest etiquetador" #: src/lib/moderation/useReportOptions.ts:70 msgid "Unwanted Sexual Content" -msgstr "" +msgstr "Contingut sexual no dessitjat" #: src/view/com/modals/UserAddRemoveLists.tsx:70 msgid "Update {displayName} in Lists" @@ -5400,7 +5400,7 @@ msgstr "Actualitza {displayName} a les Llistes" #: src/view/com/modals/ChangeHandle.tsx:509 msgid "Update to {handle}" -msgstr "" +msgstr "Actualitza a {handle}" #: src/view/com/auth/login/SetNewPasswordForm.tsx:204 msgid "Updating..." @@ -5415,31 +5415,31 @@ msgstr "Puja un fitxer de text a:" #: src/view/com/util/UserBanner.tsx:116 #: src/view/com/util/UserBanner.tsx:119 msgid "Upload from Camera" -msgstr "" +msgstr "Puja de la càmera" #: src/view/com/util/UserAvatar.tsx:343 #: src/view/com/util/UserBanner.tsx:133 msgid "Upload from Files" -msgstr "" +msgstr "Puja dels Arxius" #: src/view/com/util/UserAvatar.tsx:337 #: src/view/com/util/UserAvatar.tsx:341 #: src/view/com/util/UserBanner.tsx:127 #: src/view/com/util/UserBanner.tsx:131 msgid "Upload from Library" -msgstr "" +msgstr "Puja de la biblioteca" #: src/view/com/modals/ChangeHandle.tsx:409 msgid "Use a file on your server" -msgstr "" +msgstr "Utilitza un fitxer del teu servidor" #: src/view/screens/AppPasswords.tsx:197 msgid "Use app passwords to login to other Bluesky clients without giving full access to your account or password." -msgstr "Utilitza les contrasenyes d'aplicació per iniciar sessió en altres clients de Bluesky, sense haver de donar accés total al teu compte o contrasenya." +msgstr "Utilitza les contrasenyes d'aplicació per a iniciar sessió en altres clients de Bluesky, sense haver de donar accés total al teu compte o contrasenya." #: src/view/com/modals/ChangeHandle.tsx:518 msgid "Use bsky.social as hosting provider" -msgstr "" +msgstr "Utilitza bsky.social com a proveïdor d'allotjament" #: src/view/com/modals/ChangeHandle.tsx:517 msgid "Use default provider" @@ -5457,11 +5457,11 @@ msgstr "Utilitza el meu navegador predeterminat" #: src/view/com/modals/ChangeHandle.tsx:401 msgid "Use the DNS panel" -msgstr "" +msgstr "Utilitza el panell de DNS" #: src/view/com/modals/AddAppPasswords.tsx:155 msgid "Use this to sign into the other app along with your handle." -msgstr "Utilitza-ho per iniciar sessió a l'altra aplicació, juntament amb el teu identificador." +msgstr "Utilitza-ho per a iniciar sessió a l'altra aplicació, juntament amb el teu identificador." #: src/view/com/modals/ServerInput.tsx:105 #~ msgid "Use your domain as your Bluesky client service provider" @@ -5478,7 +5478,7 @@ msgstr "Usuari bloquejat" #: src/lib/moderation/useModerationCauseDescription.ts:48 msgid "User Blocked by \"{0}\"" -msgstr "" +msgstr "Usuari bloquejat per \"{0}\"" #: src/components/moderation/ModerationDetailsDialog.tsx:54 msgid "User Blocked by List" @@ -5486,7 +5486,7 @@ msgstr "Usuari bloquejat per una llista" #: src/lib/moderation/useModerationCauseDescription.ts:66 msgid "User Blocking You" -msgstr "" +msgstr "L'usuari t'ha bloquejat" #: src/components/moderation/ModerationDetailsDialog.tsx:71 msgid "User Blocks You" @@ -5509,7 +5509,7 @@ msgstr "Llista d'usuaris feta per <0/>" #: src/view/com/modals/UserAddRemoveLists.tsx:196 #: src/view/screens/ProfileList.tsx:775 msgid "User list by you" -msgstr "Llista d'usaris feta per tu" +msgstr "Llista d'usuaris feta per tu" #: src/view/com/modals/CreateOrEditList.tsx:196 msgid "User list created" @@ -5542,11 +5542,11 @@ msgstr "Usuaris a \"{0}\"" #: src/components/LikesDialog.tsx:85 msgid "Users that have liked this content or profile" -msgstr "" +msgstr "Usuaris a qui els ha agradat aquest contingut o perfil" #: src/view/com/modals/ChangeHandle.tsx:437 msgid "Value:" -msgstr "" +msgstr "Valor:" #: src/view/com/auth/create/Step2.tsx:243 #~ msgid "Verification code" @@ -5554,7 +5554,7 @@ msgstr "" #: src/view/com/modals/ChangeHandle.tsx:510 msgid "Verify {0}" -msgstr "" +msgstr "Verifica {0}" #: src/view/screens/Settings/index.tsx:944 msgid "Verify email" @@ -5591,11 +5591,11 @@ msgstr "Veure el registre de depuració" #: src/components/ReportDialog/SelectReportOptionView.tsx:133 msgid "View details" -msgstr "" +msgstr "Veure els detalls" #: src/components/ReportDialog/SelectReportOptionView.tsx:128 msgid "View details for reporting a copyright violation" -msgstr "" +msgstr "Veure els detalls per a informar d'una infracció dels drets d'autor" #: src/view/com/posts/FeedSlice.tsx:99 msgid "View full thread" @@ -5603,7 +5603,7 @@ msgstr "Veure el fil de debat complet" #: src/components/moderation/LabelsOnMe.tsx:51 msgid "View information about these labels" -msgstr "" +msgstr "Mostra informació sobre aquestes etiquetes" #: src/view/com/posts/FeedErrorMessage.tsx:166 msgid "View profile" @@ -5615,11 +5615,11 @@ msgstr "Veure l'avatar" #: src/components/LabelingServiceCard/index.tsx:140 msgid "View the labeling service provided by @{0}" -msgstr "" +msgstr "Veure el servei d'etiquetatge proporcionat per @{0}" #: src/view/screens/ProfileFeed.tsx:584 msgid "View users who like this feed" -msgstr "" +msgstr "Veure els usuaris a qui els agrada aquest canal" #: src/view/com/modals/LinkWarning.tsx:75 #: src/view/com/modals/LinkWarning.tsx:77 @@ -5635,11 +5635,11 @@ msgstr "Adverteix" #: src/lib/moderation/useLabelBehaviorDescription.ts:48 msgid "Warn content" -msgstr "" +msgstr "Adverteix del contingut" #: src/lib/moderation/useLabelBehaviorDescription.ts:46 msgid "Warn content and filter from feeds" -msgstr "" +msgstr "Adverteix del contingut i filtra-ho dels canals" #: src/screens/Onboarding/StepAlgoFeeds/index.tsx:134 msgid "We also think you'll like \"For You\" by Skygaze:" @@ -5647,7 +5647,7 @@ msgstr "També creiem que t'agradarà el canal \"For You\" d'Skygaze:" #: src/screens/Hashtag.tsx:132 msgid "We couldn't find any results for that hashtag." -msgstr "" +msgstr "No hem trobat cap resultat per a aquest hashtag." #: src/screens/Deactivated.tsx:133 msgid "We estimate {estimatedTime} until your account is ready." @@ -5663,23 +5663,23 @@ msgstr "Ja no hi ha més publicacions dels usuaris que segueixes. Aquí n'hi ha #: src/components/dialogs/MutedWords.tsx:204 msgid "We recommend avoiding common words that appear in many posts, since it can result in no posts being shown." -msgstr "" +msgstr "Recomanem evitar les paraules habituals que apareixen en moltes publicacions, ja que pot provocar que no es mostri cap publicació." #: src/screens/Onboarding/StepAlgoFeeds/index.tsx:124 msgid "We recommend our \"Discover\" feed:" -msgstr "Et reomanem el nostre canal \"Discover\":" +msgstr "Et recomanem el nostre canal \"Discover\":" #: src/components/dialogs/BirthDateSettings.tsx:52 msgid "We were unable to load your birth date preferences. Please try again." -msgstr "" +msgstr "No hem pogut carregar les teves preferències de data de naixement. Torna-ho a provar." #: src/screens/Moderation/index.tsx:387 msgid "We were unable to load your configured labelers at this time." -msgstr "" +msgstr "En aquest moment no hem pogut carregar els teus etiquetadors configurats." #: src/screens/Onboarding/StepInterests/index.tsx:133 msgid "We weren't able to connect. Please try again to continue setting up your account. If it continues to fail, you can skip this flow." -msgstr "No ens hem pogut connectar. Torna-ho a provar per continuar configurant el teu compte. Si continua fallant, pots ometre aquest flux." +msgstr "No ens hem pogut connectar. Torna-ho a provar per a continuar configurant el teu compte. Si continua fallant, pots ometre aquest flux." #: src/screens/Deactivated.tsx:137 msgid "We will let you know when your account is ready." @@ -5691,7 +5691,7 @@ msgstr "T'informarem quan el teu compte estigui llest." #: src/screens/Onboarding/StepInterests/index.tsx:138 msgid "We'll use this to help customize your experience." -msgstr "Ho farem servir per personalitzar la teva experiència." +msgstr "Ho farem servir per a personalitzar la teva experiència." #: src/view/com/auth/create/CreateAccount.tsx:134 msgid "We're so excited to have you join us!" @@ -5703,7 +5703,7 @@ msgstr "Ho sentim, però no hem pogut resoldre aquesta llista. Si això continua #: src/components/dialogs/MutedWords.tsx:230 msgid "We're sorry, but we weren't able to load your muted words at this time. Please try again." -msgstr "" +msgstr "Ho sentim, però no hem pogut carregar les teves paraules silenciades en aquest moment. Torna-ho a provar." #: src/view/screens/Search/Search.tsx:255 msgid "We're sorry, but your search could not be completed. Please try again in a few minutes." @@ -5716,11 +5716,11 @@ msgstr "Ens sap greu! No podem trobar la pàgina que estàs cercant." #: src/screens/Profile/Header/ProfileHeaderLabeler.tsx:319 msgid "We're sorry! You can only subscribe to ten labelers, and you've reached your limit of ten." -msgstr "" +msgstr "Ho sentim! Només et pots subscriure a deu etiquetadors i has arribat al teu límit de deu." #: src/view/com/auth/onboarding/WelcomeMobile.tsx:48 msgid "Welcome to <0>Bluesky" -msgstr "Benvingut a <0>Bluesky" +msgstr "Us donem la benvinguda a <0>Bluesky" #: src/screens/Onboarding/StepInterests/index.tsx:130 msgid "What are your interests?" @@ -5753,23 +5753,23 @@ msgstr "Qui hi pot respondre" #: src/components/ReportDialog/SelectReportOptionView.tsx:44 msgid "Why should this content be reviewed?" -msgstr "" +msgstr "Per què s'hauria de revisar aquest contingut?" #: src/components/ReportDialog/SelectReportOptionView.tsx:57 msgid "Why should this feed be reviewed?" -msgstr "" +msgstr "Per què s'hauria de revisar aquest canal?" #: src/components/ReportDialog/SelectReportOptionView.tsx:54 msgid "Why should this list be reviewed?" -msgstr "" +msgstr "Per què s'hauria de revisar aquesta llista?" #: src/components/ReportDialog/SelectReportOptionView.tsx:51 msgid "Why should this post be reviewed?" -msgstr "" +msgstr "Per què s'hauria de revisar aquesta publicació?" #: src/components/ReportDialog/SelectReportOptionView.tsx:48 msgid "Why should this user be reviewed?" -msgstr "" +msgstr "Per què s'hauria de revisar aquest usuari?" #: src/view/com/modals/crop-image/CropImage.web.tsx:102 msgid "Wide" @@ -5808,12 +5808,12 @@ msgstr "Estàs a la cua." #: src/view/com/profile/ProfileFollows.tsx:93 msgid "You are not following anyone." -msgstr "" +msgstr "No segueixes a ningú." #: src/view/com/posts/FollowingEmptyState.tsx:67 #: src/view/com/posts/FollowingEndOfFeed.tsx:68 msgid "You can also discover new Custom Feeds to follow." -msgstr "També pots descobrir nous canals personalitzats per seguir." +msgstr "També pots descobrir nous canals personalitzats per a seguir." #: src/view/com/auth/create/Step1.tsx:106 #~ msgid "You can change hosting providers at any time." @@ -5830,7 +5830,7 @@ msgstr "Ara pots iniciar sessió amb la nova contrasenya." #: src/view/com/profile/ProfileFollowers.tsx:94 msgid "You do not have any followers." -msgstr "" +msgstr "No tens cap seguidor." #: src/view/com/modals/InviteCodes.tsx:66 msgid "You don't have any invite codes yet! We'll send you some when you've been on Bluesky for a little longer." @@ -5867,20 +5867,20 @@ msgstr "Has entrat un codi invàlid. Hauria de ser tipus XXXXX-XXXXX." #: src/lib/moderation/useModerationCauseDescription.ts:109 msgid "You have hidden this post" -msgstr "" +msgstr "Has amagat aquesta publicació" #: src/components/moderation/ModerationDetailsDialog.tsx:102 msgid "You have hidden this post." -msgstr "" +msgstr "Has amagat aquesta publicació." #: src/components/moderation/ModerationDetailsDialog.tsx:95 #: src/lib/moderation/useModerationCauseDescription.ts:92 msgid "You have muted this account." -msgstr "" +msgstr "Has silenciat aquest compte." #: src/lib/moderation/useModerationCauseDescription.ts:86 msgid "You have muted this user" -msgstr "" +msgstr "Has silenciat aquest usuari" #: src/view/com/modals/ModerationDetails.tsx:87 #~ msgid "You have muted this user." @@ -5897,11 +5897,11 @@ msgstr "No tens llistes." #: src/view/screens/ModerationBlockedAccounts.tsx:132 msgid "You have not blocked any accounts yet. To block an account, go to their profile and select \"Block account\" from the menu on their account." -msgstr "" +msgstr "Encara no has bloquejat cap compte. Per a bloquejar un compte, ves al seu perfil i selecciona \"Bloqueja el compte\" al menú del seu compte." #: src/view/screens/ModerationBlockedAccounts.tsx:132 #~ msgid "You have not blocked any accounts yet. To block an account, go to their profile and selected \"Block account\" from the menu on their account." -#~ msgstr "Encara no has bloquejat cap compte. Per fer-ho, vés al seu perfil i selecciona \"Bloqueja el compte\" en el menú del seu compte." +#~ msgstr "Encara no has bloquejat cap compte. Per a fer-ho, ves al seu perfil i selecciona \"Bloqueja el compte\" en el menú del seu compte." #: src/view/screens/AppPasswords.tsx:89 msgid "You have not created any app passwords yet. You can create one by pressing the button below." @@ -5909,31 +5909,31 @@ msgstr "Encara no has creat cap contrasenya d'aplicació. Pots fer-ho amb el bot #: src/view/screens/ModerationMutedAccounts.tsx:131 msgid "You have not muted any accounts yet. To mute an account, go to their profile and select \"Mute account\" from the menu on their account." -msgstr "" +msgstr "Encara no has silenciat cap compte. per a silenciar un compte, ves al seu perfil i selecciona \"Silencia el compte\" al menú del seu compte." #: src/view/screens/ModerationMutedAccounts.tsx:131 #~ msgid "You have not muted any accounts yet. To mute an account, go to their profile and selected \"Mute account\" from the menu on their account." -#~ msgstr "Encara no has silenciat cap compte. Per fer-ho, vés al seu perfil i selecciona \"Silencia compte\" en el menú del seu compte." +#~ msgstr "Encara no has silenciat cap compte. Per a fer-ho, al seu perfil i selecciona \"Silencia compte\" en el menú del seu compte." #: src/components/dialogs/MutedWords.tsx:250 msgid "You haven't muted any words or tags yet" -msgstr "" +msgstr "Encara no has silenciat cap paraula ni etiqueta" #: src/components/moderation/LabelsOnMeDialog.tsx:69 msgid "You may appeal these labels if you feel they were placed in error." -msgstr "" +msgstr "Pots apel·lar aquestes etiquetes si creus que s'han col·locat per error," #: src/view/com/modals/ContentFilteringSettings.tsx:175 #~ msgid "You must be 18 or older to enable adult content." -#~ msgstr "Has de tenir 18 anys o més per habilitar el contingut per a adults." +#~ msgstr "Has de tenir 18 anys o més per a habilitar el contingut per a adults." #: src/screens/Onboarding/StepModeration/AdultContentEnabledPref.tsx:110 msgid "You must be 18 years or older to enable adult content" -msgstr "Has de tenir 18 anys o més per habilitar el contingut per a adults" +msgstr "Has de tenir 18 anys o més per a habilitar el contingut per a adults" #: src/components/ReportDialog/SubmitView.tsx:205 msgid "You must select at least one labeler for a report" -msgstr "" +msgstr "Has d'escollir almenys un etiquetador per a un informe" #: src/view/com/util/forms/PostDropdownBtn.tsx:144 msgid "You will no longer receive notifications for this thread" @@ -5964,11 +5964,11 @@ msgstr "Ja està tot llest!" #: src/components/moderation/ModerationDetailsDialog.tsx:99 #: src/lib/moderation/useModerationCauseDescription.ts:101 msgid "You've chosen to hide a word or tag within this post." -msgstr "" +msgstr "Has triat amagar una paraula o una etiqueta d'aquesta publicació." #: src/view/com/posts/FollowingEndOfFeed.tsx:48 msgid "You've reached the end of your feed! Find some more accounts to follow." -msgstr "Has arribat al final del vostre cabal! Cerca alguns comptes més per seguir." +msgstr "Has arribat al final del vostre cabal! Cerca alguns comptes més per a seguir." #: src/view/com/auth/create/Step1.tsx:67 msgid "Your account" @@ -6014,7 +6014,7 @@ msgstr "El teu correu encara no s'ha verificat. Et recomanem fer-ho per segureta #: src/view/com/posts/FollowingEmptyState.tsx:47 msgid "Your following feed is empty! Follow more users to see what's happening." -msgstr "El teu canal de seguint està buit! Segueix a més usuaris per saber què està passant." +msgstr "El teu canal de seguint està buit! Segueix a més usuaris per a saber què està passant." #: src/view/com/auth/create/Step2.tsx:83 msgid "Your full handle will be" @@ -6036,7 +6036,7 @@ msgstr "El teu identificador complet serà <0>@{0}" #: src/components/dialogs/MutedWords.tsx:221 msgid "Your muted words" -msgstr "" +msgstr "Les teves paraules silenciades" #: src/view/com/modals/ChangePassword.tsx:157 msgid "Your password has been changed successfully!" @@ -6059,7 +6059,7 @@ msgstr "El teu perfil" #: src/view/com/composer/Composer.tsx:282 msgid "Your reply has been published" -msgstr "S'ha publicat a teva resposta" +msgstr "S'ha publicat la teva resposta" #: src/view/com/auth/create/Step2.tsx:65 msgid "Your user handle" diff --git a/src/locale/locales/fi/messages.po b/src/locale/locales/fi/messages.po index 0be456080b..546ecd40e2 100644 --- a/src/locale/locales/fi/messages.po +++ b/src/locale/locales/fi/messages.po @@ -23,7 +23,7 @@ msgstr "(ei sähköpostiosoitetta)" #: src/screens/Profile/Header/Metrics.tsx:45 msgid "{following} following" -msgstr "{following} seuraajaa" +msgstr "{following} seurattua" #: src/view/shell/desktop/RightNav.tsx:151 #~ msgid "{invitesAvailable, plural, one {Invite codes: # available} other {Invite codes: # available}}" @@ -49,11 +49,11 @@ msgstr "<0/> jäsentä" #: src/view/shell/Drawer.tsx:97 msgid "<0>{0} following" -msgstr "" +msgstr "<0>{0} seurattua" #: src/screens/Profile/Header/Metrics.tsx:46 msgid "<0>{following} <1>following" -msgstr "<0>{following} <1>seuraajaa" +msgstr "<0>{following} <1>seurattua" #: src/view/com/auth/onboarding/RecommendedFeeds.tsx:30 msgid "<0>Choose your<1>Recommended<2>Feeds" @@ -95,55 +95,55 @@ msgstr "Saavutettavuus" #: src/components/moderation/LabelsOnMe.tsx:42 msgid "account" -msgstr "" +msgstr "käyttäjätili" #: src/view/com/auth/login/LoginForm.tsx:169 #: src/view/screens/Settings/index.tsx:327 #: src/view/screens/Settings/index.tsx:743 msgid "Account" -msgstr "Tili" +msgstr "Käyttäjätili" #: src/view/com/profile/ProfileMenu.tsx:139 msgid "Account blocked" -msgstr "Tili on estetty" +msgstr "Käyttäjtili on estetty" #: src/view/com/profile/ProfileMenu.tsx:153 msgid "Account followed" -msgstr "" +msgstr "Käyttäjätili seurannassa" #: src/view/com/profile/ProfileMenu.tsx:113 msgid "Account muted" -msgstr "Tili on hiljennetty" +msgstr "Käyttäjätili hiljennetty" #: src/components/moderation/ModerationDetailsDialog.tsx:94 #: src/lib/moderation/useModerationCauseDescription.ts:91 msgid "Account Muted" -msgstr "Tili on hiljennetty" +msgstr "Käyttäjätili hiljennetty" #: src/components/moderation/ModerationDetailsDialog.tsx:83 msgid "Account Muted by List" -msgstr "Tili on hiljennetty listalla" +msgstr "Käyttäjätili hiljennetty listalla" #: src/view/com/util/AccountDropdownBtn.tsx:41 msgid "Account options" -msgstr "Tilin asetukset" +msgstr "Käyttäjätilin asetukset" #: src/view/com/util/AccountDropdownBtn.tsx:25 msgid "Account removed from quick access" -msgstr "Tili poistettu pikalinkeistä" +msgstr "Käyttäjätili poistettu pikalinkeistä" #: src/screens/Profile/Header/ProfileHeaderStandard.tsx:130 #: src/view/com/profile/ProfileMenu.tsx:128 msgid "Account unblocked" -msgstr "Tilin esto poistettu" +msgstr "Käyttäjätilin esto poistettu" #: src/view/com/profile/ProfileMenu.tsx:166 msgid "Account unfollowed" -msgstr "" +msgstr "Käyttäjätilin seuranta lopetettu" #: src/view/com/profile/ProfileMenu.tsx:102 msgid "Account unmuted" -msgstr "Tilin hiljennys poistettu" +msgstr "Käyttäjätilin hiljennys poistettu" #: src/components/dialogs/MutedWords.tsx:165 #: src/view/com/auth/onboarding/RecommendedFeedsItem.tsx:150 @@ -164,7 +164,7 @@ msgstr "Lisää käyttäjä tähän listaan" #: src/view/screens/Settings/index.tsx:402 #: src/view/screens/Settings/index.tsx:411 msgid "Add account" -msgstr "Lisää tili" +msgstr "Lisää käyttäjätili" #: src/view/com/composer/photos/Gallery.tsx:119 #: src/view/com/composer/photos/Gallery.tsx:180 @@ -201,7 +201,7 @@ msgstr "Lisää hiljennetty sana määritettyihin asetuksiin" #: src/components/dialogs/MutedWords.tsx:87 msgid "Add muted words and tags" -msgstr "Lisää hiljennetyt sanat ja tunnisteet" +msgstr "Lisää hiljennetyt sanat ja aihetunnisteet" #: src/view/com/modals/ChangeHandle.tsx:417 msgid "Add the following DNS record to your domain:" @@ -248,12 +248,12 @@ msgstr "Aikuissisältöä" #: src/components/moderation/ModerationLabelPref.tsx:114 msgid "Adult content is disabled." -msgstr "" +msgstr "Aikuissisältö on estetty" #: src/screens/Moderation/index.tsx:377 #: src/view/screens/Settings/index.tsx:684 msgid "Advanced" -msgstr "Edistynyt" +msgstr "Edistyneemmät" #: src/view/screens/Feeds.tsx:666 msgid "All the feeds you've saved, right in one place." @@ -290,7 +290,7 @@ msgstr "Sähköposti on lähetetty aiempaan osoitteeseesi, {0}. Siinä on vahvis #: src/lib/moderation/useReportOptions.ts:26 msgid "An issue not included in these options" -msgstr "" +msgstr "Ongelma, jota ei ole sisällytetty näihin vaihtoehtoihin" #: src/view/com/profile/FollowButton.tsx:35 #: src/view/com/profile/FollowButton.tsx:45 @@ -310,7 +310,7 @@ msgstr "Eläimet" #: src/lib/moderation/useReportOptions.ts:31 msgid "Anti-Social Behavior" -msgstr "" +msgstr "Epäsosiaalinen käytös" #: src/view/screens/LanguageSettings.tsx:95 msgid "App Language" @@ -345,11 +345,11 @@ msgstr "Sovellussalasanat" #: src/components/moderation/LabelsOnMeDialog.tsx:134 #: src/components/moderation/LabelsOnMeDialog.tsx:137 msgid "Appeal" -msgstr "" +msgstr "Valita" #: src/components/moderation/LabelsOnMeDialog.tsx:202 msgid "Appeal \"{0}\" label" -msgstr "" +msgstr "Valita \"{0}\" -merkinnästä" #: src/view/com/util/forms/PostDropdownBtn.tsx:295 #~ msgid "Appeal content warning" @@ -361,7 +361,7 @@ msgstr "" #: src/components/moderation/LabelsOnMeDialog.tsx:193 msgid "Appeal submitted." -msgstr "" +msgstr "Valitus jätetty." #: src/view/com/util/moderation/LabelInfo.tsx:52 #~ msgid "Appeal this decision" @@ -381,7 +381,7 @@ msgstr "Haluatko varmasti poistaa sovellussalasanan \"{name}\"?" #: src/view/com/feeds/FeedSourceCard.tsx:280 msgid "Are you sure you want to remove {0} from your feeds?" -msgstr "" +msgstr "Haluatko varmasti poistaa {0} syötteistäsi?" #: src/view/com/composer/Composer.tsx:508 msgid "Are you sure you'd like to discard this draft?" @@ -444,7 +444,7 @@ msgstr "Syntymäpäivä:" #: src/screens/Profile/Header/ProfileHeaderStandard.tsx:278 #: src/view/com/profile/ProfileMenu.tsx:361 msgid "Block" -msgstr "" +msgstr "Estä" #: src/view/com/profile/ProfileMenu.tsx:300 #: src/view/com/profile/ProfileMenu.tsx:307 @@ -453,11 +453,11 @@ msgstr "Estä käyttäjä" #: src/view/com/profile/ProfileMenu.tsx:344 msgid "Block Account?" -msgstr "" +msgstr "Estä käyttäjätili?" #: src/view/screens/ProfileList.tsx:530 msgid "Block accounts" -msgstr "Estä käyttäjät" +msgstr "Estä käyttäjätilit" #: src/view/screens/ProfileList.tsx:478 #: src/view/screens/ProfileList.tsx:634 @@ -500,7 +500,7 @@ msgstr "Estetty viesti." #: src/screens/Profile/Sections/Labels.tsx:153 msgid "Blocking does not prevent this labeler from placing labels on your account." -msgstr "" +msgstr "Estäminen ei estä tätä merkitsijää asettamasta merkintöjä tilillesi." #: src/view/screens/ProfileList.tsx:631 msgid "Blocking is public. Blocked accounts cannot reply in your threads, mention you, or otherwise interact with you." @@ -508,7 +508,7 @@ msgstr "Estäminen on julkista. Estetyt käyttäjät eivät voi vastata viesteih #: src/view/com/profile/ProfileMenu.tsx:353 msgid "Blocking will not prevent labels from being applied on your account, but it will stop this account from replying in your threads or interacting with you." -msgstr "" +msgstr "Estäminen ei estä merkintöjen tekemistä tilillesi, mutta se estää kyseistä tiliä vastaamasta ketjuissasi tai muuten vuorovaikuttamasta kanssasi." #: src/view/com/auth/HomeLoggedOutCTA.tsx:97 #: src/view/com/auth/SplashScreen.web.tsx:133 @@ -546,7 +546,7 @@ msgstr "Bluesky on julkinen." #: src/screens/Moderation/index.tsx:535 msgid "Bluesky will not show your profile and posts to logged-out users. Other apps may not honor this request. This does not make your account private." -msgstr "Bluesky ei näytä profiiliasi ja viestejäsi kirjautumattomille käyttäjille. Toiset sovellukset eivät ehkä noudata tätä asetusta. Tämä ei tee tilistäsi yksityistä." +msgstr "Bluesky ei näytä profiiliasi ja viestejäsi kirjautumattomille käyttäjille. Toiset sovellukset eivät ehkä noudata tätä asetusta. Tämä ei tee käyttäjätilistäsi yksityistä." #: src/view/com/modals/ServerInput.tsx:78 #~ msgid "Bluesky.Social" @@ -554,11 +554,11 @@ msgstr "Bluesky ei näytä profiiliasi ja viestejäsi kirjautumattomille käytt #: src/lib/moderation/useLabelBehaviorDescription.ts:53 msgid "Blur images" -msgstr "" +msgstr "Sumenna kuvat" #: src/lib/moderation/useLabelBehaviorDescription.ts:51 msgid "Blur images and filter from feeds" -msgstr "" +msgstr "Sumenna kuvat ja suodata syötteistä" #: src/screens/Onboarding/index.tsx:33 msgid "Books" @@ -575,7 +575,7 @@ msgstr "Yritys" #: src/view/com/modals/ServerInput.tsx:115 #~ msgid "Button disabled. Input custom domain to proceed." -#~ msgstr "" +#~ msgstr "Painike poistettu käytöstä. Anna mukautettu verkkotunnus jatkaaksesi." #: src/view/com/profile/ProfileSubpageHeader.tsx:157 msgid "by —" @@ -595,7 +595,7 @@ msgstr "käyttäjältä <0/>" #: src/view/com/auth/create/Policies.tsx:87 msgid "By creating an account you agree to the {els}." -msgstr "" +msgstr "Luomalla käyttäjätilin hyväksyt {els}." #: src/view/com/profile/ProfileSubpageHeader.tsx:159 msgid "by you" @@ -646,7 +646,7 @@ msgstr "Peruuta" #: src/view/com/modals/DeleteAccount.tsx:152 #: src/view/com/modals/DeleteAccount.tsx:230 msgid "Cancel account deletion" -msgstr "Peruuta tilin poisto" +msgstr "Peruuta käyttäjätilin poisto" #: src/view/com/modals/ChangeHandle.tsx:149 msgid "Cancel change handle" @@ -675,11 +675,11 @@ msgstr "Peruuta haku" #: src/view/com/modals/LinkWarning.tsx:88 msgid "Cancels opening the linked website" -msgstr "" +msgstr "Peruuttaa linkitetyn verkkosivuston avaamisen" #: src/view/com/modals/VerifyEmail.tsx:152 msgid "Change" -msgstr "" +msgstr "Vaihda" #: src/view/screens/Settings/index.tsx:353 msgctxt "action" @@ -760,7 +760,7 @@ msgstr "Valitse algoritmit, jotka ohjaavat kokemustasi mukautettujen syötteiden #: src/screens/Onboarding/StepAlgoFeeds/index.tsx:103 #~ msgid "Choose your algorithmic feeds" -#~ msgstr "" +#~ msgstr "Valitse algoritmiperustaiset syötteet" #: src/screens/Onboarding/StepAlgoFeeds/index.tsx:103 msgid "Choose your main feeds" @@ -797,7 +797,7 @@ msgstr "" #: src/view/screens/Settings/index.tsx:881 msgid "Clears all storage data" -msgstr "" +msgstr "Tyhjentää kaikki tallennustiedot" #: src/view/screens/Support.tsx:40 msgid "click here" @@ -805,11 +805,11 @@ msgstr "klikkaa tästä" #: src/components/TagMenu/index.web.tsx:138 msgid "Click here to open tag menu for {tag}" -msgstr "Avaa tästä valikko tunnisteelle {tag}" +msgstr "Avaa tästä valikko aihetunnisteelle {tag}" #: src/components/RichText.tsx:191 msgid "Click here to open tag menu for #{tag}" -msgstr "" +msgstr "Klikkaa tästä avataksesi valikon aihetunnisteelle #{tag}." #: src/screens/Onboarding/index.tsx:35 msgid "Climate" @@ -885,7 +885,7 @@ msgstr "Yhteisöohjeet" #: src/screens/Onboarding/StepFinished.tsx:148 msgid "Complete onboarding and start using your account" -msgstr "Suorita käyttöönotto loppuun ja aloita tilisi käyttö" +msgstr "Suorita käyttöönotto loppuun ja aloita käyttäjätilisi käyttö" #: src/view/com/auth/create/Step3.tsx:73 msgid "Complete the challenge" @@ -936,7 +936,7 @@ msgstr "Vahvista sisällön kieliasetukset" #: src/view/com/modals/DeleteAccount.tsx:220 msgid "Confirm delete account" -msgstr "Vahvista tilin poisto" +msgstr "Vahvista käyttäjätilin poisto" #: src/view/com/modals/ContentFilteringSettings.tsx:156 #~ msgid "Confirm your age to enable adult content." @@ -944,11 +944,11 @@ msgstr "Vahvista tilin poisto" #: src/screens/Moderation/index.tsx:303 msgid "Confirm your age:" -msgstr "" +msgstr "Vahvista ikäsi:" #: src/screens/Moderation/index.tsx:294 msgid "Confirm your birthdate" -msgstr "" +msgstr "Vahvista syntymäaikasi" #: src/view/com/modals/ChangeEmail.tsx:157 #: src/view/com/modals/DeleteAccount.tsx:176 @@ -972,11 +972,11 @@ msgstr "Ota yhteyttä tukeen" #: src/components/moderation/LabelsOnMe.tsx:42 msgid "content" -msgstr "" +msgstr "sisältö" #: src/lib/moderation/useGlobalLabelStrings.ts:18 msgid "Content Blocked" -msgstr "" +msgstr "Sisältö estetty" #: src/view/screens/Moderation.tsx:83 #~ msgid "Content filtering" @@ -988,7 +988,7 @@ msgstr "" #: src/screens/Moderation/index.tsx:287 msgid "Content filters" -msgstr "" +msgstr "Sisältösuodattimet" #: src/view/com/modals/lang-settings/ContentLanguagesSettings.tsx:74 #: src/view/screens/LanguageSettings.tsx:278 @@ -1071,7 +1071,7 @@ msgstr "Kopioi" #: src/view/com/modals/ChangeHandle.tsx:481 msgid "Copy {0}" -msgstr "" +msgstr "Kopioi {0}" #: src/view/screens/ProfileList.tsx:388 msgid "Copy link to list" @@ -1112,7 +1112,7 @@ msgstr "Listaa ei voitu ladata" #: src/view/com/auth/SplashScreen.tsx:73 #: src/view/com/auth/SplashScreen.web.tsx:81 msgid "Create a new account" -msgstr "Luo uusi tili" +msgstr "Luo uusi käyttäjätili" #: src/view/screens/Settings/index.tsx:403 msgid "Create a new Bluesky account" @@ -1120,7 +1120,7 @@ msgstr "Luo uusi Bluesky-tili" #: src/view/com/auth/create/CreateAccount.tsx:133 msgid "Create Account" -msgstr "Luo tili" +msgstr "Luo käyttäjätili" #: src/view/com/modals/AddAppPasswords.tsx:226 msgid "Create App Password" @@ -1129,11 +1129,11 @@ msgstr "Luo sovellussalasana" #: src/view/com/auth/HomeLoggedOutCTA.tsx:54 #: src/view/com/auth/SplashScreen.tsx:68 msgid "Create new account" -msgstr "Luo uusi tili" +msgstr "Luo uusi käyttäjätili" #: src/components/ReportDialog/SelectReportOptionView.tsx:94 msgid "Create report for {0}" -msgstr "" +msgstr "Luo raportti: {0}" #: src/view/screens/AppPasswords.tsx:246 msgid "Created {0}" @@ -1145,7 +1145,7 @@ msgstr "{0} luotu" #: src/view/screens/ProfileFeed.tsx:614 #~ msgid "Created by you" -#~ msgstr "Sinun luoma sisältö" +#~ msgstr "Luomasi sisältö" #: src/view/com/composer/Composer.tsx:468 msgid "Creates a card with a thumbnail. The card links to {url}" @@ -1167,11 +1167,11 @@ msgstr "Mukautettu verkkotunnus" #: src/screens/Onboarding/StepAlgoFeeds/index.tsx:106 #: src/view/screens/Feeds.tsx:692 msgid "Custom feeds built by the community bring you new experiences and help you find the content you love." -msgstr "Yhteisön rakentamat mukautetut syötteet tuovat sinulle uusia kokemuksia ja auttavat löytämään sinulle mieluisaa sisältöä." +msgstr "Yhteisön rakentamat mukautetut syötteet tuovat sinulle uusia kokemuksia ja auttavat löytämään mieluisaa sisältöä." #: src/view/screens/PreferencesExternalEmbeds.tsx:55 msgid "Customize media from external sites." -msgstr "Muokkaa mediaa ulkoisista sivustoista." +msgstr "Muokkaa ulkoisten sivustojen mediasisältöjen asetuksia" #: src/view/screens/Settings.tsx:687 #~ msgid "Danger Zone" @@ -1202,7 +1202,7 @@ msgstr "Vianetsintäpaneeli" #: src/view/screens/AppPasswords.tsx:268 #: src/view/screens/ProfileList.tsx:613 msgid "Delete" -msgstr "" +msgstr "Poista" #: src/view/screens/Settings/index.tsx:796 msgid "Delete account" @@ -1218,7 +1218,7 @@ msgstr "Poista sovellussalasana" #: src/view/screens/AppPasswords.tsx:263 msgid "Delete app password?" -msgstr "" +msgstr "Poista sovellussalasana" #: src/view/screens/ProfileList.tsx:415 msgid "Delete List" @@ -1234,7 +1234,7 @@ msgstr "Poista käyttäjätilini" #: src/view/screens/Settings/index.tsx:808 msgid "Delete My Account…" -msgstr "Poista tilini…" +msgstr "Poista käyttäjätilini…" #: src/view/com/util/forms/PostDropdownBtn.tsx:302 #: src/view/com/util/forms/PostDropdownBtn.tsx:304 @@ -1243,7 +1243,7 @@ msgstr "Poista viesti" #: src/view/screens/ProfileList.tsx:608 msgid "Delete this list?" -msgstr "" +msgstr "Poista tämä lista?" #: src/view/com/util/forms/PostDropdownBtn.tsx:314 msgid "Delete this post?" @@ -1266,7 +1266,7 @@ msgstr "Kuvaus" #: src/view/screens/Settings.tsx:760 #~ msgid "Developer Tools" -#~ msgstr "" +#~ msgstr "Kehittäjätyökalut" #: src/view/com/composer/Composer.tsx:217 msgid "Did you want to say anything?" @@ -1281,7 +1281,7 @@ msgstr "Himmeä" #: src/lib/moderation/useLabelBehaviorDescription.ts:68 #: src/screens/Moderation/index.tsx:343 msgid "Disabled" -msgstr "" +msgstr "Poistettu käytöstä" #: src/view/com/composer/Composer.tsx:510 msgid "Discard" @@ -1293,7 +1293,7 @@ msgstr "Hylkää" #: src/view/com/composer/Composer.tsx:507 msgid "Discard draft?" -msgstr "" +msgstr "Hylkää luonnos?" #: src/screens/Moderation/index.tsx:520 #: src/screens/Moderation/index.tsx:524 @@ -1323,11 +1323,11 @@ msgstr "Näyttönimi" #: src/view/com/modals/ChangeHandle.tsx:398 msgid "DNS Panel" -msgstr "" +msgstr "DNS-paneeli" #: src/lib/moderation/useGlobalLabelStrings.ts:39 msgid "Does not include nudity." -msgstr "" +msgstr "Ei sisällä alastomuutta." #: src/view/com/modals/ChangeHandle.tsx:482 msgid "Domain Value" @@ -1397,15 +1397,15 @@ msgstr "Applen sääntöjen vuoksi aikuisviihde voidaan ottaa käyttöön vasta #: src/view/com/modals/ChangeHandle.tsx:257 msgid "e.g. alice" -msgstr "" +msgstr "esim. maija" #: src/view/com/modals/EditProfile.tsx:185 msgid "e.g. Alice Roberts" -msgstr "esim. Mikko Mallikas" +msgstr "esim. Maija Mallikas" #: src/view/com/modals/ChangeHandle.tsx:381 msgid "e.g. alice.com" -msgstr "" +msgstr "esim. liisa.fi" #: src/view/com/modals/EditProfile.tsx:203 msgid "e.g. Artist, dog-lover, and avid reader." @@ -1413,7 +1413,7 @@ msgstr "esim. Taiteilija, koiraharrastaja ja innokas lukija." #: src/lib/moderation/useGlobalLabelStrings.ts:43 msgid "E.g. artistic nudes." -msgstr "" +msgstr "Esimerkiksi taiteelliset alastonkuvat." #: src/view/com/modals/CreateOrEditList.tsx:283 msgid "e.g. Great Posters" @@ -1443,7 +1443,7 @@ msgstr "Muokkaa" #: src/view/com/util/UserAvatar.tsx:299 #: src/view/com/util/UserBanner.tsx:85 msgid "Edit avatar" -msgstr "" +msgstr "Muokkaa profiilikuvaa" #: src/view/com/composer/photos/Gallery.tsx:144 #: src/view/com/modals/EditImage.tsx:207 @@ -1462,11 +1462,11 @@ msgstr "Muokkaa moderaatiolistaa" #: src/view/screens/Feeds.tsx:434 #: src/view/screens/SavedFeeds.tsx:84 msgid "Edit My Feeds" -msgstr "Muokkaa syötteitäni" +msgstr "Muokkaa syötteitä" #: src/view/com/modals/EditProfile.tsx:152 msgid "Edit my profile" -msgstr "Muokkaa profiiliani" +msgstr "Muokkaa profiilia" #: src/screens/Profile/Header/ProfileHeaderLabeler.tsx:172 #: src/screens/Profile/Header/ProfileHeaderStandard.tsx:161 @@ -1533,7 +1533,7 @@ msgstr "Ota käyttöön vain {0}" #: src/screens/Moderation/index.tsx:331 msgid "Enable adult content" -msgstr "" +msgstr "Ota aikuissisältö käyttöön" #: src/screens/Onboarding/StepModeration/AdultContentEnabledPref.tsx:94 msgid "Enable Adult Content" @@ -1558,7 +1558,7 @@ msgstr "Ota tämä asetus käyttöön nähdäksesi vastaukset vain seuraamiltasi #: src/screens/Moderation/index.tsx:341 msgid "Enabled" -msgstr "" +msgstr "Käytössä" #: src/screens/Profile/Sections/Feed.tsx:84 msgid "End of feed" @@ -1571,7 +1571,7 @@ msgstr "Anna sovellusalasanalle nimi" #: src/components/dialogs/MutedWords.tsx:100 #: src/components/dialogs/MutedWords.tsx:101 msgid "Enter a word or tag" -msgstr "Kirjoita sana tai tunniste" +msgstr "Kirjoita sana tai aihetunniste" #: src/view/com/modals/VerifyEmail.tsx:105 msgid "Enter Confirmation Code" @@ -1632,11 +1632,11 @@ msgstr "Kaikki" #: src/lib/moderation/useReportOptions.ts:66 msgid "Excessive mentions or replies" -msgstr "" +msgstr "Liialliset maininnat tai vastaukset" #: src/view/com/modals/DeleteAccount.tsx:231 msgid "Exits account deletion process" -msgstr "" +msgstr "Keskeyttää tilin poistoprosessin" #: src/view/com/modals/ChangeHandle.tsx:150 msgid "Exits handle change process" @@ -1644,7 +1644,7 @@ msgstr "Peruuttaa käyttäjätunnuksen vaihtamisen" #: src/view/com/modals/crop-image/CropImage.web.tsx:135 msgid "Exits image cropping process" -msgstr "" +msgstr "Keskeyttää kuvan rajausprosessin" #: src/view/com/lightbox/Lightbox.web.tsx:130 msgid "Exits image view" @@ -1670,11 +1670,11 @@ msgstr "Laajenna tai pienennä viesti johon olit vastaamassa" #: src/lib/moderation/useGlobalLabelStrings.ts:47 msgid "Explicit or potentially disturbing media." -msgstr "" +msgstr "Selvästi tai mahdollisesti häiritsevä media." #: src/lib/moderation/useGlobalLabelStrings.ts:35 msgid "Explicit sexual images." -msgstr "" +msgstr "Selvästi seksuaalista kuvamateriaalia." #: src/view/screens/Settings/index.tsx:777 msgid "Export my data" @@ -1698,11 +1698,11 @@ msgstr "Ulkoiset mediat voivat sallia verkkosivustojen kerätä tietoja sinusta #: src/view/screens/PreferencesExternalEmbeds.tsx:52 #: src/view/screens/Settings/index.tsx:677 msgid "External Media Preferences" -msgstr "Ulkoisten medioiden asetukset" +msgstr "Ulkoisten mediasoittimien asetukset" #: src/view/screens/Settings/index.tsx:668 msgid "External media settings" -msgstr "Ulkoisten medioiden asetukset" +msgstr "Ulkoisten mediasoittimien asetukset" #: src/view/com/modals/AddAppPasswords.tsx:115 #: src/view/com/modals/AddAppPasswords.tsx:119 @@ -1724,7 +1724,7 @@ msgstr "Suositeltujen syötteiden lataaminen epäonnistui" #: src/view/com/lightbox/Lightbox.tsx:83 msgid "Failed to save image: {0}" -msgstr "" +msgstr "Kuvan {0} tallennus epäonnistui" #: src/Navigation.tsx:196 msgid "Feed" @@ -1780,11 +1780,11 @@ msgstr "Syötteet voivat olla myös aihepiirikohtaisia!" #: src/view/com/modals/ChangeHandle.tsx:482 msgid "File Contents" -msgstr "" +msgstr "Tiedoston sisältö" #: src/lib/moderation/useLabelBehaviorDescription.ts:66 msgid "Filter from feeds" -msgstr "" +msgstr "Suodata syötteistä" #: src/screens/Onboarding/StepFinished.tsx:151 msgid "Finalizing" @@ -1810,7 +1810,7 @@ msgstr "Etsitään samankaltaisia käyttäjätilejä" #: src/view/screens/PreferencesFollowingFeed.tsx:111 msgid "Fine-tune the content you see on your Following feed." -msgstr "Hienosäädä näkemääsi sisältöä Seuraavat-syötteessäsi." +msgstr "Hienosäädä näkemääsi sisältöä Seuratut-syötteessäsi." #: src/view/screens/PreferencesHomeFeed.tsx:111 #~ msgid "Fine-tune the content you see on your home screen." @@ -1889,7 +1889,7 @@ msgstr "Vain seuratut käyttäjät" msgid "followed you" msgstr "seurasi sinua" -#: src/view/com/profile/ProfileFollowers.tsx:109 + #: src/view/screens/ProfileFollowers.tsx:25 msgid "Followers" msgstr "Seuraajat" @@ -1899,15 +1899,15 @@ msgstr "Seuraajat" #: src/view/com/profile/ProfileFollows.tsx:108 #: src/view/screens/ProfileFollows.tsx:25 msgid "Following" -msgstr "Seuraa" +msgstr "Seurataan" #: src/screens/Profile/Header/ProfileHeaderStandard.tsx:89 msgid "Following {0}" -msgstr "Seuraa {0}" +msgstr "Seurataan {0}" #: src/view/screens/Settings/index.tsx:553 msgid "Following feed preferences" -msgstr "" +msgstr "Seuratut -syötteen asetukset" #: src/Navigation.tsx:262 #: src/view/com/home/HomeHeaderLayout.web.tsx:50 @@ -1952,7 +1952,7 @@ msgstr "Unohtunut salasana" #: src/lib/moderation/useReportOptions.ts:52 msgid "Frequently Posts Unwanted Content" -msgstr "" +msgstr "Julkaisee usein ei-toivottua sisältöä" #: src/screens/Hashtag.tsx:108 #: src/screens/Hashtag.tsx:148 @@ -1975,7 +1975,7 @@ msgstr "Aloita tästä" #: src/lib/moderation/useReportOptions.ts:37 msgid "Glaring violations of law or terms of service" -msgstr "" +msgstr "Ilmeisiä lain tai käyttöehtojen rikkomuksia" #: src/components/moderation/ScreenHider.tsx:144 #: src/components/moderation/ScreenHider.tsx:153 @@ -2005,11 +2005,11 @@ msgstr "Palaa edelliseen vaiheeseen" #: src/view/screens/NotFound.tsx:55 msgid "Go home" -msgstr "" +msgstr "Palaa alkuun" #: src/view/screens/NotFound.tsx:54 msgid "Go Home" -msgstr "" +msgstr "Palaa alkuun" #: src/view/screens/Search/Search.tsx:748 #: src/view/shell/desktop/Search.tsx:263 @@ -2034,19 +2034,19 @@ msgstr "Käyttäjätunnus" #: src/lib/moderation/useReportOptions.ts:32 msgid "Harassment, trolling, or intolerance" -msgstr "" +msgstr "Häirintä, trollaus tai suvaitsemattomuus" #: src/Navigation.tsx:282 msgid "Hashtag" -msgstr "" +msgstr "Aihetunniste" #: src/components/RichText.tsx:188 #~ msgid "Hashtag: {tag}" -#~ msgstr "Tunniste: {tag}" +#~ msgstr "Aihetunniste: {tag}" #: src/components/RichText.tsx:190 msgid "Hashtag: #{tag}" -msgstr "" +msgstr "Aihetunniste #{tag}" #: src/view/com/auth/create/CreateAccount.tsx:208 msgid "Having trouble?" @@ -2135,11 +2135,11 @@ msgstr "Hmm, meillä on vaikeuksia löytää tätä syötettä. Se saattaa olla #: src/screens/Moderation/index.tsx:61 msgid "Hmmmm, it seems we're having trouble loading this data. See below for more details. If this issue persists, please contact us." -msgstr "" +msgstr "Hmm, vaikuttaa siltä, että tämän datan lataamisessa on ongelmia. Katso lisätietoja alta. Jos ongelma jatkuu, ole hyvä ja ota yhteyttä meihin." #: src/screens/Profile/ErrorState.tsx:31 msgid "Hmmmm, we couldn't load that moderation service." -msgstr "" +msgstr "Hmm, emme pystyneet avaamaan kyseistä moderaatiopalvelua." #: src/Navigation.tsx:454 #: src/view/shell/bottom-bar/BottomBar.tsx:139 @@ -2204,11 +2204,11 @@ msgstr "" #: src/view/com/modals/ChangePassword.tsx:148 msgid "If you want to change your password, we will send you a code to verify that this is your account." -msgstr "Jos haluat vaihtaa salasanasi, lähetämme sinulle koodin varmistaaksemme, että tämä on tilisi." +msgstr "Jos haluat vaihtaa salasanasi, lähetämme sinulle koodin varmistaaksemme, että tämä on käyttäjätilisi." #: src/lib/moderation/useReportOptions.ts:36 msgid "Illegal and Urgent" -msgstr "" +msgstr "Laiton ja kiireellinen" #: src/view/com/util/images/Gallery.tsx:38 msgid "Image" @@ -2225,7 +2225,7 @@ msgstr "Kuvan ALT-teksti" #: src/lib/moderation/useReportOptions.ts:47 msgid "Impersonation or false claims about identity or affiliation" -msgstr "" +msgstr "Henkilöllisyyden tai yhteyksien vääristely tai vääriä väitteitä niistä" #: src/view/com/auth/login/SetNewPasswordForm.tsx:138 msgid "Input code sent to your email for password reset" @@ -2233,7 +2233,7 @@ msgstr "Syötä sähköpostiisi lähetetty koodi salasanan nollaamista varten" #: src/view/com/modals/DeleteAccount.tsx:184 msgid "Input confirmation code for account deletion" -msgstr "Syötä vahvistuskoodi tilin poistoa varten" +msgstr "Syötä vahvistuskoodi käyttäjätilin poistoa varten" #: src/view/com/auth/create/Step1.tsx:177 msgid "Input email for Bluesky account" @@ -2253,7 +2253,7 @@ msgstr "Syötä uusi salasana" #: src/view/com/modals/DeleteAccount.tsx:203 msgid "Input password for account deletion" -msgstr "Syötä salasana tilin poistoa varten" +msgstr "Syötä salasana käyttäjätilin poistoa varten" #: src/view/com/auth/create/Step2.tsx:196 #~ msgid "Input phone number for SMS verification" @@ -2281,7 +2281,7 @@ msgstr "Syötä salasanasi" #: src/view/com/modals/ChangeHandle.tsx:390 msgid "Input your preferred hosting provider" -msgstr "" +msgstr "Syötä haluamasi palveluntarjoaja" #: src/view/com/auth/create/Step2.tsx:80 msgid "Input your user handle" @@ -2297,7 +2297,7 @@ msgstr "Virheellinen käyttäjätunnus tai salasana" #: src/view/screens/Settings.tsx:411 #~ msgid "Invite" -#~ msgstr "" +#~ msgstr "Kutsu" #: src/view/com/modals/InviteCodes.tsx:93 msgid "Invite a Friend" @@ -2427,7 +2427,7 @@ msgstr "Lue lisää siitä, mikä on julkista Blueskyssa." #: src/components/moderation/ContentHider.tsx:152 msgid "Learn more." -msgstr "" +msgstr "Lue lisää." #: src/view/com/modals/lang-settings/ContentLanguagesSettings.tsx:82 msgid "Leave them all unchecked to see any language." @@ -2486,17 +2486,17 @@ msgstr "Tykänneet" #: src/view/com/feeds/FeedSourceCard.tsx:268 msgid "Liked by {0} {1}" -msgstr "Tykänneet {0} {1}" +msgstr "Tykännyt {0} {1}" #: src/components/LabelingServiceCard/index.tsx:72 msgid "Liked by {count} {0}" -msgstr "" +msgstr "Tykännyt {count} {0}" #: src/screens/Profile/Header/ProfileHeaderLabeler.tsx:277 #: src/screens/Profile/Header/ProfileHeaderLabeler.tsx:291 #: src/view/screens/ProfileFeed.tsx:587 msgid "Liked by {likeCount} {0}" -msgstr "Tykkäyksiä {likeCount} {0}" +msgstr "Tykännyt {likeCount} {0}" #: src/view/com/notifications/FeedItem.tsx:174 msgid "liked your custom feed" @@ -2608,7 +2608,7 @@ msgstr "Varmista, että olet menossa oikeaan paikkaan!" #: src/components/dialogs/MutedWords.tsx:83 msgid "Manage your muted words and tags" -msgstr "Hallinnoi hiljennettyjä sanojasi ja tunnisteitasi" +msgstr "Hallinnoi hiljennettyjä sanoja ja aihetunnisteita" #: src/view/com/auth/create/Step2.tsx:118 msgid "May not be longer than 253 characters" @@ -2641,7 +2641,7 @@ msgstr "Viesti palvelimelta: {0}" #: src/lib/moderation/useReportOptions.ts:45 msgid "Misleading Account" -msgstr "" +msgstr "Harhaanjohtava käyttäjätili" #: src/Navigation.tsx:119 #: src/screens/Moderation/index.tsx:106 @@ -2654,7 +2654,7 @@ msgstr "Moderointi" #: src/components/moderation/ModerationDetailsDialog.tsx:113 msgid "Moderation details" -msgstr "" +msgstr "Moderaation yksityiskohdat" #: src/view/com/lists/ListCard.tsx:93 #: src/view/com/modals/UserAddRemoveLists.tsx:206 @@ -2698,7 +2698,7 @@ msgstr "" #: src/screens/Moderation/index.tsx:217 msgid "Moderation tools" -msgstr "" +msgstr "Moderointityökalut" #: src/components/moderation/ModerationDetailsDialog.tsx:49 #: src/lib/moderation/useModerationCauseDescription.ts:40 @@ -2707,7 +2707,7 @@ msgstr "Ylläpitäjä on asettanut yleisen varoituksen sisällölle." #: src/view/com/post-thread/PostThreadItem.tsx:541 msgid "More" -msgstr "" +msgstr "Lisää" #: src/view/shell/desktop/Feeds.tsx:65 msgid "More feeds" @@ -2748,7 +2748,7 @@ msgstr "Hiljennä käyttäjät" #: src/components/TagMenu/index.tsx:209 msgid "Mute all {displayTag} posts" -msgstr "" +msgstr "Hiljennä kaikki {displayTag} viestit" #: src/components/TagMenu/index.tsx:211 #~ msgid "Mute all {tag} posts" @@ -2756,11 +2756,11 @@ msgstr "" #: src/components/dialogs/MutedWords.tsx:149 msgid "Mute in tags only" -msgstr "Hiljennä vain tunnisteissa" +msgstr "Hiljennä vain aihetunnisteissa" #: src/components/dialogs/MutedWords.tsx:134 msgid "Mute in text & tags" -msgstr "Hiljennä tekstissä ja tunnisteissa" +msgstr "Hiljennä tekstissä ja aihetunnisteissa" #: src/view/screens/ProfileList.tsx:461 #: src/view/screens/ProfileList.tsx:624 @@ -2777,11 +2777,11 @@ msgstr "Hiljennä nämä käyttäjät?" #: src/components/dialogs/MutedWords.tsx:127 msgid "Mute this word in post text and tags" -msgstr "Hiljennä tämä sana viesteissä ja tunnisteissa" +msgstr "Hiljennä tämä sana viesteissä ja aihetunnisteissa" #: src/components/dialogs/MutedWords.tsx:142 msgid "Mute this word in tags only" -msgstr "Hiljennä tämä sana vain tunnisteissa" +msgstr "Hiljennä tämä sana vain aihetunnisteissa" #: src/view/com/util/forms/PostDropdownBtn.tsx:251 #: src/view/com/util/forms/PostDropdownBtn.tsx:257 @@ -2791,7 +2791,7 @@ msgstr "Hiljennä keskustelu" #: src/view/com/util/forms/PostDropdownBtn.tsx:267 #: src/view/com/util/forms/PostDropdownBtn.tsx:269 msgid "Mute words & tags" -msgstr "Hiljennä sanat ja tunnisteet" +msgstr "Hiljennä sanat ja aihetunnisteet" #: src/view/com/lists/ListCard.tsx:102 msgid "Muted" @@ -2812,11 +2812,11 @@ msgstr "Hiljennettyjen käyttäjien viestit poistetaan syötteestäsi ja ilmoitu #: src/lib/moderation/useModerationCauseDescription.ts:85 msgid "Muted by \"{0}\"" -msgstr "" +msgstr "Hiljentäjä: \"{0}\"" #: src/screens/Moderation/index.tsx:233 msgid "Muted words & tags" -msgstr "Hiljennetyt sanat ja tunnisteet" +msgstr "Hiljennetyt sanat ja aihetunnisteet" #: src/view/screens/ProfileList.tsx:621 msgid "Muting is private. Muted accounts can interact with you, but you will not see their posts or receive notifications from them." @@ -2837,11 +2837,11 @@ msgstr "Profiilini" #: src/view/screens/Settings/index.tsx:596 msgid "My saved feeds" -msgstr "" +msgstr "Tallennetut syötteeni" #: src/view/screens/Settings/index.tsx:602 msgid "My Saved Feeds" -msgstr "Omat tallennetut syötteet" +msgstr "Tallennetut syötteeni" #: src/view/com/auth/server-input/index.tsx:118 #~ msgid "my-server.com" @@ -2860,7 +2860,7 @@ msgstr "Nimi vaaditaan" #: src/lib/moderation/useReportOptions.ts:78 #: src/lib/moderation/useReportOptions.ts:86 msgid "Name or Description Violates Community Standards" -msgstr "" +msgstr "Nimi tai kuvaus rikkoo yhteisön sääntöjä" #: src/screens/Onboarding/index.tsx:25 msgid "Nature" @@ -2880,7 +2880,7 @@ msgstr "Siirtyy profiiliisi" #: src/components/ReportDialog/SelectReportOptionView.tsx:124 msgid "Need to report a copyright violation?" -msgstr "" +msgstr "Tarvitseeko ilmoittaa tekijänoikeusrikkomuksesta?" #: src/view/com/modals/EmbedConsent.tsx:107 #: src/view/com/modals/EmbedConsent.tsx:123 @@ -3012,7 +3012,7 @@ msgstr "Ei tuloksia" #: src/components/Lists.tsx:189 msgid "No results found" -msgstr "" +msgstr "Tuloksia ei löydetty" #: src/view/screens/Feeds.tsx:495 msgid "No results found for \"{query}\"" @@ -3030,16 +3030,16 @@ msgstr "Ei kiitos" #: src/view/com/modals/Threadgate.tsx:82 msgid "Nobody" -msgstr "Ei ketään" +msgstr "Ei kukaan" #: src/components/LikedByList.tsx:102 #: src/components/LikesDialog.tsx:99 msgid "Nobody has liked this yet. Maybe you should be the first!" -msgstr "" +msgstr "Kukaan ei ole vielä tykännyt tästä. Ehkä sinun pitäisi olla ensimmäinen!" #: src/lib/moderation/useGlobalLabelStrings.ts:42 msgid "Non-sexual Nudity" -msgstr "" +msgstr "Ei-seksuaalinen alastomuus" #: src/view/com/modals/SelfLabel.tsx:135 msgid "Not Applicable." @@ -3096,7 +3096,7 @@ msgstr "Voi ei! Jokin meni pieleen." #: src/screens/Onboarding/StepModeration/AdultContentEnabledPref.tsx:127 msgid "OK" -msgstr "" +msgstr "OK" #: src/view/com/auth/login/PasswordUpdatedForm.tsx:41 msgid "Okay" @@ -3143,7 +3143,7 @@ msgstr "Avaa emoji-valitsin" #: src/view/screens/ProfileFeed.tsx:299 msgid "Open feed options menu" -msgstr "" +msgstr "Avaa syötteen asetusvalikko" #: src/view/screens/Settings/index.tsx:734 msgid "Open links with in-app browser" @@ -3151,7 +3151,7 @@ msgstr "Avaa linkit sovelluksen sisäisellä selaimella" #: src/screens/Moderation/index.tsx:229 msgid "Open muted words and tags settings" -msgstr "" +msgstr "Avaa hiljennettyjen sanojen ja aihetunnisteiden asetukset" #: src/view/screens/Moderation.tsx:92 #~ msgid "Open muted words settings" @@ -3172,7 +3172,7 @@ msgstr "Avaa storybook-sivu" #: src/view/screens/Settings/index.tsx:816 msgid "Open system log" -msgstr "" +msgstr "Avaa järjestelmäloki" #: src/view/com/util/forms/DropdownButton.tsx:154 msgid "Opens {numItems} options" @@ -3283,7 +3283,7 @@ msgstr "Avaa näkymän kaikkiin tallennettuihin syötteisiin" #: src/view/screens/Settings/index.tsx:696 msgid "Opens the app password settings" -msgstr "" +msgstr "Avaa sovelluksen salasanojen asetukset" #: src/view/screens/Settings/index.tsx:676 #~ msgid "Opens the app password settings page" @@ -3291,7 +3291,7 @@ msgstr "" #: src/view/screens/Settings/index.tsx:554 msgid "Opens the Following feed preferences" -msgstr "" +msgstr "Avaa Seuratut-syötteen asetukset" #: src/view/screens/Settings/index.tsx:535 #~ msgid "Opens the home feed preferences" @@ -3299,7 +3299,7 @@ msgstr "" #: src/view/com/modals/LinkWarning.tsx:76 msgid "Opens the linked website" -msgstr "" +msgstr "Avaa linkitetyn verkkosivun" #: src/view/screens/Settings/index.tsx:829 #: src/view/screens/Settings/index.tsx:839 @@ -3320,7 +3320,7 @@ msgstr "Asetus {0}/{numItems}" #: src/components/ReportDialog/SubmitView.tsx:162 msgid "Optionally provide additional information below:" -msgstr "" +msgstr "Voit tarvittaessa antaa lisätietoja alla:" #: src/view/com/modals/Threadgate.tsx:89 msgid "Or combine these options:" @@ -3328,7 +3328,7 @@ msgstr "Tai yhdistä nämä asetukset:" #: src/lib/moderation/useReportOptions.ts:25 msgid "Other" -msgstr "" +msgstr "Joku toinen" #: src/view/com/auth/login/ChooseAccountForm.tsx:147 msgid "Other account" @@ -3363,7 +3363,7 @@ msgstr "Salasana" #: src/view/com/modals/ChangePassword.tsx:142 msgid "Password Changed" -msgstr "" +msgstr "Salasana vaihdettu" #: src/view/com/auth/login/Login.tsx:157 msgid "Password updated" @@ -3408,7 +3408,7 @@ msgstr "Kiinnitä etusivulle" #: src/view/screens/ProfileFeed.tsx:294 msgid "Pin to Home" -msgstr "" +msgstr "Kiinnitä etusivulle" #: src/view/screens/SavedFeeds.tsx:88 msgid "Pinned Feeds" @@ -3457,7 +3457,7 @@ msgstr "Anna uniikki nimi tälle sovellussalasanalle tai käytä satunnaisesti l #: src/components/dialogs/MutedWords.tsx:68 msgid "Please enter a valid word, tag, or phrase to mute" -msgstr "" +msgstr "Ole hyvä ja syötä oikea sana, aihetunniste tai lause hiljennettäväksi." #: src/view/com/auth/create/state.ts:170 #~ msgid "Please enter the code you received by SMS." @@ -3487,7 +3487,7 @@ msgstr "" #: src/view/com/modals/AppealLabel.tsx:72 #: src/view/com/modals/AppealLabel.tsx:75 #~ msgid "Please tell us why you think this decision was incorrect." -#~ msgstr "" +#~ msgstr "Kerro meille, miksi uskot tämän päätöksen olleen virheellinen." #: src/view/com/modals/VerifyEmail.tsx:101 msgid "Please Verify Your Email" @@ -3507,7 +3507,7 @@ msgstr "Porno" #: src/lib/moderation/useGlobalLabelStrings.ts:34 msgid "Pornography" -msgstr "" +msgstr "Pornografia" #: src/view/com/composer/Composer.tsx:366 #: src/view/com/composer/Composer.tsx:374 @@ -3541,12 +3541,12 @@ msgstr "Viesti piilotettu" #: src/components/moderation/ModerationDetailsDialog.tsx:98 #: src/lib/moderation/useModerationCauseDescription.ts:99 msgid "Post Hidden by Muted Word" -msgstr "" +msgstr "Viesti piilotettu hiljennetyn sanan takia" #: src/components/moderation/ModerationDetailsDialog.tsx:101 #: src/lib/moderation/useModerationCauseDescription.ts:108 msgid "Post Hidden by You" -msgstr "" +msgstr "Sinun hiljentämä viesti" #: src/view/com/composer/select-language/SelectLangBtn.tsx:87 msgid "Post language" @@ -3571,7 +3571,7 @@ msgstr "Viestit" #: src/components/dialogs/MutedWords.tsx:90 msgid "Posts can be muted based on their text, their tags, or both." -msgstr "Viestejä voidaan hiljentää niiden tekstin, tunnisteiden tai molempien perusteella." +msgstr "Viestejä voidaan hiljentää sanojen, aihetunnisteiden tai molempien perusteella." #: src/view/com/posts/FeedErrorMessage.tsx:64 msgid "Posts hidden" @@ -3583,7 +3583,7 @@ msgstr "Mahdollisesti harhaanjohtava linkki" #: src/components/Lists.tsx:88 msgid "Press to retry" -msgstr "" +msgstr "Paina uudelleen jatkaaksesi" #: src/view/com/lightbox/Lightbox.web.tsx:150 msgid "Previous image" @@ -3617,7 +3617,7 @@ msgstr "Käsitellään..." #: src/view/screens/DebugMod.tsx:888 #: src/view/screens/Profile.tsx:340 msgid "profile" -msgstr "" +msgstr "profiili" #: src/view/shell/bottom-bar/BottomBar.tsx:251 #: src/view/shell/desktop/LeftNav.tsx:419 @@ -3633,7 +3633,7 @@ msgstr "Profiili päivitetty" #: src/view/screens/Settings/index.tsx:983 msgid "Protect your account by verifying your email." -msgstr "Suojaa tilisi vahvistamalla sähköpostiosoitteesi." +msgstr "Suojaa käyttäjätilisi vahvistamalla sähköpostiosoitteesi." #: src/screens/Onboarding/StepFinished.tsx:101 msgid "Public" @@ -3679,7 +3679,7 @@ msgstr "Suhdeluvut" #: src/view/screens/Search/Search.tsx:776 msgid "Recent Searches" -msgstr "" +msgstr "Viimeaikaiset haut" #: src/view/com/auth/onboarding/RecommendedFeeds.tsx:116 msgid "Recommended Feeds" @@ -3704,15 +3704,15 @@ msgstr "Poista" #: src/view/com/util/AccountDropdownBtn.tsx:22 msgid "Remove account" -msgstr "Poista tili" +msgstr "Poista käyttäjätili" #: src/view/com/util/UserAvatar.tsx:358 msgid "Remove Avatar" -msgstr "" +msgstr "Poista avatar" #: src/view/com/util/UserBanner.tsx:148 msgid "Remove Banner" -msgstr "" +msgstr "Poista banneri" #: src/view/com/posts/FeedErrorMessage.tsx:160 msgid "Remove feed" @@ -3720,7 +3720,7 @@ msgstr "Poista syöte" #: src/view/com/posts/FeedErrorMessage.tsx:201 msgid "Remove feed?" -msgstr "" +msgstr "Poista syöte?" #: src/view/com/feeds/FeedSourceCard.tsx:173 #: src/view/com/feeds/FeedSourceCard.tsx:233 @@ -3731,7 +3731,7 @@ msgstr "Poista syötteistäni" #: src/view/com/feeds/FeedSourceCard.tsx:278 msgid "Remove from my feeds?" -msgstr "" +msgstr "Poista syötteistäni?" #: src/view/com/composer/photos/Gallery.tsx:167 msgid "Remove image" @@ -3751,11 +3751,11 @@ msgstr "Poista uudelleenjako" #: src/view/com/feeds/FeedSourceCard.tsx:175 #~ msgid "Remove this feed from my feeds?" -#~ msgstr "Poistetaanko tämä syöte omista syötteistäni?" +#~ msgstr "Poista tämä syöte omista syötteistäni?" #: src/view/com/posts/FeedErrorMessage.tsx:202 msgid "Remove this feed from your saved feeds" -msgstr "" +msgstr "Poista tämä syöte seurannasta" #: src/view/com/posts/FeedErrorMessage.tsx:132 #~ msgid "Remove this feed from your saved feeds?" @@ -3772,7 +3772,7 @@ msgstr "Poistettu syötteistäni" #: src/view/screens/ProfileFeed.tsx:208 msgid "Removed from your feeds" -msgstr "" +msgstr "Poistettu syötteistäsi" #: src/view/com/composer/ExternalEmbed.tsx:71 msgid "Removes default thumbnail from {0}" @@ -3803,46 +3803,46 @@ msgstr "Vastaa käyttäjälle <0/>" #: src/view/com/modals/report/Modal.tsx:166 #~ msgid "Report {collectionName}" -#~ msgstr "Raportoi {collectionName}" +#~ msgstr "Ilmianna {collectionName}" #: src/view/com/profile/ProfileMenu.tsx:319 #: src/view/com/profile/ProfileMenu.tsx:322 msgid "Report Account" -msgstr "Ilmoita tili" +msgstr "Ilmianna käyttäjätili" #: src/view/screens/ProfileFeed.tsx:351 #: src/view/screens/ProfileFeed.tsx:353 msgid "Report feed" -msgstr "Ilmoita syöte" +msgstr "Ilmianna syöte" #: src/view/screens/ProfileList.tsx:429 msgid "Report List" -msgstr "Ilmoita luettelo" +msgstr "Ilmianna luettelo" #: src/view/com/util/forms/PostDropdownBtn.tsx:292 #: src/view/com/util/forms/PostDropdownBtn.tsx:294 msgid "Report post" -msgstr "Ilmoita viesti" +msgstr "Ilmianna viesti" #: src/components/ReportDialog/SelectReportOptionView.tsx:43 msgid "Report this content" -msgstr "" +msgstr "Ilmianna tämä sisältö" #: src/components/ReportDialog/SelectReportOptionView.tsx:56 msgid "Report this feed" -msgstr "" +msgstr "Ilmianna tämä syöte" #: src/components/ReportDialog/SelectReportOptionView.tsx:53 msgid "Report this list" -msgstr "" +msgstr "Ilmianna tämä lista" #: src/components/ReportDialog/SelectReportOptionView.tsx:50 msgid "Report this post" -msgstr "" +msgstr "Ilmianna tämä viesti" #: src/components/ReportDialog/SelectReportOptionView.tsx:47 msgid "Report this user" -msgstr "" +msgstr "Ilmianna tämä käyttäjä" #: src/view/com/modals/Repost.tsx:43 #: src/view/com/modals/Repost.tsx:48 @@ -3897,7 +3897,7 @@ msgstr "Pyydä koodia" #: src/view/screens/Settings/index.tsx:475 msgid "Require alt text before posting" -msgstr "Vaadi vaihtoehtoista ALT-tekstiä ennen julkaisua" +msgstr "Edellytä ALT-tekstiä ennen viestin julkaisua" #: src/view/com/auth/create/Step1.tsx:146 msgid "Required for this provider" @@ -3974,12 +3974,12 @@ msgstr "Palaa edelliselle sivulle" #: src/view/screens/NotFound.tsx:59 msgid "Returns to home page" -msgstr "" +msgstr "Palaa etusivulle" #: src/view/screens/NotFound.tsx:58 #: src/view/screens/ProfileFeed.tsx:112 msgid "Returns to previous page" -msgstr "" +msgstr "Palaa edelliselle sivulle" #: src/view/shell/desktop/RightNav.tsx:55 #~ msgid "SANDBOX. Posts and accounts are not permanent." @@ -4004,7 +4004,7 @@ msgstr "Tallenna vaihtoehtoinen ALT-teksti" #: src/components/dialogs/BirthDateSettings.tsx:119 msgid "Save birthday" -msgstr "" +msgstr "Tallenna syntymäpäivä" #: src/view/com/modals/EditProfile.tsx:232 msgid "Save Changes" @@ -4021,7 +4021,7 @@ msgstr "Tallenna kuvan rajaus" #: src/view/screens/ProfileFeed.tsx:335 #: src/view/screens/ProfileFeed.tsx:341 msgid "Save to my feeds" -msgstr "" +msgstr "Tallenna syötteisiini" #: src/view/screens/SavedFeeds.tsx:122 msgid "Saved Feeds" @@ -4029,11 +4029,11 @@ msgstr "Tallennetut syötteet" #: src/view/com/lightbox/Lightbox.tsx:81 msgid "Saved to your camera roll." -msgstr "" +msgstr "Tallennettu kameraasi" #: src/view/screens/ProfileFeed.tsx:212 msgid "Saved to your feeds" -msgstr "" +msgstr "Tallennettu syötteisiisi" #: src/view/com/modals/EditProfile.tsx:225 msgid "Saves any changes to your profile" @@ -4045,7 +4045,7 @@ msgstr "Tallentaa käyttäjätunnuksen muutoksen muotoon {handle}" #: src/view/com/modals/crop-image/CropImage.web.tsx:145 msgid "Saves image crop settings" -msgstr "" +msgstr "Tallentaa kuvan rajausasetukset" #: src/screens/Onboarding/index.tsx:36 msgid "Science" @@ -4079,19 +4079,19 @@ msgstr "Haku hakusanalla \"{query}\"" #: src/components/TagMenu/index.tsx:145 msgid "Search for all posts by @{authorHandle} with tag {displayTag}" -msgstr "" +msgstr "Hae kaikki @{authorHandle}:n julkaisut, joissa on aihetunniste {displayTag}." #: src/components/TagMenu/index.tsx:145 #~ msgid "Search for all posts by @{authorHandle} with tag {tag}" -#~ msgstr "Etsi kaikki viestit käyttäjältä @{authorHandle} tunnisteella {tag}" +#~ msgstr "Etsi kaikki viestit käyttäjältä @{authorHandle} aihetunnisteella {tag}" #: src/components/TagMenu/index.tsx:94 msgid "Search for all posts with tag {displayTag}" -msgstr "" +msgstr "Etsi kaikki viestit aihetunnisteella {displayTag}." #: src/components/TagMenu/index.tsx:90 #~ msgid "Search for all posts with tag {tag}" -#~ msgstr "Etsi kaikki viestit tunnisteella {tag}" +#~ msgstr "Etsi kaikki viestit aihetunnisteella {tag}" #: src/view/com/auth/LoggedOut.tsx:104 #: src/view/com/auth/LoggedOut.tsx:105 @@ -4113,11 +4113,11 @@ msgstr "Näytä käyttäjän {truncatedTag} viestit" #: src/components/TagMenu/index.tsx:128 msgid "See <0>{displayTag} posts" -msgstr "" +msgstr "Näytä <0>{displayTag} viestit" #: src/components/TagMenu/index.tsx:187 msgid "See <0>{displayTag} posts by this user" -msgstr "" +msgstr "Näytä tämän käyttäjän <0>{displayTag} viestit" #: src/components/TagMenu/index.tsx:128 #~ msgid "See <0>{tag} posts" @@ -4141,7 +4141,7 @@ msgstr "Valitse {item}" #: src/view/com/modals/ServerInput.tsx:75 #~ msgid "Select Bluesky Social" -#~ msgstr "" +#~ msgstr "Valitse Bluesky Social" #: src/view/com/auth/login/Login.tsx:117 msgid "Select from an existing account" @@ -4149,11 +4149,11 @@ msgstr "Valitse olemassa olevalta tililtä" #: src/view/screens/LanguageSettings.tsx:299 msgid "Select languages" -msgstr "" +msgstr "Valitse kielet" #: src/components/ReportDialog/SelectLabelerView.tsx:32 msgid "Select moderator" -msgstr "" +msgstr "Valitse moderaattori" #: src/view/com/util/Selector.tsx:107 msgid "Select option {i} of {numItems}" @@ -4198,11 +4198,11 @@ msgstr "Valitse, mitä kieliä haluat tilattujen syötteidesi sisältävän. Jos #: src/view/screens/LanguageSettings.tsx:98 msgid "Select your app language for the default text to display in the app." -msgstr "" +msgstr "Valitse sovelluksen käyttöliittymän kieli." #: src/screens/Onboarding/StepInterests/index.tsx:196 msgid "Select your interests from the options below" -msgstr "Valitse kiinnostuksenkohteesi alla olevista vaihtoehdoista" +msgstr "Valitse kiinnostuksen kohteesi alla olevista vaihtoehdoista" #: src/view/com/auth/create/Step2.tsx:155 #~ msgid "Select your phone's country" @@ -4210,7 +4210,7 @@ msgstr "Valitse kiinnostuksenkohteesi alla olevista vaihtoehdoista" #: src/view/screens/LanguageSettings.tsx:190 msgid "Select your preferred language for translations in your feed." -msgstr "Valitse haluamasi kieli käännöksille syötteessäsi." +msgstr "Valitse käännösten kieli syötteessäsi." #: src/screens/Onboarding/StepAlgoFeeds/index.tsx:116 msgid "Select your primary algorithmic feeds" @@ -4242,7 +4242,7 @@ msgstr "Lähetä palautetta" #: src/components/ReportDialog/SubmitView.tsx:214 #: src/components/ReportDialog/SubmitView.tsx:218 msgid "Send report" -msgstr "" +msgstr "Lähetä raportti" #: src/view/com/modals/report/SendReportButton.tsx:45 #~ msgid "Send Report" @@ -4272,7 +4272,7 @@ msgstr "Palvelimen osoite" #: src/screens/Moderation/index.tsx:306 msgid "Set birthdate" -msgstr "" +msgstr "Aseta syntymäaika" #: src/view/screens/Settings/index.tsx:488 #~ msgid "Set color theme to dark" @@ -4328,7 +4328,7 @@ msgstr "Aseta tämä asetus \"Kyllä\"-tilaan nähdäksesi esimerkkejä tallenne #: src/screens/Onboarding/Layout.tsx:50 msgid "Set up your account" -msgstr "Luo tili" +msgstr "Luo käyttäjätili" #: src/view/com/modals/ChangeHandle.tsx:266 msgid "Sets Bluesky username" @@ -4336,23 +4336,23 @@ msgstr "Asettaa Bluesky-käyttäjätunnuksen" #: src/view/screens/Settings/index.tsx:507 msgid "Sets color theme to dark" -msgstr "" +msgstr "Muuttaa väriteeman tummaksi" #: src/view/screens/Settings/index.tsx:500 msgid "Sets color theme to light" -msgstr "" +msgstr "Muuttaa väriteeman vaaleaksi" #: src/view/screens/Settings/index.tsx:494 msgid "Sets color theme to system setting" -msgstr "" +msgstr "Muuttaa väriteeman käyttöjärjestelmän mukaiseksi" #: src/view/screens/Settings/index.tsx:533 msgid "Sets dark theme to the dark theme" -msgstr "" +msgstr "Muuttaa tumman väriteeman tummaksi" #: src/view/screens/Settings/index.tsx:526 msgid "Sets dark theme to the dim theme" -msgstr "" +msgstr "Asettaa tumman teeman himmeäksi teemaksi" #: src/view/com/auth/login/ForgotPasswordForm.tsx:157 msgid "Sets email for password reset" @@ -4364,15 +4364,15 @@ msgstr "Asettaa palveluntarjoajan salasanan palautusta varten" #: src/view/com/modals/crop-image/CropImage.web.tsx:123 msgid "Sets image aspect ratio to square" -msgstr "" +msgstr "Asettaa kuvan kuvasuhteen neliöksi" #: src/view/com/modals/crop-image/CropImage.web.tsx:113 msgid "Sets image aspect ratio to tall" -msgstr "" +msgstr "Asettaa kuvan kuvasuhteen korkeaksi" #: src/view/com/modals/crop-image/CropImage.web.tsx:103 msgid "Sets image aspect ratio to wide" -msgstr "" +msgstr "Asettaa kuvan kuvasuhteen leveäksi" #: src/view/com/auth/create/Step1.tsx:97 #: src/view/com/auth/login/LoginForm.tsx:154 @@ -4393,7 +4393,7 @@ msgstr "Erotiikka tai muu aikuisviihde." #: src/lib/moderation/useGlobalLabelStrings.ts:38 msgid "Sexually Suggestive" -msgstr "" +msgstr "Seksuaalisesti vihjaileva" #: src/view/com/lightbox/Lightbox.tsx:141 msgctxt "action" @@ -4412,7 +4412,7 @@ msgstr "Jaa" #: src/view/com/profile/ProfileMenu.tsx:373 #: src/view/com/util/forms/PostDropdownBtn.tsx:347 msgid "Share anyway" -msgstr "" +msgstr "Jaa kuitenkin" #: src/view/screens/ProfileFeed.tsx:361 #: src/view/screens/ProfileFeed.tsx:363 @@ -4518,11 +4518,11 @@ msgstr "Näytä käyttäjät" #: src/lib/moderation/useLabelBehaviorDescription.ts:58 msgid "Show warning" -msgstr "" +msgstr "Näytä varoitus" #: src/lib/moderation/useLabelBehaviorDescription.ts:56 msgid "Show warning and filter from feeds" -msgstr "" +msgstr "Näytä varoitus ja suodata syötteistä" #: src/view/com/profile/ProfileHeader.tsx:462 #~ msgid "Shows a list of users similar to this user." @@ -4626,13 +4626,13 @@ msgstr "Ohjelmistokehitys" #: src/view/com/modals/ProfilePreview.tsx:62 #~ msgid "Something went wrong and we're not sure what." -#~ msgstr "" +#~ msgstr "Jotain meni pieleen, emmekä ole varmoja mitä." #: src/components/ReportDialog/index.tsx:52 #: src/screens/Moderation/index.tsx:116 #: src/screens/Profile/Sections/Labels.tsx:77 msgid "Something went wrong, please try again." -msgstr "" +msgstr "Jotain meni pieleen, yritä uudelleen" #: src/view/com/modals/Waitlist.tsx:51 #~ msgid "Something went wrong. Check your email and try again." @@ -4652,11 +4652,11 @@ msgstr "Lajittele saman viestin vastaukset seuraavasti:" #: src/components/moderation/LabelsOnMeDialog.tsx:147 msgid "Source:" -msgstr "" +msgstr "Lähde:" #: src/lib/moderation/useReportOptions.ts:65 msgid "Spam" -msgstr "" +msgstr "Roskapostia" #: src/lib/moderation/useReportOptions.ts:53 msgid "Spam; excessive mentions or replies" @@ -4723,7 +4723,7 @@ msgstr "Tilaa tämä lista" #: src/view/screens/Search/Search.tsx:375 msgid "Suggested Follows" -msgstr "Ehdotetut seurattavat" +msgstr "Mahdollisia seurattavia" #: src/view/com/profile/ProfileHeaderSuggestedFollows.tsx:65 msgid "Suggested for you" @@ -4741,11 +4741,11 @@ msgstr "Tuki" #: src/view/com/modals/ProfilePreview.tsx:110 #~ msgid "Swipe up to see more" -#~ msgstr "" +#~ msgstr "Pyyhkäise ylöspäin nähdäksesi lisää" #: src/view/com/modals/SwitchAccount.tsx:123 msgid "Switch Account" -msgstr "Vaihda tiliä" +msgstr "Vaihda käyttäjätiliä" #: src/view/com/modals/SwitchAccount.tsx:103 #: src/view/screens/Settings/index.tsx:139 @@ -4767,15 +4767,15 @@ msgstr "Järjestelmäloki" #: src/components/dialogs/MutedWords.tsx:324 msgid "tag" -msgstr "tunniste" +msgstr "aihetunniste" #: src/components/TagMenu/index.tsx:78 msgid "Tag menu: {displayTag}" -msgstr "" +msgstr "Aihetunnistevalikko: {displayTag}" #: src/components/TagMenu/index.tsx:74 #~ msgid "Tag menu: {tag}" -#~ msgstr "Tunnistevalikko: {tag}" +#~ msgstr "Aihetunnistevalikko: {tag}" #: src/view/com/modals/crop-image/CropImage.web.tsx:112 msgid "Tall" @@ -4817,11 +4817,11 @@ msgstr "Tekstikenttä" #: src/components/ReportDialog/SubmitView.tsx:78 msgid "Thank you. Your report has been sent." -msgstr "" +msgstr "Kiitos. Raporttisi on lähetetty." #: src/view/com/modals/ChangeHandle.tsx:466 msgid "That contains the following:" -msgstr "" +msgstr "Se sisältää seuraavaa:" #: src/view/com/auth/create/CreateAccount.tsx:94 msgid "That handle is already taken." @@ -4830,11 +4830,11 @@ msgstr "Tuo käyttätunnus on jo käytössä." #: src/screens/Profile/Header/ProfileHeaderStandard.tsx:274 #: src/view/com/profile/ProfileMenu.tsx:349 msgid "The account will be able to interact with you after unblocking." -msgstr "Tili voi olla vuorovaikutuksessa kanssasi, kun estäminen on poistettu." +msgstr "Käyttäjä voi olla vuorovaikutuksessa kanssasi, kun poistat eston." #: src/components/moderation/ModerationDetailsDialog.tsx:128 msgid "the author" -msgstr "" +msgstr "kirjoittaja" #: src/view/screens/CommunityGuidelines.tsx:36 msgid "The Community Guidelines have been moved to <0/>" @@ -4924,7 +4924,7 @@ msgstr "Ongelma listojesi hakemisessa. Napauta tästä yrittääksesi uudelleen. #: src/components/ReportDialog/SubmitView.tsx:83 msgid "There was an issue sending your report. Please check your internet connection." -msgstr "" +msgstr "Raportin lähettämisessä ilmeni ongelma. Tarkista internet-yhteytesi." #: src/screens/Onboarding/StepModeration/AdultContentEnabledPref.tsx:65 msgid "There was an issue syncing your preferences with the server" @@ -4977,7 +4977,7 @@ msgstr "Tämä {screenDescription} on liputettu:" #: src/components/moderation/ScreenHider.tsx:112 msgid "This account has requested that users sign in to view their profile." -msgstr "Tämä tili pyytää käyttäjiä kirjautumaan sisään nähdäkseen profiilinsa." +msgstr "Tämä käyttäjätili on pyytänyt, että käyttät kirjautuvat sisään nähdäkseen profiilinsa." #: src/components/moderation/LabelsOnMeDialog.tsx:205 msgid "This appeal will be sent to <0>{0}." @@ -4985,11 +4985,11 @@ msgstr "" #: src/lib/moderation/useGlobalLabelStrings.ts:19 msgid "This content has been hidden by the moderators." -msgstr "" +msgstr "Moderaattorit ovat piilottaneet tämän sisällön." #: src/lib/moderation/useGlobalLabelStrings.ts:24 msgid "This content has received a general warning from moderators." -msgstr "" +msgstr "Tämä sisältö on saanut yleisen varoituksen moderaattoreilta." #: src/view/com/modals/EmbedConsent.tsx:68 msgid "This content is hosted by {0}. Do you want to enable external media?" @@ -5064,19 +5064,19 @@ msgstr "Tämä viesti on poistettu." #: src/view/com/util/forms/PostDropdownBtn.tsx:344 msgid "This post is only visible to logged-in users. It won't be visible to people who aren't logged in." -msgstr "" +msgstr "Tämä julkaisu on näkyvissä vain kirjautuneille käyttäjille. Sitä ei näytetä kirjautumattomille henkilöille." #: src/view/com/util/forms/PostDropdownBtn.tsx:326 msgid "This post will be hidden from feeds." -msgstr "" +msgstr "Tämä julkaisu piilotetaan syötteistä." #: src/view/com/profile/ProfileMenu.tsx:370 msgid "This profile is only visible to logged-in users. It won't be visible to people who aren't logged in." -msgstr "" +msgstr "Tämä profiili on näkyvissä vain kirjautuneille käyttäjille. Sitä ei näytetä kirjautumattomille henkilöille." #: src/view/com/auth/create/Policies.tsx:46 msgid "This service has not provided terms of service or a privacy policy." -msgstr "" +msgstr "Tämä palvelu ei ole toimittanut käyttöehtoja tai tietosuojakäytäntöä." #: src/view/com/modals/ChangeHandle.tsx:446 msgid "This should create a domain record at:" @@ -5084,16 +5084,16 @@ msgstr "" #: src/view/com/profile/ProfileFollowers.tsx:95 msgid "This user doesn't have any followers." -msgstr "" +msgstr "Tällä käyttäjällä ei ole yhtään seuraajaa" #: src/components/moderation/ModerationDetailsDialog.tsx:73 #: src/lib/moderation/useModerationCauseDescription.ts:68 msgid "This user has blocked you. You cannot view their content." -msgstr "Tämä käyttäjä on estänyt sinut. Et voi nähdä heidän sisältöään." +msgstr "Tämä käyttäjä on estänyt sinut. Et voi nähdä hänen sisältöä." #: src/lib/moderation/useGlobalLabelStrings.ts:30 msgid "This user has requested that their content only be shown to signed-in users." -msgstr "" +msgstr "Tämä käyttäjä on pyytänyt, että hänen sisältö näkyy vain kirjautuneille" #: src/view/com/modals/ModerationDetails.tsx:42 #~ msgid "This user is included in the <0/> list which you have blocked." @@ -5105,11 +5105,11 @@ msgstr "" #: src/components/moderation/ModerationDetailsDialog.tsx:56 msgid "This user is included in the <0>{0} list which you have blocked." -msgstr "" +msgstr "Tämä käyttäjä on <0>{0}-listassa, jonka olet estänyt." #: src/components/moderation/ModerationDetailsDialog.tsx:85 msgid "This user is included in the <0>{0} list which you have muted." -msgstr "" +msgstr "Tämä käyttäjä on <0>{0}-listassa, jonka olet hiljentänyt." #: src/view/com/modals/ModerationDetails.tsx:74 #~ msgid "This user is included the <0/> list which you have muted." @@ -5117,7 +5117,7 @@ msgstr "" #: src/view/com/profile/ProfileFollows.tsx:94 msgid "This user isn't following anyone." -msgstr "" +msgstr "Tämä käyttäjä ei seuraa ketään." #: src/view/com/modals/SelfLabel.tsx:137 msgid "This warning is only available for posts with media attached." @@ -5133,7 +5133,7 @@ msgstr "Tämä poistaa {0}:n hiljennetyistä sanoistasi. Voit lisätä sen takai #: src/view/screens/Settings/index.tsx:574 msgid "Thread preferences" -msgstr "" +msgstr "Keskusteluketjun asetukset" #: src/view/screens/PreferencesThreads.tsx:53 #: src/view/screens/Settings/index.tsx:584 @@ -5150,7 +5150,7 @@ msgstr "Keskusteluketjujen asetukset" #: src/components/ReportDialog/SelectLabelerView.tsx:35 msgid "To whom would you like to send this report?" -msgstr "" +msgstr "Kenelle haluaisit lähettää tämän raportin?" #: src/components/dialogs/MutedWords.tsx:113 msgid "Toggle between muted word options." @@ -5162,7 +5162,7 @@ msgstr "Vaihda pudotusvalikko" #: src/screens/Moderation/index.tsx:334 msgid "Toggle to enable or disable adult content" -msgstr "" +msgstr "Vaihda ottaaksesi käyttöön tai poistaaksesi käytöstä aikuisille tarkoitettu sisältö." #: src/view/com/modals/EditImage.tsx:271 msgid "Transformations" @@ -5215,12 +5215,12 @@ msgstr "Poista esto" #: src/view/com/profile/ProfileMenu.tsx:299 #: src/view/com/profile/ProfileMenu.tsx:305 msgid "Unblock Account" -msgstr "Poista tilin esto" +msgstr "Poista käyttäjätilin esto" #: src/screens/Profile/Header/ProfileHeaderStandard.tsx:272 #: src/view/com/profile/ProfileMenu.tsx:343 msgid "Unblock Account?" -msgstr "" +msgstr "Poista esto?" #: src/view/com/modals/Repost.tsx:42 #: src/view/com/modals/Repost.tsx:55 @@ -5232,7 +5232,7 @@ msgstr "Kumoa uudelleenjako" #: src/view/com/auth/onboarding/RecommendedFollowsItem.tsx:141 #: src/view/com/profile/ProfileHeaderSuggestedFollows.tsx:246 msgid "Unfollow" -msgstr "" +msgstr "Älä seuraa" #: src/view/com/profile/FollowButton.tsx:60 msgctxt "action" @@ -5246,7 +5246,7 @@ msgstr "Lopeta seuraaminen {0}" #: src/view/com/profile/ProfileMenu.tsx:241 #: src/view/com/profile/ProfileMenu.tsx:251 msgid "Unfollow Account" -msgstr "" +msgstr "Lopeta käyttäjätilin seuraaminen" #: src/view/com/auth/create/state.ts:262 msgid "Unfortunately, you do not meet the requirements to create an account." @@ -5258,7 +5258,7 @@ msgstr "En tykkää" #: src/view/screens/ProfileFeed.tsx:572 msgid "Unlike this feed" -msgstr "" +msgstr "Poista tykkäys tästä syötteestä" #: src/components/TagMenu/index.tsx:249 #: src/view/screens/ProfileList.tsx:579 @@ -5272,11 +5272,11 @@ msgstr "Poista hiljennys {truncatedTag}" #: src/view/com/profile/ProfileMenu.tsx:278 #: src/view/com/profile/ProfileMenu.tsx:284 msgid "Unmute Account" -msgstr "Poista tilin hiljennys" +msgstr "Poista käyttäjätilin hiljennys" #: src/components/TagMenu/index.tsx:208 msgid "Unmute all {displayTag} posts" -msgstr "" +msgstr "Poista hiljennys kaikista {displayTag}-julkaisuista" #: src/components/TagMenu/index.tsx:210 #~ msgid "Unmute all {tag} posts" @@ -5294,7 +5294,7 @@ msgstr "Poista kiinnitys" #: src/view/screens/ProfileFeed.tsx:291 msgid "Unpin from home" -msgstr "" +msgstr "Poista kiinnitys etusivulta" #: src/view/screens/ProfileList.tsx:444 msgid "Unpin moderation list" @@ -5306,7 +5306,7 @@ msgstr "Poista moderointilistan kiinnitys" #: src/screens/Profile/Header/ProfileHeaderLabeler.tsx:220 msgid "Unsubscribe" -msgstr "" +msgstr "Peruuta tilaus" #: src/screens/Profile/Header/ProfileHeaderLabeler.tsx:184 msgid "Unsubscribe from this labeler" @@ -5314,7 +5314,7 @@ msgstr "" #: src/lib/moderation/useReportOptions.ts:70 msgid "Unwanted Sexual Content" -msgstr "" +msgstr "Ei-toivottu seksuaalinen sisältö" #: src/view/com/modals/UserAddRemoveLists.tsx:70 msgid "Update {displayName} in Lists" @@ -5326,7 +5326,7 @@ msgstr "Päivitä {displayName} listoissa" #: src/view/com/modals/ChangeHandle.tsx:509 msgid "Update to {handle}" -msgstr "" +msgstr "Päivitä {handle}"" #: src/view/com/auth/login/SetNewPasswordForm.tsx:204 msgid "Updating..." @@ -5341,23 +5341,23 @@ msgstr "Lataa tekstitiedosto kohteeseen:" #: src/view/com/util/UserBanner.tsx:116 #: src/view/com/util/UserBanner.tsx:119 msgid "Upload from Camera" -msgstr "" +msgstr "Lataa kamerasta" #: src/view/com/util/UserAvatar.tsx:343 #: src/view/com/util/UserBanner.tsx:133 msgid "Upload from Files" -msgstr "" +msgstr "Lataa tiedostoista" #: src/view/com/util/UserAvatar.tsx:337 #: src/view/com/util/UserAvatar.tsx:341 #: src/view/com/util/UserBanner.tsx:127 #: src/view/com/util/UserBanner.tsx:131 msgid "Upload from Library" -msgstr "" +msgstr "Lataa kirjastosta" #: src/view/com/modals/ChangeHandle.tsx:409 msgid "Use a file on your server" -msgstr "" +msgstr "Käytä palvelimellasi olevaa tiedostoa" #: src/view/screens/AppPasswords.tsx:197 msgid "Use app passwords to login to other Bluesky clients without giving full access to your account or password." @@ -5404,15 +5404,15 @@ msgstr "Käyttäjä estetty" #: src/lib/moderation/useModerationCauseDescription.ts:48 msgid "User Blocked by \"{0}\"" -msgstr "" +msgstr "\"{0}\" on estänyt käyttäjän." #: src/components/moderation/ModerationDetailsDialog.tsx:54 msgid "User Blocked by List" -msgstr "Käyttäjä estetty listan vuoksi" +msgstr "Käyttäjä on estetty listalla" #: src/lib/moderation/useModerationCauseDescription.ts:66 msgid "User Blocking You" -msgstr "" +msgstr "Käyttäjä on estänyt sinut" #: src/components/moderation/ModerationDetailsDialog.tsx:71 msgid "User Blocks You" @@ -5435,7 +5435,7 @@ msgstr "Käyttäjälistan on tehnyt <0/>" #: src/view/com/modals/UserAddRemoveLists.tsx:196 #: src/view/screens/ProfileList.tsx:775 msgid "User list by you" -msgstr "Sinun käyttäjälistasi" +msgstr "Käyttäjälistasi" #: src/view/com/modals/CreateOrEditList.tsx:196 msgid "User list created" @@ -5464,11 +5464,11 @@ msgstr "käyttäjät, joita <0/> seuraa" #: src/view/com/modals/Threadgate.tsx:106 msgid "Users in \"{0}\"" -msgstr "Käyttäjät ryhmässä \"{0}\"" +msgstr "Käyttäjät listassa \"{0}\"" #: src/components/LikesDialog.tsx:85 msgid "Users that have liked this content or profile" -msgstr "" +msgstr "Käyttäjät, jotka ovat pitäneet tästä sisällöstä tai profiilista" #: src/view/com/modals/ChangeHandle.tsx:437 msgid "Value:" @@ -5480,7 +5480,7 @@ msgstr "" #: src/view/com/modals/ChangeHandle.tsx:510 msgid "Verify {0}" -msgstr "" +msgstr "Vahvista {0}" #: src/view/screens/Settings/index.tsx:944 msgid "Verify email" @@ -5517,11 +5517,11 @@ msgstr "Katso vianmääritystietue" #: src/components/ReportDialog/SelectReportOptionView.tsx:133 msgid "View details" -msgstr "" +msgstr "Näytä tiedot" #: src/components/ReportDialog/SelectReportOptionView.tsx:128 msgid "View details for reporting a copyright violation" -msgstr "" +msgstr "Näytä tiedot tekijänoikeusrikkomuksen ilmoittamisesta" #: src/view/com/posts/FeedSlice.tsx:99 msgid "View full thread" @@ -5573,7 +5573,7 @@ msgstr "Uskomme myös, että pitäisit Skygazen \"For You\" -syötteestä:" #: src/screens/Hashtag.tsx:132 msgid "We couldn't find any results for that hashtag." -msgstr "" +msgstr "Emme löytäneet tuloksia tuolla aihetunnisteella." #: src/screens/Deactivated.tsx:133 msgid "We estimate {estimatedTime} until your account is ready." @@ -5593,7 +5593,7 @@ msgstr "Emme enää löytäneet viestejä seurattavilta. Tässä on uusin tekij #: src/components/dialogs/MutedWords.tsx:204 msgid "We recommend avoiding common words that appear in many posts, since it can result in no posts being shown." -msgstr "Suosittelemme välttämään yleisiä sanoja, jotka esiintyvät monissa viesteissä. Se voi johtaa siihen, ettei viestejä näytetä." +msgstr "Suosittelemme välttämään yleisiä sanoja, jotka esiintyvät monissa viesteissä. Se voi johtaa siihen, ettei mitään viestejä näytetä." #: src/screens/Onboarding/StepAlgoFeeds/index.tsx:124 msgid "We recommend our \"Discover\" feed:" @@ -5613,7 +5613,7 @@ msgstr "Yhteyden muodostaminen ei onnistunut. Yritä uudelleen jatkaaksesi tilis #: src/screens/Deactivated.tsx:137 msgid "We will let you know when your account is ready." -msgstr "Ilmoitamme sinulle, kun tilisi on valmis." +msgstr "Ilmoitamme sinulle, kun käyttäjätilisi on valmis." #: src/view/com/modals/AppealLabel.tsx:48 #~ msgid "We'll look into your appeal promptly." @@ -5680,23 +5680,23 @@ msgstr "Kuka voi vastata" #: src/components/ReportDialog/SelectReportOptionView.tsx:44 msgid "Why should this content be reviewed?" -msgstr "" +msgstr "Miksi tämä sisältö tulisi arvioida?" #: src/components/ReportDialog/SelectReportOptionView.tsx:57 msgid "Why should this feed be reviewed?" -msgstr "" +msgstr "Miksi tämä syöte tulisi arvioida?" #: src/components/ReportDialog/SelectReportOptionView.tsx:54 msgid "Why should this list be reviewed?" -msgstr "" +msgstr "Miksi tämä lista tulisi arvioida?" #: src/components/ReportDialog/SelectReportOptionView.tsx:51 msgid "Why should this post be reviewed?" -msgstr "" +msgstr "Miksi tämä viesti tulisi arvioida?" #: src/components/ReportDialog/SelectReportOptionView.tsx:48 msgid "Why should this user be reviewed?" -msgstr "" +msgstr "Miksi tämä käyttäjä tulisi arvioida?" #: src/view/com/modals/crop-image/CropImage.web.tsx:102 msgid "Wide" @@ -5761,7 +5761,7 @@ msgstr "Voit nyt kirjautua sisään uudella salasanallasi." #: src/view/com/profile/ProfileFollowers.tsx:94 msgid "You do not have any followers." -msgstr "" +msgstr "Sinulla ei ole kyhtään seuraajaa." #: src/view/com/modals/InviteCodes.tsx:66 msgid "You don't have any invite codes yet! We'll send you some when you've been on Bluesky for a little longer." @@ -5787,7 +5787,7 @@ msgstr "Olet estänyt tekijän tai sinut on estetty tekijän toimesta." #: src/lib/moderation/useModerationCauseDescription.ts:50 #: src/lib/moderation/useModerationCauseDescription.ts:58 msgid "You have blocked this user. You cannot view their content." -msgstr "Olet estänyt tämän käyttäjän. Et voi nähdä heidän sisältöään." +msgstr "Olet estänyt tämän käyttäjän. Et voi nähdä hänen sisältöä." #: src/view/com/auth/login/SetNewPasswordForm.tsx:57 #: src/view/com/auth/login/SetNewPasswordForm.tsx:92 @@ -5848,7 +5848,7 @@ msgstr "" #: src/components/dialogs/MutedWords.tsx:250 msgid "You haven't muted any words or tags yet" -msgstr "Et ole vielä hiljentänyt yhtään sanaa tai tunnistetta" +msgstr "Et ole vielä hiljentänyt yhtään sanaa tai aihetunnistetta" #: src/components/moderation/LabelsOnMeDialog.tsx:69 msgid "You may appeal these labels if you feel they were placed in error." @@ -5903,15 +5903,15 @@ msgstr "Olet saavuttanut syötteesi lopun! Etsi lisää käyttäjiä seurattavak #: src/view/com/auth/create/Step1.tsx:67 msgid "Your account" -msgstr "Tilisi" +msgstr "Käyttäjätilisi" #: src/view/com/modals/DeleteAccount.tsx:67 msgid "Your account has been deleted" -msgstr "Tilisi on poistettu" +msgstr "Käyttäjätilisi on poistettu" #: src/view/screens/Settings/ExportCarDialog.tsx:47 msgid "Your account repository, containing all public data records, can be downloaded as a \"CAR\" file. This file does not include media embeds, such as images, or your private data, which must be fetched separately." -msgstr "Tilisi arkisto, joka sisältää kaikki julkiset tietueet, voidaan ladata \"CAR\"-tiedostona. Tämä tiedosto ei sisällä upotettuja mediaelementtejä, kuten kuvia, tai yksityisiä tietojasi, jotka on haettava erikseen." +msgstr "Käyttäjätilisi arkisto, joka sisältää kaikki julkiset tietueet, voidaan ladata \"CAR\"-tiedostona. Tämä tiedosto ei sisällä upotettuja mediaelementtejä, kuten kuvia, tai yksityisiä tietojasi, jotka on haettava erikseen." #: src/view/com/auth/create/Step1.tsx:215 msgid "Your birth date" diff --git a/src/locale/locales/ga/messages.po b/src/locale/locales/ga/messages.po new file mode 100644 index 0000000000..2e04e4e8e4 --- /dev/null +++ b/src/locale/locales/ga/messages.po @@ -0,0 +1,4637 @@ +msgid "" +msgstr "" +"Project-Id-Version: bsky\n" +"Report-Msgid-Bugs-To: \n" +"POT-Creation-Date: 2023-11-05 16:01-0800\n" +"PO-Revision-Date: 2023-11-05 16:01-0800\n" +"Last-Translator: Kevin Scannell \n" +"Language-Team: Irish \n" +"Language: ga\n" +"MIME-Version: 1.0\n" +"Content-Type: text/plain; charset=UTF-8\n" +"Content-Transfer-Encoding: 8bit\n" +"Plural-Forms: nplurals=5; plural=n==1 ? 0 : n==2 ? 1 : n<7 ? 2 : n < 11 ? 3 : 4\n" + +#: src/view/com/modals/VerifyEmail.tsx:142 +msgid "(no email)" +msgstr "(gan ríomhphost)" + +#: src/view/com/profile/ProfileHeader.tsx:592 +msgid "{following} following" +msgstr "{following} á leanúint" + +#: src/view/screens/Settings.tsx:NaN +#~ msgid "{invitesAvailable} invite code available" +#~ msgstr "{invitesAvailable} chód cuiridh ar fáil" + +#: src/view/screens/Settings.tsx:NaN +#~ msgid "{invitesAvailable} invite codes available" +#~ msgstr "{invitesAvailable} cód cuiridh ar fáil" + +#: src/view/shell/Drawer.tsx:440 +msgid "{numUnreadNotifications} unread" +msgstr "{numUnreadNotifications} gan léamh" + +#: src/view/com/threadgate/WhoCanReply.tsx:158 +msgid "<0/> members" +msgstr "<0/> ball" + +#: src/view/com/profile/ProfileHeader.tsx:594 +msgid "<0>{following} <1>following" +msgstr "<0>{following} <1>á leanúint" + +#: src/view/com/auth/onboarding/RecommendedFeeds.tsx:30 +msgid "<0>Choose your<1>Recommended<2>Feeds" +msgstr "<0>Roghnaigh do chuid<1>Fothaí<2>Molta" + +#: src/view/com/auth/onboarding/RecommendedFollows.tsx:37 +msgid "<0>Follow some<1>Recommended<2>Users" +msgstr "<0>Lean cúpla<1>Úsáideoirí<2>Molta" + +#: src/view/com/auth/onboarding/WelcomeDesktop.tsx:21 +msgid "<0>Welcome to<1>Bluesky" +msgstr "<0>Fáilte go<1>Bluesky" + +#: src/view/com/profile/ProfileHeader.tsx:557 +msgid "⚠Invalid Handle" +msgstr "⚠Leasainm Neamhbhailí" + +#: src/view/com/util/moderation/LabelInfo.tsx:45 +msgid "A content warning has been applied to this {0}." +msgstr "Cuireadh rabhadh ábhair leis an {0} seo." + +#: src/lib/hooks/useOTAUpdate.ts:16 +msgid "A new version of the app is available. Please update to continue using the app." +msgstr "Tá leagan nua den aip ar fáil. Uasdátaigh leis an aip a úsáid anois." + +#: src/view/com/util/ViewHeader.tsx:83 +#: src/view/screens/Search/Search.tsx:624 +msgid "Access navigation links and settings" +msgstr "Oscail nascanna agus socruithe" + +#: src/view/com/pager/FeedsTabBarMobile.tsx:89 +msgid "Access profile and other navigation links" +msgstr "Oscail próifíl agus nascanna eile" + +#: src/view/com/modals/EditImage.tsx:299 +#: src/view/screens/Settings/index.tsx:451 +msgid "Accessibility" +msgstr "Inrochtaineacht" + +#: src/view/com/auth/login/LoginForm.tsx:166 +#: src/view/screens/Settings/index.tsx:308 +#: src/view/screens/Settings/index.tsx:721 +msgid "Account" +msgstr "Cuntas" + +#: src/view/com/profile/ProfileHeader.tsx:245 +msgid "Account blocked" +msgstr "Cuntas blocáilte" + +#: src/view/com/profile/ProfileHeader.tsx:212 +msgid "Account muted" +msgstr "Cuireadh an cuntas i bhfolach" + +#: src/view/com/modals/ModerationDetails.tsx:86 +msgid "Account Muted" +msgstr "Cuireadh an cuntas i bhfolach" + +#: src/view/com/modals/ModerationDetails.tsx:72 +msgid "Account Muted by List" +msgstr "Cuireadh an cuntas i bhfolach trí liosta" + +#: src/view/com/util/AccountDropdownBtn.tsx:41 +msgid "Account options" +msgstr "Roghanna cuntais" + +#: src/view/com/util/AccountDropdownBtn.tsx:25 +msgid "Account removed from quick access" +msgstr "Baineadh an cuntas ón mearliosta" + +#: src/view/com/profile/ProfileHeader.tsx:267 +msgid "Account unblocked" +msgstr "Cuntas díbhlocáilte" + +#: src/view/com/profile/ProfileHeader.tsx:225 +msgid "Account unmuted" +msgstr "Níl an cuntas i bhfolach a thuilleadh" + +#: src/view/com/auth/onboarding/RecommendedFeedsItem.tsx:150 +#: src/view/com/modals/ListAddRemoveUsers.tsx:264 +#: src/view/com/modals/UserAddRemoveLists.tsx:219 +#: src/view/screens/ProfileList.tsx:812 +msgid "Add" +msgstr "Cuir leis" + +#: src/view/com/modals/SelfLabel.tsx:56 +msgid "Add a content warning" +msgstr "Cuir rabhadh faoin ábhar leis" + +#: src/view/screens/ProfileList.tsx:802 +msgid "Add a user to this list" +msgstr "Cuir cuntas leis an liosta seo" + +#: src/view/screens/Settings/index.tsx:383 +#: src/view/screens/Settings/index.tsx:392 +msgid "Add account" +msgstr "Cuir cuntas leis seo" + +#: src/view/com/composer/photos/Gallery.tsx:119 +#: src/view/com/composer/photos/Gallery.tsx:180 +#: src/view/com/modals/AltImage.tsx:116 +msgid "Add alt text" +msgstr "Cuir téacs malartach leis seo" + +#: src/view/screens/AppPasswords.tsx:102 +#: src/view/screens/AppPasswords.tsx:143 +#: src/view/screens/AppPasswords.tsx:156 +msgid "Add App Password" +msgstr "Cuir pasfhocal aipe leis seo" + +#: src/view/com/modals/report/InputIssueDetails.tsx:41 +#: src/view/com/modals/report/Modal.tsx:191 +msgid "Add details" +msgstr "Cuir mionsonraí leis seo" + +#: src/view/com/modals/report/Modal.tsx:194 +msgid "Add details to report" +msgstr "Cuir mionsonraí leis an tuairisc" + +#: src/view/com/composer/Composer.tsx:446 +msgid "Add link card" +msgstr "Cuir cárta leanúna leis seo" + +#: src/view/com/composer/Composer.tsx:451 +msgid "Add link card:" +msgstr "Cuir cárta leanúna leis seo:" + +#: src/view/com/modals/ChangeHandle.tsx:417 +msgid "Add the following DNS record to your domain:" +msgstr "Cuir an taifead DNS seo a leanas le d'fhearann:" + +#: src/view/com/profile/ProfileHeader.tsx:309 +msgid "Add to Lists" +msgstr "Cuir le liostaí" + +#: src/view/com/feeds/FeedSourceCard.tsx:243 +#: src/view/screens/ProfileFeed.tsx:272 +msgid "Add to my feeds" +msgstr "Cuir le mo chuid fothaí" + +#: src/view/com/auth/onboarding/RecommendedFeedsItem.tsx:139 +msgid "Added" +msgstr "Curtha leis" + +#: src/view/com/modals/ListAddRemoveUsers.tsx:191 +#: src/view/com/modals/UserAddRemoveLists.tsx:144 +msgid "Added to list" +msgstr "Curtha leis an liosta" + +#: src/view/com/feeds/FeedSourceCard.tsx:125 +msgid "Added to my feeds" +msgstr "Curtha le mo chuid fothaí" + +#: src/view/screens/PreferencesHomeFeed.tsx:173 +msgid "Adjust the number of likes a reply must have to be shown in your feed." +msgstr "Sonraigh an méid moltaí ar fhreagra atá de dhíth le bheith le feiceáil i d'fhotha." + +#: src/view/com/modals/SelfLabel.tsx:75 +msgid "Adult Content" +msgstr "Ábhar do dhaoine fásta" + +#: src/view/com/modals/ContentFilteringSettings.tsx:141 +msgid "Adult content can only be enabled via the Web at <0/>." +msgstr "Ní féidir ábhar do dhaoine fásta a chur ar fáil ach tríd an nGréasán ag <0/>." + +#: src/screens/Onboarding/StepModeration/AdultContentEnabledPref.tsx:78 +#~ msgid "Adult content can only be enabled via the Web at <0>bsky.app." +#~ msgstr "Ní féidir ábhar do dhaoine fásta a chur ar fáil ach tríd an nGréasán ag <0>bsky.app." + +#: src/view/screens/Settings/index.tsx:664 +msgid "Advanced" +msgstr "Ardleibhéal" + +#: src/view/screens/Feeds.tsx:666 +msgid "All the feeds you've saved, right in one place." +msgstr "Na fothaí go léir a shábháil tú, in áit amháin." + +#: src/view/com/auth/login/ForgotPasswordForm.tsx:221 +#: src/view/com/modals/ChangePassword.tsx:168 +msgid "Already have a code?" +msgstr "An bhfuil cód agat cheana?" + +#: src/view/com/auth/login/ChooseAccountForm.tsx:98 +msgid "Already signed in as @{0}" +msgstr "Logáilte isteach cheana mar @{0}" + +#: src/view/com/composer/photos/Gallery.tsx:130 +msgid "ALT" +msgstr "ALT" + +#: src/view/com/modals/EditImage.tsx:315 +msgid "Alt text" +msgstr "Téacs malartach" + +#: src/view/com/composer/photos/Gallery.tsx:209 +msgid "Alt text describes images for blind and low-vision users, and helps give context to everyone." +msgstr "Cuireann an téacs malartach síos ar na híomhánna do dhaoine atá dall nó a bhfuil lagú radhairc orthu agus cuireann sé an comhthéacs ar fáil do chuile dhuine." + +#: src/view/com/modals/VerifyEmail.tsx:124 +msgid "An email has been sent to {0}. It includes a confirmation code which you can enter below." +msgstr "Cuireadh teachtaireacht ríomhphoist chuig {0}. Tá cód dearbhaithe faoi iamh. Is féidir leat an cód a chur isteach thíos anseo." + +#: src/view/com/modals/ChangeEmail.tsx:119 +msgid "An email has been sent to your previous address, {0}. It includes a confirmation code which you can enter below." +msgstr "Cuireadh teachtaireacht ríomhphoist chuig do sheanseoladh. {0}. Tá cód dearbhaithe faoi iamh." + +#: src/view/com/profile/FollowButton.tsx:30 +#: src/view/com/profile/FollowButton.tsx:40 +msgid "An issue occurred, please try again." +msgstr "Tharla fadhb. Déan iarracht eile, le do thoil." + +#: src/view/com/notifications/FeedItem.tsx:236 +#: src/view/com/threadgate/WhoCanReply.tsx:178 +msgid "and" +msgstr "agus" + +#: src/screens/Onboarding/index.tsx:32 +msgid "Animals" +msgstr "Ainmhithe" + +#: src/view/screens/LanguageSettings.tsx:95 +msgid "App Language" +msgstr "Teanga na haipe" + +#: src/view/screens/AppPasswords.tsx:228 +msgid "App password deleted" +msgstr "Pasfhocal na haipe scriosta" + +#: src/view/com/modals/AddAppPasswords.tsx:134 +msgid "App Password names can only contain letters, numbers, spaces, dashes, and underscores." +msgstr "Ní féidir ach litreacha, uimhreacha, spásanna, daiseanna agus fostríocanna a bheith in ainmneacha phasfhocal na haipe." + +#: src/view/com/modals/AddAppPasswords.tsx:99 +msgid "App Password names must be at least 4 characters long." +msgstr "Caithfear 4 charachtar ar a laghad a bheith in ainmneacha phasfhocal na haipe." + +#: src/view/screens/Settings/index.tsx:675 +msgid "App password settings" +msgstr "Socruithe phasfhocal na haipe" + +#: src/view/screens/Settings.tsx:650 +#~ msgid "App passwords" +#~ msgstr "Pasfhocal na haipe" + +#: src/Navigation.tsx:237 +#: src/view/screens/AppPasswords.tsx:187 +#: src/view/screens/Settings/index.tsx:684 +msgid "App Passwords" +msgstr "Pasfhocal na haipe" + +#: src/view/com/util/forms/PostDropdownBtn.tsx:250 +msgid "Appeal content warning" +msgstr "Déan achomharc in aghaidh rabhadh ábhair." + +#: src/view/com/modals/AppealLabel.tsx:65 +msgid "Appeal Content Warning" +msgstr "Achomharc in aghaidh rabhadh ábhair" + +#: src/view/com/util/moderation/LabelInfo.tsx:52 +msgid "Appeal this decision" +msgstr "Dean achomharc in aghaidh an chinnidh seo" + +#: src/view/com/util/moderation/LabelInfo.tsx:56 +msgid "Appeal this decision." +msgstr "Dean achomharc in aghaidh an chinnidh seo." + +#: src/view/screens/Settings/index.tsx:466 +msgid "Appearance" +msgstr "Cuma" + +#: src/view/screens/AppPasswords.tsx:224 +msgid "Are you sure you want to delete the app password \"{name}\"?" +msgstr "An bhfuil tú cinnte gur mhaith leat pasfhocal na haipe “{name}” a scriosadh?" + +#: src/view/com/composer/Composer.tsx:143 +msgid "Are you sure you'd like to discard this draft?" +msgstr "An bhfuil tú cinnte gur mhaith leat an dréacht seo a scriosadh?" + +#: src/view/screens/ProfileList.tsx:364 +msgid "Are you sure?" +msgstr "Lánchinnte?" + +#: src/view/com/util/forms/PostDropdownBtn.tsx:233 +msgid "Are you sure? This cannot be undone." +msgstr "An bhfuil tú cinnte? Ní féidir é seo a chealú." + +#: src/view/com/composer/select-language/SuggestedLanguage.tsx:60 +msgid "Are you writing in <0>{0}?" +msgstr "An bhfuil tú ag scríobh sa teanga <0>{0}?" + +#: src/screens/Onboarding/index.tsx:26 +msgid "Art" +msgstr "Ealaín" + +#: src/view/com/modals/SelfLabel.tsx:123 +msgid "Artistic or non-erotic nudity." +msgstr "Lomnochtacht ealaíonta nó gan a bheith gáirsiúil." + +#: src/view/com/auth/create/CreateAccount.tsx:154 +#: src/view/com/auth/login/ChooseAccountForm.tsx:151 +#: src/view/com/auth/login/ForgotPasswordForm.tsx:174 +#: src/view/com/auth/login/LoginForm.tsx:259 +#: src/view/com/auth/login/SetNewPasswordForm.tsx:179 +#: src/view/com/modals/report/InputIssueDetails.tsx:46 +#: src/view/com/post-thread/PostThread.tsx:471 +#: src/view/com/post-thread/PostThread.tsx:521 +#: src/view/com/post-thread/PostThread.tsx:529 +#: src/view/com/profile/ProfileHeader.tsx:648 +#: src/view/com/util/ViewHeader.tsx:81 +msgid "Back" +msgstr "Ar ais" + +#: src/view/com/post-thread/PostThread.tsx:479 +msgctxt "action" +msgid "Back" +msgstr "Ar ais" + +#: src/screens/Onboarding/StepSuggestedAccounts/index.tsx:136 +msgid "Based on your interest in {interestsText}" +msgstr "Toisc go bhfuil suim agat in {interestsText}" + +#: src/view/screens/Settings/index.tsx:523 +msgid "Basics" +msgstr "Bunrudaí" + +#: src/view/com/auth/create/Step1.tsx:250 +#: src/view/com/modals/BirthDateSettings.tsx:73 +msgid "Birthday" +msgstr "Breithlá" + +#: src/view/screens/Settings/index.tsx:340 +msgid "Birthday:" +msgstr "Breithlá:" + +#: src/view/com/profile/ProfileHeader.tsx:238 +#: src/view/com/profile/ProfileHeader.tsx:345 +msgid "Block Account" +msgstr "Blocáil an cuntas seo" + +#: src/view/screens/ProfileList.tsx:555 +msgid "Block accounts" +msgstr "Blocáil na cuntais seo" + +#: src/view/screens/ProfileList.tsx:505 +msgid "Block list" +msgstr "Liosta blocála" + +#: src/view/screens/ProfileList.tsx:315 +msgid "Block these accounts?" +msgstr "An bhfuil fonn ort na cuntais seo a bhlocáil?" + +#: src/view/screens/ProfileList.tsx:319 +msgid "Block this List" +msgstr "Blocáil an liosta seo" + +#: src/view/com/lists/ListCard.tsx:109 +#: src/view/com/util/post-embeds/QuoteEmbed.tsx:60 +msgid "Blocked" +msgstr "Blocáilte" + +#: src/view/screens/Moderation.tsx:123 +msgid "Blocked accounts" +msgstr "Cuntais bhlocáilte" + +#: src/Navigation.tsx:130 +#: src/view/screens/ModerationBlockedAccounts.tsx:107 +msgid "Blocked Accounts" +msgstr "Cuntais bhlocáilte" + +#: src/view/com/profile/ProfileHeader.tsx:240 +msgid "Blocked accounts cannot reply in your threads, mention you, or otherwise interact with you." +msgstr "Ní féidir leis na cuntais bhlocáilte freagra a thabhairt ar do chomhráite, tagairt a dhéanamh duit, ná aon phlé eile a bheith acu leat." + +#: src/view/screens/ModerationBlockedAccounts.tsx:115 +msgid "Blocked accounts cannot reply in your threads, mention you, or otherwise interact with you. You will not see their content and they will be prevented from seeing yours." +msgstr "Ní féidir leis na cuntais bhlocáilte freagra a thabhairt ar do chomhráite, tagairt a dhéanamh duit, ná aon phlé eile a bheith acu leat. Ní fheicfidh tú a gcuid ábhair agus ní fheicfidh siad do chuid ábhair." + +#: src/view/com/post-thread/PostThread.tsx:324 +msgid "Blocked post." +msgstr "Postáil bhlocáilte." + +#: src/view/screens/ProfileList.tsx:317 +msgid "Blocking is public. Blocked accounts cannot reply in your threads, mention you, or otherwise interact with you." +msgstr "Tá an bhlocáil poiblí. Ní féidir leis na cuntais bhlocáilte freagra a thabhairt ar do chomhráite, tagairt a dhéanamh duit, ná aon phlé eile a bheith acu leat." + +#: src/view/com/auth/HomeLoggedOutCTA.tsx:93 +#: src/view/com/auth/SplashScreen.web.tsx:133 +msgid "Blog" +msgstr "Blag" + +#: src/view/com/auth/HomeLoggedOutCTA.tsx:31 +#: src/view/com/auth/server-input/index.tsx:89 +#: src/view/com/auth/server-input/index.tsx:90 +msgid "Bluesky" +msgstr "Bluesky" + +#: src/view/com/auth/server-input/index.tsx:150 +msgid "Bluesky is an open network where you can choose your hosting provider. Custom hosting is now available in beta for developers." +msgstr "Is líonra oscailte é Bluesky, lenar féidir leat do sholáthraí óstála féin a roghnú. Tá leagan béite d'óstáil shaincheaptha ar fáil d'fhorbróirí anois." + +#: src/view/com/auth/onboarding/WelcomeDesktop.tsx:80 +#: src/view/com/auth/onboarding/WelcomeMobile.tsx:80 +msgid "Bluesky is flexible." +msgstr "Tá Bluesky solúbtha." + +#: src/view/com/auth/onboarding/WelcomeDesktop.tsx:69 +#: src/view/com/auth/onboarding/WelcomeMobile.tsx:69 +msgid "Bluesky is open." +msgstr "Tá Bluesky oscailte." + +#: src/view/com/auth/onboarding/WelcomeDesktop.tsx:56 +#: src/view/com/auth/onboarding/WelcomeMobile.tsx:56 +msgid "Bluesky is public." +msgstr "Tá Bluesky poiblí." + +#: src/view/com/modals/Waitlist.tsx:70 +msgid "Bluesky uses invites to build a healthier community. If you don't know anybody with an invite, you can sign up for the waitlist and we'll send one soon." +msgstr "Baineann Bluesky úsáid as cuirí le pobal níos sláintiúla a thógáil. Mura bhfuil aithne agat ar dhuine a bhfuil cuireadh acu is féidir leat d’ainm a chur ar an liosta feithimh agus cuirfidh muid cuireadh chugat roimh i bhfad." + +#: src/view/screens/Moderation.tsx:226 +msgid "Bluesky will not show your profile and posts to logged-out users. Other apps may not honor this request. This does not make your account private." +msgstr "Ní thaispeánfaidh Bluesky do phróifíl ná do chuid postálacha d’úsáideoirí atá logáilte amach. Is féidir nach gcloífidh aipeanna eile leis an iarratas seo. I bhfocail eile, ní bheidh do chuntas anseo príobháideach." + +#: src/view/com/modals/ServerInput.tsx:78 +#~ msgid "Bluesky.Social" +#~ msgstr "Bluesky.Social" + +#: src/screens/Onboarding/index.tsx:33 +msgid "Books" +msgstr "Leabhair" + +#: src/view/screens/Settings/index.tsx:859 +msgid "Build version {0} {1}" +msgstr "Leagan {0} {1}" + +#: src/view/com/auth/HomeLoggedOutCTA.tsx:87 +#: src/view/com/auth/SplashScreen.web.tsx:128 +msgid "Business" +msgstr "Gnó" + +#: src/view/com/modals/ServerInput.tsx:115 +#~ msgid "Button disabled. Input custom domain to proceed." +#~ msgstr "Cnaipe as feidhm. Úsáid sainfhearann le leanúint ar aghaidh." + +#: src/view/com/profile/ProfileSubpageHeader.tsx:157 +msgid "by —" +msgstr "le —" + +#: src/view/com/auth/onboarding/RecommendedFeedsItem.tsx:100 +msgid "by {0}" +msgstr "le {0}" + +#: src/view/com/profile/ProfileSubpageHeader.tsx:161 +msgid "by <0/>" +msgstr "le <0/>" + +#: src/view/com/profile/ProfileSubpageHeader.tsx:159 +msgid "by you" +msgstr "leat" + +#: src/view/com/composer/photos/OpenCameraBtn.tsx:60 +#: src/view/com/util/UserAvatar.tsx:224 +#: src/view/com/util/UserBanner.tsx:40 +msgid "Camera" +msgstr "Ceamara" + +#: src/view/com/modals/AddAppPasswords.tsx:216 +msgid "Can only contain letters, numbers, spaces, dashes, and underscores. Must be at least 4 characters long, but no more than 32 characters long." +msgstr "Ní féidir ach litreacha, uimhreacha, spásanna, daiseanna agus fostríocanna a bheith ann. Caithfear 4 charachtar ar a laghad a bheith ann agus gan níos mó ná 32 charachtar." + +#: src/components/Prompt.tsx:91 +#: src/view/com/composer/Composer.tsx:300 +#: src/view/com/composer/Composer.tsx:305 +#: src/view/com/modals/ChangeEmail.tsx:218 +#: src/view/com/modals/ChangeEmail.tsx:220 +#: src/view/com/modals/ChangePassword.tsx:265 +#: src/view/com/modals/ChangePassword.tsx:268 +#: src/view/com/modals/CreateOrEditList.tsx:355 +#: src/view/com/modals/EditImage.tsx:323 +#: src/view/com/modals/EditProfile.tsx:249 +#: src/view/com/modals/InAppBrowserConsent.tsx:78 +#: src/view/com/modals/LinkWarning.tsx:87 +#: src/view/com/modals/Repost.tsx:87 +#: src/view/com/modals/VerifyEmail.tsx:247 +#: src/view/com/modals/VerifyEmail.tsx:253 +#: src/view/com/modals/Waitlist.tsx:142 +#: src/view/screens/Search/Search.tsx:693 +#: src/view/shell/desktop/Search.tsx:238 +msgid "Cancel" +msgstr "Cealaigh" + +#: src/view/com/modals/Confirm.tsx:88 +#: src/view/com/modals/Confirm.tsx:91 +#: src/view/com/modals/CreateOrEditList.tsx:360 +#: src/view/com/modals/DeleteAccount.tsx:156 +#: src/view/com/modals/DeleteAccount.tsx:234 +msgctxt "action" +msgid "Cancel" +msgstr "Cealaigh" + +#: src/view/com/modals/DeleteAccount.tsx:152 +#: src/view/com/modals/DeleteAccount.tsx:230 +msgid "Cancel account deletion" +msgstr "Ná scrios an chuntas" + +#: src/view/com/modals/ChangeHandle.tsx:149 +msgid "Cancel change handle" +msgstr "Ná hathraigh an leasainm" + +#: src/view/com/modals/crop-image/CropImage.web.tsx:134 +msgid "Cancel image crop" +msgstr "Cealaigh bearradh na híomhá" + +#: src/view/com/modals/EditProfile.tsx:244 +msgid "Cancel profile editing" +msgstr "Cealaigh eagarthóireacht na próifíle" + +#: src/view/com/modals/Repost.tsx:78 +msgid "Cancel quote post" +msgstr "Ná déan athlua na postála" + +#: src/view/com/modals/ListAddRemoveUsers.tsx:87 +#: src/view/shell/desktop/Search.tsx:234 +msgid "Cancel search" +msgstr "Cealaigh an cuardach" + +#: src/view/com/modals/Waitlist.tsx:136 +msgid "Cancel waitlist signup" +msgstr "Ná sábháil d’ainm ar an liosta feithimh" + +#: src/view/screens/Settings/index.tsx:334 +msgctxt "action" +msgid "Change" +msgstr "Athraigh" + +#: src/view/screens/Settings/index.tsx:696 +msgid "Change handle" +msgstr "Athraigh mo leasainm" + +#: src/view/com/modals/ChangeHandle.tsx:161 +#: src/view/screens/Settings/index.tsx:705 +msgid "Change Handle" +msgstr "Athraigh mo leasainm" + +#: src/view/com/modals/VerifyEmail.tsx:147 +msgid "Change my email" +msgstr "Athraigh mo ríomhphost" + +#: src/view/screens/Settings/index.tsx:732 +msgid "Change password" +msgstr "Athraigh mo phasfhocal" + +#: src/view/screens/Settings/index.tsx:741 +msgid "Change Password" +msgstr "Athraigh mo phasfhocal" + +#: src/view/com/composer/select-language/SuggestedLanguage.tsx:73 +msgid "Change post language to {0}" +msgstr "Athraigh an teanga phostála go {0}" + +#: src/view/screens/Settings/index.tsx:733 +msgid "Change your Bluesky password" +msgstr "Athraigh do phasfhocal Bluesky" + +#: src/view/com/modals/ChangeEmail.tsx:109 +msgid "Change Your Email" +msgstr "Athraigh do ríomhphost" + +#: src/screens/Deactivated.tsx:72 +#: src/screens/Deactivated.tsx:76 +msgid "Check my status" +msgstr "Seiceáil mo stádas" + +#: src/view/com/auth/onboarding/RecommendedFeeds.tsx:121 +msgid "Check out some recommended feeds. Tap + to add them to your list of pinned feeds." +msgstr "Cuir súil ar na fothaí seo. Brúigh + len iad a chur le liosta na bhfothaí atá greamaithe agat." + +#: src/view/com/auth/onboarding/RecommendedFollows.tsx:185 +msgid "Check out some recommended users. Follow them to see similar users." +msgstr "Cuir súil ar na húsáideoirí seo. Lean iad le húsáideoirí atá cosúil leo a fheiceáil." + +#: src/view/com/modals/DeleteAccount.tsx:169 +msgid "Check your inbox for an email with the confirmation code to enter below:" +msgstr "Féach ar do bhosca ríomhphoist le haghaidh teachtaireachta leis an gcód dearbhaithe atá le cur isteach thíos." + +#: src/view/com/modals/Threadgate.tsx:72 +msgid "Choose \"Everybody\" or \"Nobody\"" +msgstr "Roghnaigh “Chuile Dhuine” nó “Duine Ar Bith”" + +#: src/view/screens/Settings/index.tsx:697 +msgid "Choose a new Bluesky username or create" +msgstr "Roghnaigh leasainm Bluesky nua nó cruthaigh leasainm" + +#: src/view/com/auth/server-input/index.tsx:79 +msgid "Choose Service" +msgstr "Roghnaigh Seirbhís" + +#: src/screens/Onboarding/StepFinished.tsx:135 +msgid "Choose the algorithms that power your custom feeds." +msgstr "Roghnaigh na halgartaim le haghaidh do chuid sainfhothaí." + +#: src/view/com/auth/onboarding/WelcomeDesktop.tsx:83 +#: src/view/com/auth/onboarding/WelcomeMobile.tsx:83 +msgid "Choose the algorithms that power your experience with custom feeds." +msgstr "Roghnaigh na halgartaim a shainíonn an dóigh a n-oibríonn do chuid sainfhothaí." + +#: src/screens/Onboarding/StepAlgoFeeds/index.tsx:103 +#~ msgid "Choose your algorithmic feeds" +#~ msgstr "Roghnaigh do chuid fothaí algartamacha" + +#: src/screens/Onboarding/StepAlgoFeeds/index.tsx:103 +msgid "Choose your main feeds" +msgstr "Roghnaigh do phríomhfhothaí" + +#: src/view/com/auth/create/Step1.tsx:219 +msgid "Choose your password" +msgstr "Roghnaigh do phasfhocal" + +#: src/view/screens/Settings/index.tsx:834 +#: src/view/screens/Settings/index.tsx:835 +msgid "Clear all legacy storage data" +msgstr "Glan na sonraí oidhreachta ar fad atá i dtaisce." + +#: src/view/screens/Settings/index.tsx:837 +msgid "Clear all legacy storage data (restart after this)" +msgstr "Glan na sonraí oidhreachta ar fad atá i dtaisce. Ansin atosaigh." + +#: src/view/screens/Settings/index.tsx:846 +#: src/view/screens/Settings/index.tsx:847 +msgid "Clear all storage data" +msgstr "Glan na sonraí ar fad atá i dtaisce." + +#: src/view/screens/Settings/index.tsx:849 +msgid "Clear all storage data (restart after this)" +msgstr "Glan na sonraí ar fad atá i dtaisce. Ansin atosaigh." + +#: src/view/com/util/forms/SearchInput.tsx:88 +#: src/view/screens/Search/Search.tsx:674 +msgid "Clear search query" +msgstr "Glan an cuardach" + +#: src/view/screens/Support.tsx:40 +msgid "click here" +msgstr "cliceáil anseo" + +#: src/screens/Onboarding/index.tsx:35 +msgid "Climate" +msgstr "Aeráid" + +#: src/view/com/modals/ChangePassword.tsx:265 +#: src/view/com/modals/ChangePassword.tsx:268 +msgid "Close" +msgstr "Dún" + +#: src/components/Dialog/index.web.tsx:78 +msgid "Close active dialog" +msgstr "Dún an dialóg oscailte" + +#: src/view/com/auth/login/PasswordUpdatedForm.tsx:38 +msgid "Close alert" +msgstr "Dún an rabhadh" + +#: src/view/com/util/BottomSheetCustomBackdrop.tsx:33 +msgid "Close bottom drawer" +msgstr "Dún an tarraiceán íochtair" + +#: src/view/com/lightbox/ImageViewing/components/ImageDefaultHeader.tsx:26 +msgid "Close image" +msgstr "Dún an íomhá" + +#: src/view/com/lightbox/Lightbox.web.tsx:119 +msgid "Close image viewer" +msgstr "Dún amharcóir na n-íomhánna" + +#: src/view/shell/index.web.tsx:49 +msgid "Close navigation footer" +msgstr "Dún an buntásc" + +#: src/view/shell/index.web.tsx:50 +msgid "Closes bottom navigation bar" +msgstr "Dúnann sé seo an barra nascleanúna ag an mbun" + +#: src/view/com/auth/login/PasswordUpdatedForm.tsx:39 +msgid "Closes password update alert" +msgstr "Dúnann sé seo an rabhadh faoi uasdátú an phasfhocail" + +#: src/view/com/composer/Composer.tsx:302 +msgid "Closes post composer and discards post draft" +msgstr "Dúnann sé seo cumadóir na postálacha agus ní shábhálann sé an dréacht" + +#: src/view/com/lightbox/ImageViewing/components/ImageDefaultHeader.tsx:27 +msgid "Closes viewer for header image" +msgstr "Dúnann sé seo an t-amharcóir le haghaidh íomhá an cheanntáisc" + +#: src/view/com/notifications/FeedItem.tsx:317 +msgid "Collapses list of users for a given notification" +msgstr "Laghdaíonn sé seo liosta na n-úsáideoirí le haghaidh an fhógra sin" + +#: src/screens/Onboarding/index.tsx:41 +msgid "Comedy" +msgstr "Greann" + +#: src/screens/Onboarding/index.tsx:27 +msgid "Comics" +msgstr "Greannáin" + +#: src/Navigation.tsx:227 +#: src/view/screens/CommunityGuidelines.tsx:32 +msgid "Community Guidelines" +msgstr "Treoirlínte an phobail" + +#: src/screens/Onboarding/StepFinished.tsx:148 +msgid "Complete onboarding and start using your account" +msgstr "Críochnaigh agus tosaigh ag baint úsáide as do chuntas." + +#: src/view/com/auth/create/Step3.tsx:73 +msgid "Complete the challenge" +msgstr "Freagair an dúshlán" + +#: src/view/com/composer/Composer.tsx:417 +msgid "Compose posts up to {MAX_GRAPHEME_LENGTH} characters in length" +msgstr "Scríobh postálacha chomh fada le {MAX_GRAPHEME_LENGTH} litir agus carachtair eile" + +#: src/view/com/composer/Prompt.tsx:24 +msgid "Compose reply" +msgstr "Scríobh freagra" + +#: src/screens/Onboarding/StepModeration/ModerationOption.tsx:67 +msgid "Configure content filtering setting for category: {0}" +msgstr "Socraigh scagadh an ábhair le haghaidh catagóir: {0}" + +#: src/components/Prompt.tsx:113 +#: src/view/com/modals/AppealLabel.tsx:98 +#: src/view/com/modals/SelfLabel.tsx:154 +#: src/view/com/modals/VerifyEmail.tsx:231 +#: src/view/com/modals/VerifyEmail.tsx:233 +#: src/view/screens/PreferencesHomeFeed.tsx:308 +#: src/view/screens/PreferencesThreads.tsx:159 +msgid "Confirm" +msgstr "Dearbhaigh" + +#: src/view/com/modals/Confirm.tsx:75 +#: src/view/com/modals/Confirm.tsx:78 +msgctxt "action" +msgid "Confirm" +msgstr "Dearbhaigh" + +#: src/view/com/modals/ChangeEmail.tsx:193 +#: src/view/com/modals/ChangeEmail.tsx:195 +msgid "Confirm Change" +msgstr "Dearbhaigh an t-athrú" + +#: src/view/com/modals/lang-settings/ConfirmLanguagesButton.tsx:34 +msgid "Confirm content language settings" +msgstr "Dearbhaigh socruithe le haghaidh teanga an ábhair" + +#: src/view/com/modals/DeleteAccount.tsx:220 +msgid "Confirm delete account" +msgstr "Dearbhaigh scriosadh an chuntais" + +#: src/view/com/modals/ContentFilteringSettings.tsx:156 +msgid "Confirm your age to enable adult content." +msgstr "Dearbhaigh d’aois chun ábhar do dhaoine fásta a fháil." + +#: src/view/com/modals/ChangeEmail.tsx:157 +#: src/view/com/modals/DeleteAccount.tsx:182 +#: src/view/com/modals/VerifyEmail.tsx:165 +msgid "Confirmation code" +msgstr "Cód dearbhaithe" + +#: src/view/com/modals/Waitlist.tsx:120 +msgid "Confirms signing up {email} to the waitlist" +msgstr "Dearbhaíonn sé seo go gcuirfear {email} leis an liosta feithimh" + +#: src/view/com/auth/create/CreateAccount.tsx:189 +#: src/view/com/auth/login/LoginForm.tsx:278 +msgid "Connecting..." +msgstr "Ag nascadh…" + +#: src/view/com/auth/create/CreateAccount.tsx:209 +msgid "Contact support" +msgstr "Teagmháil le Support" + +#: src/view/screens/Moderation.tsx:81 +msgid "Content filtering" +msgstr "Scagadh ábhair" + +#: src/view/com/modals/ContentFilteringSettings.tsx:44 +msgid "Content Filtering" +msgstr "Scagadh Ábhair" + +#: src/view/com/modals/lang-settings/ContentLanguagesSettings.tsx:74 +#: src/view/screens/LanguageSettings.tsx:278 +msgid "Content Languages" +msgstr "Teangacha ábhair" + +#: src/view/com/modals/ModerationDetails.tsx:65 +msgid "Content Not Available" +msgstr "Ábhar nach bhfuil ar fáil" + +#: src/view/com/modals/ModerationDetails.tsx:33 +#: src/view/com/util/moderation/ScreenHider.tsx:78 +msgid "Content Warning" +msgstr "Rabhadh ábhair" + +#: src/view/com/composer/labels/LabelsBtn.tsx:31 +msgid "Content warnings" +msgstr "Rabhadh ábhair" + +#: src/screens/Onboarding/StepAlgoFeeds/index.tsx:170 +#: src/screens/Onboarding/StepFollowingFeed.tsx:153 +#: src/screens/Onboarding/StepInterests/index.tsx:248 +#: src/screens/Onboarding/StepModeration/index.tsx:118 +#: src/screens/Onboarding/StepTopicalFeeds.tsx:108 +#: src/view/com/auth/onboarding/RecommendedFeeds.tsx:148 +#: src/view/com/auth/onboarding/RecommendedFollows.tsx:209 +msgid "Continue" +msgstr "Lean ar aghaidh" + +#: src/screens/Onboarding/StepFollowingFeed.tsx:150 +#: src/screens/Onboarding/StepInterests/index.tsx:245 +#: src/screens/Onboarding/StepModeration/index.tsx:115 +#: src/screens/Onboarding/StepTopicalFeeds.tsx:105 +msgid "Continue to next step" +msgstr "Lean ar aghaidh go dtí an chéad chéim eile" + +#: src/screens/Onboarding/StepAlgoFeeds/index.tsx:167 +msgid "Continue to the next step" +msgstr "Lean ar aghaidh go dtí an chéad chéim eile" + +#: src/screens/Onboarding/StepSuggestedAccounts/index.tsx:191 +msgid "Continue to the next step without following any accounts" +msgstr "Lean ar aghaidh go dtí an chéad chéim eile gan aon chuntas a leanúint" + +#: src/screens/Onboarding/index.tsx:44 +msgid "Cooking" +msgstr "Cócaireacht" + +#: src/view/com/modals/AddAppPasswords.tsx:195 +#: src/view/com/modals/InviteCodes.tsx:182 +msgid "Copied" +msgstr "Cóipeáilte" + +#: src/view/screens/Settings/index.tsx:241 +msgid "Copied build version to clipboard" +msgstr "Leagan cóipeáilte sa ghearrthaisce" + +#: src/view/com/modals/AddAppPasswords.tsx:76 +#: src/view/com/modals/InviteCodes.tsx:152 +#: src/view/com/util/forms/PostDropdownBtn.tsx:112 +msgid "Copied to clipboard" +msgstr "Cóipeáilte sa ghearrthaisce" + +#: src/view/com/modals/AddAppPasswords.tsx:189 +msgid "Copies app password" +msgstr "Cóipeálann sé seo pasfhocal na haipe" + +#: src/view/com/modals/AddAppPasswords.tsx:188 +msgid "Copy" +msgstr "Cóipeáil" + +#: src/view/screens/ProfileList.tsx:417 +msgid "Copy link to list" +msgstr "Cóipeáil an nasc leis an liosta" + +#: src/view/com/util/forms/PostDropdownBtn.tsx:153 +msgid "Copy link to post" +msgstr "Cóipeáil an nasc leis an bpostáil" + +#: src/view/com/profile/ProfileHeader.tsx:294 +msgid "Copy link to profile" +msgstr "Cóipeáil an nasc leis an bpróifíl" + +#: src/view/com/util/forms/PostDropdownBtn.tsx:139 +msgid "Copy post text" +msgstr "Cóipeáil téacs na postála" + +#: src/Navigation.tsx:232 +#: src/view/screens/CopyrightPolicy.tsx:29 +msgid "Copyright Policy" +msgstr "An polasaí maidir le cóipcheart" + +#: src/view/screens/ProfileFeed.tsx:96 +msgid "Could not load feed" +msgstr "Ní féidir an fotha a lódáil" + +#: src/view/screens/ProfileList.tsx:888 +msgid "Could not load list" +msgstr "Ní féidir an liosta a lódáil" + +#: src/view/com/auth/create/Step2.tsx:91 +#~ msgid "Country" +#~ msgstr "Tír" + +#: src/view/com/auth/HomeLoggedOutCTA.tsx:62 +#: src/view/com/auth/SplashScreen.tsx:71 +#: src/view/com/auth/SplashScreen.web.tsx:81 +msgid "Create a new account" +msgstr "Cruthaigh cuntas nua" + +#: src/view/screens/Settings/index.tsx:384 +msgid "Create a new Bluesky account" +msgstr "Cruthaigh cuntas nua Bluesky" + +#: src/view/com/auth/create/CreateAccount.tsx:129 +msgid "Create Account" +msgstr "Cruthaigh cuntas" + +#: src/view/com/modals/AddAppPasswords.tsx:226 +msgid "Create App Password" +msgstr "Cruthaigh pasfhocal aipe" + +#: src/view/com/auth/HomeLoggedOutCTA.tsx:54 +#: src/view/com/auth/SplashScreen.tsx:68 +msgid "Create new account" +msgstr "Cruthaigh cuntas nua" + +#: src/view/screens/AppPasswords.tsx:249 +msgid "Created {0}" +msgstr "Cruthaíodh {0}" + +#: src/view/screens/ProfileFeed.tsx:616 +msgid "Created by <0/>" +msgstr "Cruthaithe ag <0/>" + +#: src/view/screens/ProfileFeed.tsx:614 +msgid "Created by you" +msgstr "Cruthaithe agat" + +#: src/view/com/composer/Composer.tsx:448 +msgid "Creates a card with a thumbnail. The card links to {url}" +msgstr "Cruthaíonn sé seo cárta le mionsamhail. Nascann an cárta le {url}." + +#: src/screens/Onboarding/index.tsx:29 +msgid "Culture" +msgstr "Cultúr" + +#: src/view/com/auth/server-input/index.tsx:95 +#: src/view/com/auth/server-input/index.tsx:96 +msgid "Custom" +msgstr "Saincheaptha" + +#: src/view/com/modals/ChangeHandle.tsx:389 +msgid "Custom domain" +msgstr "Sainfhearann" + +#: src/screens/Onboarding/StepAlgoFeeds/index.tsx:106 +#: src/view/screens/Feeds.tsx:692 +msgid "Custom feeds built by the community bring you new experiences and help you find the content you love." +msgstr "Cruthaíonn an pobal fothaí chun eispéiris nua a chur ar fáil duit, agus chun cabhrú leat teacht ar an ábhar a thaitníonn leat" + +#: src/view/screens/PreferencesExternalEmbeds.tsx:55 +msgid "Customize media from external sites." +msgstr "Oiriúnaigh na meáin ó shuíomhanna seachtracha" + +#: src/view/screens/Settings.tsx:687 +#~ msgid "Danger Zone" +#~ msgstr "Limistéar Contúirte" + +#: src/view/screens/Settings/index.tsx:485 +#: src/view/screens/Settings/index.tsx:511 +msgid "Dark" +msgstr "Dorcha" + +#: src/view/screens/Debug.tsx:63 +msgid "Dark mode" +msgstr "Modh dorcha" + +#: src/view/screens/Settings/index.tsx:498 +msgid "Dark Theme" +msgstr "Téama Dorcha" + +#: src/view/screens/Debug.tsx:83 +msgid "Debug panel" +msgstr "Painéal dífhabhtaithe" + +#: src/view/screens/Settings/index.tsx:772 +msgid "Delete account" +msgstr "Scrios an cuntas" + +#: src/view/com/modals/DeleteAccount.tsx:87 +msgid "Delete Account" +msgstr "Scrios an Cuntas" + +#: src/view/screens/AppPasswords.tsx:222 +#: src/view/screens/AppPasswords.tsx:242 +msgid "Delete app password" +msgstr "Scrios pasfhocal na haipe" + +#: src/view/screens/ProfileList.tsx:363 +#: src/view/screens/ProfileList.tsx:444 +msgid "Delete List" +msgstr "Scrios an liosta" + +#: src/view/com/modals/DeleteAccount.tsx:223 +msgid "Delete my account" +msgstr "Scrios mo chuntas" + +#: src/view/screens/Settings.tsx:706 +#~ msgid "Delete my account…" +#~ msgstr "Scrios mo chuntas" + +#: src/view/screens/Settings/index.tsx:784 +msgid "Delete My Account…" +msgstr "Scrios mo chuntas…" + +#: src/view/com/util/forms/PostDropdownBtn.tsx:228 +msgid "Delete post" +msgstr "Scrios an phostáil" + +#: src/view/com/util/forms/PostDropdownBtn.tsx:232 +msgid "Delete this post?" +msgstr "An bhfuil fonn ort an phostáil seo a scriosadh?" + +#: src/view/com/util/post-embeds/QuoteEmbed.tsx:69 +msgid "Deleted" +msgstr "Scriosta" + +#: src/view/com/post-thread/PostThread.tsx:316 +msgid "Deleted post." +msgstr "Scriosadh an phostáil." + +#: src/view/com/modals/CreateOrEditList.tsx:300 +#: src/view/com/modals/CreateOrEditList.tsx:321 +#: src/view/com/modals/EditProfile.tsx:198 +#: src/view/com/modals/EditProfile.tsx:210 +msgid "Description" +msgstr "Cur síos" + +#: src/view/screens/Settings.tsx:760 +#~ msgid "Developer Tools" +#~ msgstr "Áiseanna forbróra" + +#: src/view/com/composer/Composer.tsx:211 +msgid "Did you want to say anything?" +msgstr "Ar mhaith leat rud éigin a rá?" + +#: src/view/screens/Settings/index.tsx:504 +msgid "Dim" +msgstr "Breacdhorcha" + +#: src/view/com/composer/Composer.tsx:144 +msgid "Discard" +msgstr "Ná sábháil" + +#: src/view/com/composer/Composer.tsx:138 +msgid "Discard draft" +msgstr "Ná sábháil an dréacht" + +#: src/view/screens/Moderation.tsx:207 +msgid "Discourage apps from showing my account to logged-out users" +msgstr "Cuir ina luí ar aipeanna gan mo chuntas a thaispeáint d'úsáideoirí atá logáilte amach" + +#: src/view/com/posts/FollowingEmptyState.tsx:74 +#: src/view/com/posts/FollowingEndOfFeed.tsx:75 +msgid "Discover new custom feeds" +msgstr "Aimsigh sainfhothaí nua" + +#: src/view/screens/Feeds.tsx:473 +#~ msgid "Discover new feeds" +#~ msgstr "Aimsigh fothaí nua" + +#: src/view/screens/Feeds.tsx:689 +msgid "Discover New Feeds" +msgstr "Aimsigh Fothaí Nua" + +#: src/view/com/modals/EditProfile.tsx:192 +msgid "Display name" +msgstr "Ainm taispeána" + +#: src/view/com/modals/EditProfile.tsx:180 +msgid "Display Name" +msgstr "Ainm Taispeána" + +#: src/view/com/modals/ChangeHandle.tsx:487 +msgid "Domain verified!" +msgstr "Fearann dearbhaithe!" + +#: src/view/com/auth/create/Step1.tsx:170 +msgid "Don't have an invite code?" +msgstr "Níl cód cuiridh agat?" + +#: src/view/com/auth/onboarding/RecommendedFollows.tsx:86 +#: src/view/com/modals/EditImage.tsx:333 +#: src/view/com/modals/ListAddRemoveUsers.tsx:144 +#: src/view/com/modals/SelfLabel.tsx:157 +#: src/view/com/modals/Threadgate.tsx:129 +#: src/view/com/modals/Threadgate.tsx:132 +#: src/view/com/modals/UserAddRemoveLists.tsx:95 +#: src/view/com/modals/UserAddRemoveLists.tsx:98 +#: src/view/screens/PreferencesThreads.tsx:162 +msgctxt "action" +msgid "Done" +msgstr "Déanta" + +#: src/view/com/auth/server-input/index.tsx:165 +#: src/view/com/auth/server-input/index.tsx:166 +#: src/view/com/modals/AddAppPasswords.tsx:226 +#: src/view/com/modals/AltImage.tsx:139 +#: src/view/com/modals/ContentFilteringSettings.tsx:88 +#: src/view/com/modals/ContentFilteringSettings.tsx:96 +#: src/view/com/modals/crop-image/CropImage.web.tsx:152 +#: src/view/com/modals/InviteCodes.tsx:80 +#: src/view/com/modals/InviteCodes.tsx:123 +#: src/view/com/modals/ListAddRemoveUsers.tsx:142 +#: src/view/screens/PreferencesHomeFeed.tsx:311 +#: src/view/screens/Settings/ExportCarDialog.tsx:93 +#: src/view/screens/Settings/ExportCarDialog.tsx:94 +msgid "Done" +msgstr "Déanta" + +#: src/view/com/modals/lang-settings/ConfirmLanguagesButton.tsx:42 +msgid "Done{extraText}" +msgstr "Déanta{extraText}" + +#: src/view/com/auth/login/ChooseAccountForm.tsx:45 +msgid "Double tap to sign in" +msgstr "Tapáil faoi dhó le logáil isteach" + +#: src/view/screens/Settings/index.tsx:755 +msgid "Download Bluesky account data (repository)" +msgstr "Íoslódáil na sonraí ó do chuntas Bluesky (cartlann)" + +#: src/view/screens/Settings/ExportCarDialog.tsx:59 +#: src/view/screens/Settings/ExportCarDialog.tsx:63 +msgid "Download CAR file" +msgstr "Íoslódáil comhad CAR" + +#: src/view/com/composer/text-input/TextInput.web.tsx:247 +msgid "Drop to add images" +msgstr "Scaoil anseo chun íomhánna a chur leis" + +#: src/screens/Onboarding/StepModeration/AdultContentEnabledPref.tsx:111 +msgid "Due to Apple policies, adult content can only be enabled on the web after completing sign up." +msgstr "De bharr pholasaí Apple, ní féidir ábhar do dhaoine fásta ar an nGréasán a fháil roimh an logáil isteach a chríochnú." + +#: src/view/com/modals/EditProfile.tsx:185 +msgid "e.g. Alice Roberts" +msgstr "m.sh. Cáit Ní Dhuibhir" + +#: src/view/com/modals/EditProfile.tsx:203 +msgid "e.g. Artist, dog-lover, and avid reader." +msgstr "m.sh. Ealaíontóir, File, Eolaí" + +#: src/view/com/modals/CreateOrEditList.tsx:283 +msgid "e.g. Great Posters" +msgstr "m.sh. Na cuntais is fearr" + +#: src/view/com/modals/CreateOrEditList.tsx:284 +msgid "e.g. Spammers" +msgstr "m.sh. Seoltóirí turscair" + +#: src/view/com/modals/CreateOrEditList.tsx:312 +msgid "e.g. The posters who never miss." +msgstr "m.sh. Na cuntais nach dteipeann orthu riamh" + +#: src/view/com/modals/CreateOrEditList.tsx:313 +msgid "e.g. Users that repeatedly reply with ads." +msgstr "m.sh. Úsáideoirí a fhreagraíonn le fógraí" + +#: src/view/com/modals/InviteCodes.tsx:96 +msgid "Each code works once. You'll receive more invite codes periodically." +msgstr "Oibríonn gach cód uair amháin. Gheobhaidh tú tuilleadh cód go tráthrialta." + +#: src/view/com/lists/ListMembers.tsx:149 +msgctxt "action" +msgid "Edit" +msgstr "Eagar" + +#: src/view/com/composer/photos/Gallery.tsx:144 +#: src/view/com/modals/EditImage.tsx:207 +msgid "Edit image" +msgstr "Cuir an íomhá seo in eagar" + +#: src/view/screens/ProfileList.tsx:432 +msgid "Edit list details" +msgstr "Athraigh mionsonraí an liosta" + +#: src/view/com/modals/CreateOrEditList.tsx:250 +msgid "Edit Moderation List" +msgstr "Athraigh liosta na modhnóireachta" + +#: src/Navigation.tsx:242 +#: src/view/screens/Feeds.tsx:434 +#: src/view/screens/SavedFeeds.tsx:84 +msgid "Edit My Feeds" +msgstr "Athraigh mo chuid fothaí" + +#: src/view/com/modals/EditProfile.tsx:152 +msgid "Edit my profile" +msgstr "Athraigh mo phróifíl" + +#: src/view/com/profile/ProfileHeader.tsx:417 +msgid "Edit profile" +msgstr "Athraigh an phróifíl" + +#: src/view/com/profile/ProfileHeader.tsx:422 +msgid "Edit Profile" +msgstr "Athraigh an Phróifíl" + +#: src/view/screens/Feeds.tsx:355 +msgid "Edit Saved Feeds" +msgstr "Athraigh na fothaí sábháilte" + +#: src/view/com/modals/CreateOrEditList.tsx:245 +msgid "Edit User List" +msgstr "Athraigh an liosta d’úsáideoirí" + +#: src/view/com/modals/EditProfile.tsx:193 +msgid "Edit your display name" +msgstr "Athraigh d’ainm taispeána" + +#: src/view/com/modals/EditProfile.tsx:211 +msgid "Edit your profile description" +msgstr "Athraigh an cur síos ort sa phróifíl" + +#: src/screens/Onboarding/index.tsx:34 +msgid "Education" +msgstr "Oideachas" + +#: src/view/com/auth/create/Step1.tsx:199 +#: src/view/com/auth/login/ForgotPasswordForm.tsx:156 +#: src/view/com/modals/ChangeEmail.tsx:141 +#: src/view/com/modals/Waitlist.tsx:88 +msgid "Email" +msgstr "Ríomhphost" + +#: src/view/com/auth/create/Step1.tsx:190 +#: src/view/com/auth/login/ForgotPasswordForm.tsx:147 +msgid "Email address" +msgstr "Seoladh ríomhphoist" + +#: src/view/com/modals/ChangeEmail.tsx:56 +#: src/view/com/modals/ChangeEmail.tsx:88 +msgid "Email updated" +msgstr "Seoladh ríomhphoist uasdátaithe" + +#: src/view/com/modals/ChangeEmail.tsx:111 +msgid "Email Updated" +msgstr "Seoladh ríomhphoist uasdátaithe" + +#: src/view/com/modals/VerifyEmail.tsx:78 +msgid "Email verified" +msgstr "Ríomhphost dearbhaithe" + +#: src/view/screens/Settings/index.tsx:312 +msgid "Email:" +msgstr "Ríomhphost:" + +#: src/view/com/modals/EmbedConsent.tsx:113 +msgid "Enable {0} only" +msgstr "Cuir {0} amháin ar fáil" + +#: src/view/com/modals/ContentFilteringSettings.tsx:167 +msgid "Enable Adult Content" +msgstr "Cuir ábhar do dhaoine fásta ar fáil" + +#: src/screens/Onboarding/StepModeration/AdultContentEnabledPref.tsx:76 +#: src/screens/Onboarding/StepModeration/AdultContentEnabledPref.tsx:77 +msgid "Enable adult content in your feeds" +msgstr "Cuir ábhar do dhaoine fásta ar fáil i do chuid fothaí" + +#: src/view/com/modals/EmbedConsent.tsx:97 +msgid "Enable External Media" +msgstr "Cuir meáin sheachtracha ar fáil" + +#: src/view/screens/PreferencesExternalEmbeds.tsx:75 +msgid "Enable media players for" +msgstr "Cuir seinnteoirí na meán ar fáil le haghaidh" + +#: src/view/screens/PreferencesHomeFeed.tsx:147 +msgid "Enable this setting to only see replies between people you follow." +msgstr "Cuir an socrú seo ar siúl le gan ach freagraí i measc na ndaoine a leanann tú a fheiceáil." + +#: src/view/screens/Profile.tsx:455 +msgid "End of feed" +msgstr "Deireadh an fhotha" + +#: src/view/com/modals/AddAppPasswords.tsx:166 +msgid "Enter a name for this App Password" +msgstr "Cuir isteach ainm don phasfhocal aipe seo" + +#: src/view/com/modals/VerifyEmail.tsx:105 +msgid "Enter Confirmation Code" +msgstr "Cuir isteach an cód dearbhaithe" + +#: src/view/com/modals/ChangePassword.tsx:151 +msgid "Enter the code you received to change your password." +msgstr "Cuir isteach an cód a fuair tú chun do phasfhocal a athrú." + +#: src/view/com/modals/ChangeHandle.tsx:371 +msgid "Enter the domain you want to use" +msgstr "Cuir isteach an fearann is maith leat a úsáid" + +#: src/view/com/auth/login/ForgotPasswordForm.tsx:107 +msgid "Enter the email you used to create your account. We'll send you a \"reset code\" so you can set a new password." +msgstr "Cuir isteach an seoladh ríomhphoist a d’úsáid tú le do chuntas a chruthú. Cuirfidh muid “cód athshocraithe” chugat le go mbeidh tú in ann do phasfhocal a athrú." + +#: src/view/com/auth/create/Step1.tsx:251 +#: src/view/com/modals/BirthDateSettings.tsx:74 +msgid "Enter your birth date" +msgstr "Cuir isteach do bhreithlá" + +#: src/view/com/modals/Waitlist.tsx:78 +msgid "Enter your email" +msgstr "Cuir isteach do sheoladh ríomhphoist" + +#: src/view/com/auth/create/Step1.tsx:195 +msgid "Enter your email address" +msgstr "Cuir isteach do sheoladh ríomhphoist" + +#: src/view/com/modals/ChangeEmail.tsx:41 +msgid "Enter your new email above" +msgstr "Cuir isteach do sheoladh ríomhphoist nua thuas" + +#: src/view/com/modals/ChangeEmail.tsx:117 +msgid "Enter your new email address below." +msgstr "Cuir isteach do sheoladh ríomhphoist nua thíos." + +#: src/view/com/auth/create/Step2.tsx:188 +#~ msgid "Enter your phone number" +#~ msgstr "Cuir isteach d’uimhir ghutháin" + +#: src/view/com/auth/login/Login.tsx:99 +msgid "Enter your username and password" +msgstr "Cuir isteach do leasainm agus do phasfhocal" + +#: src/view/com/auth/create/Step3.tsx:67 +msgid "Error receiving captcha response." +msgstr "Earráid agus an freagra ar an captcha á phróiseáil." + +#: src/view/screens/Search/Search.tsx:109 +msgid "Error:" +msgstr "Earráid:" + +#: src/view/com/modals/Threadgate.tsx:76 +msgid "Everybody" +msgstr "Chuile dhuine" + +#: src/view/com/modals/ChangeHandle.tsx:150 +msgid "Exits handle change process" +msgstr "Fágann sé seo athrú do leasainm" + +#: src/view/com/lightbox/Lightbox.web.tsx:120 +msgid "Exits image view" +msgstr "Fágann sé seo an radharc ar an íomhá" + +#: src/view/com/modals/ListAddRemoveUsers.tsx:88 +#: src/view/shell/desktop/Search.tsx:235 +msgid "Exits inputting search query" +msgstr "Fágann sé seo an cuardach" + +#: src/view/com/modals/Waitlist.tsx:138 +msgid "Exits signing up for waitlist with {email}" +msgstr "Fágann sé seo an síniú ar an liosta feithimh le {email}" + +#: src/view/com/lightbox/Lightbox.web.tsx:163 +msgid "Expand alt text" +msgstr "Taispeáin an téacs malartach ina iomláine" + +#: src/view/com/composer/ComposerReplyTo.tsx:81 +#: src/view/com/composer/ComposerReplyTo.tsx:84 +msgid "Expand or collapse the full post you are replying to" +msgstr "Leathnaigh nó laghdaigh an téacs iomlán a bhfuil tú ag freagairt" + +#: src/view/screens/Settings/index.tsx:753 +msgid "Export my data" +msgstr "Easpórtáil mo chuid sonraí" + +#: src/view/screens/Settings/ExportCarDialog.tsx:44 +#: src/view/screens/Settings/index.tsx:764 +msgid "Export My Data" +msgstr "Easpórtáil mo chuid sonraí" + +#: src/view/com/modals/EmbedConsent.tsx:64 +msgid "External Media" +msgstr "Meáin sheachtracha" + +#: src/view/com/modals/EmbedConsent.tsx:75 +#: src/view/screens/PreferencesExternalEmbeds.tsx:66 +msgid "External media may allow websites to collect information about you and your device. No information is sent or requested until you press the \"play\" button." +msgstr "Is féidir le meáin sheachtracha cumas a thabhairt do shuíomhanna ar an nGréasán eolas fútsa agus faoi do ghléas a chnuasach. Ní sheoltar ná iarrtar aon eolas go dtí go mbrúnn tú an cnaipe “play”." + +#: src/Navigation.tsx:258 +#: src/view/screens/PreferencesExternalEmbeds.tsx:52 +#: src/view/screens/Settings/index.tsx:657 +msgid "External Media Preferences" +msgstr "Roghanna maidir le meáin sheachtracha" + +#: src/view/screens/Settings/index.tsx:648 +msgid "External media settings" +msgstr "Socruithe maidir le meáin sheachtracha" + +#: src/view/com/modals/AddAppPasswords.tsx:115 +#: src/view/com/modals/AddAppPasswords.tsx:119 +msgid "Failed to create app password." +msgstr "Teip ar phasfhocal aipe a chruthú." + +#: src/view/com/modals/CreateOrEditList.tsx:206 +msgid "Failed to create the list. Check your internet connection and try again." +msgstr "Teip ar chruthú an liosta. Seiceáil do nasc leis an idirlíon agus déan iarracht eile." + +#: src/view/com/util/forms/PostDropdownBtn.tsx:88 +msgid "Failed to delete post, please try again" +msgstr "Teip ar scriosadh na postála. Déan iarracht eile." + +#: src/view/com/auth/onboarding/RecommendedFeeds.tsx:109 +#: src/view/com/auth/onboarding/RecommendedFeeds.tsx:141 +msgid "Failed to load recommended feeds" +msgstr "Teip ar lódáil na bhfothaí molta" + +#: src/Navigation.tsx:192 +msgid "Feed" +msgstr "Fotha" + +#: src/view/com/feeds/FeedSourceCard.tsx:229 +msgid "Feed by {0}" +msgstr "Fotha le {0}" + +#: src/view/screens/Feeds.tsx:605 +msgid "Feed offline" +msgstr "Fotha as líne" + +#: src/view/com/feeds/FeedPage.tsx:143 +msgid "Feed Preferences" +msgstr "Roghanna fotha" + +#: src/view/shell/desktop/RightNav.tsx:61 +#: src/view/shell/Drawer.tsx:311 +msgid "Feedback" +msgstr "Aiseolas" + +#: src/Navigation.tsx:442 +#: src/view/screens/Feeds.tsx:419 +#: src/view/screens/Feeds.tsx:524 +#: src/view/screens/Profile.tsx:184 +#: src/view/shell/bottom-bar/BottomBar.tsx:181 +#: src/view/shell/desktop/LeftNav.tsx:342 +#: src/view/shell/Drawer.tsx:476 +#: src/view/shell/Drawer.tsx:477 +msgid "Feeds" +msgstr "Fothaí" + +#: src/screens/Onboarding/StepAlgoFeeds/index.tsx:106 +#~ msgid "Feeds are created by users and can give you entirely new experiences." +#~ msgstr "Cruthaíonn úsáideoirí fothaí a d'fhéadfadh eispéiris úrnua a thabhairt duit." + +#: src/screens/Onboarding/StepAlgoFeeds/index.tsx:106 +#~ msgid "Feeds are created by users and organizations. They offer you varied experiences and suggest content you may like using algorithms." +#~ msgstr "Is iad úsáideoirí agus eagraíochtaí a chruthaíonn na fothaí. Is féidir leo radharcanna úrnua a oscailt duit." + +#: src/view/com/auth/onboarding/RecommendedFeeds.tsx:57 +msgid "Feeds are created by users to curate content. Choose some feeds that you find interesting." +msgstr "Is iad na húsáideoirí a chruthaíonn na fothaí le hábhar is spéis leo a chur ar fáil. Roghnaigh cúpla fotha a bhfuil suim agat iontu." + +#: src/view/screens/SavedFeeds.tsx:156 +msgid "Feeds are custom algorithms that users build with a little coding expertise. <0/> for more information." +msgstr "Is sainalgartaim iad na fothaí. Cruthaíonn úsáideoirí a bhfuil beagán taithí acu ar chódáil iad. <0/> le tuilleadh eolais a fháil." + +#: src/screens/Onboarding/StepTopicalFeeds.tsx:70 +msgid "Feeds can be topical as well!" +msgstr "Is féidir le fothaí a bheith bunaithe ar chúrsaí reatha freisin!" + +#: src/screens/Onboarding/StepFinished.tsx:151 +msgid "Finalizing" +msgstr "Ag cur crích air" + +#: src/view/com/posts/CustomFeedEmptyState.tsx:47 +#: src/view/com/posts/FollowingEmptyState.tsx:57 +#: src/view/com/posts/FollowingEndOfFeed.tsx:58 +msgid "Find accounts to follow" +msgstr "Aimsigh fothaí le leanúint" + +#: src/view/screens/Search/Search.tsx:439 +msgid "Find users on Bluesky" +msgstr "Aimsigh úsáideoirí ar Bluesky" + +#: src/view/screens/Search/Search.tsx:437 +msgid "Find users with the search tool on the right" +msgstr "Aimsigh úsáideoirí leis an uirlis chuardaigh ar dheis" + +#: src/view/com/auth/onboarding/RecommendedFollowsItem.tsx:150 +msgid "Finding similar accounts..." +msgstr "Cuntais eile atá cosúil leis seo á n-aimsiú..." + +#: src/view/screens/PreferencesHomeFeed.tsx:111 +msgid "Fine-tune the content you see on your home screen." +msgstr "Mionathraigh an t-ábhar a fheiceann tú ar do scáileán baile." + +#: src/view/screens/PreferencesThreads.tsx:60 +msgid "Fine-tune the discussion threads." +msgstr "Mionathraigh na snáitheanna chomhrá" + +#: src/screens/Onboarding/index.tsx:38 +msgid "Fitness" +msgstr "Folláine" + +#: src/screens/Onboarding/StepFinished.tsx:131 +msgid "Flexible" +msgstr "Solúbtha" + +#: src/view/com/modals/EditImage.tsx:115 +msgid "Flip horizontal" +msgstr "Iompaigh go cothrománach é" + +#: src/view/com/modals/EditImage.tsx:120 +#: src/view/com/modals/EditImage.tsx:287 +msgid "Flip vertically" +msgstr "Iompaigh go hingearach é" + +#: src/screens/Onboarding/StepSuggestedAccounts/index.tsx:181 +#: src/view/com/post-thread/PostThreadFollowBtn.tsx:136 +#: src/view/com/profile/ProfileHeader.tsx:512 +msgid "Follow" +msgstr "Lean" + +#: src/view/com/profile/FollowButton.tsx:64 +msgctxt "action" +msgid "Follow" +msgstr "Lean" + +#: src/screens/Onboarding/StepSuggestedAccounts/index.tsx:58 +#: src/view/com/post-thread/PostThreadFollowBtn.tsx:122 +#: src/view/com/profile/ProfileHeader.tsx:503 +msgid "Follow {0}" +msgstr "Lean {0}" + +#: src/screens/Onboarding/StepSuggestedAccounts/index.tsx:179 +msgid "Follow All" +msgstr "Lean iad uile" + +#: src/screens/Onboarding/StepSuggestedAccounts/index.tsx:174 +msgid "Follow selected accounts and continue to the next step" +msgstr "Lean na cuntais roghnaithe agus téigh ar aghaidh go dtí an chéad chéim eile" + +#: src/view/com/auth/onboarding/RecommendedFollows.tsx:64 +msgid "Follow some users to get started. We can recommend you more users based on who you find interesting." +msgstr "Lean cúpla cuntas mar thosú. Tig linn níos mó úsáideoirí a mholadh duit a mbeadh suim agat iontu." + +#: src/view/com/profile/ProfileCard.tsx:194 +msgid "Followed by {0}" +msgstr "Leanta ag {0}" + +#: src/view/com/modals/Threadgate.tsx:98 +msgid "Followed users" +msgstr "Cuntais a leanann tú" + +#: src/view/screens/PreferencesHomeFeed.tsx:154 +msgid "Followed users only" +msgstr "Cuntais a leanann tú amháin" + +#: src/view/com/notifications/FeedItem.tsx:166 +msgid "followed you" +msgstr "— lean sé/sí thú" + +#: src/view/screens/ProfileFollowers.tsx:25 +msgid "Followers" +msgstr "Leantóirí" + +#: src/view/com/post-thread/PostThreadFollowBtn.tsx:136 +#: src/view/com/profile/ProfileHeader.tsx:494 +#: src/view/screens/ProfileFollows.tsx:25 +msgid "Following" +msgstr "Á leanúint" + +#: src/view/com/profile/ProfileHeader.tsx:148 +msgid "Following {0}" +msgstr "Ag leanúint {0}" + +#: src/view/com/profile/ProfileHeader.tsx:545 +msgid "Follows you" +msgstr "Leanann sé/sí thú" + +#: src/view/com/profile/ProfileCard.tsx:141 +msgid "Follows You" +msgstr "Leanann sé/sí thú" + +#: src/screens/Onboarding/index.tsx:43 +msgid "Food" +msgstr "Bia" + +#: src/view/com/modals/DeleteAccount.tsx:111 +msgid "For security reasons, we'll need to send a confirmation code to your email address." +msgstr "Ar chúiseanna slándála, beidh orainn cód dearbhaithe a chur chuig do sheoladh ríomhphoist." + +#: src/view/com/modals/AddAppPasswords.tsx:209 +msgid "For security reasons, you won't be able to view this again. If you lose this password, you'll need to generate a new one." +msgstr "Ar chúiseanna slándála, ní bheidh tú in ann é seo a fheiceáil arís. Má chailleann tú an pasfhocal seo beidh ort ceann nua a chruthú." + +#: src/view/com/auth/login/LoginForm.tsx:241 +msgid "Forgot" +msgstr "Dearmadta" + +#: src/view/com/auth/login/LoginForm.tsx:238 +msgid "Forgot password" +msgstr "Pasfhocal dearmadta" + +#: src/view/com/auth/login/Login.tsx:127 +#: src/view/com/auth/login/Login.tsx:143 +msgid "Forgot Password" +msgstr "Pasfhocal dearmadta" + +#: src/view/com/posts/FeedItem.tsx:186 +msgctxt "from-feed" +msgid "From <0/>" +msgstr "Ó <0/>" + +#: src/view/com/composer/photos/SelectPhotoBtn.tsx:43 +msgid "Gallery" +msgstr "Gailearaí" + +#: src/view/com/modals/VerifyEmail.tsx:189 +#: src/view/com/modals/VerifyEmail.tsx:191 +msgid "Get Started" +msgstr "Ar aghaidh leat anois!" + +#: src/view/com/auth/LoggedOut.tsx:81 +#: src/view/com/auth/LoggedOut.tsx:82 +#: src/view/com/util/moderation/ScreenHider.tsx:123 +#: src/view/shell/desktop/LeftNav.tsx:104 +msgid "Go back" +msgstr "Ar ais" + +#: src/view/screens/ProfileFeed.tsx:105 +#: src/view/screens/ProfileFeed.tsx:110 +#: src/view/screens/ProfileList.tsx:897 +#: src/view/screens/ProfileList.tsx:902 +msgid "Go Back" +msgstr "Ar ais" + +#: src/screens/Onboarding/Layout.tsx:104 +#: src/screens/Onboarding/Layout.tsx:193 +msgid "Go back to previous step" +msgstr "Fill ar an gcéim roimhe seo" + +#: src/view/screens/Search/Search.tsx:724 +#: src/view/shell/desktop/Search.tsx:262 +msgid "Go to @{queryMaybeHandle}" +msgstr "Téigh go dtí @{queryMaybeHandle}" + +#: src/view/com/auth/login/ForgotPasswordForm.tsx:189 +#: src/view/com/auth/login/ForgotPasswordForm.tsx:218 +#: src/view/com/auth/login/LoginForm.tsx:288 +#: src/view/com/auth/login/SetNewPasswordForm.tsx:195 +#: src/view/com/modals/ChangePassword.tsx:165 +msgid "Go to next" +msgstr "Téigh go dtí an chéad rud eile" + +#: src/view/com/modals/ChangeHandle.tsx:265 +msgid "Handle" +msgstr "Leasainm" + +#: src/view/com/auth/create/CreateAccount.tsx:204 +msgid "Having trouble?" +msgstr "Fadhb ort?" + +#: src/view/shell/desktop/RightNav.tsx:90 +#: src/view/shell/Drawer.tsx:321 +msgid "Help" +msgstr "Cúnamh" + +#: src/screens/Onboarding/StepSuggestedAccounts/index.tsx:132 +msgid "Here are some accounts for you to follow" +msgstr "Seo cúpla cuntas le leanúint duit" + +#: src/screens/Onboarding/StepTopicalFeeds.tsx:79 +msgid "Here are some popular topical feeds. You can choose to follow as many as you like." +msgstr "Seo cúpla fotha a bhfuil ráchairt orthu. Is féidir leat an méid acu is mian leat a leanúint." + +#: src/screens/Onboarding/StepTopicalFeeds.tsx:74 +msgid "Here are some topical feeds based on your interests: {interestsText}. You can choose to follow as many as you like." +msgstr "Seo cúpla fotha a phléann le rudaí a bhfuil suim agat iontu: {interestsText}. Is féidir leat an méid acu is mian leat a leanúint." + +#: src/view/com/modals/AddAppPasswords.tsx:153 +msgid "Here is your app password." +msgstr "Seo é do phasfhocal aipe." + +#: src/screens/Onboarding/StepModeration/ModerationOption.tsx:41 +#: src/view/com/modals/ContentFilteringSettings.tsx:251 +#: src/view/com/util/moderation/ContentHider.tsx:105 +#: src/view/com/util/moderation/PostHider.tsx:108 +msgid "Hide" +msgstr "Cuir i bhfolach" + +#: src/view/com/modals/ContentFilteringSettings.tsx:224 +#: src/view/com/notifications/FeedItem.tsx:325 +msgctxt "action" +msgid "Hide" +msgstr "Cuir i bhfolach" + +#: src/view/com/util/forms/PostDropdownBtn.tsx:187 +msgid "Hide post" +msgstr "Cuir an phostáil seo i bhfolach" + +#: src/view/com/util/moderation/ContentHider.tsx:67 +#: src/view/com/util/moderation/PostHider.tsx:61 +msgid "Hide the content" +msgstr "Cuir an t-ábhar seo i bhfolach" + +#: src/view/com/util/forms/PostDropdownBtn.tsx:191 +msgid "Hide this post?" +msgstr "An bhfuil fonn ort an phostáil seo a chur i bhfolach?" + +#: src/view/com/notifications/FeedItem.tsx:315 +msgid "Hide user list" +msgstr "Cuir liosta na gcuntas i bhfolach" + +#: src/view/com/profile/ProfileHeader.tsx:486 +msgid "Hides posts from {0} in your feed" +msgstr "Cuireann sé seo na postálacha ó {0} i d’fhotha i bhfolach" + +#: src/view/com/posts/FeedErrorMessage.tsx:111 +msgid "Hmm, some kind of issue occurred when contacting the feed server. Please let the feed owner know about this issue." +msgstr "Hmm. Tharla fadhb éigin sa dul i dteagmháil le freastalaí an fhotha seo. Cuir é seo in iúl d’úinéir an fhotha, le do thoil." + +#: src/view/com/posts/FeedErrorMessage.tsx:99 +msgid "Hmm, the feed server appears to be misconfigured. Please let the feed owner know about this issue." +msgstr "Hmm. Is cosúil nach bhfuil freastalaí an fhotha seo curtha le chéile i gceart. Cuir é seo in iúl d’úinéir an fhotha, le do thoil." + +#: src/view/com/posts/FeedErrorMessage.tsx:105 +msgid "Hmm, the feed server appears to be offline. Please let the feed owner know about this issue." +msgstr "Hmm. Is cosúil go bhfuil freastalaí an fhotha as líne. Cuir é seo in iúl d’úinéir an fhotha, le do thoil." + +#: src/view/com/posts/FeedErrorMessage.tsx:102 +msgid "Hmm, the feed server gave a bad response. Please let the feed owner know about this issue." +msgstr "Hmm. Thug freastalaí an fhotha drochfhreagra. Cuir é seo in iúl d’úinéir an fhotha, le do thoil." + +#: src/view/com/posts/FeedErrorMessage.tsx:96 +msgid "Hmm, we're having trouble finding this feed. It may have been deleted." +msgstr "Hmm. Ní féidir linn an fotha seo a aimsiú. Is féidir gur scriosadh é." + +#: src/Navigation.tsx:432 +#: src/view/shell/bottom-bar/BottomBar.tsx:137 +#: src/view/shell/desktop/LeftNav.tsx:306 +#: src/view/shell/Drawer.tsx:398 +#: src/view/shell/Drawer.tsx:399 +msgid "Home" +msgstr "Baile" + +#: src/Navigation.tsx:247 +#: src/view/com/pager/FeedsTabBarMobile.tsx:123 +#: src/view/screens/PreferencesHomeFeed.tsx:104 +#: src/view/screens/Settings/index.tsx:543 +msgid "Home Feed Preferences" +msgstr "Roghanna le haghaidh an fhotha baile" + +#: src/view/com/auth/create/Step1.tsx:82 +#: src/view/com/auth/login/ForgotPasswordForm.tsx:120 +msgid "Hosting provider" +msgstr "Soláthraí óstála" + +#: src/view/com/modals/InAppBrowserConsent.tsx:44 +msgid "How should we open this link?" +msgstr "Conas ar cheart dúinn an nasc seo a oscailt?" + +#: src/view/com/modals/VerifyEmail.tsx:214 +msgid "I have a code" +msgstr "Tá cód agam" + +#: src/view/com/modals/VerifyEmail.tsx:216 +msgid "I have a confirmation code" +msgstr "Tá cód dearbhaithe agam" + +#: src/view/com/modals/ChangeHandle.tsx:283 +msgid "I have my own domain" +msgstr "Tá fearann de mo chuid féin agam" + +#: src/view/com/lightbox/Lightbox.web.tsx:165 +msgid "If alt text is long, toggles alt text expanded state" +msgstr "Má tá an téacs malartach rófhada, athraíonn sé seo go téacs leathnaithe" + +#: src/view/com/modals/SelfLabel.tsx:127 +msgid "If none are selected, suitable for all ages." +msgstr "Mura roghnaítear tada, tá sé oiriúnach do gach aois." + +#: src/view/com/modals/ChangePassword.tsx:146 +msgid "If you want to change your password, we will send you a code to verify that this is your account." +msgstr "Más mian leat do phasfhocal a athrú, seolfaimid cód duit chun dearbhú gur leatsa an cuntas seo." + +#: src/view/com/util/images/Gallery.tsx:38 +msgid "Image" +msgstr "Íomhá" + +#: src/view/com/modals/AltImage.tsx:120 +msgid "Image alt text" +msgstr "Téacs malartach le híomhá" + +#: src/view/com/util/UserAvatar.tsx:311 +#: src/view/com/util/UserBanner.tsx:118 +msgid "Image options" +msgstr "Roghanna maidir leis an íomhá" + +#: src/view/com/auth/login/SetNewPasswordForm.tsx:138 +msgid "Input code sent to your email for password reset" +msgstr "Cuir isteach an cód a seoladh chuig do ríomhphost leis an bpasfhocal a athrú" + +#: src/view/com/modals/DeleteAccount.tsx:184 +msgid "Input confirmation code for account deletion" +msgstr "Cuir isteach an cód dearbhaithe leis an gcuntas a scriosadh" + +#: src/view/com/auth/create/Step1.tsx:200 +msgid "Input email for Bluesky account" +msgstr "Cuir isteach an ríomhphost don chuntas Bluesky" + +#: src/view/com/auth/create/Step1.tsx:158 +msgid "Input invite code to proceed" +msgstr "Cuir isteach an cód cuiridh le dul ar aghaidh" + +#: src/view/com/modals/AddAppPasswords.tsx:180 +msgid "Input name for app password" +msgstr "Cuir isteach an t-ainm le haghaidh phasfhocal na haipe" + +#: src/view/com/auth/login/SetNewPasswordForm.tsx:162 +msgid "Input new password" +msgstr "Cuir isteach an pasfhocal nua" + +#: src/view/com/modals/DeleteAccount.tsx:203 +msgid "Input password for account deletion" +msgstr "Cuir isteach an pasfhocal chun an cuntas a scriosadh" + +#: src/view/com/auth/create/Step2.tsx:196 +#~ msgid "Input phone number for SMS verification" +#~ msgstr "Cuir isteach an uimhir ghutháin le haghaidh dhearbhú SMS" + +#: src/view/com/auth/login/LoginForm.tsx:230 +msgid "Input the password tied to {identifier}" +msgstr "Cuir isteach an pasfhocal ceangailte le {identifier}" + +#: src/view/com/auth/login/LoginForm.tsx:197 +msgid "Input the username or email address you used at signup" +msgstr "Cuir isteach an leasainm nó an seoladh ríomhphoist a d’úsáid tú nuair a chláraigh tú" + +#: src/view/com/auth/create/Step2.tsx:271 +#~ msgid "Input the verification code we have texted to you" +#~ msgstr "Cuir isteach an cód dearbhaithe a chuir muid chugat i dteachtaireacht téacs" + +#: src/view/com/modals/Waitlist.tsx:90 +msgid "Input your email to get on the Bluesky waitlist" +msgstr "Cuir isteach do ríomhphost le bheith ar an liosta feithimh" + +#: src/view/com/auth/login/LoginForm.tsx:229 +msgid "Input your password" +msgstr "Cuir isteach do phasfhocal" + +#: src/view/com/auth/create/Step2.tsx:45 +msgid "Input your user handle" +msgstr "Cuir isteach do leasainm" + +#: src/view/com/post-thread/PostThreadItem.tsx:223 +msgid "Invalid or unsupported post record" +msgstr "Taifead postála atá neamhbhailí nó gan bhunús" + +#: src/view/com/auth/login/LoginForm.tsx:113 +msgid "Invalid username or password" +msgstr "Leasainm nó pasfhocal míchruinn" + +#: src/view/screens/Settings.tsx:411 +#~ msgid "Invite" +#~ msgstr "Cuireadh" + +#: src/view/com/modals/InviteCodes.tsx:93 +msgid "Invite a Friend" +msgstr "Tabhair cuireadh chuig cara leat" + +#: src/view/com/auth/create/Step1.tsx:148 +#: src/view/com/auth/create/Step1.tsx:157 +msgid "Invite code" +msgstr "Cód cuiridh" + +#: src/view/com/auth/create/state.ts:158 +msgid "Invite code not accepted. Check that you input it correctly and try again." +msgstr "Níor glacadh leis an gcód cuiridh. Bí cinnte gur scríobh tú i gceart é agus bain triail eile as." + +#: src/view/com/modals/InviteCodes.tsx:170 +msgid "Invite codes: {0} available" +msgstr "Cóid chuiridh: {0} ar fáil" + +#: src/view/shell/Drawer.tsx:645 +#~ msgid "Invite codes: {invitesAvailable} available" +#~ msgstr "Cóid chuiridh: {invitesAvailable} ar fáil" + +#: src/view/com/modals/InviteCodes.tsx:169 +msgid "Invite codes: 1 available" +msgstr "Cóid chuiridh: 1 ar fáil" + +#: src/screens/Onboarding/StepFollowingFeed.tsx:64 +msgid "It shows posts from the people you follow as they happen." +msgstr "Taispeánann sé postálacha ó na daoine a leanann tú nuair a fhoilsítear iad." + +#: src/view/com/auth/HomeLoggedOutCTA.tsx:99 +#: src/view/com/auth/SplashScreen.web.tsx:138 +msgid "Jobs" +msgstr "Jabanna" + +#: src/view/com/modals/Waitlist.tsx:67 +msgid "Join the waitlist" +msgstr "Cuir d’ainm ar an liosta feithimh" + +#: src/view/com/auth/create/Step1.tsx:174 +#: src/view/com/auth/create/Step1.tsx:178 +msgid "Join the waitlist." +msgstr "Cuir d’ainm ar an liosta feithimh." + +#: src/view/com/modals/Waitlist.tsx:128 +msgid "Join Waitlist" +msgstr "Cuir d’ainm ar an liosta feithimh" + +#: src/screens/Onboarding/index.tsx:24 +msgid "Journalism" +msgstr "Iriseoireacht" + +#: src/view/com/composer/select-language/SelectLangBtn.tsx:104 +msgid "Language selection" +msgstr "Rogha teanga" + +#: src/view/screens/Settings/index.tsx:594 +msgid "Language settings" +msgstr "Socruithe teanga" + +#: src/Navigation.tsx:140 +#: src/view/screens/LanguageSettings.tsx:89 +msgid "Language Settings" +msgstr "Socruithe teanga" + +#: src/view/screens/Settings/index.tsx:603 +msgid "Languages" +msgstr "Teangacha" + +#: src/view/com/auth/create/StepHeader.tsx:20 +msgid "Last step!" +msgstr "An chéim dheireanach!" + +#: src/view/com/util/moderation/ContentHider.tsx:103 +msgid "Learn more" +msgstr "Le tuilleadh a fhoghlaim" + +#: src/view/com/util/moderation/PostAlerts.tsx:47 +#: src/view/com/util/moderation/ProfileHeaderAlerts.tsx:65 +#: src/view/com/util/moderation/ScreenHider.tsx:104 +msgid "Learn More" +msgstr "Le tuilleadh a fhoghlaim" + +#: src/view/com/util/moderation/ContentHider.tsx:85 +#: src/view/com/util/moderation/PostAlerts.tsx:40 +#: src/view/com/util/moderation/PostHider.tsx:78 +#: src/view/com/util/moderation/ProfileHeaderAlerts.tsx:49 +#: src/view/com/util/moderation/ScreenHider.tsx:101 +msgid "Learn more about this warning" +msgstr "Le tuilleadh a fhoghlaim faoin rabhadh seo" + +#: src/view/screens/Moderation.tsx:243 +msgid "Learn more about what is public on Bluesky." +msgstr "Le tuilleadh a fhoghlaim faoi céard atá poiblí ar Bluesky" + +#: src/view/com/modals/lang-settings/ContentLanguagesSettings.tsx:82 +msgid "Leave them all unchecked to see any language." +msgstr "Fág iad uile gan tic le teanga ar bith a fheiceáil." + +#: src/view/com/modals/LinkWarning.tsx:51 +msgid "Leaving Bluesky" +msgstr "Ag fágáil slán ag Bluesky" + +#: src/screens/Deactivated.tsx:128 +msgid "left to go." +msgstr "le déanamh fós." + +#: src/view/screens/Settings/index.tsx:278 +msgid "Legacy storage cleared, you need to restart the app now." +msgstr "Stóráil oidhreachta scriosta, tá ort an aip a atosú anois." + +#: src/view/com/auth/login/Login.tsx:128 +#: src/view/com/auth/login/Login.tsx:144 +msgid "Let's get your password reset!" +msgstr "Socraímis do phasfhocal arís!" + +#: src/screens/Onboarding/StepFinished.tsx:151 +msgid "Let's go!" +msgstr "Ar aghaidh linn!" + +#: src/view/com/util/UserAvatar.tsx:248 +#: src/view/com/util/UserBanner.tsx:62 +msgid "Library" +msgstr "Leabharlann" + +#: src/view/screens/Settings/index.tsx:479 +msgid "Light" +msgstr "Sorcha" + +#: src/view/com/util/post-ctrls/PostCtrls.tsx:182 +#: src/view/com/util/post-ctrls/PostCtrls.tsx:216 +msgid "Like" +msgstr "Mol" + +#: src/view/screens/ProfileFeed.tsx:591 +msgid "Like this feed" +msgstr "Mol an fotha seo" + +#: src/Navigation.tsx:197 +msgid "Liked by" +msgstr "Molta ag" + +#: src/view/screens/PostLikedBy.tsx:27 +#: src/view/screens/ProfileFeedLikedBy.tsx:27 +msgid "Liked By" +msgstr "Molta ag" + +#: src/view/com/feeds/FeedSourceCard.tsx:277 +msgid "Liked by {0} {1}" +msgstr "Molta ag {0} {1}" + +#: src/view/screens/ProfileFeed.tsx:606 +msgid "Liked by {likeCount} {0}" +msgstr "Molta ag {likeCount} {0}" + +#: src/view/com/notifications/FeedItem.tsx:170 +msgid "liked your custom feed" +msgstr "a mhol do shainfhotha" + +#: src/view/com/notifications/FeedItem.tsx:155 +msgid "liked your post" +msgstr "a mhol do phostáil" + +#: src/view/screens/Profile.tsx:183 +msgid "Likes" +msgstr "Moltaí" + +#: src/view/com/post-thread/PostThreadItem.tsx:180 +msgid "Likes on this post" +msgstr "Moltaí don phostáil seo" + +#: src/Navigation.tsx:166 +msgid "List" +msgstr "Liosta" + +#: src/view/com/modals/CreateOrEditList.tsx:261 +msgid "List Avatar" +msgstr "Abhatár an Liosta" + +#: src/view/screens/ProfileList.tsx:323 +msgid "List blocked" +msgstr "Liosta blocáilte" + +#: src/view/com/feeds/FeedSourceCard.tsx:231 +msgid "List by {0}" +msgstr "Liosta le {0}" + +#: src/view/screens/ProfileList.tsx:377 +msgid "List deleted" +msgstr "Scriosadh an liosta" + +#: src/view/screens/ProfileList.tsx:282 +msgid "List muted" +msgstr "Balbhaíodh an liosta" + +#: src/view/com/modals/CreateOrEditList.tsx:275 +msgid "List Name" +msgstr "Ainm an liosta" + +#: src/view/screens/ProfileList.tsx:342 +msgid "List unblocked" +msgstr "Liosta díbhlocáilte" + +#: src/view/screens/ProfileList.tsx:301 +msgid "List unmuted" +msgstr "Liosta nach bhfuil balbhaithe níos mó" + +#: src/Navigation.tsx:110 +#: src/view/screens/Profile.tsx:185 +#: src/view/shell/desktop/LeftNav.tsx:379 +#: src/view/shell/Drawer.tsx:492 +#: src/view/shell/Drawer.tsx:493 +msgid "Lists" +msgstr "Liostaí" + +#: src/view/com/post-thread/PostThread.tsx:333 +#: src/view/com/post-thread/PostThread.tsx:341 +msgid "Load more posts" +msgstr "Lódáil tuilleadh postálacha" + +#: src/view/screens/Notifications.tsx:159 +msgid "Load new notifications" +msgstr "Lódáil fógraí nua" + +#: src/view/com/feeds/FeedPage.tsx:181 +#: src/view/screens/Profile.tsx:440 +#: src/view/screens/ProfileFeed.tsx:494 +#: src/view/screens/ProfileList.tsx:680 +msgid "Load new posts" +msgstr "Lódáil postálacha nua" + +#: src/view/com/composer/text-input/mobile/Autocomplete.tsx:95 +msgid "Loading..." +msgstr "Ag lódáil …" + +#: src/view/com/modals/ServerInput.tsx:50 +#~ msgid "Local dev server" +#~ msgstr "Freastálaí forbróra áitiúil" + +#: src/Navigation.tsx:207 +msgid "Log" +msgstr "Logleabhar" + +#: src/screens/Deactivated.tsx:149 +#: src/screens/Deactivated.tsx:152 +#: src/screens/Deactivated.tsx:178 +#: src/screens/Deactivated.tsx:181 +msgid "Log out" +msgstr "Logáil amach" + +#: src/view/screens/Moderation.tsx:136 +msgid "Logged-out visibility" +msgstr "Feiceálacht le linn a bheith logáilte amach" + +#: src/view/com/auth/login/ChooseAccountForm.tsx:133 +msgid "Login to account that is not listed" +msgstr "Logáil isteach ar chuntas nach bhfuil liostáilte" + +#: src/view/com/modals/LinkWarning.tsx:65 +msgid "Make sure this is where you intend to go!" +msgstr "Bí cinnte go bhfuil tú ag iarraidh cuairt a thabhairt ar an áit sin!" + +#: src/view/screens/Profile.tsx:182 +msgid "Media" +msgstr "Meáin" + +#: src/view/com/threadgate/WhoCanReply.tsx:139 +msgid "mentioned users" +msgstr "úsáideoirí luaite" + +#: src/view/com/modals/Threadgate.tsx:93 +msgid "Mentioned users" +msgstr "Úsáideoirí luaite" + +#: src/view/com/util/ViewHeader.tsx:81 +#: src/view/screens/Search/Search.tsx:623 +msgid "Menu" +msgstr "Clár" + +#: src/view/com/posts/FeedErrorMessage.tsx:197 +msgid "Message from server: {0}" +msgstr "Teachtaireacht ón bhfreastalaí: {0}" + +#: src/Navigation.tsx:115 +#: src/view/screens/Moderation.tsx:64 +#: src/view/screens/Settings/index.tsx:625 +#: src/view/shell/desktop/LeftNav.tsx:397 +#: src/view/shell/Drawer.tsx:511 +#: src/view/shell/Drawer.tsx:512 +msgid "Moderation" +msgstr "Modhnóireacht" + +#: src/view/com/lists/ListCard.tsx:92 +#: src/view/com/modals/UserAddRemoveLists.tsx:206 +msgid "Moderation list by {0}" +msgstr "Liosta modhnóireachta le {0}" + +#: src/view/screens/ProfileList.tsx:774 +msgid "Moderation list by <0/>" +msgstr "Liosta modhnóireachta le <0/>" + +#: src/view/com/lists/ListCard.tsx:90 +#: src/view/com/modals/UserAddRemoveLists.tsx:204 +#: src/view/screens/ProfileList.tsx:772 +msgid "Moderation list by you" +msgstr "Liosta modhnóireachta leat" + +#: src/view/com/modals/CreateOrEditList.tsx:197 +msgid "Moderation list created" +msgstr "Liosta modhnóireachta cruthaithe" + +#: src/view/com/modals/CreateOrEditList.tsx:183 +msgid "Moderation list updated" +msgstr "Liosta modhnóireachta uasdátaithe" + +#: src/view/screens/Moderation.tsx:95 +msgid "Moderation lists" +msgstr "Liostaí modhnóireachta" + +#: src/Navigation.tsx:120 +#: src/view/screens/ModerationModlists.tsx:58 +msgid "Moderation Lists" +msgstr "Liostaí modhnóireachta" + +#: src/view/screens/Settings/index.tsx:619 +msgid "Moderation settings" +msgstr "Socruithe modhnóireachta" + +#: src/view/com/modals/ModerationDetails.tsx:35 +msgid "Moderator has chosen to set a general warning on the content." +msgstr "Chuir an modhnóir rabhadh ginearálta ar an ábhar." + +#: src/view/shell/desktop/Feeds.tsx:63 +msgid "More feeds" +msgstr "Tuilleadh fothaí" + +#: src/view/com/profile/ProfileHeader.tsx:522 +#: src/view/screens/ProfileFeed.tsx:362 +#: src/view/screens/ProfileList.tsx:616 +msgid "More options" +msgstr "Tuilleadh roghanna" + +#: src/view/com/util/forms/PostDropdownBtn.tsx:270 +msgid "More post options" +msgstr "Tuilleadh roghanna postála" + +#: src/view/screens/PreferencesThreads.tsx:82 +msgid "Most-liked replies first" +msgstr "Freagraí a fuair an méid is mó moltaí ar dtús" + +#: src/view/com/profile/ProfileHeader.tsx:326 +msgid "Mute Account" +msgstr "Cuir an cuntas i bhfolach" + +#: src/view/screens/ProfileList.tsx:543 +msgid "Mute accounts" +msgstr "Cuir na cuntais i bhfolach" + +#: src/view/screens/ProfileList.tsx:490 +msgid "Mute list" +msgstr "Cuir an liosta i bhfolach" + +#: src/view/screens/ProfileList.tsx:274 +msgid "Mute these accounts?" +msgstr "An bhfuil fonn ort na cuntais seo a chur i bhfolach" + +#: src/view/screens/ProfileList.tsx:278 +msgid "Mute this List" +msgstr "Cuir an liosta seo i bhfolach" + +#: src/view/com/util/forms/PostDropdownBtn.tsx:171 +msgid "Mute thread" +msgstr "Cuir an snáithe seo i bhfolach" + +#: src/view/com/lists/ListCard.tsx:101 +msgid "Muted" +msgstr "Curtha i bhfolach" + +#: src/view/screens/Moderation.tsx:109 +msgid "Muted accounts" +msgstr "Cuntais a cuireadh i bhfolach" + +#: src/Navigation.tsx:125 +#: src/view/screens/ModerationMutedAccounts.tsx:107 +msgid "Muted Accounts" +msgstr "Cuntais a Cuireadh i bhFolach" + +#: src/view/screens/ModerationMutedAccounts.tsx:115 +msgid "Muted accounts have their posts removed from your feed and from your notifications. Mutes are completely private." +msgstr "Baintear na postálacha ó na cuntais a chuir tú i bhfolach as d’fhotha agus as do chuid fógraí. Is príobháideach ar fad é an cur i bhfolach." + +#: src/view/screens/ProfileList.tsx:276 +msgid "Muting is private. Muted accounts can interact with you, but you will not see their posts or receive notifications from them." +msgstr "Tá an cur i bhfolach príobháideach. Is féidir leis na cuntais a chuir tú i bhfolach do chuid postálacha a fheiceáil agus is féidir leo scríobh chugat ach ní fheicfidh tú a gcuid postálacha eile ná aon fhógraí uathu." + +#: src/view/com/modals/BirthDateSettings.tsx:56 +msgid "My Birthday" +msgstr "Mo Bhreithlá" + +#: src/view/screens/Feeds.tsx:663 +msgid "My Feeds" +msgstr "Mo Chuid Fothaí" + +#: src/view/shell/desktop/LeftNav.tsx:65 +msgid "My Profile" +msgstr "Mo Phróifíl" + +#: src/view/screens/Settings/index.tsx:582 +msgid "My Saved Feeds" +msgstr "Na Fothaí a Shábháil Mé" + +#: src/view/com/auth/server-input/index.tsx:118 +msgid "my-server.com" +msgstr "my-server.com" + +#~ msgid "Ná bíodh gan fáil ar do chuid leantóirí ná ar do chuid dáta go deo." +#~ msgstr "Cuir an comhrá seo i bhfolach" + +#: src/view/com/modals/AddAppPasswords.tsx:179 +#: src/view/com/modals/CreateOrEditList.tsx:290 +msgid "Name" +msgstr "Ainm" + +#: src/view/com/modals/CreateOrEditList.tsx:145 +msgid "Name is required" +msgstr "Tá an t-ainm riachtanach" + +#: src/screens/Onboarding/index.tsx:25 +msgid "Nature" +msgstr "Nádúr" + +#: src/view/com/auth/login/ForgotPasswordForm.tsx:190 +#: src/view/com/auth/login/ForgotPasswordForm.tsx:219 +#: src/view/com/auth/login/LoginForm.tsx:289 +#: src/view/com/auth/login/SetNewPasswordForm.tsx:196 +#: src/view/com/modals/ChangePassword.tsx:166 +msgid "Navigates to the next screen" +msgstr "Téann sé seo chuig an gcéad scáileán eile" + +#: src/view/shell/Drawer.tsx:71 +msgid "Navigates to your profile" +msgstr "Téann sé seo chuig do phróifíl" + +#: src/view/com/modals/EmbedConsent.tsx:107 +#: src/view/com/modals/EmbedConsent.tsx:123 +msgid "Never load embeds from {0}" +msgstr "Ná lódáil ábhar leabaithe ó {0} go deo" + +#: src/view/com/auth/onboarding/WelcomeDesktop.tsx:72 +#: src/view/com/auth/onboarding/WelcomeMobile.tsx:72 +msgid "Never lose access to your followers and data." +msgstr "Ná bíodh gan fáil ar do chuid leantóirí ná ar do chuid dáta go deo." + +#: src/screens/Onboarding/StepFinished.tsx:119 +msgid "Never lose access to your followers or data." +msgstr "Ná bíodh gan fáil ar do chuid leantóirí ná ar do chuid dáta go deo." + +#: src/view/screens/Lists.tsx:76 +msgctxt "action" +msgid "New" +msgstr "Nua" + +#: src/view/screens/ModerationModlists.tsx:78 +msgid "New" +msgstr "Nua" + +#: src/view/com/modals/CreateOrEditList.tsx:252 +msgid "New Moderation List" +msgstr "Liosta modhnóireachta nua" + +#: src/view/com/auth/login/SetNewPasswordForm.tsx:150 +msgid "New password" +msgstr "Pasfhocal Nua" + +#: src/view/com/modals/ChangePassword.tsx:215 +msgid "New Password" +msgstr "Pasfhocal Nua" + +#: src/view/com/feeds/FeedPage.tsx:192 +msgctxt "action" +msgid "New post" +msgstr "Postáil nua" + +#: src/view/screens/Feeds.tsx:555 +#: src/view/screens/Notifications.tsx:168 +#: src/view/screens/Profile.tsx:382 +#: src/view/screens/ProfileFeed.tsx:432 +#: src/view/screens/ProfileList.tsx:195 +#: src/view/screens/ProfileList.tsx:223 +#: src/view/shell/desktop/LeftNav.tsx:248 +msgid "New post" +msgstr "Postáil nua" + +#: src/view/shell/desktop/LeftNav.tsx:258 +msgctxt "action" +msgid "New Post" +msgstr "Postáil nua" + +#: src/view/com/modals/CreateOrEditList.tsx:247 +msgid "New User List" +msgstr "Liosta Nua d’Úsáideoirí" + +#: src/view/screens/PreferencesThreads.tsx:79 +msgid "Newest replies first" +msgstr "Na freagraí is déanaí ar dtús" + +#: src/screens/Onboarding/index.tsx:23 +msgid "News" +msgstr "Nuacht" + +#: src/view/com/auth/create/CreateAccount.tsx:168 +#: src/view/com/auth/login/ForgotPasswordForm.tsx:182 +#: src/view/com/auth/login/ForgotPasswordForm.tsx:192 +#: src/view/com/auth/login/LoginForm.tsx:291 +#: src/view/com/auth/login/SetNewPasswordForm.tsx:187 +#: src/view/com/auth/login/SetNewPasswordForm.tsx:198 +#: src/view/com/auth/onboarding/RecommendedFeeds.tsx:79 +#: src/view/com/modals/ChangePassword.tsx:251 +#: src/view/com/modals/ChangePassword.tsx:253 +msgid "Next" +msgstr "Ar aghaidh" + +#: src/view/com/auth/onboarding/WelcomeDesktop.tsx:103 +msgctxt "action" +msgid "Next" +msgstr "Ar aghaidh" + +#: src/view/com/lightbox/Lightbox.web.tsx:149 +msgid "Next image" +msgstr "An chéad íomhá eile" + +#: src/view/screens/PreferencesHomeFeed.tsx:129 +#: src/view/screens/PreferencesHomeFeed.tsx:200 +#: src/view/screens/PreferencesHomeFeed.tsx:235 +#: src/view/screens/PreferencesHomeFeed.tsx:272 +#: src/view/screens/PreferencesThreads.tsx:106 +#: src/view/screens/PreferencesThreads.tsx:129 +msgid "No" +msgstr "Níl" + +#: src/view/screens/ProfileFeed.tsx:584 +#: src/view/screens/ProfileList.tsx:754 +msgid "No description" +msgstr "Gan chur síos" + +#: src/view/com/profile/ProfileHeader.tsx:169 +msgid "No longer following {0}" +msgstr "Ní leantar {0} níos mó" + +#: src/view/com/notifications/Feed.tsx:109 +msgid "No notifications yet!" +msgstr "Níl aon fhógra ann fós!" + +#: src/view/com/composer/text-input/mobile/Autocomplete.tsx:97 +#: src/view/com/composer/text-input/web/Autocomplete.tsx:191 +msgid "No result" +msgstr "Gan torthaí" + +#: src/view/screens/Feeds.tsx:495 +msgid "No results found for \"{query}\"" +msgstr "Gan torthaí ar “{query}”" + +#: src/view/com/modals/ListAddRemoveUsers.tsx:127 +#: src/view/screens/Search/Search.tsx:280 +#: src/view/screens/Search/Search.tsx:308 +msgid "No results found for {query}" +msgstr "Gan torthaí ar {query}" + +#: src/view/com/modals/EmbedConsent.tsx:129 +msgid "No thanks" +msgstr "Níor mhaith liom é sin." + +#: src/view/com/modals/Threadgate.tsx:82 +msgid "Nobody" +msgstr "Duine ar bith" + +#: src/view/com/modals/SelfLabel.tsx:135 +msgid "Not Applicable." +msgstr "Ní bhaineann sé sin le hábhar." + +#: src/Navigation.tsx:105 +#: src/view/screens/Profile.tsx:106 +msgid "Not Found" +msgstr "Ní bhfuarthas é sin" + +#: src/view/com/modals/VerifyEmail.tsx:246 +#: src/view/com/modals/VerifyEmail.tsx:252 +msgid "Not right now" +msgstr "Ní anois" + +#: src/view/screens/Moderation.tsx:233 +msgid "Note: Bluesky is an open and public network. This setting only limits the visibility of your content on the Bluesky app and website, and other apps may not respect this setting. Your content may still be shown to logged-out users by other apps and websites." +msgstr "Nod leat: is gréasán oscailte poiblí Bluesky. Ní chuireann an socrú seo srian ar fheiceálacht do chuid ábhair ach amháin ar aip agus suíomh Bluesky. Is féidir nach gcloífidh aipeanna eile leis an socrú seo. Is féidir go dtaispeánfar do chuid ábhair d’úsáideoirí atá lógáilte amach ar aipeanna agus suíomhanna eile." + +#: src/Navigation.tsx:447 +#: src/view/screens/Notifications.tsx:124 +#: src/view/screens/Notifications.tsx:148 +#: src/view/shell/bottom-bar/BottomBar.tsx:205 +#: src/view/shell/desktop/LeftNav.tsx:361 +#: src/view/shell/Drawer.tsx:435 +#: src/view/shell/Drawer.tsx:436 +msgid "Notifications" +msgstr "Fógraí" + +#: src/view/com/modals/SelfLabel.tsx:103 +msgid "Nudity" +msgstr "Lomnochtacht" + +#: src/view/com/util/ErrorBoundary.tsx:35 +msgid "Oh no!" +msgstr "Úps!" + +#: src/screens/Onboarding/StepInterests/index.tsx:128 +msgid "Oh no! Something went wrong." +msgstr "Úps! Theip ar rud éigin." + +#: src/view/com/auth/login/PasswordUpdatedForm.tsx:41 +msgid "Okay" +msgstr "Maith go leor" + +#: src/view/screens/PreferencesThreads.tsx:78 +msgid "Oldest replies first" +msgstr "Na freagraí is sine ar dtús" + +#: src/view/screens/Settings/index.tsx:234 +msgid "Onboarding reset" +msgstr "Atosú an chláraithe" + +#: src/view/com/composer/Composer.tsx:375 +msgid "One or more images is missing alt text." +msgstr "Tá téacs malartach de dhíth ar íomhá amháin nó níos mó acu." + +#: src/view/com/threadgate/WhoCanReply.tsx:100 +msgid "Only {0} can reply." +msgstr "Ní féidir ach le {0} freagra a thabhairt." + +#: src/view/screens/AppPasswords.tsx:65 +#: src/view/screens/Profile.tsx:106 +msgid "Oops!" +msgstr "Úps!" + +#: src/screens/Onboarding/StepFinished.tsx:115 +msgid "Open" +msgstr "Oscail" + +#: src/view/com/composer/Composer.tsx:470 +#: src/view/com/composer/Composer.tsx:471 +msgid "Open emoji picker" +msgstr "Oscail roghnóir na n-emoji" + +#: src/view/screens/Settings/index.tsx:712 +msgid "Open links with in-app browser" +msgstr "Oscail nascanna leis an mbrabhsálaí san aip" + +#: src/view/com/pager/FeedsTabBarMobile.tsx:87 +msgid "Open navigation" +msgstr "Oscail an nascleanúint" + +#: src/view/screens/Settings/index.tsx:804 +msgid "Open storybook page" +msgstr "Oscail leathanach an Storybook" + +#: src/view/com/util/forms/DropdownButton.tsx:154 +msgid "Opens {numItems} options" +msgstr "Osclaíonn sé seo {numItems} rogha" + +#: src/view/screens/Log.tsx:54 +msgid "Opens additional details for a debug entry" +msgstr "Osclaíonn sé seo tuilleadh sonraí le haghaidh iontráil dífhabhtaithe" + +#: src/view/com/notifications/FeedItem.tsx:348 +msgid "Opens an expanded list of users in this notification" +msgstr "Osclaíonn sé seo liosta méadaithe d’úsáideoirí san fhógra seo" + +#: src/view/com/composer/photos/OpenCameraBtn.tsx:61 +msgid "Opens camera on device" +msgstr "Osclaíonn sé seo an ceamara ar an ngléas" + +#: src/view/com/composer/Prompt.tsx:25 +msgid "Opens composer" +msgstr "Osclaíonn sé seo an t-eagarthóir" + +#: src/view/screens/Settings/index.tsx:595 +msgid "Opens configurable language settings" +msgstr "Osclaíonn sé seo na socruithe teanga is féidir a dhéanamh" + +#: src/view/com/composer/photos/SelectPhotoBtn.tsx:44 +msgid "Opens device photo gallery" +msgstr "Osclaíonn sé seo gailearaí na ngrianghraf ar an ngléas" + +#: src/view/com/profile/ProfileHeader.tsx:419 +msgid "Opens editor for profile display name, avatar, background image, and description" +msgstr "Osclaíonn sé seo an t-eagarthóir le haghaidh gach a bhfuil i do phróifíl: an t-ainm, an t-abhatár, an íomhá sa chúlra, agus an cur síos." + +#: src/view/screens/Settings/index.tsx:649 +msgid "Opens external embeds settings" +msgstr "Osclaíonn sé seo na socruithe le haghaidh leabuithe seachtracha" + +#: src/view/com/profile/ProfileHeader.tsx:574 +msgid "Opens followers list" +msgstr "Osclaíonn sé seo liosta na leantóirí" + +#: src/view/com/profile/ProfileHeader.tsx:593 +msgid "Opens following list" +msgstr "Osclaíonn sé seo liosta na ndaoine a leanann tú" + +#: src/view/screens/Settings.tsx:412 +#~ msgid "Opens invite code list" +#~ msgstr "Osclaíonn sé seo liosta na gcód cuiridh" + +#: src/view/com/modals/InviteCodes.tsx:172 +msgid "Opens list of invite codes" +msgstr "Osclaíonn sé seo liosta na gcód cuiridh" + +#: src/view/screens/Settings/index.tsx:774 +msgid "Opens modal for account deletion confirmation. Requires email code." +msgstr "Osclaíonn sé seo an fhuinneog le scriosadh an chuntais a dhearbhú. Tá cód ríomhphoist riachtanach." + +#: src/view/com/modals/ChangeHandle.tsx:281 +msgid "Opens modal for using custom domain" +msgstr "Osclaíonn sé seo an fhuinneog le sainfhearann a úsáid" + +#: src/view/screens/Settings/index.tsx:620 +msgid "Opens moderation settings" +msgstr "Osclaíonn sé seo socruithe na modhnóireachta" + +#: src/view/com/auth/login/LoginForm.tsx:239 +msgid "Opens password reset form" +msgstr "Osclaíonn sé seo an fhoirm leis an bpasfhocal a athrú" + +#: src/view/screens/Feeds.tsx:356 +msgid "Opens screen to edit Saved Feeds" +msgstr "Osclaíonn sé seo an scáileán leis na fothaí sábháilte a athrú" + +#: src/view/screens/Settings/index.tsx:576 +msgid "Opens screen with all saved feeds" +msgstr "Osclaíonn sé seo an scáileán leis na fothaí sábháilte go léir" + +#: src/view/screens/Settings/index.tsx:676 +msgid "Opens the app password settings page" +msgstr "Osclaíonn sé seo an leathanach a bhfuil socruithe phasfhocal na haipe air" + +#: src/view/screens/Settings/index.tsx:535 +msgid "Opens the home feed preferences" +msgstr "Osclaíonn sé seo roghanna fhotha an bhaile" + +#: src/view/screens/Settings/index.tsx:805 +msgid "Opens the storybook page" +msgstr "Osclaíonn sé seo leathanach an Storybook" + +#: src/view/screens/Settings/index.tsx:793 +msgid "Opens the system log page" +msgstr "Osclaíonn sé seo logleabhar an chórais" + +#: src/view/screens/Settings/index.tsx:556 +msgid "Opens the threads preferences" +msgstr "Osclaíonn sé seo roghanna na snáitheanna" + +#: src/view/com/util/forms/DropdownButton.tsx:280 +msgid "Option {0} of {numItems}" +msgstr "Rogha {0} as {numItems}" + +#: src/view/com/modals/Threadgate.tsx:89 +msgid "Or combine these options:" +msgstr "Nó cuir na roghanna seo le chéile:" + +#: src/view/com/auth/login/ChooseAccountForm.tsx:138 +msgid "Other account" +msgstr "Cuntas eile" + +#: src/view/com/modals/ServerInput.tsx:88 +#~ msgid "Other service" +#~ msgstr "Seirbhís eile" + +#: src/view/com/composer/select-language/SelectLangBtn.tsx:91 +msgid "Other..." +msgstr "Eile…" + +#: src/view/screens/NotFound.tsx:45 +msgid "Page not found" +msgstr "Leathanach gan aimsiú" + +#: src/view/screens/NotFound.tsx:42 +msgid "Page Not Found" +msgstr "Leathanach gan aimsiú" + +#: src/view/com/auth/create/Step1.tsx:214 +#: src/view/com/auth/create/Step1.tsx:224 +#: src/view/com/auth/login/LoginForm.tsx:226 +#: src/view/com/auth/login/SetNewPasswordForm.tsx:161 +#: src/view/com/modals/DeleteAccount.tsx:202 +msgid "Password" +msgstr "Pasfhocal" + +#: src/view/com/auth/login/Login.tsx:157 +msgid "Password updated" +msgstr "Pasfhocal uasdátaithe" + +#: src/view/com/auth/login/PasswordUpdatedForm.tsx:28 +msgid "Password updated!" +msgstr "Pasfhocal uasdátaithe!" + +#: src/Navigation.tsx:160 +msgid "People followed by @{0}" +msgstr "Na daoine atá leanta ag @{0}" + +#: src/Navigation.tsx:153 +msgid "People following @{0}" +msgstr "Na leantóirí atá ag @{0}" + +#: src/view/com/lightbox/Lightbox.tsx:66 +msgid "Permission to access camera roll is required." +msgstr "Tá cead de dhíth le rolla an cheamara a oscailt." + +#: src/view/com/lightbox/Lightbox.tsx:72 +msgid "Permission to access camera roll was denied. Please enable it in your system settings." +msgstr "Ní bhfuarthas cead le rolla an cheamara a oscailt. Athraigh socruithe an chórais len é seo a chur ar fáil, le do thoil." + +#: src/screens/Onboarding/index.tsx:31 +msgid "Pets" +msgstr "Peataí" + +#: src/view/com/auth/create/Step2.tsx:183 +#~ msgid "Phone number" +#~ msgstr "Uimhir ghutháin" + +#: src/view/com/modals/SelfLabel.tsx:121 +msgid "Pictures meant for adults." +msgstr "Pictiúir le haghaidh daoine fásta." + +#: src/view/screens/ProfileFeed.tsx:353 +#: src/view/screens/ProfileList.tsx:580 +msgid "Pin to home" +msgstr "Greamaigh le baile" + +#: src/view/screens/SavedFeeds.tsx:88 +msgid "Pinned Feeds" +msgstr "Fothaí greamaithe" + +#: src/view/com/util/post-embeds/ExternalGifEmbed.tsx:111 +msgid "Play {0}" +msgstr "Seinn {0}" + +#: src/view/com/util/post-embeds/ExternalPlayerEmbed.tsx:54 +#: src/view/com/util/post-embeds/ExternalPlayerEmbed.tsx:55 +msgid "Play Video" +msgstr "Seinn an físeán" + +#: src/view/com/util/post-embeds/ExternalGifEmbed.tsx:110 +msgid "Plays the GIF" +msgstr "Seinneann sé seo an GIF" + +#: src/view/com/auth/create/state.ts:124 +msgid "Please choose your handle." +msgstr "Roghnaigh do leasainm, le do thoil." + +#: src/view/com/auth/create/state.ts:117 +msgid "Please choose your password." +msgstr "Roghnaigh do phasfhocal, le do thoil." + +#: src/view/com/auth/create/state.ts:131 +msgid "Please complete the verification captcha." +msgstr "Déan an captcha, le do thoil." + +#: src/view/com/modals/ChangeEmail.tsx:67 +msgid "Please confirm your email before changing it. This is a temporary requirement while email-updating tools are added, and it will soon be removed." +msgstr "Dearbhaigh do ríomhphost roimh é a athrú. Riachtanas sealadach é seo le linn dúinn acmhainní a chur isteach le haghaidh uasdátú an ríomhphoist. Scriosfar é seo roimh i bhfad." + +#: src/view/com/modals/AddAppPasswords.tsx:90 +msgid "Please enter a name for your app password. All spaces is not allowed." +msgstr "Cuir isteach ainm le haghaidh phasfhocal na haipe, le do thoil. Ní cheadaítear spásanna gan aon rud eile ann." + +#: src/view/com/auth/create/Step2.tsx:206 +#~ msgid "Please enter a phone number that can receive SMS text messages." +#~ msgstr "Cuir isteach uimhir ghutháin atá in ann teachtaireachtaí SMS a fháil, le do thoil." + +#: src/view/com/modals/AddAppPasswords.tsx:145 +msgid "Please enter a unique name for this App Password or use our randomly generated one." +msgstr "Cuir isteach ainm nach bhfuil in úsáid cheana féin le haghaidh Phasfhocal na hAipe nó bain úsáid as an gceann a chruthóidh muid go randamach." + +#: src/view/com/auth/create/state.ts:170 +#~ msgid "Please enter the code you received by SMS." +#~ msgstr "Cuir isteach an cód a fuair tú trí SMS, le do thoil." + +#: src/view/com/auth/create/Step2.tsx:282 +#~ msgid "Please enter the verification code sent to {phoneNumberFormatted}." +#~ msgstr "Cuir isteach an cód dearbhaithe a cuireadh chuig {phoneNumberFormatted}, le do thoil." + +#: src/view/com/auth/create/state.ts:103 +msgid "Please enter your email." +msgstr "Cuir isteach do sheoladh ríomhphoist, le do thoil." + +#: src/view/com/modals/DeleteAccount.tsx:191 +msgid "Please enter your password as well:" +msgstr "Cuir isteach do phasfhocal freisin, le do thoil." + +#: src/view/com/modals/AppealLabel.tsx:72 +#: src/view/com/modals/AppealLabel.tsx:75 +msgid "Please tell us why you think this content warning was incorrectly applied!" +msgstr "Abair linn, le do thoil, cén fáth a gcreideann tú gur cuireadh an rabhadh ábhair seo i bhfeidhm go mícheart." + +#: src/view/com/modals/AppealLabel.tsx:72 +#: src/view/com/modals/AppealLabel.tsx:75 +#~ msgid "Please tell us why you think this decision was incorrect." +#~ msgstr "Abair linn, le do thoil, cén fáth a gcreideann tú go bhfuil an cinneadh seo mícheart." + +#: src/view/com/modals/VerifyEmail.tsx:101 +msgid "Please Verify Your Email" +msgstr "Dearbhaigh do ríomhphost, le do thoil." + +#: src/view/com/composer/Composer.tsx:215 +msgid "Please wait for your link card to finish loading" +msgstr "Fan le lódáil ar fad do chárta naisc, le do thoil." + +#: src/screens/Onboarding/index.tsx:37 +msgid "Politics" +msgstr "Polaitíocht" + +#: src/view/com/modals/SelfLabel.tsx:111 +msgid "Porn" +msgstr "Pornagrafaíocht" + +#: src/view/com/composer/Composer.tsx:350 +#: src/view/com/composer/Composer.tsx:358 +msgctxt "action" +msgid "Post" +msgstr "Postáil" + +#: src/view/com/post-thread/PostThread.tsx:303 +msgctxt "description" +msgid "Post" +msgstr "Postáil" + +#: src/view/com/post-thread/PostThreadItem.tsx:172 +msgid "Post by {0}" +msgstr "Postáil ó {0}" + +#: src/Navigation.tsx:172 +#: src/Navigation.tsx:179 +#: src/Navigation.tsx:186 +msgid "Post by @{0}" +msgstr "Postáil ó @{0}" + +#: src/view/com/util/forms/PostDropdownBtn.tsx:84 +msgid "Post deleted" +msgstr "Scriosadh an phostáil" + +#: src/view/com/post-thread/PostThread.tsx:461 +msgid "Post hidden" +msgstr "Cuireadh an phostáil i bhfolach" + +#: src/view/com/composer/select-language/SelectLangBtn.tsx:87 +msgid "Post language" +msgstr "Teanga postála" + +#: src/view/com/modals/lang-settings/PostLanguagesSettings.tsx:75 +msgid "Post Languages" +msgstr "Teangacha postála" + +#: src/view/com/post-thread/PostThread.tsx:513 +msgid "Post not found" +msgstr "Ní bhfuarthas an phostáil" + +#: src/view/screens/Profile.tsx:180 +msgid "Posts" +msgstr "Postálacha" + +#: src/view/com/posts/FeedErrorMessage.tsx:64 +msgid "Posts hidden" +msgstr "Cuireadh na postálacha i bhfolach" + +#: src/view/com/modals/LinkWarning.tsx:46 +msgid "Potentially Misleading Link" +msgstr "Is féidir go bhfuil an nasc seo míthreorach." + +#: src/view/com/lightbox/Lightbox.web.tsx:135 +msgid "Previous image" +msgstr "An íomhá roimhe seo" + +#: src/view/screens/LanguageSettings.tsx:187 +msgid "Primary Language" +msgstr "Príomhtheanga" + +#: src/view/screens/PreferencesThreads.tsx:97 +msgid "Prioritize Your Follows" +msgstr "Tabhair Tosaíocht do Do Chuid Leantóirí" + +#: src/view/screens/Settings/index.tsx:632 +#: src/view/shell/desktop/RightNav.tsx:72 +msgid "Privacy" +msgstr "Príobháideacht" + +#: src/Navigation.tsx:217 +#: src/view/screens/PrivacyPolicy.tsx:29 +#: src/view/screens/Settings/index.tsx:891 +#: src/view/shell/Drawer.tsx:262 +msgid "Privacy Policy" +msgstr "Polasaí príobháideachta" + +#: src/view/com/auth/login/ForgotPasswordForm.tsx:198 +msgid "Processing..." +msgstr "Á phróiseáil..." + +#: src/view/shell/bottom-bar/BottomBar.tsx:247 +#: src/view/shell/desktop/LeftNav.tsx:415 +#: src/view/shell/Drawer.tsx:70 +#: src/view/shell/Drawer.tsx:546 +#: src/view/shell/Drawer.tsx:547 +msgid "Profile" +msgstr "Próifíl" + +#: src/view/com/modals/EditProfile.tsx:128 +msgid "Profile updated" +msgstr "Próifíl uasdátaithe" + +#: src/view/screens/Settings/index.tsx:949 +msgid "Protect your account by verifying your email." +msgstr "Dearbhaigh do ríomhphost le do chuntas a chosaint." + +#: src/screens/Onboarding/StepFinished.tsx:101 +msgid "Public" +msgstr "Poiblí" + +#: src/view/screens/ModerationModlists.tsx:61 +msgid "Public, shareable lists of users to mute or block in bulk." +msgstr "Liostaí poiblí agus inroinnte d’úsáideoirí le cur i bhfolach nó le blocáil ar an mórchóir" + +#: src/view/screens/Lists.tsx:61 +msgid "Public, shareable lists which can drive feeds." +msgstr "Liostaí poiblí agus inroinnte atá in ann fothaí a bheathú" + +#: src/view/com/composer/Composer.tsx:335 +msgid "Publish post" +msgstr "Foilsigh an phostáil" + +#: src/view/com/composer/Composer.tsx:335 +msgid "Publish reply" +msgstr "Foilsigh an freagra" + +#: src/view/com/modals/Repost.tsx:65 +msgctxt "action" +msgid "Quote post" +msgstr "Luaigh an phostáil seo" + +#: src/view/com/util/post-ctrls/RepostButton.web.tsx:58 +msgid "Quote post" +msgstr "Postáil athluaite" + +#: src/view/com/modals/Repost.tsx:70 +msgctxt "action" +msgid "Quote Post" +msgstr "Luaigh an phostáil seo" + +#: src/view/screens/PreferencesThreads.tsx:86 +msgid "Random (aka \"Poster's Roulette\")" +msgstr "Randamach" + +#: src/view/com/modals/EditImage.tsx:236 +msgid "Ratios" +msgstr "Cóimheasa" + +#: src/view/com/auth/onboarding/RecommendedFeeds.tsx:116 +msgid "Recommended Feeds" +msgstr "Fothaí molta" + +#: src/view/com/auth/onboarding/RecommendedFollows.tsx:180 +msgid "Recommended Users" +msgstr "Cuntais mholta" + +#: src/view/com/modals/ListAddRemoveUsers.tsx:264 +#: src/view/com/modals/SelfLabel.tsx:83 +#: src/view/com/modals/UserAddRemoveLists.tsx:219 +#: src/view/com/util/UserAvatar.tsx:285 +#: src/view/com/util/UserBanner.tsx:91 +msgid "Remove" +msgstr "Scrios" + +#: src/view/com/feeds/FeedSourceCard.tsx:106 +msgid "Remove {0} from my feeds?" +msgstr "An bhfuil fonn ort {0} a bhaint de do chuid fothaí?" + +#: src/view/com/util/AccountDropdownBtn.tsx:22 +msgid "Remove account" +msgstr "Bain an cuntas de" + +#: src/view/com/posts/FeedErrorMessage.tsx:131 +#: src/view/com/posts/FeedErrorMessage.tsx:166 +msgid "Remove feed" +msgstr "Bain an fotha de" + +#: src/view/com/feeds/FeedSourceCard.tsx:105 +#: src/view/com/feeds/FeedSourceCard.tsx:167 +#: src/view/com/feeds/FeedSourceCard.tsx:172 +#: src/view/com/feeds/FeedSourceCard.tsx:243 +#: src/view/screens/ProfileFeed.tsx:272 +msgid "Remove from my feeds" +msgstr "Bain de mo chuid fothaí" + +#: src/view/com/composer/photos/Gallery.tsx:167 +msgid "Remove image" +msgstr "Bain an íomhá de" + +#: src/view/com/composer/ExternalEmbed.tsx:70 +msgid "Remove image preview" +msgstr "Bain réamhléiriú den íomhá" + +#: src/view/com/modals/Repost.tsx:47 +msgid "Remove repost" +msgstr "Scrios an athphostáil" + +#: src/view/com/feeds/FeedSourceCard.tsx:173 +msgid "Remove this feed from my feeds?" +msgstr "An bhfuil fonn ort an fotha seo a bhaint de do chuid fothaí?" + +#: src/view/com/posts/FeedErrorMessage.tsx:132 +msgid "Remove this feed from your saved feeds?" +msgstr "An bhfuil fonn ort an fotha seo a bhaint de do chuid fothaí sábháilte?" + +#: src/view/com/modals/ListAddRemoveUsers.tsx:199 +#: src/view/com/modals/UserAddRemoveLists.tsx:152 +msgid "Removed from list" +msgstr "Baineadh den liosta é" + +#: src/view/com/feeds/FeedSourceCard.tsx:111 +#: src/view/com/feeds/FeedSourceCard.tsx:178 +msgid "Removed from my feeds" +msgstr "Baineadh de do chuid fothaí é" + +#: src/view/com/composer/ExternalEmbed.tsx:71 +msgid "Removes default thumbnail from {0}" +msgstr "Baineann sé seo an mhionsamhail réamhshocraithe de {0}" + +#: src/view/screens/Profile.tsx:181 +msgid "Replies" +msgstr "Freagraí" + +#: src/view/com/threadgate/WhoCanReply.tsx:98 +msgid "Replies to this thread are disabled" +msgstr "Ní féidir freagraí a thabhairt ar an gcomhrá seo" + +#: src/view/com/composer/Composer.tsx:348 +msgctxt "action" +msgid "Reply" +msgstr "Freagair" + +#: src/view/screens/PreferencesHomeFeed.tsx:144 +msgid "Reply Filters" +msgstr "Scagairí freagra" + +#: src/view/com/post/Post.tsx:166 +#: src/view/com/posts/FeedItem.tsx:284 +msgctxt "description" +msgid "Reply to <0/>" +msgstr "Freagra ar <0/>" + +#: src/view/com/modals/report/Modal.tsx:166 +msgid "Report {collectionName}" +msgstr "Déan gearán faoi {collectionName}" + +#: src/view/com/profile/ProfileHeader.tsx:360 +msgid "Report Account" +msgstr "Déan gearán faoi chuntas" + +#: src/view/screens/ProfileFeed.tsx:292 +msgid "Report feed" +msgstr "Déan gearán faoi fhotha" + +#: src/view/screens/ProfileList.tsx:458 +msgid "Report List" +msgstr "Déan gearán faoi liosta" + +#: src/view/com/modals/report/SendReportButton.tsx:37 +#: src/view/com/util/forms/PostDropdownBtn.tsx:210 +msgid "Report post" +msgstr "Déan gearán faoi phostáil" + +#: src/view/com/modals/Repost.tsx:43 +#: src/view/com/modals/Repost.tsx:48 +#: src/view/com/modals/Repost.tsx:53 +#: src/view/com/util/post-ctrls/RepostButton.tsx:61 +msgctxt "action" +msgid "Repost" +msgstr "Athphostáil" + +#: src/view/com/util/post-ctrls/RepostButton.web.tsx:48 +msgid "Repost" +msgstr "Athphostáil" + +#: src/view/com/util/post-ctrls/RepostButton.web.tsx:94 +#: src/view/com/util/post-ctrls/RepostButton.web.tsx:105 +msgid "Repost or quote post" +msgstr "Athphostáil nó luaigh postáil" + +#: src/view/screens/PostRepostedBy.tsx:27 +msgid "Reposted By" +msgstr "Athphostáilte ag" + +#: src/view/com/posts/FeedItem.tsx:204 +msgid "Reposted by {0}" +msgstr "Athphostáilte ag {0}" + +#: src/view/com/posts/FeedItem.tsx:221 +msgid "Reposted by <0/>" +msgstr "Athphostáilte ag <0/>" + +#: src/view/com/notifications/FeedItem.tsx:162 +msgid "reposted your post" +msgstr "— d'athphostáil sé/sí do phostáil" + +#: src/view/com/post-thread/PostThreadItem.tsx:185 +msgid "Reposts of this post" +msgstr "Athphostálacha den phostáil seo" + +#: src/view/com/modals/ChangeEmail.tsx:181 +#: src/view/com/modals/ChangeEmail.tsx:183 +msgid "Request Change" +msgstr "Iarr Athrú" + +#: src/view/com/auth/create/Step2.tsx:219 +#~ msgid "Request code" +#~ msgstr "Iarr cód" + +#: src/view/com/modals/ChangePassword.tsx:239 +#: src/view/com/modals/ChangePassword.tsx:241 +msgid "Request Code" +msgstr "Iarr Cód" + +#: src/view/screens/Settings/index.tsx:456 +msgid "Require alt text before posting" +msgstr "Bíodh téacs malartach ann roimh phostáil i gcónaí" + +#: src/view/com/auth/create/Step1.tsx:153 +msgid "Required for this provider" +msgstr "Riachtanach don soláthraí seo" + +#: src/view/com/auth/login/SetNewPasswordForm.tsx:124 +#: src/view/com/auth/login/SetNewPasswordForm.tsx:136 +msgid "Reset code" +msgstr "Cód athshocraithe" + +#: src/view/com/modals/ChangePassword.tsx:190 +msgid "Reset Code" +msgstr "Cód Athshocraithe" + +#: src/view/screens/Settings/index.tsx:824 +msgid "Reset onboarding" +msgstr "Athshocraigh an próiseas cláraithe" + +#: src/view/screens/Settings/index.tsx:827 +msgid "Reset onboarding state" +msgstr "Athshocraigh an próiseas cláraithe" + +#: src/view/com/auth/login/ForgotPasswordForm.tsx:104 +msgid "Reset password" +msgstr "Athshocraigh an pasfhocal" + +#: src/view/screens/Settings/index.tsx:814 +msgid "Reset preferences" +msgstr "Athshocraigh na roghanna" + +#: src/view/screens/Settings/index.tsx:817 +msgid "Reset preferences state" +msgstr "Athshocraigh na roghanna" + +#: src/view/screens/Settings/index.tsx:825 +msgid "Resets the onboarding state" +msgstr "Athshocraíonn sé seo an clárú" + +#: src/view/screens/Settings/index.tsx:815 +msgid "Resets the preferences state" +msgstr "Athshocraíonn sé seo na roghanna" + +#: src/view/com/auth/login/LoginForm.tsx:269 +msgid "Retries login" +msgstr "Baineann sé seo triail eile as an logáil isteach" + +#: src/view/com/util/error/ErrorMessage.tsx:57 +#: src/view/com/util/error/ErrorScreen.tsx:74 +msgid "Retries the last action, which errored out" +msgstr "Baineann sé seo triail eile as an ngníomh is déanaí, ar theip air" + +#: src/screens/Onboarding/StepInterests/index.tsx:221 +#: src/screens/Onboarding/StepInterests/index.tsx:224 +#: src/view/com/auth/create/CreateAccount.tsx:177 +#: src/view/com/auth/create/CreateAccount.tsx:182 +#: src/view/com/auth/login/LoginForm.tsx:268 +#: src/view/com/auth/login/LoginForm.tsx:271 +#: src/view/com/util/error/ErrorMessage.tsx:55 +#: src/view/com/util/error/ErrorScreen.tsx:72 +msgid "Retry" +msgstr "Bain triail eile as" + +#: src/view/com/auth/create/Step2.tsx:247 +#~ msgid "Retry." +#~ msgstr "Bain triail eile as." + +#: src/view/screens/ProfileList.tsx:898 +msgid "Return to previous page" +msgstr "Fill ar an leathanach roimhe seo" + +#: src/view/shell/desktop/RightNav.tsx:55 +#~ msgid "SANDBOX. Posts and accounts are not permanent." +#~ msgstr "BOSCA GAINIMH. Ní choinneofar póstálacha ná cuntais." + +#: src/view/com/lightbox/Lightbox.tsx:132 +#: src/view/com/modals/CreateOrEditList.tsx:345 +msgctxt "action" +msgid "Save" +msgstr "Sábháil" + +#: src/view/com/modals/BirthDateSettings.tsx:94 +#: src/view/com/modals/BirthDateSettings.tsx:97 +#: src/view/com/modals/ChangeHandle.tsx:173 +#: src/view/com/modals/CreateOrEditList.tsx:337 +#: src/view/com/modals/EditProfile.tsx:224 +#: src/view/screens/ProfileFeed.tsx:345 +msgid "Save" +msgstr "Sábháil" + +#: src/view/com/modals/AltImage.tsx:130 +msgid "Save alt text" +msgstr "Sábháil an téacs malartach" + +#: src/view/com/modals/EditProfile.tsx:232 +msgid "Save Changes" +msgstr "Sábháil na hathruithe" + +#: src/view/com/modals/ChangeHandle.tsx:170 +msgid "Save handle change" +msgstr "Sábháil an leasainm nua" + +#: src/view/com/modals/crop-image/CropImage.web.tsx:144 +msgid "Save image crop" +msgstr "Sábháil an pictiúr bearrtha" + +#: src/view/screens/SavedFeeds.tsx:122 +msgid "Saved Feeds" +msgstr "Fothaí Sábháilte" + +#: src/view/com/modals/EditProfile.tsx:225 +msgid "Saves any changes to your profile" +msgstr "Sábhálann sé seo na hathruithe a rinne tú ar do phróifíl" + +#: src/view/com/modals/ChangeHandle.tsx:171 +msgid "Saves handle change to {handle}" +msgstr "Sábhálann sé seo athrú an leasainm go {handle}" + +#: src/screens/Onboarding/index.tsx:36 +msgid "Science" +msgstr "Eolaíocht" + +#: src/view/screens/ProfileList.tsx:854 +msgid "Scroll to top" +msgstr "Fill ar an mbarr" + +#: src/Navigation.tsx:437 +#: src/view/com/auth/LoggedOut.tsx:122 +#: src/view/com/modals/ListAddRemoveUsers.tsx:75 +#: src/view/com/util/forms/SearchInput.tsx:67 +#: src/view/com/util/forms/SearchInput.tsx:79 +#: src/view/screens/Search/Search.tsx:418 +#: src/view/screens/Search/Search.tsx:645 +#: src/view/screens/Search/Search.tsx:663 +#: src/view/shell/bottom-bar/BottomBar.tsx:159 +#: src/view/shell/desktop/LeftNav.tsx:324 +#: src/view/shell/desktop/Search.tsx:214 +#: src/view/shell/desktop/Search.tsx:223 +#: src/view/shell/Drawer.tsx:362 +#: src/view/shell/Drawer.tsx:363 +msgid "Search" +msgstr "Cuardaigh" + +#: src/view/screens/Search/Search.tsx:712 +#: src/view/shell/desktop/Search.tsx:255 +msgid "Search for \"{query}\"" +msgstr "Déan cuardach ar “{query}”" + +#: src/view/com/auth/LoggedOut.tsx:104 +#: src/view/com/auth/LoggedOut.tsx:105 +#: src/view/com/modals/ListAddRemoveUsers.tsx:70 +msgid "Search for users" +msgstr "Cuardaigh úsáideoirí" + +#: src/view/com/modals/ChangeEmail.tsx:110 +msgid "Security Step Required" +msgstr "Céim Slándála de dhíth" + +#: src/view/screens/SavedFeeds.tsx:163 +msgid "See this guide" +msgstr "Féach ar an treoirleabhar seo" + +#: src/view/com/auth/HomeLoggedOutCTA.tsx:39 +msgid "See what's next" +msgstr "Féach an chéad rud eile" + +#: src/view/com/util/Selector.tsx:106 +msgid "Select {item}" +msgstr "Roghnaigh {item}" + +#: src/view/com/modals/ServerInput.tsx:75 +#~ msgid "Select Bluesky Social" +#~ msgstr "Roghnaigh Bluesky Social" + +#: src/view/com/auth/login/Login.tsx:117 +msgid "Select from an existing account" +msgstr "Roghnaigh ó chuntas atá ann" + +#: src/view/com/util/Selector.tsx:107 +msgid "Select option {i} of {numItems}" +msgstr "Roghnaigh rogha {i} as {numItems}" + +#: src/view/com/auth/create/Step1.tsx:103 +#: src/view/com/auth/login/LoginForm.tsx:150 +msgid "Select service" +msgstr "Roghnaigh seirbhís" + +#: src/screens/Onboarding/StepSuggestedAccounts/index.tsx:52 +msgid "Select some accounts below to follow" +msgstr "Roghnaigh cúpla cuntas le leanúint" + +#: src/view/com/auth/server-input/index.tsx:82 +msgid "Select the service that hosts your data." +msgstr "Roghnaigh an tseirbhís a óstálann do chuid sonraí." + +#: src/screens/Onboarding/StepModeration/index.tsx:49 +#~ msgid "Select the types of content that you want to see (or not see), and we'll handle the rest." +#~ msgstr "Roghnaigh na rudaí ba mhaith leat a fheiceáil (nó gan a fheiceáil), agus leanfaimid ar aghaidh as sin." + +#: src/screens/Onboarding/StepTopicalFeeds.tsx:90 +msgid "Select topical feeds to follow from the list below" +msgstr "Roghnaigh fothaí le leanúint ón liosta thíos" + +#: src/screens/Onboarding/StepModeration/index.tsx:75 +msgid "Select what you want to see (or not see), and we’ll handle the rest." +msgstr "Roghnaigh na rudaí ba mhaith leat a fheiceáil (nó gan a fheiceáil), agus leanfaimid ar aghaidh as sin" + +#: src/view/screens/LanguageSettings.tsx:281 +msgid "Select which languages you want your subscribed feeds to include. If none are selected, all languages will be shown." +msgstr "Roghnaigh na teangacha ba mhaith leat a fheiceáil i do chuid fothaí. Mura roghnaíonn tú, taispeánfar ábhar i ngach teanga duit." + +#: src/view/screens/LanguageSettings.tsx:98 +msgid "Select your app language for the default text to display in the app" +msgstr "Roghnaigh teanga na roghchlár a fheicfidh tú san aip" + +#: src/screens/Onboarding/StepInterests/index.tsx:196 +msgid "Select your interests from the options below" +msgstr "Roghnaigh na rudaí a bhfuil suim agat iontu as na roghanna thíos" + +#: src/view/com/auth/create/Step2.tsx:155 +#~ msgid "Select your phone's country" +#~ msgstr "Roghnaigh tír do ghutháin" + +#: src/view/screens/LanguageSettings.tsx:190 +msgid "Select your preferred language for translations in your feed." +msgstr "Do rogha teanga nuair a dhéanfar aistriúchán ar ábhar i d'fhotha." + +#: src/screens/Onboarding/StepAlgoFeeds/index.tsx:116 +msgid "Select your primary algorithmic feeds" +msgstr "Roghnaigh do phríomhfhothaí algartamacha" + +#: src/screens/Onboarding/StepAlgoFeeds/index.tsx:142 +msgid "Select your secondary algorithmic feeds" +msgstr "Roghnaigh do chuid fothaí algartamacha tánaisteacha" + +#: src/view/com/modals/VerifyEmail.tsx:202 +#: src/view/com/modals/VerifyEmail.tsx:204 +msgid "Send Confirmation Email" +msgstr "Seol ríomhphost dearbhaithe" + +#: src/view/com/modals/DeleteAccount.tsx:131 +msgid "Send email" +msgstr "Seol ríomhphost" + +#: src/view/com/modals/DeleteAccount.tsx:144 +msgctxt "action" +msgid "Send Email" +msgstr "Seol ríomhphost" + +#: src/view/shell/Drawer.tsx:295 +#: src/view/shell/Drawer.tsx:316 +msgid "Send feedback" +msgstr "Seol aiseolas" + +#: src/view/com/modals/report/SendReportButton.tsx:45 +msgid "Send Report" +msgstr "Seol an tuairisc" + +#: src/view/com/modals/DeleteAccount.tsx:133 +msgid "Sends email with confirmation code for account deletion" +msgstr "Seolann sé seo ríomhphost ina bhfuil cód dearbhaithe chun an cuntas a scriosadh" + +#: src/view/com/auth/server-input/index.tsx:110 +msgid "Server address" +msgstr "Seoladh an fhreastalaí" + +#: src/view/com/modals/ContentFilteringSettings.tsx:311 +msgid "Set {value} for {labelGroup} content moderation policy" +msgstr "Socraigh {value} le haghaidh polasaí modhnóireachta {labelGroup}" + +#: src/view/com/modals/ContentFilteringSettings.tsx:160 +#: src/view/com/modals/ContentFilteringSettings.tsx:179 +msgctxt "action" +msgid "Set Age" +msgstr "Cén aois thú?" + +#: src/view/screens/Settings/index.tsx:488 +msgid "Set color theme to dark" +msgstr "Roghnaigh an modh dorcha" + +#: src/view/screens/Settings/index.tsx:481 +msgid "Set color theme to light" +msgstr "Roghnaigh an modh sorcha" + +#: src/view/screens/Settings/index.tsx:475 +msgid "Set color theme to system setting" +msgstr "Úsáid scéim dathanna an chórais" + +#: src/view/screens/Settings/index.tsx:514 +msgid "Set dark theme to the dark theme" +msgstr "Úsáid an téama dorcha mar théama dorcha" + +#: src/view/screens/Settings/index.tsx:507 +msgid "Set dark theme to the dim theme" +msgstr "Úsáid an téama breacdhorcha mar théama dorcha" + +#: src/view/com/auth/login/SetNewPasswordForm.tsx:104 +msgid "Set new password" +msgstr "Socraigh pasfhocal nua" + +#: src/view/com/auth/create/Step1.tsx:225 +msgid "Set password" +msgstr "Socraigh pasfhocal" + +#: src/view/screens/PreferencesHomeFeed.tsx:225 +msgid "Set this setting to \"No\" to hide all quote posts from your feed. Reposts will still be visible." +msgstr "Roghnaigh “Níl” chun postálacha athluaite a chur i bhfolach i d'fhotha. Feicfidh tú athphostálacha fós." + +#: src/view/screens/PreferencesHomeFeed.tsx:122 +msgid "Set this setting to \"No\" to hide all replies from your feed." +msgstr "Roghnaigh “Níl” chun freagraí a chur i bhfolach i d'fhotha." + +#: src/view/screens/PreferencesHomeFeed.tsx:191 +msgid "Set this setting to \"No\" to hide all reposts from your feed." +msgstr "Roghnaigh “Níl” chun athphostálacha a chur i bhfolach i d'fhotha." + +#: src/view/screens/PreferencesThreads.tsx:122 +msgid "Set this setting to \"Yes\" to show replies in a threaded view. This is an experimental feature." +msgstr "Roghnaigh “Tá” le freagraí a thaispeáint i snáitheanna. Is gné thurgnamhach é seo." + +#: src/view/screens/PreferencesHomeFeed.tsx:261 +msgid "Set this setting to \"Yes\" to show samples of your saved feeds in your following feed. This is an experimental feature." +msgstr "Roghnaigh “Tá” le samplaí ó do chuid fothaí sábháilte a thaispeáint in ”Á Leanúint”. Is gné thurgnamhach é seo." + +#: src/screens/Onboarding/Layout.tsx:50 +msgid "Set up your account" +msgstr "Socraigh do chuntas" + +#: src/view/com/modals/ChangeHandle.tsx:266 +msgid "Sets Bluesky username" +msgstr "Socraíonn sé seo d'ainm úsáideora ar Bluesky" + +#: src/view/com/auth/login/ForgotPasswordForm.tsx:157 +msgid "Sets email for password reset" +msgstr "Socraíonn sé seo an seoladh ríomhphoist le haghaidh athshocrú an phasfhocail" + +#: src/view/com/auth/login/ForgotPasswordForm.tsx:122 +msgid "Sets hosting provider for password reset" +msgstr "Socraíonn sé seo an soláthraí óstála le haghaidh athshocrú an phasfhocail" + +#: src/view/com/auth/create/Step1.tsx:104 +#: src/view/com/auth/login/LoginForm.tsx:151 +msgid "Sets server for the Bluesky client" +msgstr "Socraíonn sé seo freastalaí an chliaint Bluesky" + +#: src/Navigation.tsx:135 +#: src/view/screens/Settings/index.tsx:294 +#: src/view/shell/desktop/LeftNav.tsx:433 +#: src/view/shell/Drawer.tsx:567 +#: src/view/shell/Drawer.tsx:568 +msgid "Settings" +msgstr "Socruithe" + +#: src/view/com/modals/SelfLabel.tsx:125 +msgid "Sexual activity or erotic nudity." +msgstr "Gníomhaíocht ghnéasach nó lomnochtacht gháirsiúil." + +#: src/view/com/lightbox/Lightbox.tsx:141 +msgctxt "action" +msgid "Share" +msgstr "Comhroinn" + +#: src/view/com/profile/ProfileHeader.tsx:294 +#: src/view/com/util/forms/PostDropdownBtn.tsx:153 +#: src/view/screens/ProfileList.tsx:417 +msgid "Share" +msgstr "Comhroinn" + +#: src/view/screens/ProfileFeed.tsx:304 +msgid "Share feed" +msgstr "Comhroinn an fotha" + +#: src/screens/Onboarding/StepModeration/ModerationOption.tsx:43 +#: src/view/com/modals/ContentFilteringSettings.tsx:266 +#: src/view/com/util/moderation/ContentHider.tsx:107 +#: src/view/com/util/moderation/PostHider.tsx:108 +#: src/view/screens/Settings/index.tsx:344 +msgid "Show" +msgstr "Taispeáin" + +#: src/view/screens/PreferencesHomeFeed.tsx:68 +msgid "Show all replies" +msgstr "Taispeáin gach freagra" + +#: src/view/com/util/moderation/ScreenHider.tsx:132 +msgid "Show anyway" +msgstr "Taispeáin mar sin féin" + +#: src/view/com/modals/EmbedConsent.tsx:87 +msgid "Show embeds from {0}" +msgstr "Taispeáin ábhar leabaithe ó {0}" + +#: src/view/com/profile/ProfileHeader.tsx:458 +msgid "Show follows similar to {0}" +msgstr "Taispeáin cuntais cosúil le {0}" + +#: src/view/com/post-thread/PostThreadItem.tsx:535 +#: src/view/com/post/Post.tsx:197 +#: src/view/com/posts/FeedItem.tsx:360 +msgid "Show More" +msgstr "Tuilleadh" + +#: src/view/screens/PreferencesHomeFeed.tsx:258 +msgid "Show Posts from My Feeds" +msgstr "Taispeáin postálacha ó mo chuid fothaí" + +#: src/view/screens/PreferencesHomeFeed.tsx:222 +msgid "Show Quote Posts" +msgstr "Taispeáin postálacha athluaite" + +#: src/screens/Onboarding/StepFollowingFeed.tsx:118 +msgid "Show quote-posts in Following feed" +msgstr "Taispeáin postálacha athluaite san fhotha “Á Leanúint”" + +#: src/screens/Onboarding/StepFollowingFeed.tsx:134 +msgid "Show quotes in Following" +msgstr "Taispeáin postálacha athluaite san fhotha “Á Leanúint”" + +#: src/screens/Onboarding/StepFollowingFeed.tsx:94 +msgid "Show re-posts in Following feed" +msgstr "Taispeáin athphostálacha san fhotha “Á Leanúint”" + +#: src/view/screens/PreferencesHomeFeed.tsx:119 +msgid "Show Replies" +msgstr "Taispeáin freagraí" + +#: src/view/screens/PreferencesThreads.tsx:100 +msgid "Show replies by people you follow before all other replies." +msgstr "Taispeáin freagraí ó na daoine a leanann tú roimh aon fhreagra eile." + +#: src/screens/Onboarding/StepFollowingFeed.tsx:86 +msgid "Show replies in Following" +msgstr "Taispeáin freagraí san fhotha “Á Leanúint”" + +#: src/screens/Onboarding/StepFollowingFeed.tsx:70 +msgid "Show replies in Following feed" +msgstr "Taispeáin freagraí san fhotha “Á Leanúint”" + +#: src/view/screens/PreferencesHomeFeed.tsx:70 +msgid "Show replies with at least {value} {0}" +msgstr "Taispeáin freagraí a bhfuil ar a laghad {value} {0} acu" + +#: src/view/screens/PreferencesHomeFeed.tsx:188 +msgid "Show Reposts" +msgstr "Taispeáin athphostálacha" + +#: src/screens/Onboarding/StepFollowingFeed.tsx:110 +msgid "Show reposts in Following" +msgstr "Taispeáin athphostálacha san fhotha “Á Leanúint”" + +#: src/view/com/util/moderation/ContentHider.tsx:67 +#: src/view/com/util/moderation/PostHider.tsx:61 +msgid "Show the content" +msgstr "Taispeáin an t-ábhar" + +#: src/view/com/notifications/FeedItem.tsx:346 +msgid "Show users" +msgstr "Taispeáin úsáideoirí" + +#: src/view/com/profile/ProfileHeader.tsx:461 +msgid "Shows a list of users similar to this user." +msgstr "Taispeánann sé seo liosta úsáideoirí cosúil leis an úsáideoir seo." + +#: src/view/com/post-thread/PostThreadFollowBtn.tsx:124 +#: src/view/com/profile/ProfileHeader.tsx:505 +msgid "Shows posts from {0} in your feed" +msgstr "Taispeánann sé seo postálacha ó {0} i d'fhotha" + +#: src/view/com/auth/HomeLoggedOutCTA.tsx:70 +#: src/view/com/auth/login/Login.tsx:98 +#: src/view/com/auth/SplashScreen.tsx:79 +#: src/view/shell/bottom-bar/BottomBar.tsx:285 +#: src/view/shell/bottom-bar/BottomBar.tsx:286 +#: src/view/shell/bottom-bar/BottomBar.tsx:288 +#: src/view/shell/bottom-bar/BottomBarWeb.tsx:178 +#: src/view/shell/bottom-bar/BottomBarWeb.tsx:179 +#: src/view/shell/bottom-bar/BottomBarWeb.tsx:181 +#: src/view/shell/NavSignupCard.tsx:58 +#: src/view/shell/NavSignupCard.tsx:59 +msgid "Sign in" +msgstr "Logáil isteach" + +#: src/view/com/auth/HomeLoggedOutCTA.tsx:78 +#: src/view/com/auth/SplashScreen.tsx:82 +#: src/view/com/auth/SplashScreen.web.tsx:91 +msgid "Sign In" +msgstr "Logáil isteach" + +#: src/view/com/auth/login/ChooseAccountForm.tsx:44 +msgid "Sign in as {0}" +msgstr "Logáil isteach mar {0}" + +#: src/view/com/auth/login/ChooseAccountForm.tsx:118 +#: src/view/com/auth/login/Login.tsx:116 +msgid "Sign in as..." +msgstr "Logáil isteach mar..." + +#: src/view/com/auth/login/LoginForm.tsx:137 +msgid "Sign into" +msgstr "Logáil isteach i" + +#: src/view/com/modals/SwitchAccount.tsx:64 +#: src/view/com/modals/SwitchAccount.tsx:69 +#: src/view/screens/Settings/index.tsx:100 +#: src/view/screens/Settings/index.tsx:103 +msgid "Sign out" +msgstr "Logáil amach" + +#: src/view/shell/bottom-bar/BottomBar.tsx:275 +#: src/view/shell/bottom-bar/BottomBar.tsx:276 +#: src/view/shell/bottom-bar/BottomBar.tsx:278 +#: src/view/shell/bottom-bar/BottomBarWeb.tsx:168 +#: src/view/shell/bottom-bar/BottomBarWeb.tsx:169 +#: src/view/shell/bottom-bar/BottomBarWeb.tsx:171 +#: src/view/shell/NavSignupCard.tsx:49 +#: src/view/shell/NavSignupCard.tsx:50 +#: src/view/shell/NavSignupCard.tsx:52 +msgid "Sign up" +msgstr "Cláraigh" + +#: src/view/shell/NavSignupCard.tsx:42 +msgid "Sign up or sign in to join the conversation" +msgstr "Cláraigh nó logáil isteach chun páirt a ghlacadh sa chomhrá" + +#: src/view/com/util/moderation/ScreenHider.tsx:76 +msgid "Sign-in Required" +msgstr "Caithfidh tú logáil isteach" + +#: src/view/screens/Settings/index.tsx:355 +msgid "Signed in as" +msgstr "Logáilte isteach mar" + +#: src/view/com/auth/login/ChooseAccountForm.tsx:103 +msgid "Signed in as @{0}" +msgstr "Logáilte isteach mar @{0}" + +#: src/view/com/modals/SwitchAccount.tsx:66 +msgid "Signs {0} out of Bluesky" +msgstr "Logálann sé seo {0} amach as Bluesky" + +#: src/screens/Onboarding/StepInterests/index.tsx:235 +#: src/screens/Onboarding/StepSuggestedAccounts/index.tsx:195 +#: src/view/com/auth/onboarding/WelcomeMobile.tsx:33 +msgid "Skip" +msgstr "Ná bac leis" + +#: src/screens/Onboarding/StepInterests/index.tsx:232 +msgid "Skip this flow" +msgstr "Ná bac leis an bpróiseas seo" + +#: src/view/com/auth/create/Step2.tsx:82 +#~ msgid "SMS verification" +#~ msgstr "Dearbhú SMS" + +#: src/screens/Onboarding/index.tsx:40 +msgid "Software Dev" +msgstr "Forbairt Bogearraí" + +#: src/view/com/modals/ProfilePreview.tsx:62 +#~ msgid "Something went wrong and we're not sure what." +#~ msgstr "Chuaigh rud éigin ó rath, agus nílimid cinnte céard a bhí ann." + +#: src/view/com/modals/Waitlist.tsx:51 +msgid "Something went wrong. Check your email and try again." +msgstr "Chuaigh rud éigin ó rath. Féach ar do ríomhphost agus bain triail eile as." + +#: src/App.native.tsx:61 +msgid "Sorry! Your session expired. Please log in again." +msgstr "Ár leithscéal. Chuaigh do sheisiún i léig. Ní mór duit logáil isteach arís." + +#: src/view/screens/PreferencesThreads.tsx:69 +msgid "Sort Replies" +msgstr "Sórtáil freagraí" + +#: src/view/screens/PreferencesThreads.tsx:72 +msgid "Sort replies to the same post by:" +msgstr "Sórtáil freagraí ar an bpostáil chéanna de réir:" + +#: src/screens/Onboarding/index.tsx:30 +msgid "Sports" +msgstr "Spórt" + +#: src/view/com/modals/crop-image/CropImage.web.tsx:122 +msgid "Square" +msgstr "Cearnóg" + +#: src/view/com/modals/ServerInput.tsx:62 +#~ msgid "Staging" +#~ msgstr "Freastalaí tástála" + +#: src/view/screens/Settings/index.tsx:871 +msgid "Status page" +msgstr "Leathanach stádais" + +#: src/view/com/auth/create/StepHeader.tsx:22 +msgid "Step {0} of {numSteps}" +msgstr "Céim {0} as {numSteps}" + +#: src/view/screens/Settings/index.tsx:274 +msgid "Storage cleared, you need to restart the app now." +msgstr "Stóráil scriosta, tá ort an aip a atosú anois." + +#: src/Navigation.tsx:202 +#: src/view/screens/Settings/index.tsx:807 +msgid "Storybook" +msgstr "Storybook" + +#: src/view/com/modals/AppealLabel.tsx:101 +msgid "Submit" +msgstr "Seol" + +#: src/view/screens/ProfileList.tsx:607 +msgid "Subscribe" +msgstr "Liostáil" + +#: src/screens/Onboarding/StepAlgoFeeds/FeedCard.tsx:173 +#: src/screens/Onboarding/StepAlgoFeeds/FeedCard.tsx:307 +msgid "Subscribe to the {0} feed" +msgstr "Liostáil leis an bhfotha {0}" + +#: src/view/screens/ProfileList.tsx:603 +msgid "Subscribe to this list" +msgstr "Liostáil leis an liosta seo" + +#: src/view/screens/Search/Search.tsx:373 +msgid "Suggested Follows" +msgstr "Cuntais le leanúint" + +#: src/view/com/profile/ProfileHeaderSuggestedFollows.tsx:64 +msgid "Suggested for you" +msgstr "Molta duit" + +#: src/view/com/modals/SelfLabel.tsx:95 +msgid "Suggestive" +msgstr "Gáirsiúil" + +#: src/Navigation.tsx:212 +#: src/view/screens/Support.tsx:30 +#: src/view/screens/Support.tsx:33 +msgid "Support" +msgstr "Tacaíocht" + +#: src/view/com/modals/ProfilePreview.tsx:110 +#~ msgid "Swipe up to see more" +#~ msgstr "Svaidhpeáil aníos le tuilleadh a fheiceáil" + +#: src/view/com/modals/SwitchAccount.tsx:117 +msgid "Switch Account" +msgstr "Athraigh an cuntas" + +#: src/view/com/modals/SwitchAccount.tsx:97 +#: src/view/screens/Settings/index.tsx:130 +msgid "Switch to {0}" +msgstr "Athraigh go {0}" + +#: src/view/com/modals/SwitchAccount.tsx:98 +#: src/view/screens/Settings/index.tsx:131 +msgid "Switches the account you are logged in to" +msgstr "Athraíonn sé seo an cuntas beo" + +#: src/view/screens/Settings/index.tsx:472 +msgid "System" +msgstr "Córas" + +#: src/view/screens/Settings/index.tsx:795 +msgid "System log" +msgstr "Logleabhar an chórais" + +#: src/view/com/modals/crop-image/CropImage.web.tsx:112 +msgid "Tall" +msgstr "Ard" + +#: src/view/com/util/images/AutoSizedImage.tsx:70 +msgid "Tap to view fully" +msgstr "Tapáil leis an rud iomlán a fheiceáil" + +#: src/screens/Onboarding/index.tsx:39 +msgid "Tech" +msgstr "Teic" + +#: src/view/shell/desktop/RightNav.tsx:81 +msgid "Terms" +msgstr "Téarmaí" + +#: src/Navigation.tsx:222 +#: src/view/screens/Settings/index.tsx:885 +#: src/view/screens/TermsOfService.tsx:29 +#: src/view/shell/Drawer.tsx:256 +msgid "Terms of Service" +msgstr "Téarmaí Seirbhíse" + +#: src/view/com/modals/AppealLabel.tsx:70 +#: src/view/com/modals/report/InputIssueDetails.tsx:51 +msgid "Text input field" +msgstr "Réimse téacs" + +#: src/view/com/auth/create/CreateAccount.tsx:90 +msgid "That handle is already taken." +msgstr "Tá an leasainm sin in úsáid cheana féin." + +#: src/view/com/profile/ProfileHeader.tsx:262 +msgid "The account will be able to interact with you after unblocking." +msgstr "Beidh an cuntas seo in ann caidreamh a dhéanamh leat tar éis duit é a dhíbhlocáil" + +#: src/view/screens/CommunityGuidelines.tsx:36 +msgid "The Community Guidelines have been moved to <0/>" +msgstr "Bogadh Treoirlínte an Phobail go dtí <0/>" + +#: src/view/screens/CopyrightPolicy.tsx:33 +msgid "The Copyright Policy has been moved to <0/>" +msgstr "Bogadh an Polasaí Cóipchirt go dtí <0/>" + +#: src/screens/Onboarding/Layout.tsx:60 +msgid "The following steps will help customize your Bluesky experience." +msgstr "Cuideoidh na céimeanna seo a leanas leat Bluesky a chur in oiriúint duit féin." + +#: src/view/com/post-thread/PostThread.tsx:516 +msgid "The post may have been deleted." +msgstr "Is féidir gur scriosadh an phostáil seo." + +#: src/view/screens/PrivacyPolicy.tsx:33 +msgid "The Privacy Policy has been moved to <0/>" +msgstr "Bogadh Polasaí na Príobháideachta go dtí <0/>" + +#: src/view/screens/Support.tsx:36 +msgid "The support form has been moved. If you need help, please <0/> or visit {HELP_DESK_URL} to get in touch with us." +msgstr "Bogadh an fhoirm tacaíochta go dtí <0/>. Má tá cuidiú ag teastáil uait, <0/> le do thoil, nó tabhair cuairt ar {HELP_DESK_URL} le dul i dteagmháil linn." + +#: src/view/screens/TermsOfService.tsx:33 +msgid "The Terms of Service have been moved to" +msgstr "Bogadh ár dTéarmaí Seirbhíse go dtí" + +#: src/screens/Onboarding/StepAlgoFeeds/index.tsx:150 +msgid "There are many feeds to try:" +msgstr "Tá a lán fothaí ann le blaiseadh:" + +#: src/view/screens/ProfileFeed.tsx:549 +msgid "There was an an issue contacting the server, please check your internet connection and try again." +msgstr "Bhí fadhb ann maidir le dul i dteagmháil leis an bhfreastalaí. Seiceáil do cheangal leis an idirlíon agus bain triail eile as, le do thoil." + +#: src/view/com/posts/FeedErrorMessage.tsx:139 +msgid "There was an an issue removing this feed. Please check your internet connection and try again." +msgstr "Bhí fadhb ann maidir leis an bhfotha seo a bhaint. Seiceáil do cheangal leis an idirlíon agus bain triail eile as, le do thoil." + +#: src/view/screens/ProfileFeed.tsx:209 +msgid "There was an an issue updating your feeds, please check your internet connection and try again." +msgstr "Bhí fadhb ann maidir le huasdátú do chuid fothaí. Seiceáil do cheangal leis an idirlíon agus bain triail eile as, le do thoil." + +#: src/view/screens/ProfileFeed.tsx:236 +#: src/view/screens/ProfileList.tsx:266 +#: src/view/screens/SavedFeeds.tsx:209 +#: src/view/screens/SavedFeeds.tsx:231 +#: src/view/screens/SavedFeeds.tsx:252 +msgid "There was an issue contacting the server" +msgstr "Bhí fadhb ann maidir le teagmháil a dhéanamh leis an bhfreastalaí" + +#: src/view/com/auth/onboarding/RecommendedFeedsItem.tsx:57 +#: src/view/com/auth/onboarding/RecommendedFeedsItem.tsx:66 +#: src/view/com/feeds/FeedSourceCard.tsx:113 +#: src/view/com/feeds/FeedSourceCard.tsx:127 +#: src/view/com/feeds/FeedSourceCard.tsx:181 +msgid "There was an issue contacting your server" +msgstr "Bhí fadhb ann maidir le teagmháil a dhéanamh le do fhreastálaí" + +#: src/view/com/notifications/Feed.tsx:117 +msgid "There was an issue fetching notifications. Tap here to try again." +msgstr "Bhí fadhb ann maidir le fógraí a fháil. Tapáil anseo le triail eile a bhaint as." + +#: src/view/com/posts/Feed.tsx:263 +msgid "There was an issue fetching posts. Tap here to try again." +msgstr "Bhí fadhb ann maidir le postálacha a fháil. Tapáil anseo le triail eile a bhaint as." + +#: src/view/com/lists/ListMembers.tsx:172 +msgid "There was an issue fetching the list. Tap here to try again." +msgstr "Bhí fadhb ann maidir leis an liosta a fháil. Tapáil anseo le triail eile a bhaint as." + +#: src/view/com/feeds/ProfileFeedgens.tsx:148 +#: src/view/com/lists/ProfileLists.tsx:155 +msgid "There was an issue fetching your lists. Tap here to try again." +msgstr "Bhí fadhb ann maidir le do chuid liostaí a fháil. Tapáil anseo le triail eile a bhaint as." + +#: src/screens/Onboarding/StepModeration/AdultContentEnabledPref.tsx:63 +#: src/view/com/modals/ContentFilteringSettings.tsx:126 +msgid "There was an issue syncing your preferences with the server" +msgstr "Bhí fadhb ann maidir le do chuid roghanna a shioncronú leis an bhfreastalaí" + +#: src/view/screens/AppPasswords.tsx:66 +msgid "There was an issue with fetching your app passwords" +msgstr "Bhí fadhb ann maidir le do chuid pasfhocal don aip a fháil" + +#: src/view/com/post-thread/PostThreadFollowBtn.tsx:93 +#: src/view/com/post-thread/PostThreadFollowBtn.tsx:105 +#: src/view/com/profile/ProfileHeader.tsx:156 +#: src/view/com/profile/ProfileHeader.tsx:177 +#: src/view/com/profile/ProfileHeader.tsx:216 +#: src/view/com/profile/ProfileHeader.tsx:229 +#: src/view/com/profile/ProfileHeader.tsx:249 +#: src/view/com/profile/ProfileHeader.tsx:271 +msgid "There was an issue! {0}" +msgstr "Bhí fadhb ann! {0}" + +#: src/view/screens/ProfileList.tsx:287 +#: src/view/screens/ProfileList.tsx:306 +#: src/view/screens/ProfileList.tsx:328 +#: src/view/screens/ProfileList.tsx:347 +msgid "There was an issue. Please check your internet connection and try again." +msgstr "Bhí fadhb ann. Seiceáil do cheangal leis an idirlíon, le do thoil, agus bain triail eile as." + +#: src/view/com/util/ErrorBoundary.tsx:36 +msgid "There was an unexpected issue in the application. Please let us know if this happened to you!" +msgstr "D’éirigh fadhb gan choinne leis an aip. Abair linn, le do thoil, má tharla sé sin duit!" + +#: src/screens/Deactivated.tsx:106 +msgid "There's been a rush of new users to Bluesky! We'll activate your account as soon as we can." +msgstr "Tá ráchairt ar Bluesky le déanaí! Cuirfidh muid do chuntas ag obair chomh luath agus is féidir." + +#: src/view/com/auth/create/Step2.tsx:55 +#~ msgid "There's something wrong with this number. Please choose your country and enter your full phone number!" +#~ msgstr "Tá rud éigin mícheart leis an uimhir seo. Roghnaigh do thír, le do thoil, agus cuir d’uimhir ghutháin iomlán isteach." + +#: src/screens/Onboarding/StepSuggestedAccounts/index.tsx:138 +msgid "These are popular accounts you might like:" +msgstr "Is cuntais iad seo a bhfuil a lán leantóirí acu. Is féidir go dtaitneoidh siad leat." + +#: src/view/com/util/moderation/ScreenHider.tsx:88 +msgid "This {screenDescription} has been flagged:" +msgstr "Cuireadh bratach leis an {screenDescription} seo:" + +#: src/view/com/util/moderation/ScreenHider.tsx:83 +msgid "This account has requested that users sign in to view their profile." +msgstr "Ní mór duit logáil isteach le próifíl an chuntais seo a fheiceáil." + +#: src/view/com/modals/EmbedConsent.tsx:68 +msgid "This content is hosted by {0}. Do you want to enable external media?" +msgstr "Tá an t-ábhar seo ar fáil ó {0}. An bhfuil fonn ort na meáin sheachtracha a thaispeáint?" + +#: src/view/com/modals/ModerationDetails.tsx:67 +msgid "This content is not available because one of the users involved has blocked the other." +msgstr "Níl an t-ábhar seo le feiceáil toisc gur bhlocáil duine de na húsáideoirí an duine eile." + +#: src/view/com/posts/FeedErrorMessage.tsx:108 +msgid "This content is not viewable without a Bluesky account." +msgstr "Níl an t-ábhar seo le feiceáil gan chuntas Bluesky." + +#: src/view/screens/Settings/ExportCarDialog.tsx:75 +msgid "This feature is in beta. You can read more about repository exports in <0>this blogpost." +msgstr "Tá an ghné seo á tástáil fós. Tig leat níos mó faoi chartlanna easpórtáilte a léamh sa <0>bhlagphost seo." + +#: src/view/com/posts/FeedErrorMessage.tsx:114 +msgid "This feed is currently receiving high traffic and is temporarily unavailable. Please try again later." +msgstr "Tá ráchairt an-mhór ar an bhfotha seo faoi láthair. Níl sé ar fáil anois díreach dá bhrí sin. Bain triail eile as níos déanaí, le do thoil." + +#: src/view/screens/Profile.tsx:420 +#: src/view/screens/ProfileFeed.tsx:475 +#: src/view/screens/ProfileList.tsx:660 +msgid "This feed is empty!" +msgstr "Tá an fotha seo folamh!" + +#: src/view/com/posts/CustomFeedEmptyState.tsx:37 +msgid "This feed is empty! You may need to follow more users or tune your language settings." +msgstr "Tá an fotha seo folamh! Is féidir go mbeidh ort tuilleadh úsáideoirí a leanúint nó do shocruithe teanga a athrú." + +#: src/view/com/modals/BirthDateSettings.tsx:61 +msgid "This information is not shared with other users." +msgstr "Ní roinntear an t-eolas seo le húsáideoirí eile." + +#: src/view/com/modals/VerifyEmail.tsx:119 +msgid "This is important in case you ever need to change your email or reset your password." +msgstr "Tá sé seo tábhachtach má bhíonn ort do ríomhphost nó do phasfhocal a athrú." + +#: src/view/com/modals/LinkWarning.tsx:58 +msgid "This link is taking you to the following website:" +msgstr "Téann an nasc seo go dtí an suíomh idirlín seo:" + +#: src/view/screens/ProfileList.tsx:834 +msgid "This list is empty!" +msgstr "Tá an liosta seo folamh!" + +#: src/view/com/modals/AddAppPasswords.tsx:106 +msgid "This name is already in use" +msgstr "Tá an t-ainm seo in úsáid cheana féin" + +#: src/view/com/post-thread/PostThreadItem.tsx:122 +msgid "This post has been deleted." +msgstr "Scriosadh an phostáil seo." + +#: src/view/com/modals/ModerationDetails.tsx:62 +msgid "This user has blocked you. You cannot view their content." +msgstr "Tá an t-úsáideoir seo tar éis thú a bhlocáil. Ní féidir leat a gcuid ábhair a fheiceáil." + +#: src/view/com/modals/ModerationDetails.tsx:42 +msgid "This user is included in the <0/> list which you have blocked." +msgstr "Tá an t-úsáideoir seo ar an liosta <0/> a bhlocáil tú." + +#: src/view/com/modals/ModerationDetails.tsx:74 +msgid "This user is included in the <0/> list which you have muted." +msgstr "Tá an t-úsáideoir seo ar an liosta <0/> a chuir tú i bhfolach." + +#: src/view/com/modals/ModerationDetails.tsx:74 +#~ msgid "This user is included the <0/> list which you have muted." +#~ msgstr "Tá an t-úsáideoir seo ar an liosta <0/> a chuir tú i bhfolach." + +#: src/view/com/modals/SelfLabel.tsx:137 +msgid "This warning is only available for posts with media attached." +msgstr "Níl an rabhadh seo ar fáil ach le haghaidh postálacha a bhfuil meáin ceangailte leo." + +#: src/view/com/util/forms/PostDropdownBtn.tsx:192 +msgid "This will hide this post from your feeds." +msgstr "Leis seo ní bheidh an phostáil seo le feiceáil ar do chuid fothaí." + +#: src/view/screens/PreferencesThreads.tsx:53 +#: src/view/screens/Settings/index.tsx:565 +msgid "Thread Preferences" +msgstr "Roghanna Snáitheanna" + +#: src/view/screens/PreferencesThreads.tsx:119 +msgid "Threaded Mode" +msgstr "Modh Snáithithe" + +#: src/Navigation.tsx:252 +msgid "Threads Preferences" +msgstr "Roghanna Snáitheanna" + +#: src/view/com/util/forms/DropdownButton.tsx:246 +msgid "Toggle dropdown" +msgstr "Scoránaigh an bosca anuas" + +#: src/view/com/modals/EditImage.tsx:271 +msgid "Transformations" +msgstr "Trasfhoirmithe" + +#: src/view/com/post-thread/PostThreadItem.tsx:682 +#: src/view/com/post-thread/PostThreadItem.tsx:684 +#: src/view/com/util/forms/PostDropdownBtn.tsx:125 +msgid "Translate" +msgstr "Aistrigh" + +#: src/view/com/util/error/ErrorScreen.tsx:82 +msgctxt "action" +msgid "Try again" +msgstr "Bain triail eile as" + +#: src/view/screens/ProfileList.tsx:505 +msgid "Un-block list" +msgstr "Díbhlocáil an liosta" + +#: src/view/screens/ProfileList.tsx:490 +msgid "Un-mute list" +msgstr "Ná coinnigh an liosta sin i bhfolach níos mó" + +#: src/view/com/auth/create/CreateAccount.tsx:58 +#: src/view/com/auth/login/ForgotPasswordForm.tsx:87 +#: src/view/com/auth/login/Login.tsx:76 +#: src/view/com/auth/login/LoginForm.tsx:118 +#: src/view/com/modals/ChangePassword.tsx:70 +msgid "Unable to contact your service. Please check your Internet connection." +msgstr "Ní féidir teagmháil a dhéanamh le do sheirbhís. Seiceáil do cheangal leis an idirlíon, le do thoil." + +#: src/view/com/profile/ProfileHeader.tsx:432 +#: src/view/screens/ProfileList.tsx:589 +msgid "Unblock" +msgstr "Díbhlocáil" + +#: src/view/com/profile/ProfileHeader.tsx:435 +msgctxt "action" +msgid "Unblock" +msgstr "Díbhlocáil" + +#: src/view/com/profile/ProfileHeader.tsx:260 +#: src/view/com/profile/ProfileHeader.tsx:344 +msgid "Unblock Account" +msgstr "Díbhlocáil an cuntas" + +#: src/view/com/modals/Repost.tsx:42 +#: src/view/com/modals/Repost.tsx:55 +#: src/view/com/util/post-ctrls/RepostButton.tsx:60 +#: src/view/com/util/post-ctrls/RepostButton.web.tsx:48 +msgid "Undo repost" +msgstr "Cuir stop leis an athphostáil" + +#: src/view/com/profile/FollowButton.tsx:55 +msgctxt "action" +msgid "Unfollow" +msgstr "Dílean" + +#: src/view/com/profile/ProfileHeader.tsx:484 +msgid "Unfollow {0}" +msgstr "Dílean {0}" + +#: src/view/com/auth/create/state.ts:262 +msgid "Unfortunately, you do not meet the requirements to create an account." +msgstr "Ar an drochuair, ní chomhlíonann tú na riachtanais le cuntas a chruthú." + +#: src/view/com/util/post-ctrls/PostCtrls.tsx:182 +#: src/view/com/util/post-ctrls/PostCtrls.tsx:216 +msgid "Unlike" +msgstr "Dímhol" + +#: src/view/screens/ProfileList.tsx:596 +msgid "Unmute" +msgstr "Ná coinnigh i bhfolach" + +#: src/view/com/profile/ProfileHeader.tsx:325 +msgid "Unmute Account" +msgstr "Ná coinnigh an cuntas seo i bhfolach níos mó" + +#: src/view/com/util/forms/PostDropdownBtn.tsx:171 +msgid "Unmute thread" +msgstr "Ná coinnigh an snáithe seo i bhfolach níos mó" + +#: src/view/screens/ProfileFeed.tsx:353 +#: src/view/screens/ProfileList.tsx:580 +msgid "Unpin" +msgstr "Díghreamaigh" + +#: src/view/screens/ProfileList.tsx:473 +msgid "Unpin moderation list" +msgstr "Díghreamaigh an liosta modhnóireachta" + +#: src/view/screens/ProfileFeed.tsx:345 +msgid "Unsave" +msgstr "Díshábháil" + +#: src/view/com/modals/UserAddRemoveLists.tsx:70 +msgid "Update {displayName} in Lists" +msgstr "Uasdátú {displayName} sna Liostaí" + +#: src/lib/hooks/useOTAUpdate.ts:15 +msgid "Update Available" +msgstr "Uasdátú ar fáil" + +#: src/view/com/auth/login/SetNewPasswordForm.tsx:204 +msgid "Updating..." +msgstr "Á uasdátú…" + +#: src/view/com/modals/ChangeHandle.tsx:455 +msgid "Upload a text file to:" +msgstr "Uaslódáil comhad téacs chuig:" + +#: src/view/screens/AppPasswords.tsx:195 +msgid "Use app passwords to login to other Bluesky clients without giving full access to your account or password." +msgstr "Bain úsáid as pasfhocail na haipe le logáil isteach ar chliaint eile de chuid Bluesky gan fáil iomlán ar do chuntas ná do phasfhocal a thabhairt dóibh." + +#: src/view/com/modals/ChangeHandle.tsx:515 +msgid "Use default provider" +msgstr "Úsáid an soláthraí réamhshocraithe" + +#: src/view/com/modals/InAppBrowserConsent.tsx:56 +#: src/view/com/modals/InAppBrowserConsent.tsx:58 +msgid "Use in-app browser" +msgstr "Úsáid an brabhsálaí san aip seo" + +#: src/view/com/modals/InAppBrowserConsent.tsx:66 +#: src/view/com/modals/InAppBrowserConsent.tsx:68 +msgid "Use my default browser" +msgstr "Úsáid an brabhsálaí réamhshocraithe atá agam" + +#: src/view/com/modals/AddAppPasswords.tsx:155 +msgid "Use this to sign into the other app along with your handle." +msgstr "Úsáid é seo le logáil isteach ar an aip eile in éindí le do leasainm." + +#: src/view/com/modals/ServerInput.tsx:105 +#~ msgid "Use your domain as your Bluesky client service provider" +#~ msgstr "Úsáid d’fhearann féin mar sholáthraí seirbhíse cliaint Bluesky" + +#: src/view/com/modals/InviteCodes.tsx:200 +msgid "Used by:" +msgstr "In úsáid ag:" + +#: src/view/com/modals/ModerationDetails.tsx:54 +msgid "User Blocked" +msgstr "Úsáideoir blocáilte" + +#: src/view/com/modals/ModerationDetails.tsx:40 +msgid "User Blocked by List" +msgstr "Úsáideoir blocáilte le liosta" + +#: src/view/com/modals/ModerationDetails.tsx:60 +msgid "User Blocks You" +msgstr "Blocálann an t-úsáideoir seo thú" + +#: src/view/com/auth/create/Step2.tsx:44 +msgid "User handle" +msgstr "Leasainm" + +#: src/view/com/lists/ListCard.tsx:84 +#: src/view/com/modals/UserAddRemoveLists.tsx:198 +msgid "User list by {0}" +msgstr "Liosta úsáideoirí le {0}" + +#: src/view/screens/ProfileList.tsx:762 +msgid "User list by <0/>" +msgstr "Liosta úsáideoirí le <0/>" + +#: src/view/com/lists/ListCard.tsx:82 +#: src/view/com/modals/UserAddRemoveLists.tsx:196 +#: src/view/screens/ProfileList.tsx:760 +msgid "User list by you" +msgstr "Liosta úsáideoirí leat" + +#: src/view/com/modals/CreateOrEditList.tsx:196 +msgid "User list created" +msgstr "Liosta úsáideoirí cruthaithe" + +#: src/view/com/modals/CreateOrEditList.tsx:182 +msgid "User list updated" +msgstr "Liosta úsáideoirí uasdátaithe" + +#: src/view/screens/Lists.tsx:58 +msgid "User Lists" +msgstr "Liostaí Úsáideoirí" + +#: src/view/com/auth/login/LoginForm.tsx:177 +#: src/view/com/auth/login/LoginForm.tsx:195 +msgid "Username or email address" +msgstr "Ainm úsáideora nó ríomhphost" + +#: src/view/screens/ProfileList.tsx:796 +msgid "Users" +msgstr "Úsáideoirí" + +#: src/view/com/threadgate/WhoCanReply.tsx:143 +msgid "users followed by <0/>" +msgstr "Úsáideoirí a bhfuil <0/> á leanúint" + +#: src/view/com/modals/Threadgate.tsx:106 +msgid "Users in \"{0}\"" +msgstr "Úsáideoirí in ”{0}“" + +#: src/view/com/auth/create/Step2.tsx:243 +#~ msgid "Verification code" +#~ msgstr "Cód dearbhaithe" + +#: src/view/screens/Settings/index.tsx:910 +msgid "Verify email" +msgstr "Dearbhaigh ríomhphost" + +#: src/view/screens/Settings/index.tsx:935 +msgid "Verify my email" +msgstr "Dearbhaigh mo ríomhphost" + +#: src/view/screens/Settings/index.tsx:944 +msgid "Verify My Email" +msgstr "Dearbhaigh Mo Ríomhphost" + +#: src/view/com/modals/ChangeEmail.tsx:205 +#: src/view/com/modals/ChangeEmail.tsx:207 +msgid "Verify New Email" +msgstr "Dearbhaigh an Ríomhphost Nua" + +#: src/view/com/modals/VerifyEmail.tsx:103 +msgid "Verify Your Email" +msgstr "Dearbhaigh Do Ríomhphost" + +#: src/screens/Onboarding/index.tsx:42 +msgid "Video Games" +msgstr "Físchluichí" + +#: src/view/com/profile/ProfileHeader.tsx:661 +msgid "View {0}'s avatar" +msgstr "Féach ar an abhatár atá ag {0}" + +#: src/view/screens/Log.tsx:52 +msgid "View debug entry" +msgstr "Féach ar an iontráil dífhabhtaithe" + +#: src/view/com/posts/FeedSlice.tsx:103 +msgid "View full thread" +msgstr "Féach ar an snáithe iomlán" + +#: src/view/com/posts/FeedErrorMessage.tsx:172 +msgid "View profile" +msgstr "Féach ar an bpróifíl" + +#: src/view/com/profile/ProfileSubpageHeader.tsx:128 +msgid "View the avatar" +msgstr "Féach ar an abhatár" + +#: src/view/com/modals/LinkWarning.tsx:75 +msgid "Visit Site" +msgstr "Tabhair cuairt ar an suíomh" + +#: src/screens/Onboarding/StepModeration/ModerationOption.tsx:42 +#: src/view/com/modals/ContentFilteringSettings.tsx:259 +msgid "Warn" +msgstr "Rabhadh" + +#: src/screens/Onboarding/StepAlgoFeeds/index.tsx:134 +msgid "We also think you'll like \"For You\" by Skygaze:" +msgstr "Creidimid go dtaitneoidh “For You” le Skygaze leat:" + +#: src/screens/Deactivated.tsx:133 +msgid "We estimate {estimatedTime} until your account is ready." +msgstr "Measaimid go mbeidh do chuntas réidh i gceann {estimatedTime}" + +#: src/screens/Onboarding/StepFinished.tsx:93 +msgid "We hope you have a wonderful time. Remember, Bluesky is:" +msgstr "Tá súil againn go mbeidh an-chraic agat anseo. Ná déan dearmad go bhfuil Bluesky:" + +#: src/view/com/posts/DiscoverFallbackHeader.tsx:29 +msgid "We ran out of posts from your follows. Here's the latest from <0/>." +msgstr "Níl aon ábhar nua le taispeáint ó na cuntais a leanann tú. Seo duit an t-ábhar is déanaí ó <0/>." + +#: src/screens/Onboarding/StepAlgoFeeds/index.tsx:118 +#~ msgid "We recommend \"For You\" by Skygaze:" +#~ msgstr "Creidimid go dtaitneoidh “For You” le Skygaze leat:" + +#: src/screens/Onboarding/StepAlgoFeeds/index.tsx:124 +msgid "We recommend our \"Discover\" feed:" +msgstr "Molaimid an fotha “Discover”." + +#: src/screens/Onboarding/StepInterests/index.tsx:133 +msgid "We weren't able to connect. Please try again to continue setting up your account. If it continues to fail, you can skip this flow." +msgstr "Níorbh fhéidir linn ceangal a bhunú. Bain triail eile as do chuntas a shocrú. Má mhaireann an fhadhb, ní gá duit an próiseas seo a chur i gcrích." + +#: src/screens/Deactivated.tsx:137 +msgid "We will let you know when your account is ready." +msgstr "Déarfaidh muid leat nuair a bheidh do chuntas réidh." + +#: src/view/com/modals/AppealLabel.tsx:48 +msgid "We'll look into your appeal promptly." +msgstr "Fiosróimid d'achomharc gan mhoill." + +#: src/screens/Onboarding/StepInterests/index.tsx:138 +msgid "We'll use this to help customize your experience." +msgstr "Bainfimid úsáid as seo chun an suíomh a chur in oiriúint duit." + +#: src/view/com/auth/create/CreateAccount.tsx:130 +msgid "We're so excited to have you join us!" +msgstr "Tá muid an-sásta go bhfuil tú linn!" + +#: src/view/screens/ProfileList.tsx:85 +msgid "We're sorry, but we were unable to resolve this list. If this persists, please contact the list creator, @{handleOrDid}." +msgstr "Ár leithscéal, ach ní féidir linn an liosta seo a thaispeáint. Má mhaireann an fhadhb, déan teagmháil leis an duine a chruthaigh an liosta, @{handleOrDid}." + +#: src/view/screens/Search/Search.tsx:253 +msgid "We're sorry, but your search could not be completed. Please try again in a few minutes." +msgstr "Ár leithscéal, ach níorbh fhéidir linn do chuardach a chur i gcrích. Bain triail eile as i gceann cúpla nóiméad." + +#: src/view/screens/NotFound.tsx:48 +msgid "We're sorry! We can't find the page you were looking for." +msgstr "Ár leithscéal, ach ní féidir linn an leathanach atá tú ag lorg a aimsiú." + +#: src/view/com/auth/onboarding/WelcomeMobile.tsx:46 +msgid "Welcome to <0>Bluesky" +msgstr "Fáilte go <0>Bluesky" + +#: src/screens/Onboarding/StepInterests/index.tsx:130 +msgid "What are your interests?" +msgstr "Cad iad na rudaí a bhfuil suim agat iontu?" + +#: src/view/com/modals/report/Modal.tsx:169 +msgid "What is the issue with this {collectionName}?" +msgstr "Cad é an fhadhb le {collectionName}?" + +#: src/view/com/auth/SplashScreen.tsx:59 +#: src/view/com/composer/Composer.tsx:279 +msgid "What's up?" +msgstr "Aon scéal?" + +#: src/view/com/modals/lang-settings/PostLanguagesSettings.tsx:78 +msgid "Which languages are used in this post?" +msgstr "Cad iad na teangacha sa phostáil seo?" + +#: src/view/com/modals/lang-settings/ContentLanguagesSettings.tsx:77 +msgid "Which languages would you like to see in your algorithmic feeds?" +msgstr "Cad iad na teangacha ba mhaith leat a fheiceáil i do chuid fothaí algartamacha?" + +#: src/view/com/composer/threadgate/ThreadgateBtn.tsx:47 +#: src/view/com/modals/Threadgate.tsx:66 +msgid "Who can reply" +msgstr "Cé atá in ann freagra a thabhairt" + +#: src/view/com/modals/crop-image/CropImage.web.tsx:102 +msgid "Wide" +msgstr "Leathan" + +#: src/view/com/composer/Composer.tsx:415 +msgid "Write post" +msgstr "Scríobh postáil" + +#: src/view/com/composer/Composer.tsx:278 +#: src/view/com/composer/Prompt.tsx:33 +msgid "Write your reply" +msgstr "Scríobh freagra" + +#: src/screens/Onboarding/index.tsx:28 +msgid "Writers" +msgstr "Scríbhneoirí" + +#: src/view/com/auth/create/Step2.tsx:263 +#~ msgid "XXXXXX" +#~ msgstr "XXXXXX" + +#: src/view/com/composer/select-language/SuggestedLanguage.tsx:77 +#: src/view/screens/PreferencesHomeFeed.tsx:129 +#: src/view/screens/PreferencesHomeFeed.tsx:201 +#: src/view/screens/PreferencesHomeFeed.tsx:236 +#: src/view/screens/PreferencesHomeFeed.tsx:271 +#: src/view/screens/PreferencesThreads.tsx:106 +#: src/view/screens/PreferencesThreads.tsx:129 +msgid "Yes" +msgstr "Tá" + +#: src/screens/Onboarding/StepModeration/index.tsx:46 +#~ msgid "You are in control" +#~ msgstr "Tá sé faoi do stiúir" + +#: src/screens/Deactivated.tsx:130 +msgid "You are in line." +msgstr "Tá tú sa scuaine." + +#: src/view/com/posts/FollowingEmptyState.tsx:67 +#: src/view/com/posts/FollowingEndOfFeed.tsx:68 +msgid "You can also discover new Custom Feeds to follow." +msgstr "Is féidir leat sainfhothaí nua a aimsiú le leanúint." + +#: src/screens/Onboarding/StepAlgoFeeds/index.tsx:123 +#~ msgid "You can also try our \"Discover\" algorithm:" +#~ msgstr "Tig leat freisin triail a bhaint as ár n-algartam “Discover”:" + +#: src/screens/Onboarding/StepFollowingFeed.tsx:142 +msgid "You can change these settings later." +msgstr "Is féidir leat na socruithe seo a athrú níos déanaí." + +#: src/view/com/auth/login/Login.tsx:158 +#: src/view/com/auth/login/PasswordUpdatedForm.tsx:31 +msgid "You can now sign in with your new password." +msgstr "Is féidir leat logáil isteach le do phasfhocal nua anois." + +#: src/view/com/modals/InviteCodes.tsx:66 +msgid "You don't have any invite codes yet! We'll send you some when you've been on Bluesky for a little longer." +msgstr "Níl aon chóid chuiridh agat fós! Cuirfidh muid cúpla cód chugat tar éis duit beagán ama a chaitheamh anseo." + +#: src/view/screens/SavedFeeds.tsx:102 +msgid "You don't have any pinned feeds." +msgstr "Níl aon fhothaí greamaithe agat." + +#: src/view/screens/Feeds.tsx:452 +msgid "You don't have any saved feeds!" +msgstr "Níl aon fhothaí sábháilte agat!" + +#: src/view/screens/SavedFeeds.tsx:135 +msgid "You don't have any saved feeds." +msgstr "Níl aon fhothaí sábháilte agat." + +#: src/view/com/post-thread/PostThread.tsx:464 +msgid "You have blocked the author or you have been blocked by the author." +msgstr "Bhlocáil tú an t-údar nó tá tú blocáilte ag an údar." + +#: src/view/com/modals/ModerationDetails.tsx:56 +msgid "You have blocked this user. You cannot view their content." +msgstr "Bhlocáil tú an cuntas seo. Ní féidir leat a gcuid ábhar a fheiceáil." + +#: src/view/com/auth/login/SetNewPasswordForm.tsx:57 +#: src/view/com/auth/login/SetNewPasswordForm.tsx:92 +#: src/view/com/modals/ChangePassword.tsx:87 +#: src/view/com/modals/ChangePassword.tsx:121 +msgid "You have entered an invalid code. It should look like XXXXX-XXXXX." +msgstr "Tá tú tar éis cód míchruinn a chur isteach. Ba cheart an cruth seo a bheith air: XXXXX-XXXXX." + +#: src/view/com/modals/ModerationDetails.tsx:87 +msgid "You have muted this user." +msgstr "Chuir tú an cuntas seo i bhfolach." + +#: src/view/com/feeds/ProfileFeedgens.tsx:136 +msgid "You have no feeds." +msgstr "Níl aon fhothaí agat." + +#: src/view/com/lists/MyLists.tsx:89 +#: src/view/com/lists/ProfileLists.tsx:140 +msgid "You have no lists." +msgstr "Níl aon liostaí agat." + +#: src/view/screens/ModerationBlockedAccounts.tsx:132 +msgid "You have not blocked any accounts yet. To block an account, go to their profile and selected \"Block account\" from the menu on their account." +msgstr "Níor bhlocáil tú aon chuntas fós. Le cuntas a bhlocáil, téigh go dtí a bpróifíl agus roghnaigh “Blocáil an cuntas seo” ar an gclár ansin." + +#: src/view/screens/AppPasswords.tsx:87 +msgid "You have not created any app passwords yet. You can create one by pressing the button below." +msgstr "Níor chruthaigh tú aon phasfhocal aipe fós. Is féidir leat ceann a chruthú ach brú ar an gcnaipe thíos." + +#: src/view/screens/ModerationMutedAccounts.tsx:131 +msgid "You have not muted any accounts yet. To mute an account, go to their profile and selected \"Mute account\" from the menu on their account." +msgstr "Níor chuir tú aon chuntas i bhfolach fós. Le cuntas a chur i bhfolach, téigh go dtí a bpróifíl agus roghnaigh “Cuir an cuntas i bhfolach” ar an gclár ansin." + +#: src/view/com/modals/ContentFilteringSettings.tsx:175 +msgid "You must be 18 or older to enable adult content." +msgstr "Caithfidh tú a bheith 18 mbliana d’aois nó níos sine le hábhar do dhaoine fásta a fháil." + +#: src/screens/Onboarding/StepModeration/AdultContentEnabledPref.tsx:103 +msgid "You must be 18 years or older to enable adult content" +msgstr "Caithfidh tú a bheith 18 mbliana d’aois nó níos sine le hábhar do dhaoine fásta a fháil." + +#: src/view/com/util/forms/PostDropdownBtn.tsx:98 +msgid "You will no longer receive notifications for this thread" +msgstr "Ní bhfaighidh tú fógraí don snáithe seo a thuilleadh." + +#: src/view/com/util/forms/PostDropdownBtn.tsx:101 +msgid "You will now receive notifications for this thread" +msgstr "Gheobhaidh tú fógraí don snáithe seo anois." + +#: src/view/com/auth/login/SetNewPasswordForm.tsx:107 +msgid "You will receive an email with a \"reset code.\" Enter that code here, then enter your new password." +msgstr "Gheobhaidh tú teachtaireacht ríomhphoist le “cód athshocraithe” ann. Cuir an cód sin isteach anseo, ansin cuir do phasfhocal nua isteach." + +#: src/screens/Onboarding/StepModeration/index.tsx:72 +msgid "You're in control" +msgstr "Tá sé faoi do stiúir" + +#: src/screens/Deactivated.tsx:87 +#: src/screens/Deactivated.tsx:88 +#: src/screens/Deactivated.tsx:103 +msgid "You're in line" +msgstr "Tá tú sa scuaine" + +#: src/screens/Onboarding/StepFinished.tsx:90 +msgid "You're ready to go!" +msgstr "Tá tú réidh!" + +#: src/view/com/posts/FollowingEndOfFeed.tsx:48 +msgid "You've reached the end of your feed! Find some more accounts to follow." +msgstr "Tháinig tú go deireadh d’fhotha! Aimsigh cuntais eile le leanúint." + +#: src/view/com/auth/create/Step1.tsx:74 +msgid "Your account" +msgstr "Do chuntas" + +#: src/view/com/modals/DeleteAccount.tsx:67 +msgid "Your account has been deleted" +msgstr "Scriosadh do chuntas" + +#: src/view/screens/Settings/ExportCarDialog.tsx:47 +msgid "Your account repository, containing all public data records, can be downloaded as a \"CAR\" file. This file does not include media embeds, such as images, or your private data, which must be fetched separately." +msgstr "Is féidir cartlann do chuntais, a bhfuil na taifid phoiblí uile inti, a íoslódáil mar chomhad “CAR”. Ní bheidh aon mheáin leabaithe (íomhánna, mar shampla) ná do shonraí príobháideacha inti. Ní mór iad a fháil ar dhóigh eile." + +#: src/view/com/auth/create/Step1.tsx:238 +msgid "Your birth date" +msgstr "Do bhreithlá" + +#: src/view/com/modals/InAppBrowserConsent.tsx:47 +msgid "Your choice will be saved, but can be changed later in settings." +msgstr "Sábhálfar do rogha, ach is féidir é athrú níos déanaí sna socruithe." + +#: src/screens/Onboarding/StepFollowingFeed.tsx:61 +msgid "Your default feed is \"Following\"" +msgstr "Is é “Following” d’fhotha réamhshocraithe" + +#: src/view/com/auth/create/state.ts:110 +#: src/view/com/auth/login/ForgotPasswordForm.tsx:70 +#: src/view/com/modals/ChangePassword.tsx:54 +msgid "Your email appears to be invalid." +msgstr "Is cosúil go bhfuil do ríomhphost neamhbhailí." + +#: src/view/com/modals/Waitlist.tsx:109 +msgid "Your email has been saved! We'll be in touch soon." +msgstr "Cláraíodh do sheoladh ríomhphost! Beidh muid i dteagmháil leat go luath." + +#: src/view/com/modals/ChangeEmail.tsx:125 +msgid "Your email has been updated but not verified. As a next step, please verify your new email." +msgstr "Uasdátaíodh do sheoladh ríomhphoist ach níor dearbhaíodh é. An chéad chéim eile anois ná do sheoladh nua a dhearbhú, le do thoil." + +#: src/view/com/modals/VerifyEmail.tsx:114 +msgid "Your email has not yet been verified. This is an important security step which we recommend." +msgstr "Níor dearbhaíodh do sheoladh ríomhphoist fós. Is tábhachtach an chéim shábháilteachta é sin agus molaimid é." + +#: src/view/com/posts/FollowingEmptyState.tsx:47 +msgid "Your following feed is empty! Follow more users to see what's happening." +msgstr "Tá an fotha de na daoine a leanann tú folamh! Lean tuilleadh úsáideoirí le feiceáil céard atá ar siúl." + +#: src/view/com/auth/create/Step2.tsx:48 +msgid "Your full handle will be" +msgstr "Do leasainm iomlán anseo:" + +#: src/view/com/modals/ChangeHandle.tsx:270 +msgid "Your full handle will be <0>@{0}" +msgstr "Do leasainm iomlán anseo: <0>@{0}" + +#: src/view/screens/Settings.tsx:NaN +#: src/view/shell/Drawer.tsx:660 +#~ msgid "Your invite codes are hidden when logged in using an App Password" +#~ msgstr "Níl do chuid cód cuiridh le feiceáil nuair atá tú logáilte isteach le pasfhocal aipe" + +#: src/view/com/modals/ChangePassword.tsx:155 +msgid "Your password has been changed successfully!" +msgstr "Athraíodh do phasfhocal!" + +#: src/view/com/composer/Composer.tsx:267 +msgid "Your post has been published" +msgstr "Foilsíodh do phostáil" + +#: src/screens/Onboarding/StepFinished.tsx:105 +#: src/view/com/auth/onboarding/WelcomeDesktop.tsx:59 +#: src/view/com/auth/onboarding/WelcomeMobile.tsx:59 +msgid "Your posts, likes, and blocks are public. Mutes are private." +msgstr "Tá do chuid postálacha, moltaí, agus blocálacha poiblí. Is príobháideach iad na cuntais a chuireann tú i bhfolach." + +#: src/view/com/modals/SwitchAccount.tsx:84 +#: src/view/screens/Settings/index.tsx:118 +msgid "Your profile" +msgstr "Do phróifíl" + +#: src/view/com/composer/Composer.tsx:266 +msgid "Your reply has been published" +msgstr "Foilsíodh do fhreagra" + +#: src/view/com/auth/create/Step2.tsx:28 +msgid "Your user handle" +msgstr "Do leasainm" diff --git a/src/locale/locales/id/messages.po b/src/locale/locales/id/messages.po index c0005b4386..001520fb2b 100644 --- a/src/locale/locales/id/messages.po +++ b/src/locale/locales/id/messages.po @@ -4,8 +4,8 @@ msgstr "" "Report-Msgid-Bugs-To: \n" "POT-Creation-Date: 2023-12-28 11:56+07000\n" "PO-Revision-Date: \n" -"Last-Translator: GID0317\n" -"Language-Team: GID0317, danninov, thinkbyte1024, mary-ext\n" +"Last-Translator: danninov\n" +"Language-Team: GID0317, danninov, thinkbyte1024, mary-ext, kodebanget\n" "Language: id\n" "MIME-Version: 1.0\n" "Content-Type: text/plain; charset=utf-8\n" @@ -260,7 +260,7 @@ msgstr "Konten Dewasa" #: src/screens/Onboarding/StepModeration/AdultContentEnabledPref.tsx:78 #~ msgid "Adult content can only be enabled via the Web at <0>bsky.app." -#~ msgstr "" +#~ msgstr "Konten dewasa hanya dapat diaktifkan melalui Web di <0>bsky.app." #: src/components/moderation/ModerationLabelPref.tsx:114 msgid "Adult content is disabled." @@ -278,7 +278,7 @@ msgstr "" #: src/view/com/auth/login/ForgotPasswordForm.tsx:221 #: src/view/com/modals/ChangePassword.tsx:170 msgid "Already have a code?" -msgstr "" +msgstr "Sudah memiliki kode?" #: src/view/com/auth/login/ChooseAccountForm.tsx:103 msgid "Already signed in as @{0}" @@ -322,7 +322,7 @@ msgstr "dan" #: src/screens/Onboarding/index.tsx:32 msgid "Animals" -msgstr "" +msgstr "Hewan" #: src/lib/moderation/useReportOptions.ts:31 msgid "Anti-Social Behavior" @@ -421,7 +421,7 @@ msgstr "Apakah Anda menulis dalam <0>{0}?" #: src/screens/Onboarding/index.tsx:26 msgid "Art" -msgstr "" +msgstr "Seni" #: src/view/com/modals/SelfLabel.tsx:123 msgid "Artistic or non-erotic nudity." @@ -446,7 +446,7 @@ msgstr "Kembali" #: src/screens/Onboarding/StepSuggestedAccounts/index.tsx:136 msgid "Based on your interest in {interestsText}" -msgstr "" +msgstr "Berdasarkan minat Anda pada {interestsText}" #: src/view/screens/Settings/index.tsx:542 msgid "Basics" @@ -582,7 +582,7 @@ msgstr "" #: src/screens/Onboarding/index.tsx:33 msgid "Books" -msgstr "" +msgstr "Buku" #: src/view/screens/Settings/index.tsx:893 msgid "Build version {0} {1}" @@ -724,20 +724,20 @@ msgstr "Ubah email saya" #: src/view/screens/Settings/index.tsx:754 msgid "Change password" -msgstr "" +msgstr "Ubah kata sandi" #: src/view/com/modals/ChangePassword.tsx:141 #: src/view/screens/Settings/index.tsx:765 msgid "Change Password" -msgstr "" +msgstr "Ubah Kata Sandi" #: src/view/com/composer/select-language/SuggestedLanguage.tsx:73 msgid "Change post language to {0}" msgstr "Ubah bahasa postingan menjadi {0}" #: src/view/screens/Settings/index.tsx:733 -#~ msgid "Change your Bluesky password" -#~ msgstr "" +msgid "Change your Bluesky password" +msgstr "Ubah kata sandi Bluesky Anda" #: src/view/com/modals/ChangeEmail.tsx:109 msgid "Change Your Email" @@ -746,7 +746,7 @@ msgstr "Ubah Email Anda" #: src/screens/Deactivated.tsx:72 #: src/screens/Deactivated.tsx:76 msgid "Check my status" -msgstr "" +msgstr "Periksa status saya" #: src/view/com/auth/onboarding/RecommendedFeeds.tsx:121 msgid "Check out some recommended feeds. Tap + to add them to your list of pinned feeds." @@ -774,20 +774,20 @@ msgstr "Pilih Layanan" #: src/screens/Onboarding/StepFinished.tsx:135 msgid "Choose the algorithms that power your custom feeds." -msgstr "" +msgstr "Pilih algoritma yang akan digunakan untuk feed khusus Anda." #: src/view/com/auth/onboarding/WelcomeDesktop.tsx:83 #: src/view/com/auth/onboarding/WelcomeMobile.tsx:85 msgid "Choose the algorithms that power your experience with custom feeds." -msgstr "Pilih algoritma yang akan digunakan untuk kustom feed Anda." +msgstr "Pilih algoritma yang akan digunakan untuk feed khusus Anda." #: src/screens/Onboarding/StepAlgoFeeds/index.tsx:103 #~ msgid "Choose your algorithmic feeds" -#~ msgstr "" +#~ msgstr "Pilih feed algoritma Anda" #: src/screens/Onboarding/StepAlgoFeeds/index.tsx:103 msgid "Choose your main feeds" -msgstr "" +msgstr "Pilih feed utama Anda" #: src/view/com/auth/create/Step1.tsx:196 msgid "Choose your password" @@ -836,17 +836,17 @@ msgstr "" #: src/screens/Onboarding/index.tsx:35 msgid "Climate" -msgstr "" +msgstr "Iklim" #: src/view/com/modals/ChangePassword.tsx:267 #: src/view/com/modals/ChangePassword.tsx:270 msgid "Close" -msgstr "" +msgstr "Tutup" #: src/components/Dialog/index.web.tsx:84 #: src/components/Dialog/index.web.tsx:198 msgid "Close active dialog" -msgstr "" +msgstr "Tutup dialog aktif" #: src/view/com/auth/login/PasswordUpdatedForm.tsx:38 msgid "Close alert" @@ -895,11 +895,11 @@ msgstr "Menciutkan daftar pengguna untuk notifikasi tertentu" #: src/screens/Onboarding/index.tsx:41 msgid "Comedy" -msgstr "" +msgstr "Komedi" #: src/screens/Onboarding/index.tsx:27 msgid "Comics" -msgstr "" +msgstr "Komik" #: src/Navigation.tsx:241 #: src/view/screens/CommunityGuidelines.tsx:32 @@ -908,7 +908,7 @@ msgstr "Panduan Komunitas" #: src/screens/Onboarding/StepFinished.tsx:148 msgid "Complete onboarding and start using your account" -msgstr "" +msgstr "Selesaikan onboarding dan mulai menggunakan akun Anda" #: src/view/com/auth/create/Step3.tsx:73 msgid "Complete the challenge" @@ -926,7 +926,7 @@ msgstr "Tulis balasan" #: src/components/moderation/ModerationLabelPref.tsx:149 #: src/screens/Onboarding/StepModeration/ModerationOption.tsx:81 msgid "Configure content filtering setting for category: {0}" -msgstr "" +msgstr "Konfigurasikan pengaturan penyaringan konten untuk kategori: {0}" #: src/components/moderation/ModerationLabelPref.tsx:116 msgid "Configured in <0>moderation settings." @@ -991,7 +991,7 @@ msgstr "Menghubungkan..." #: src/view/com/auth/create/CreateAccount.tsx:213 msgid "Contact support" -msgstr "" +msgstr "Hubungi pusat bantuan" #: src/components/moderation/LabelsOnMe.tsx:42 msgid "content" @@ -1054,19 +1054,19 @@ msgstr "Lanjutkan" #: src/screens/Onboarding/StepModeration/index.tsx:99 #: src/screens/Onboarding/StepTopicalFeeds.tsx:111 msgid "Continue to next step" -msgstr "" +msgstr "Lanjutkan ke langkah berikutnya" #: src/screens/Onboarding/StepAlgoFeeds/index.tsx:167 msgid "Continue to the next step" -msgstr "" +msgstr "Lanjutkan ke langkah berikutnya" #: src/screens/Onboarding/StepSuggestedAccounts/index.tsx:191 msgid "Continue to the next step without following any accounts" -msgstr "" +msgstr "Lanjutkan ke langkah berikutnya tanpa mengikuti akun apa pun" #: src/screens/Onboarding/index.tsx:44 msgid "Cooking" -msgstr "" +msgstr "Memasak" #: src/view/com/modals/AddAppPasswords.tsx:195 #: src/view/com/modals/InviteCodes.tsx:182 @@ -1129,7 +1129,7 @@ msgstr "Tidak dapat memuat daftar" #: src/view/com/auth/create/Step2.tsx:91 #~ msgid "Country" -#~ msgstr "" +#~ msgstr "Negara" #: src/view/com/auth/HomeLoggedOutCTA.tsx:64 #: src/view/com/auth/SplashScreen.tsx:73 @@ -1176,7 +1176,7 @@ msgstr "Buat kartu dengan gambar kecil. Tautan kartu ke {url}" #: src/screens/Onboarding/index.tsx:29 msgid "Culture" -msgstr "" +msgstr "Budaya" #: src/view/com/auth/server-input/index.tsx:95 #: src/view/com/auth/server-input/index.tsx:96 @@ -1190,7 +1190,7 @@ msgstr "Domain kustom" #: src/screens/Onboarding/StepAlgoFeeds/index.tsx:106 #: src/view/screens/Feeds.tsx:692 msgid "Custom feeds built by the community bring you new experiences and help you find the content you love." -msgstr "" +msgstr "Feed khusus yang dibuat oleh komunitas memberikan pengalaman baru dan membantu Anda menemukan konten yang Anda sukai." #: src/view/screens/PreferencesExternalEmbeds.tsx:55 msgid "Customize media from external sites." @@ -1211,7 +1211,7 @@ msgstr "Mode gelap" #: src/view/screens/Settings/index.tsx:517 msgid "Dark Theme" -msgstr "" +msgstr "Tema Gelap" #: src/Navigation.tsx:204 #~ msgid "Debug" @@ -1261,7 +1261,7 @@ msgstr "Hapus akun saya" #: src/view/screens/Settings/index.tsx:808 msgid "Delete My Account…" -msgstr "" +msgstr "Hapus Akun Saya…" #: src/view/com/util/forms/PostDropdownBtn.tsx:302 #: src/view/com/util/forms/PostDropdownBtn.tsx:304 @@ -1305,7 +1305,7 @@ msgstr "Apakah Anda ingin mengatakan sesuatu?" #: src/view/screens/Settings/index.tsx:523 msgid "Dim" -msgstr "" +msgstr "Redup" #: src/lib/moderation/useLabelBehaviorDescription.ts:32 #: src/lib/moderation/useLabelBehaviorDescription.ts:42 @@ -1420,11 +1420,11 @@ msgstr "" #: src/view/com/composer/text-input/TextInput.web.tsx:249 msgid "Drop to add images" -msgstr "" +msgstr "Lepaskan untuk menambahkan gambar" #: src/screens/Onboarding/StepModeration/AdultContentEnabledPref.tsx:120 msgid "Due to Apple policies, adult content can only be enabled on the web after completing sign up." -msgstr "" +msgstr "Sesuai dengan kebijakan Apple, konten dewasa hanya dapat diaktifkan di web setelah menyelesaikan pendaftaran." #: src/view/com/modals/ChangeHandle.tsx:257 msgid "e.g. alice" @@ -1528,7 +1528,7 @@ msgstr "Ubah deskripsi profil Anda" #: src/screens/Onboarding/index.tsx:34 msgid "Education" -msgstr "" +msgstr "Pendidikan" #: src/view/com/auth/create/Step1.tsx:176 #: src/view/com/auth/login/ForgotPasswordForm.tsx:156 @@ -1573,7 +1573,7 @@ msgstr "Aktifkan Konten Dewasa" #: src/screens/Onboarding/StepModeration/AdultContentEnabledPref.tsx:78 #: src/screens/Onboarding/StepModeration/AdultContentEnabledPref.tsx:79 msgid "Enable adult content in your feeds" -msgstr "" +msgstr "Aktifkan konten dewasa di feed Anda" #: src/view/com/modals/EmbedConsent.tsx:97 msgid "Enable External Media" @@ -1614,7 +1614,7 @@ msgstr "Masukkan Kode Konfirmasi" #: src/view/com/modals/ChangePassword.tsx:153 msgid "Enter the code you received to change your password." -msgstr "" +msgstr "Masukkan kode yang Anda terima untuk mengubah kata sandi Anda." #: src/view/com/modals/ChangeHandle.tsx:371 msgid "Enter the domain you want to use" @@ -1647,7 +1647,7 @@ msgstr "Masukkan alamat email baru Anda di bawah ini." #: src/view/com/auth/create/Step2.tsx:188 #~ msgid "Enter your phone number" -#~ msgstr "" +#~ msgstr "Masukkan nomor telepon Anda" #: src/view/com/auth/login/Login.tsx:99 msgid "Enter your username and password" @@ -1795,11 +1795,11 @@ msgstr "Feed" #: src/screens/Onboarding/StepAlgoFeeds/index.tsx:106 #~ msgid "Feeds are created by users and can give you entirely new experiences." -#~ msgstr "" +#~ msgstr "Feed dibuat oleh pengguna dan dapat memberikan Anda pengalaman yang benar-benar baru." #: src/screens/Onboarding/StepAlgoFeeds/index.tsx:106 #~ msgid "Feeds are created by users and organizations. They offer you varied experiences and suggest content you may like using algorithms." -#~ msgstr "" +#~ msgstr "Feed dibuat oleh pengguna dan organisasi. Mereka menawarkan Anda pengalaman yang beragam dan menyarankan konten yang mungkin Anda sukai menggunakan algoritma." #: src/view/com/auth/onboarding/RecommendedFeeds.tsx:57 msgid "Feeds are created by users to curate content. Choose some feeds that you find interesting." @@ -1811,7 +1811,7 @@ msgstr "Feed adalah algoritma khusus yang dibuat oleh pengguna dengan sedikit ke #: src/screens/Onboarding/StepTopicalFeeds.tsx:76 msgid "Feeds can be topical as well!" -msgstr "" +msgstr "Feed juga bisa tentang tren terkini!" #: src/view/com/modals/ChangeHandle.tsx:482 msgid "File Contents" @@ -1823,7 +1823,7 @@ msgstr "" #: src/screens/Onboarding/StepFinished.tsx:151 msgid "Finalizing" -msgstr "" +msgstr "Menyelesaikan" #: src/view/com/posts/CustomFeedEmptyState.tsx:47 #: src/view/com/posts/FollowingEmptyState.tsx:57 @@ -1857,11 +1857,11 @@ msgstr "Atur utasan diskusi." #: src/screens/Onboarding/index.tsx:38 msgid "Fitness" -msgstr "" +msgstr "Kebugaran" #: src/screens/Onboarding/StepFinished.tsx:131 msgid "Flexible" -msgstr "" +msgstr "Fleksibel" #: src/view/com/modals/EditImage.tsx:115 msgid "Flip horizontal" @@ -1898,15 +1898,15 @@ msgstr "" #: src/screens/Onboarding/StepSuggestedAccounts/index.tsx:179 msgid "Follow All" -msgstr "" +msgstr "Ikuti Semua" #: src/screens/Onboarding/StepSuggestedAccounts/index.tsx:174 msgid "Follow selected accounts and continue to the next step" -msgstr "" +msgstr "Ikuti akun yang dipilih dan lanjutkan ke langkah berikutnya" #: src/screens/Onboarding/StepSuggestedAccounts/index.tsx:174 #~ msgid "Follow selected accounts and continue to then next step" -#~ msgstr "" +#~ msgstr "Ikuti akun yang dipilih dan lanjutkan ke langkah berikutnya" #: src/view/com/auth/onboarding/RecommendedFollows.tsx:64 msgid "Follow some users to get started. We can recommend you more users based on who you find interesting." @@ -1969,7 +1969,7 @@ msgstr "Mengikuti Anda" #: src/screens/Onboarding/index.tsx:43 msgid "Food" -msgstr "" +msgstr "Makanan" #: src/view/com/modals/DeleteAccount.tsx:111 msgid "For security reasons, we'll need to send a confirmation code to your email address." @@ -2043,7 +2043,7 @@ msgstr "Kembali" #: src/screens/Onboarding/Layout.tsx:104 #: src/screens/Onboarding/Layout.tsx:193 msgid "Go back to previous step" -msgstr "" +msgstr "Kembali ke langkah sebelumnya" #: src/view/screens/NotFound.tsx:55 msgid "Go home" @@ -2056,7 +2056,7 @@ msgstr "" #: src/view/screens/Search/Search.tsx:748 #: src/view/shell/desktop/Search.tsx:263 msgid "Go to @{queryMaybeHandle}" -msgstr "" +msgstr "Kembali ke @{queryMaybeHandle}" #: src/view/com/auth/login/ForgotPasswordForm.tsx:189 #: src/view/com/auth/login/ForgotPasswordForm.tsx:218 @@ -2092,7 +2092,7 @@ msgstr "" #: src/view/com/auth/create/CreateAccount.tsx:208 msgid "Having trouble?" -msgstr "" +msgstr "Mengalami masalah?" #: src/view/shell/desktop/RightNav.tsx:90 #: src/view/shell/Drawer.tsx:324 @@ -2101,19 +2101,19 @@ msgstr "Bantuan" #: src/screens/Onboarding/StepSuggestedAccounts/index.tsx:132 msgid "Here are some accounts for you to follow" -msgstr "" +msgstr "Berikut beberapa akun untuk Anda ikuti" #: src/screens/Onboarding/StepSuggestedAccounts/index.tsx:132 #~ msgid "Here are some accounts for your to follow" -#~ msgstr "" +#~ msgstr "Berikut beberapa akun untuk Anda ikuti" #: src/screens/Onboarding/StepTopicalFeeds.tsx:85 msgid "Here are some popular topical feeds. You can choose to follow as many as you like." -msgstr "" +msgstr "Berikut beberapa feed topik terkini yang populer. Anda dapat memilih untuk mengikuti sebanyak yang Anda suka." #: src/screens/Onboarding/StepTopicalFeeds.tsx:80 msgid "Here are some topical feeds based on your interests: {interestsText}. You can choose to follow as many as you like." -msgstr "" +msgstr "Berikut beberapa feed topik terkini terdasarkan minat Anda: {interestsText}. Anda dapat memilih untuk mengikuti sebanyak yang Anda suka." #: src/view/com/modals/AddAppPasswords.tsx:153 msgid "Here is your app password." @@ -2254,7 +2254,7 @@ msgstr "" #: src/view/com/modals/ChangePassword.tsx:148 msgid "If you want to change your password, we will send you a code to verify that this is your account." -msgstr "" +msgstr "Jika Anda ingin mengubah kata sandi, kami akan mengirimkan kode untuk memverifikasi bahwa ini adalah akun Anda." #: src/lib/moderation/useReportOptions.ts:36 msgid "Illegal and Urgent" @@ -2287,7 +2287,7 @@ msgstr "Masukkan kode konfirmasi untuk penghapusan akun" #: src/view/com/auth/create/Step1.tsx:177 msgid "Input email for Bluesky account" -msgstr "" +msgstr "Masukkan email untuk akun Bluesky" #: src/view/com/auth/create/Step2.tsx:109 #~ msgid "Input email for Bluesky waitlist" @@ -2315,7 +2315,7 @@ msgstr "Masukkan kata sandi untuk penghapusan akun" #: src/view/com/auth/create/Step2.tsx:196 #~ msgid "Input phone number for SMS verification" -#~ msgstr "" +#~ msgstr "Masukkan nomor telepon untuk verifikasi SMS" #: src/view/com/auth/login/LoginForm.tsx:233 msgid "Input the password tied to {identifier}" @@ -2327,7 +2327,7 @@ msgstr "Masukkan nama pengguna atau alamat email yang Anda gunakan saat mendafta #: src/view/com/auth/create/Step2.tsx:271 #~ msgid "Input the verification code we have texted to you" -#~ msgstr "" +#~ msgstr "Masukkan kode verifikasi yang telah kami kirimkan melalui SMS" #: src/view/com/modals/Waitlist.tsx:90 #~ msgid "Input your email to get on the Bluesky waitlist" @@ -2384,7 +2384,7 @@ msgstr "Kode undangan: 1 tersedia" #: src/screens/Onboarding/StepFollowingFeed.tsx:64 msgid "It shows posts from the people you follow as they happen." -msgstr "" +msgstr "Feed ini menampilkan postingan secara langsung dari orang yang Anda ikuti." #: src/view/com/auth/HomeLoggedOutCTA.tsx:103 #: src/view/com/auth/SplashScreen.web.tsx:138 @@ -2406,7 +2406,7 @@ msgstr "Karir" #: src/screens/Onboarding/index.tsx:24 msgid "Journalism" -msgstr "" +msgstr "Jurnalisme" #: src/components/moderation/LabelsOnMe.tsx:59 msgid "label has been placed on this {labelTarget}" @@ -2497,7 +2497,7 @@ msgstr "Meninggalkan Bluesky" #: src/screens/Deactivated.tsx:128 msgid "left to go." -msgstr "" +msgstr "yang tersisa" #: src/view/screens/Settings/index.tsx:296 msgid "Legacy storage cleared, you need to restart the app now." @@ -2510,7 +2510,7 @@ msgstr "Reset kata sandi Anda!" #: src/screens/Onboarding/StepFinished.tsx:151 msgid "Let's go!" -msgstr "" +msgstr "Ayo!" #: src/view/com/util/UserAvatar.tsx:248 #: src/view/com/util/UserBanner.tsx:62 @@ -2540,7 +2540,7 @@ msgstr "Disukai oleh" #: src/view/screens/PostLikedBy.tsx:27 #: src/view/screens/ProfileFeedLikedBy.tsx:27 msgid "Liked By" -msgstr "" +msgstr "Disukai Oleh" #: src/view/com/feeds/FeedSourceCard.tsx:268 msgid "Liked by {0} {1}" @@ -2558,15 +2558,15 @@ msgstr "Disukai oleh {likeCount} {0}" #: src/view/com/notifications/FeedItem.tsx:174 msgid "liked your custom feed" -msgstr "" +msgstr "menyukai feed khusus Anda" #: src/view/com/notifications/FeedItem.tsx:171 #~ msgid "liked your custom feed '{0}'" -#~ msgstr "" +#~ msgstr "menyukai feed khusus Anda '{0}'" #: src/view/com/notifications/FeedItem.tsx:171 #~ msgid "liked your custom feed{0}" -#~ msgstr "menyukai feed Anda{0}" +#~ msgstr "menyukai feed khusus Anda{0}" #: src/view/com/notifications/FeedItem.tsx:159 msgid "liked your post" @@ -2658,7 +2658,7 @@ msgstr "Catatan" #: src/screens/Deactivated.tsx:178 #: src/screens/Deactivated.tsx:181 msgid "Log out" -msgstr "" +msgstr "Keluar" #: src/screens/Moderation/index.tsx:444 msgid "Logged-out visibility" @@ -2936,7 +2936,7 @@ msgstr "" #: src/screens/Onboarding/index.tsx:25 msgid "Nature" -msgstr "" +msgstr "Alam" #: src/view/com/auth/login/ForgotPasswordForm.tsx:190 #: src/view/com/auth/login/ForgotPasswordForm.tsx:219 @@ -2966,7 +2966,7 @@ msgstr "Tidak akan lagi kehilangan akses ke data dan pengikut Anda." #: src/screens/Onboarding/StepFinished.tsx:119 msgid "Never lose access to your followers or data." -msgstr "" +msgstr "Tidak akan lagi kehilangan akses ke data dan pengikut Anda." #: src/components/dialogs/MutedWords.tsx:293 #~ msgid "Nevermind" @@ -2996,7 +2996,7 @@ msgstr "Kata sandi baru" #: src/view/com/modals/ChangePassword.tsx:217 msgid "New Password" -msgstr "" +msgstr "Kata Sandi Baru" #: src/view/com/feeds/FeedPage.tsx:135 msgctxt "action" @@ -3031,7 +3031,7 @@ msgstr "Balasan terbaru terlebih dahulu" #: src/screens/Onboarding/index.tsx:23 msgid "News" -msgstr "" +msgstr "Berita" #: src/view/com/auth/create/CreateAccount.tsx:172 #: src/view/com/auth/login/ForgotPasswordForm.tsx:182 @@ -3167,7 +3167,7 @@ msgstr "Oh tidak!" #: src/screens/Onboarding/StepInterests/index.tsx:128 msgid "Oh no! Something went wrong." -msgstr "" +msgstr "Oh tidak! Sepertinya ada yang salah." #: src/screens/Onboarding/StepModeration/AdultContentEnabledPref.tsx:127 msgid "OK" @@ -3205,7 +3205,7 @@ msgstr "Uups!" #: src/screens/Onboarding/StepFinished.tsx:115 msgid "Open" -msgstr "" +msgstr "Buka" #: src/view/screens/Moderation.tsx:75 #~ msgid "Open content filtering settings" @@ -3350,7 +3350,7 @@ msgstr "Membuka formulir pengaturan ulang kata sandi" #: src/view/com/home/HomeHeaderLayout.web.tsx:63 #: src/view/screens/Feeds.tsx:356 msgid "Opens screen to edit Saved Feeds" -msgstr "Membuka layar untuk mengedit Umpan Tersimpan" +msgstr "Membuka layar untuk mengedit Feed Tersimpan" #: src/view/screens/Settings/index.tsx:597 msgid "Opens screen with all saved feeds" @@ -3403,7 +3403,7 @@ msgstr "Atau gabungkan opsi-opsi berikut:" #: src/screens/Onboarding/StepAlgoFeeds/index.tsx:122 #~ msgid "Or you can try our \"Discover\" algorithm:" -#~ msgstr "" +#~ msgstr "Atau Anda dapat mencoba algoritma \"Temukan\" kami:" #: src/lib/moderation/useReportOptions.ts:25 msgid "Other" @@ -3428,7 +3428,7 @@ msgstr "Halaman tidak ditemukan" #: src/view/screens/NotFound.tsx:42 msgid "Page Not Found" -msgstr "" +msgstr "Halaman Tidak Ditemukan" #: src/view/com/auth/create/Step1.tsx:191 #: src/view/com/auth/create/Step1.tsx:201 @@ -3470,11 +3470,11 @@ msgstr "Izin untuk mengakses rol kamera ditolak. Silakan aktifkan di pengaturan #: src/screens/Onboarding/index.tsx:31 msgid "Pets" -msgstr "" +msgstr "Hewan Peliharaan" #: src/view/com/auth/create/Step2.tsx:183 #~ msgid "Phone number" -#~ msgstr "" +#~ msgstr "Nomor telepon" #: src/view/com/modals/SelfLabel.tsx:121 msgid "Pictures meant for adults." @@ -3528,7 +3528,7 @@ msgstr "Masukkan nama untuk kata sandi aplikasi Anda. Semua spasi tidak diperbol #: src/view/com/auth/create/Step2.tsx:206 #~ msgid "Please enter a phone number that can receive SMS text messages." -#~ msgstr "" +#~ msgstr "Masukkan nomor telepon yang dapat menerima pesan teks SMS." #: src/view/com/modals/AddAppPasswords.tsx:145 msgid "Please enter a unique name for this App Password or use our randomly generated one." @@ -3540,11 +3540,11 @@ msgstr "" #: src/view/com/auth/create/state.ts:170 #~ msgid "Please enter the code you received by SMS." -#~ msgstr "" +#~ msgstr "Masukkan kode yang Anda terima melalui SMS." #: src/view/com/auth/create/Step2.tsx:282 #~ msgid "Please enter the verification code sent to {phoneNumberFormatted}." -#~ msgstr "" +#~ msgstr "Masukkan kode verifikasi yang dikirim ke {phoneNumberFormatted}." #: src/view/com/auth/create/state.ts:103 msgid "Please enter your email." @@ -3576,7 +3576,7 @@ msgstr "Harap tunggu hingga kartu tautan Anda selesai dimuat" #: src/screens/Onboarding/index.tsx:37 msgid "Politics" -msgstr "" +msgstr "Politik" #: src/view/com/modals/SelfLabel.tsx:111 msgid "Porn" @@ -3717,7 +3717,7 @@ msgstr "Amankan akun Anda dengan memverifikasi email Anda." #: src/screens/Onboarding/StepFinished.tsx:101 msgid "Public" -msgstr "" +msgstr "Publik" #: src/view/screens/ModerationModlists.tsx:61 msgid "Public, shareable lists of users to mute or block in bulk." @@ -3950,11 +3950,11 @@ msgstr "Posting ulang atau kutip postingan" #: src/view/screens/PostRepostedBy.tsx:27 msgid "Reposted By" -msgstr "" +msgstr "Diposting Ulang Oleh" #: src/view/com/posts/FeedItem.tsx:197 msgid "Reposted by {0}" -msgstr "" +msgstr "Diposting ulang oleh {0}" #: src/view/com/posts/FeedItem.tsx:206 #~ msgid "Reposted by {0})" @@ -3979,12 +3979,12 @@ msgstr "Ajukan Perubahan" #: src/view/com/auth/create/Step2.tsx:219 #~ msgid "Request code" -#~ msgstr "" +#~ msgstr "Minta kode" #: src/view/com/modals/ChangePassword.tsx:241 #: src/view/com/modals/ChangePassword.tsx:243 msgid "Request Code" -msgstr "" +msgstr "Minta Kode" #: src/view/screens/Settings/index.tsx:475 msgid "Require alt text before posting" @@ -4002,7 +4002,7 @@ msgstr "Kode reset" #: src/view/com/modals/ChangePassword.tsx:192 msgid "Reset Code" -msgstr "" +msgstr "Kode Reset" #: src/view/screens/Settings/index.tsx:824 #~ msgid "Reset onboarding" @@ -4057,7 +4057,7 @@ msgstr "Ulangi" #: src/view/com/auth/create/Step2.tsx:247 #~ msgid "Retry." -#~ msgstr "" +#~ msgstr "Ulangi" #: src/view/screens/ProfileList.tsx:917 msgid "Return to previous page" @@ -4140,7 +4140,7 @@ msgstr "" #: src/screens/Onboarding/index.tsx:36 msgid "Science" -msgstr "" +msgstr "Sains" #: src/view/screens/ProfileList.tsx:873 msgid "Scroll to top" @@ -4166,7 +4166,7 @@ msgstr "Cari" #: src/view/screens/Search/Search.tsx:736 #: src/view/shell/desktop/Search.tsx:256 msgid "Search for \"{query}\"" -msgstr "" +msgstr "Cari \"{query}\"" #: src/components/TagMenu/index.tsx:145 msgid "Search for all posts by @{authorHandle} with tag {displayTag}" @@ -4257,7 +4257,7 @@ msgstr "Pilih layanan" #: src/screens/Onboarding/StepSuggestedAccounts/index.tsx:52 msgid "Select some accounts below to follow" -msgstr "" +msgstr "Pilih beberapa akun di bawah ini untuk diikuti" #: src/components/ReportDialog/SubmitView.tsx:135 msgid "Select the moderation service(s) to report to" @@ -4269,15 +4269,15 @@ msgstr "" #: src/screens/Onboarding/StepModeration/index.tsx:49 #~ msgid "Select the types of content that you want to see (or not see), and we'll handle the rest." -#~ msgstr "" +#~ msgstr "Pilih jenis konten yang ingin Anda lihat (atau tidak lihat), dan kami akan menangani sisanya." #: src/screens/Onboarding/StepTopicalFeeds.tsx:96 msgid "Select topical feeds to follow from the list below" -msgstr "" +msgstr "Pilih feed terkini untuk diikuti dari daftar di bawah ini" #: src/screens/Onboarding/StepModeration/index.tsx:62 msgid "Select what you want to see (or not see), and we’ll handle the rest." -msgstr "" +msgstr "Pilih apa yang ingin Anda lihat (atau tidak lihat), dan kami akan menangani sisanya." #: src/view/screens/LanguageSettings.tsx:281 msgid "Select which languages you want your subscribed feeds to include. If none are selected, all languages will be shown." @@ -4293,11 +4293,11 @@ msgstr "" #: src/screens/Onboarding/StepInterests/index.tsx:196 msgid "Select your interests from the options below" -msgstr "" +msgstr "Pilih minat Anda dari opsi di bawah ini" #: src/view/com/auth/create/Step2.tsx:155 #~ msgid "Select your phone's country" -#~ msgstr "" +#~ msgstr "Pilih negara telepon Anda" #: src/view/screens/LanguageSettings.tsx:190 msgid "Select your preferred language for translations in your feed." @@ -4305,11 +4305,11 @@ msgstr "Pilih bahasa yang disukai untuk penerjemahaan feed Anda." #: src/screens/Onboarding/StepAlgoFeeds/index.tsx:116 msgid "Select your primary algorithmic feeds" -msgstr "" +msgstr "Pilih feed algoritma utama Anda" #: src/screens/Onboarding/StepAlgoFeeds/index.tsx:142 msgid "Select your secondary algorithmic feeds" -msgstr "" +msgstr "Pilih feed algoritma sekunder Anda" #: src/view/com/modals/VerifyEmail.tsx:202 #: src/view/com/modals/VerifyEmail.tsx:204 @@ -4381,12 +4381,13 @@ msgstr "" #~ msgstr "Atur tema warna ke pengaturan sistem" #: src/view/screens/Settings/index.tsx:514 -#~ msgid "Set dark theme to the dark theme" -#~ msgstr "" +msgid "Set dark theme to the dark theme" +msgstr "Atur tema gelap ke tema gelap" #: src/view/screens/Settings/index.tsx:507 -#~ msgid "Set dark theme to the dim theme" -#~ msgstr "" +msgid "Set dark theme to the dim theme" +msgstr "Atur tema gelap ke tema redup" + #: src/view/com/auth/login/SetNewPasswordForm.tsx:104 msgid "Set new password" @@ -4414,15 +4415,15 @@ msgstr "Pilih \"Ya\" untuk menampilkan balasan dalam bentuk utasan. Ini merupaka #: src/view/screens/PreferencesHomeFeed.tsx:261 #~ msgid "Set this setting to \"Yes\" to show samples of your saved feeds in your following feed. This is an experimental feature." -#~ msgstr "Pilih \"Ya\" untuk menampilkan beberapa sampel dari feed tersimpan Anda pada feed mengikuti. Ini merupakan fitur eksperimental." +#~ msgstr "Pilih \"Ya\" untuk menampilkan beberapa sampel dari feed tersimpan di feed mengikuti Anda. Ini merupakan fitur eksperimental." #: src/view/screens/PreferencesFollowingFeed.tsx:261 msgid "Set this setting to \"Yes\" to show samples of your saved feeds in your Following feed. This is an experimental feature." -msgstr "" +msgstr "Pilih \"Ya\" untuk menampilkan beberapa sampel dari feed tersimpan di feed Mengikuti Anda. Ini merupakan fitur eksperimental" #: src/screens/Onboarding/Layout.tsx:50 msgid "Set up your account" -msgstr "" +msgstr "Atur akun Anda" #: src/view/com/modals/ChangeHandle.tsx:266 msgid "Sets Bluesky username" @@ -4567,15 +4568,15 @@ msgstr "Tampilkan Kutipan Postingan" #: src/screens/Onboarding/StepFollowingFeed.tsx:118 msgid "Show quote-posts in Following feed" -msgstr "" +msgstr "Tampilkan kutipan postingan di feed Mengikuti" #: src/screens/Onboarding/StepFollowingFeed.tsx:134 msgid "Show quotes in Following" -msgstr "" +msgstr "Tampilkan kutipan di Mengikuti" #: src/screens/Onboarding/StepFollowingFeed.tsx:94 msgid "Show re-posts in Following feed" -msgstr "" +msgstr "Tampilkan posting ulang di feed Mengikuti" #: src/view/screens/PreferencesFollowingFeed.tsx:119 msgid "Show Replies" @@ -4587,11 +4588,11 @@ msgstr "Tampilkan balasan dari orang yang Anda ikuti sebelum balasan lainnya." #: src/screens/Onboarding/StepFollowingFeed.tsx:86 msgid "Show replies in Following" -msgstr "" +msgstr "Tampilkan balasan di Mengikuti" #: src/screens/Onboarding/StepFollowingFeed.tsx:70 msgid "Show replies in Following feed" -msgstr "" +msgstr "Tampilkan balasan di feed Mengikuti" #: src/view/screens/PreferencesFollowingFeed.tsx:70 msgid "Show replies with at least {value} {0}" @@ -4603,7 +4604,7 @@ msgstr "Tampilkan Posting Ulang" #: src/screens/Onboarding/StepFollowingFeed.tsx:110 msgid "Show reposts in Following" -msgstr "" +msgstr "Tampilkan posting ulang di Mengikuti" #: src/components/moderation/ContentHider.tsx:68 #: src/components/moderation/PostHider.tsx:64 @@ -4712,15 +4713,15 @@ msgstr "Lewati" #: src/screens/Onboarding/StepInterests/index.tsx:232 msgid "Skip this flow" -msgstr "" +msgstr "Lewati tahap ini" #: src/view/com/auth/create/Step2.tsx:82 #~ msgid "SMS verification" -#~ msgstr "" +#~ msgstr "Verifikasi SMS" #: src/screens/Onboarding/index.tsx:40 msgid "Software Dev" -msgstr "" +msgstr "Pengembang Perangkat Lunak" #: src/view/com/modals/ProfilePreview.tsx:62 #~ msgid "Something went wrong and we're not sure what." @@ -4766,7 +4767,7 @@ msgstr "" #: src/screens/Onboarding/index.tsx:30 msgid "Sports" -msgstr "" +msgstr "Olahraga" #: src/view/com/modals/crop-image/CropImage.web.tsx:122 msgid "Square" @@ -4782,7 +4783,7 @@ msgstr "Halaman status" #: src/view/com/auth/create/StepHeader.tsx:22 msgid "Step {0} of {numSteps}" -msgstr "" +msgstr "Langkah {0} dari {numSteps}" #: src/view/com/auth/create/StepHeader.tsx:15 #~ msgid "Step {step} of 3" @@ -4817,7 +4818,7 @@ msgstr "" #: src/screens/Onboarding/StepAlgoFeeds/FeedCard.tsx:173 #: src/screens/Onboarding/StepAlgoFeeds/FeedCard.tsx:308 msgid "Subscribe to the {0} feed" -msgstr "" +msgstr "Langganan ke feed {0}" #: src/screens/Profile/Header/ProfileHeaderLabeler.tsx:185 msgid "Subscribe to this labeler" @@ -4893,7 +4894,7 @@ msgstr "Ketuk untuk melihat sepenuhnya" #: src/screens/Onboarding/index.tsx:39 msgid "Tech" -msgstr "" +msgstr "Teknologi" #: src/view/shell/desktop/RightNav.tsx:81 msgid "Terms" @@ -4960,7 +4961,7 @@ msgstr "" #: src/screens/Onboarding/Layout.tsx:60 msgid "The following steps will help customize your Bluesky experience." -msgstr "" +msgstr "Langkah berikut akan membantu menyesuaikan pengalaman Bluesky Anda." #: src/view/com/post-thread/PostThread.tsx:153 #: src/view/com/post-thread/PostThread.tsx:165 @@ -4984,7 +4985,7 @@ msgstr "Ketentuan Layanan telah dipindahkan ke" #: src/screens/Onboarding/StepAlgoFeeds/index.tsx:150 msgid "There are many feeds to try:" -msgstr "" +msgstr "Ada banyak feed untuk dicoba:" #: src/screens/Profile/Header/ProfileHeaderLabeler.tsx:113 #: src/view/screens/ProfileFeed.tsx:543 @@ -5070,19 +5071,19 @@ msgstr "Sepertinya ada masalah pada aplikasi. Harap beri tahu kami jika Anda men #: src/screens/Deactivated.tsx:106 msgid "There's been a rush of new users to Bluesky! We'll activate your account as soon as we can." -msgstr "" +msgstr "Sedang ada lonjakan pengguna baru di Bluesky! Kami akan mengaktifkan akun Anda secepat mungkin." #: src/view/com/auth/create/Step2.tsx:55 #~ msgid "There's something wrong with this number. Please choose your country and enter your full phone number!" -#~ msgstr "" +#~ msgstr "Ada kesalahan pada nomor ini. Mohon pilih negara dan masukkan nomor telepon lengkap Anda!" #: src/screens/Onboarding/StepSuggestedAccounts/index.tsx:138 msgid "These are popular accounts you might like:" -msgstr "" +msgstr "Berikut adalah akun populer yang mungkin Anda sukai:" #: src/screens/Onboarding/StepSuggestedAccounts/index.tsx:138 #~ msgid "These are popular accounts you might like." -#~ msgstr "" +#~ msgstr "Berikut adalah akun populer yang mungkin Anda sukai." #~ msgid "This {0} has been labeled." #~ msgstr "Ini {0} telah diberi label." @@ -5220,16 +5221,16 @@ msgstr "" #~ msgstr "Pengguna ini termasuk dalam daftar <0/> yang telah Anda blokir." #: src/view/com/modals/ModerationDetails.tsx:74 -#~ msgid "This user is included in the <0/> list which you have muted." -#~ msgstr "" +msgid "This user is included in the <0/> list which you have muted." +msgstr "Pengguna ini termasuk dalam daftar <0/> yang telah Anda bisukan." #: src/components/moderation/ModerationDetailsDialog.tsx:56 msgid "This user is included in the <0>{0} list which you have blocked." -msgstr "" +msgstr "Pengguna ini termasuk dalam daftar <0>{0} yang telah Anda blokir" #: src/components/moderation/ModerationDetailsDialog.tsx:85 msgid "This user is included in the <0>{0} list which you have muted." -msgstr "" +msgstr "Pengguna ini termasuk dalam daftar <0>{0} yang telah Anda bisukan" #: src/view/com/modals/ModerationDetails.tsx:74 #~ msgid "This user is included the <0/> list which you have muted." @@ -5599,7 +5600,7 @@ msgstr "" #: src/view/com/auth/create/Step2.tsx:243 #~ msgid "Verification code" -#~ msgstr "" +#~ msgstr "Kode verifikasi" #: src/view/com/modals/ChangeHandle.tsx:510 msgid "Verify {0}" @@ -5628,7 +5629,7 @@ msgstr "Verifikasi Email Anda" #: src/screens/Onboarding/index.tsx:42 msgid "Video Games" -msgstr "" +msgstr "Permainan Video" #: src/screens/Profile/Header/Shell.tsx:110 msgid "View {0}'s avatar" @@ -5692,7 +5693,7 @@ msgstr "" #: src/screens/Onboarding/StepAlgoFeeds/index.tsx:134 msgid "We also think you'll like \"For You\" by Skygaze:" -msgstr "" +msgstr "Sepertinya Anda juga akan menyukai \"For You\" oleh Skygaze:" #: src/screens/Hashtag.tsx:132 msgid "We couldn't find any results for that hashtag." @@ -5700,11 +5701,11 @@ msgstr "" #: src/screens/Deactivated.tsx:133 msgid "We estimate {estimatedTime} until your account is ready." -msgstr "" +msgstr "Kami perkirakan {estimatedTime} hingga akun Anda siap." #: src/screens/Onboarding/StepFinished.tsx:93 msgid "We hope you have a wonderful time. Remember, Bluesky is:" -msgstr "" +msgstr "Semoga Anda senang dan betah di sini. Ingat, Bluesky adalah:" #: src/view/com/posts/DiscoverFallbackHeader.tsx:29 #~ msgid "We ran out of posts from your follows. Here's the latest from" @@ -5712,11 +5713,11 @@ msgstr "" #: src/view/com/posts/DiscoverFallbackHeader.tsx:29 msgid "We ran out of posts from your follows. Here's the latest from <0/>." -msgstr "" +msgstr "Kami kehabisan postingan dari akun yang Anda ikuti. Inilah yang terbaru dari <0/>." #: src/screens/Onboarding/StepAlgoFeeds/index.tsx:118 #~ msgid "We recommend \"For You\" by Skygaze:" -#~ msgstr "" +#~ msgstr "Kami merekomendasikan \"For You\" oleh Skygaze:" #: src/components/dialogs/MutedWords.tsx:204 msgid "We recommend avoiding common words that appear in many posts, since it can result in no posts being shown." @@ -5724,7 +5725,7 @@ msgstr "" #: src/screens/Onboarding/StepAlgoFeeds/index.tsx:124 msgid "We recommend our \"Discover\" feed:" -msgstr "" +msgstr "Kami merekomendasikan feed \"Discover\" kami:" #: src/components/dialogs/BirthDateSettings.tsx:52 msgid "We were unable to load your birth date preferences. Please try again." @@ -5736,11 +5737,11 @@ msgstr "" #: src/screens/Onboarding/StepInterests/index.tsx:133 msgid "We weren't able to connect. Please try again to continue setting up your account. If it continues to fail, you can skip this flow." -msgstr "" +msgstr "Sepertinya ada masalah koneksi. Mohon coba lagi untuk melanjutkan pengaturan akun Anda. Jika terus gagal, Anda dapat melewati langkah ini." #: src/screens/Deactivated.tsx:137 msgid "We will let you know when your account is ready." -msgstr "" +msgstr "Kami akan memberi tahu Anda ketika akun Anda siap." #: src/view/com/modals/AppealLabel.tsx:48 #~ msgid "We'll look into your appeal promptly." @@ -5748,7 +5749,7 @@ msgstr "" #: src/screens/Onboarding/StepInterests/index.tsx:138 msgid "We'll use this to help customize your experience." -msgstr "" +msgstr "Kami akan menggunakan ini untuk menyesuaikan pengalaman Anda." #: src/view/com/auth/create/CreateAccount.tsx:134 msgid "We're so excited to have you join us!" @@ -5781,7 +5782,7 @@ msgstr "Selamat Datang di <0>Bluesky" #: src/screens/Onboarding/StepInterests/index.tsx:130 msgid "What are your interests?" -msgstr "" +msgstr "Apa saja minat Anda?" #: src/view/com/modals/report/Modal.tsx:169 #~ msgid "What is the issue with this {collectionName}?" @@ -5843,11 +5844,11 @@ msgstr "Tulis balasan Anda" #: src/screens/Onboarding/index.tsx:28 msgid "Writers" -msgstr "" +msgstr "Penulis" #: src/view/com/auth/create/Step2.tsx:263 #~ msgid "XXXXXX" -#~ msgstr "" +#~ msgstr "XXXXXX" #: src/view/com/composer/select-language/SuggestedLanguage.tsx:77 #: src/view/screens/PreferencesFollowingFeed.tsx:129 @@ -5861,11 +5862,11 @@ msgstr "Ya" #: src/screens/Onboarding/StepModeration/index.tsx:46 #~ msgid "You are in control" -#~ msgstr "" +#~ msgstr "Anda memiliki kendali" #: src/screens/Deactivated.tsx:130 msgid "You are in line." -msgstr "" +msgstr "Anda sedang dalam antrian." #: src/view/com/profile/ProfileFollows.tsx:93 msgid "You are not following anyone." @@ -5878,7 +5879,7 @@ msgstr "Anda juga dapat menemukan Feed Khusus baru untuk diikuti." #: src/screens/Onboarding/StepAlgoFeeds/index.tsx:123 #~ msgid "You can also try our \"Discover\" algorithm:" -#~ msgstr "" +#~ msgstr "Anda juga dapat mencoba algoritma \"Discover\" kami:" #: src/view/com/auth/create/Step1.tsx:106 #~ msgid "You can change hosting providers at any time." @@ -5886,7 +5887,7 @@ msgstr "Anda juga dapat menemukan Feed Khusus baru untuk diikuti." #: src/screens/Onboarding/StepFollowingFeed.tsx:142 msgid "You can change these settings later." -msgstr "" +msgstr "Anda dapat mengubah pengaturan ini nanti." #: src/view/com/auth/login/Login.tsx:158 #: src/view/com/auth/login/PasswordUpdatedForm.tsx:31 @@ -5928,7 +5929,7 @@ msgstr "Anda telah memblokir pengguna ini. Anda tidak dapat melihat konten merek #: src/view/com/modals/ChangePassword.tsx:87 #: src/view/com/modals/ChangePassword.tsx:121 msgid "You have entered an invalid code. It should look like XXXXX-XXXXX." -msgstr "" +msgstr "Anda telah memasukkan kode yang tidak valid. Seharusnya terlihat seperti XXXXX-XXXXX." #: src/lib/moderation/useModerationCauseDescription.ts:109 msgid "You have hidden this post" @@ -5994,7 +5995,7 @@ msgstr "" #: src/screens/Onboarding/StepModeration/AdultContentEnabledPref.tsx:110 msgid "You must be 18 years or older to enable adult content" -msgstr "" +msgstr "Anda harus berusia 18 tahun atau lebih untuk mengaktifkan konten dewasa" #: src/components/ReportDialog/SubmitView.tsx:205 msgid "You must select at least one labeler for a report" @@ -6014,17 +6015,17 @@ msgstr "Anda akan menerima email berisikan \"kode reset\". Masukkan kode tersebu #: src/screens/Onboarding/StepModeration/index.tsx:59 msgid "You're in control" -msgstr "" +msgstr "Anda memiliki kendali" #: src/screens/Deactivated.tsx:87 #: src/screens/Deactivated.tsx:88 #: src/screens/Deactivated.tsx:103 msgid "You're in line" -msgstr "" +msgstr "Anda sedang dalam antrian" #: src/screens/Onboarding/StepFinished.tsx:90 msgid "You're ready to go!" -msgstr "" +msgstr "Anda siap untuk mulai!" #: src/components/moderation/ModerationDetailsDialog.tsx:99 #: src/lib/moderation/useModerationCauseDescription.ts:101 @@ -6057,7 +6058,7 @@ msgstr "Pilihan Anda akan disimpan, tetapi dapat diubah nanti di pengaturan." #: src/screens/Onboarding/StepFollowingFeed.tsx:61 msgid "Your default feed is \"Following\"" -msgstr "" +msgstr "Feed bawaan Anda adalah \"Mengikuti\"" #: src/view/com/auth/create/state.ts:110 #: src/view/com/auth/login/ForgotPasswordForm.tsx:70 @@ -6105,7 +6106,7 @@ msgstr "" #: src/view/com/modals/ChangePassword.tsx:157 msgid "Your password has been changed successfully!" -msgstr "" +msgstr "Kata sandi Anda telah berhasil diubah!" #: src/view/com/composer/Composer.tsx:283 msgid "Your post has been published" diff --git a/src/locale/locales/ja/messages.po b/src/locale/locales/ja/messages.po index ca09edd542..fc9c7884ea 100644 --- a/src/locale/locales/ja/messages.po +++ b/src/locale/locales/ja/messages.po @@ -8,9 +8,9 @@ msgstr "" "Language: ja\n" "Project-Id-Version: \n" "Report-Msgid-Bugs-To: \n" -"PO-Revision-Date: 2024-01-30 19:00+0900\n" +"PO-Revision-Date: 2024-03-24 09:30+0900\n" "Last-Translator: Hima-Zinn\n" -"Language-Team: Hima-Zinn, tkusano, dolciss, oboenikui, noritada, middlingphys\n" +"Language-Team: Hima-Zinn, tkusano, dolciss, oboenikui, noritada, middlingphys, hibiki, reindex-ot, haoyayoi, vyv03354\n" "Plural-Forms: \n" #: src/view/com/modals/VerifyEmail.tsx:142 @@ -32,7 +32,7 @@ msgstr "メールがありません" #: src/screens/Profile/Header/Metrics.tsx:45 msgid "{following} following" -msgstr "{following}人をフォロー中" +msgstr "{following} フォロー" #: src/view/shell/desktop/RightNav.tsx:151 #~ msgid "{invitesAvailable, plural, one {Invite codes: # available} other {Invite codes: # available}}" @@ -66,11 +66,11 @@ msgstr "<0/>のメンバー" #: src/view/shell/Drawer.tsx:97 msgid "<0>{0} following" -msgstr "" +msgstr "<0>{0} フォロー" #: src/screens/Profile/Header/Metrics.tsx:46 msgid "<0>{following} <1>following" -msgstr "<0>{following}<1>人をフォロー中" +msgstr "<0>{following} <1>フォロー" #: src/view/com/auth/onboarding/RecommendedFeeds.tsx:30 msgid "<0>Choose your<1>Recommended<2>Feeds" @@ -86,7 +86,7 @@ msgstr "<1>Bluesky<0>へようこそ" #: src/screens/Profile/Header/Handle.tsx:42 msgid "⚠Invalid Handle" -msgstr "⚠不正なハンドル" +msgstr "⚠無効なハンドル" #: src/view/com/util/moderation/LabelInfo.tsx:45 #~ msgid "A content warning has been applied to this {0}." @@ -112,7 +112,7 @@ msgstr "アクセシビリティ" #: src/components/moderation/LabelsOnMe.tsx:42 msgid "account" -msgstr "" +msgstr "アカウント" #: src/view/com/auth/login/LoginForm.tsx:169 #: src/view/screens/Settings/index.tsx:327 @@ -126,7 +126,7 @@ msgstr "アカウントをブロックしました" #: src/view/com/profile/ProfileMenu.tsx:153 msgid "Account followed" -msgstr "" +msgstr "アカウントをフォローしました" #: src/view/com/profile/ProfileMenu.tsx:113 msgid "Account muted" @@ -156,7 +156,7 @@ msgstr "アカウントのブロックを解除しました" #: src/view/com/profile/ProfileMenu.tsx:166 msgid "Account unfollowed" -msgstr "" +msgstr "アカウントのフォローを解除しました" #: src/view/com/profile/ProfileMenu.tsx:102 msgid "Account unmuted" @@ -202,7 +202,7 @@ msgstr "アプリパスワードを追加" #: src/view/com/modals/report/Modal.tsx:194 #~ msgid "Add details to report" -#~ msgstr "レポートに詳細を追加" +#~ msgstr "報告に詳細を追加" #: src/view/com/composer/Composer.tsx:466 msgid "Add link card" @@ -214,11 +214,11 @@ msgstr "リンクカードを追加:" #: src/components/dialogs/MutedWords.tsx:158 msgid "Add mute word for configured settings" -msgstr "" +msgstr "ミュートするワードを設定に追加" #: src/components/dialogs/MutedWords.tsx:87 msgid "Add muted words and tags" -msgstr "" +msgstr "ミュートするワードとタグを追加" #: src/view/com/modals/ChangeHandle.tsx:417 msgid "Add the following DNS record to your domain:" @@ -248,7 +248,7 @@ msgstr "マイフィードに追加" #: src/view/screens/PreferencesFollowingFeed.tsx:173 msgid "Adjust the number of likes a reply must have to be shown in your feed." -msgstr "返信がフィードに表示されるために必要な「いいね」の数を調整します。" +msgstr "返信がフィードに表示されるために必要ないいねの数を調整します。" #: src/screens/Onboarding/StepModeration/AdultContentEnabledPref.tsx:117 #: src/view/com/modals/SelfLabel.tsx:75 @@ -265,7 +265,7 @@ msgstr "成人向けコンテンツ" #: src/components/moderation/ModerationLabelPref.tsx:114 msgid "Adult content is disabled." -msgstr "" +msgstr "成人向けコンテンツは無効になっています。" #: src/screens/Moderation/index.tsx:377 #: src/view/screens/Settings/index.tsx:684 @@ -274,7 +274,7 @@ msgstr "高度な設定" #: src/view/screens/Feeds.tsx:666 msgid "All the feeds you've saved, right in one place." -msgstr "" +msgstr "保存したすべてのフィードを1箇所にまとめます。" #: src/view/com/auth/login/ForgotPasswordForm.tsx:221 #: src/view/com/modals/ChangePassword.tsx:170 @@ -307,7 +307,7 @@ msgstr "以前のメールアドレス{0}にメールが送信されました。 #: src/lib/moderation/useReportOptions.ts:26 msgid "An issue not included in these options" -msgstr "" +msgstr "ほかの選択肢にはあてはまらない問題" #: src/view/com/profile/FollowButton.tsx:35 #: src/view/com/profile/FollowButton.tsx:45 @@ -327,7 +327,7 @@ msgstr "動物" #: src/lib/moderation/useReportOptions.ts:31 msgid "Anti-Social Behavior" -msgstr "" +msgstr "反社会的な行動" #: src/view/screens/LanguageSettings.tsx:95 msgid "App Language" @@ -343,7 +343,7 @@ msgstr "アプリパスワードの名前には、英数字、スペース、ハ #: src/view/com/modals/AddAppPasswords.tsx:99 msgid "App Password names must be at least 4 characters long." -msgstr "アプリパスワードの名前は長さが4文字以上である必要があります。" +msgstr "アプリパスワードの名前は長さが4文字以上である必要があります。" #: src/view/screens/Settings/index.tsx:695 msgid "App password settings" @@ -362,11 +362,11 @@ msgstr "アプリパスワード" #: src/components/moderation/LabelsOnMeDialog.tsx:134 #: src/components/moderation/LabelsOnMeDialog.tsx:137 msgid "Appeal" -msgstr "" +msgstr "異議を申し立てる" #: src/components/moderation/LabelsOnMeDialog.tsx:202 msgid "Appeal \"{0}\" label" -msgstr "" +msgstr "「{0}」のラベルに異議を申し立てる" #: src/view/com/util/forms/PostDropdownBtn.tsx:337 #: src/view/com/util/forms/PostDropdownBtn.tsx:346 @@ -383,7 +383,7 @@ msgstr "" #: src/components/moderation/LabelsOnMeDialog.tsx:193 msgid "Appeal submitted." -msgstr "" +msgstr "異議申し立てを提出しました。" #: src/view/com/util/moderation/LabelInfo.tsx:52 #~ msgid "Appeal this decision" @@ -403,7 +403,7 @@ msgstr "アプリパスワード「{name}」を本当に削除しますか?" #: src/view/com/feeds/FeedSourceCard.tsx:280 msgid "Are you sure you want to remove {0} from your feeds?" -msgstr "" +msgstr "あなたのフィードから{0}を削除してもよろしいですか?" #: src/view/com/composer/Composer.tsx:508 msgid "Are you sure you'd like to discard this draft?" @@ -448,7 +448,7 @@ msgstr "戻る" #: src/screens/Onboarding/StepSuggestedAccounts/index.tsx:136 msgid "Based on your interest in {interestsText}" -msgstr "「{interestsText}」への興味に基づいたおすすめです。" +msgstr "{interestsText}への興味に基づいたおすすめ" #: src/view/screens/Settings/index.tsx:542 msgid "Basics" @@ -466,7 +466,7 @@ msgstr "誕生日:" #: src/screens/Profile/Header/ProfileHeaderStandard.tsx:278 #: src/view/com/profile/ProfileMenu.tsx:361 msgid "Block" -msgstr "" +msgstr "ブロック" #: src/view/com/profile/ProfileMenu.tsx:300 #: src/view/com/profile/ProfileMenu.tsx:307 @@ -475,7 +475,7 @@ msgstr "アカウントをブロック" #: src/view/com/profile/ProfileMenu.tsx:344 msgid "Block Account?" -msgstr "" +msgstr "アカウントをブロックしますか?" #: src/view/screens/ProfileList.tsx:530 msgid "Block accounts" @@ -522,7 +522,7 @@ msgstr "投稿をブロックしました。" #: src/screens/Profile/Sections/Labels.tsx:153 msgid "Blocking does not prevent this labeler from placing labels on your account." -msgstr "" +msgstr "ブロックしてもこのラベラーがあなたのアカウントにラベルを貼ることができます。" #: src/view/screens/ProfileList.tsx:631 msgid "Blocking is public. Blocked accounts cannot reply in your threads, mention you, or otherwise interact with you." @@ -530,7 +530,7 @@ msgstr "ブロックしたことは公開されます。ブロック中のアカ #: src/view/com/profile/ProfileMenu.tsx:353 msgid "Blocking will not prevent labels from being applied on your account, but it will stop this account from replying in your threads or interacting with you." -msgstr "" +msgstr "ブロックしてもこのラベラーがあなたのアカウントにラベルを貼ることができますが、このアカウントがあなたのスレッドに返信したり、やりとりをしたりといったことはできなくなります。" #: src/view/com/auth/HomeLoggedOutCTA.tsx:97 #: src/view/com/auth/SplashScreen.web.tsx:133 @@ -545,7 +545,7 @@ msgstr "Bluesky" #: src/view/com/auth/server-input/index.tsx:150 msgid "Bluesky is an open network where you can choose your hosting provider. Custom hosting is now available in beta for developers." -msgstr "Bluesky は、ホスティング プロバイダーを選択できるオープン ネットワークです。 カスタム ホスティングは、開発者向けのベータ版で利用できるようになりました。" +msgstr "Bluesky は、ホスティング プロバイダーを選択できるオープン ネットワークです。 カスタムホスティングは、開発者向けのベータ版で利用できるようになりました。" #: src/view/com/auth/onboarding/WelcomeDesktop.tsx:80 #: src/view/com/auth/onboarding/WelcomeMobile.tsx:82 @@ -576,11 +576,11 @@ msgstr "Blueskyはログアウトしたユーザーにあなたのプロフィ #: src/lib/moderation/useLabelBehaviorDescription.ts:53 msgid "Blur images" -msgstr "" +msgstr "画像をぼかす" #: src/lib/moderation/useLabelBehaviorDescription.ts:51 msgid "Blur images and filter from feeds" -msgstr "" +msgstr "画像のぼかしとフィードからのフィルタリング" #: src/screens/Onboarding/index.tsx:33 msgid "Books" @@ -609,7 +609,7 @@ msgstr "作成者:{0}" #: src/components/LabelingServiceCard/index.tsx:57 msgid "By {0}" -msgstr "" +msgstr "作成者:{0}" #: src/view/com/profile/ProfileSubpageHeader.tsx:161 msgid "by <0/>" @@ -617,7 +617,7 @@ msgstr "作成者:<0/>" #: src/view/com/auth/create/Policies.tsx:87 msgid "By creating an account you agree to the {els}." -msgstr "" +msgstr "アカウントを作成することで、{els}に同意したものとみなされます。" #: src/view/com/profile/ProfileSubpageHeader.tsx:159 msgid "by you" @@ -629,7 +629,7 @@ msgstr "カメラ" #: src/view/com/modals/AddAppPasswords.tsx:216 msgid "Can only contain letters, numbers, spaces, dashes, and underscores. Must be at least 4 characters long, but no more than 32 characters long." -msgstr "英数字、スペース、ハイフン、アンダースコアのみが使用可能です。長さは4文字以上32文字以下である必要があります。" +msgstr "英数字、スペース、ハイフン、アンダースコアのみが使用可能です。長さは4文字以上32文字以下である必要があります。" #: src/components/Menu/index.tsx:213 #: src/components/Prompt.tsx:116 @@ -701,7 +701,7 @@ msgstr "検索をキャンセル" #: src/view/com/modals/LinkWarning.tsx:88 msgid "Cancels opening the linked website" -msgstr "" +msgstr "リンク先のウェブサイトを開くことをキャンセル" #: src/view/com/modals/VerifyEmail.tsx:152 msgid "Change" @@ -769,7 +769,7 @@ msgstr "「全員」か「返信不可」のどちらかを選択" #: src/view/screens/Settings/index.tsx:697 #~ msgid "Choose a new Bluesky username or create" -#~ msgstr "Blueskyの別のユーザー名を選択するか、新規に作成します" +#~ msgstr "Blueskyの別のユーザー名を選択するか、新規作成します" #: src/view/com/auth/server-input/index.tsx:79 msgid "Choose Service" @@ -802,7 +802,7 @@ msgstr "レガシーストレージデータをすべてクリア" #: src/view/screens/Settings/index.tsx:871 msgid "Clear all legacy storage data (restart after this)" -msgstr "すべてのレガシーストレージデータをクリア(この後再起動します)" +msgstr "すべてのレガシーストレージデータをクリア(このあと再起動します)" #: src/view/screens/Settings/index.tsx:880 msgid "Clear all storage data" @@ -810,7 +810,7 @@ msgstr "すべてのストレージデータをクリア" #: src/view/screens/Settings/index.tsx:883 msgid "Clear all storage data (restart after this)" -msgstr "すべてのストレージデータをクリア(この後再起動します)" +msgstr "すべてのストレージデータをクリア(このあと再起動します)" #: src/view/com/util/forms/SearchInput.tsx:88 #: src/view/screens/Search/Search.tsx:698 @@ -819,11 +819,11 @@ msgstr "検索クエリをクリア" #: src/view/screens/Settings/index.tsx:869 msgid "Clears all legacy storage data" -msgstr "" +msgstr "すべてのレガシーストレージデータをクリア" #: src/view/screens/Settings/index.tsx:881 msgid "Clears all storage data" -msgstr "" +msgstr "すべてのストレージデータをクリア" #: src/view/screens/Support.tsx:40 msgid "click here" @@ -831,11 +831,11 @@ msgstr "こちらをクリック" #: src/components/TagMenu/index.web.tsx:138 msgid "Click here to open tag menu for {tag}" -msgstr "" +msgstr "{tag}のタグメニューをクリックして表示" #: src/components/RichText.tsx:191 msgid "Click here to open tag menu for #{tag}" -msgstr "" +msgstr "#{tag}のタグメニューをクリックして表示" #: src/screens/Onboarding/index.tsx:35 msgid "Climate" @@ -874,7 +874,7 @@ msgstr "ナビゲーションフッターを閉じる" #: src/components/Menu/index.tsx:207 #: src/components/TagMenu/index.tsx:262 msgid "Close this dialog" -msgstr "" +msgstr "このダイアログを閉じる" #: src/view/shell/index.web.tsx:56 msgid "Closes bottom navigation bar" @@ -886,7 +886,7 @@ msgstr "パスワード更新アラートを閉じる" #: src/view/com/composer/Composer.tsx:318 msgid "Closes post composer and discards post draft" -msgstr "投稿の編集画面を閉じ、下書きを削除する" +msgstr "投稿の編集画面を閉じて下書きを削除する" #: src/view/com/lightbox/ImageViewing/components/ImageDefaultHeader.tsx:37 msgid "Closes viewer for header image" @@ -915,7 +915,7 @@ msgstr "初期設定を完了してアカウントを使い始める" #: src/view/com/auth/create/Step3.tsx:73 msgid "Complete the challenge" -msgstr "" +msgstr "テストをクリアしてください" #: src/view/com/composer/Composer.tsx:437 msgid "Compose posts up to {MAX_GRAPHEME_LENGTH} characters in length" @@ -929,11 +929,11 @@ msgstr "返信を作成" #: src/components/moderation/ModerationLabelPref.tsx:149 #: src/screens/Onboarding/StepModeration/ModerationOption.tsx:81 msgid "Configure content filtering setting for category: {0}" -msgstr "このカテゴリのコンテンツフィルタリングを設定: {0}" +msgstr "このカテゴリのコンテンツフィルタリングを設定:{0}" #: src/components/moderation/ModerationLabelPref.tsx:116 msgid "Configured in <0>moderation settings." -msgstr "" +msgstr "<0>モデレーションの設定で設定されています。" #: src/components/Prompt.tsx:152 #: src/components/Prompt.tsx:155 @@ -970,11 +970,11 @@ msgstr "アカウントの削除を確認" #: src/screens/Moderation/index.tsx:303 msgid "Confirm your age:" -msgstr "" +msgstr "年齢の確認:" #: src/screens/Moderation/index.tsx:294 msgid "Confirm your birthdate" -msgstr "" +msgstr "生年月日の確認" #: src/view/com/modals/ChangeEmail.tsx:157 #: src/view/com/modals/DeleteAccount.tsx:176 @@ -998,11 +998,11 @@ msgstr "サポートに連絡" #: src/components/moderation/LabelsOnMe.tsx:42 msgid "content" -msgstr "" +msgstr "コンテンツ" #: src/lib/moderation/useGlobalLabelStrings.ts:18 msgid "Content Blocked" -msgstr "" +msgstr "ブロックされたコンテンツ" #: src/view/screens/Moderation.tsx:83 #~ msgid "Content filtering" @@ -1014,7 +1014,7 @@ msgstr "" #: src/screens/Moderation/index.tsx:287 msgid "Content filters" -msgstr "" +msgstr "コンテンツのフィルター" #: src/view/com/modals/lang-settings/ContentLanguagesSettings.tsx:74 #: src/view/screens/LanguageSettings.tsx:278 @@ -1039,7 +1039,7 @@ msgstr "コンテンツの警告" #: src/components/Menu/index.web.tsx:84 msgid "Context menu backdrop, click to close the menu." -msgstr "" +msgstr "コンテキストメニューの背景をクリックし、メニューを閉じる。" #: src/screens/Onboarding/StepAlgoFeeds/index.tsx:170 #: src/screens/Onboarding/StepFollowingFeed.tsx:153 @@ -1097,7 +1097,7 @@ msgstr "コピー" #: src/view/com/modals/ChangeHandle.tsx:481 msgid "Copy {0}" -msgstr "" +msgstr "{0}をコピー" #: src/view/screens/ProfileList.tsx:388 msgid "Copy link to list" @@ -1124,11 +1124,11 @@ msgstr "著作権ポリシー" #: src/view/screens/ProfileFeed.tsx:102 msgid "Could not load feed" -msgstr "フィードのロードに失敗しました" +msgstr "フィードの読み込みに失敗しました" #: src/view/screens/ProfileList.tsx:907 msgid "Could not load list" -msgstr "リストのロードに失敗しました" +msgstr "リストの読み込みに失敗しました" #: src/view/com/auth/create/Step2.tsx:91 #~ msgid "Country" @@ -1159,11 +1159,11 @@ msgstr "新しいアカウントを作成" #: src/components/ReportDialog/SelectReportOptionView.tsx:94 msgid "Create report for {0}" -msgstr "" +msgstr "{0}の報告を作成" #: src/view/screens/AppPasswords.tsx:246 msgid "Created {0}" -msgstr "{0}を作成済み" +msgstr "{0}に作成" #: src/view/screens/ProfileFeed.tsx:616 #~ msgid "Created by <0/>" @@ -1222,7 +1222,7 @@ msgstr "ダークテーマ" #: src/view/screens/Settings/index.tsx:841 msgid "Debug Moderation" -msgstr "" +msgstr "モデレーションをデバッグ" #: src/view/screens/Debug.tsx:83 msgid "Debug panel" @@ -1232,7 +1232,7 @@ msgstr "デバッグパネル" #: src/view/screens/AppPasswords.tsx:268 #: src/view/screens/ProfileList.tsx:613 msgid "Delete" -msgstr "" +msgstr "削除" #: src/view/screens/Settings/index.tsx:796 msgid "Delete account" @@ -1248,7 +1248,7 @@ msgstr "アプリパスワードを削除" #: src/view/screens/AppPasswords.tsx:263 msgid "Delete app password?" -msgstr "" +msgstr "アプリパスワードを削除しますか?" #: src/view/screens/ProfileList.tsx:415 msgid "Delete List" @@ -1273,7 +1273,7 @@ msgstr "投稿を削除" #: src/view/screens/ProfileList.tsx:608 msgid "Delete this list?" -msgstr "" +msgstr "このリストを削除しますか?" #: src/view/com/util/forms/PostDropdownBtn.tsx:314 msgid "Delete this post?" @@ -1315,7 +1315,7 @@ msgstr "グレー" #: src/lib/moderation/useLabelBehaviorDescription.ts:68 #: src/screens/Moderation/index.tsx:343 msgid "Disabled" -msgstr "" +msgstr "無効" #: src/view/com/composer/Composer.tsx:510 msgid "Discard" @@ -1327,7 +1327,7 @@ msgstr "破棄" #: src/view/com/composer/Composer.tsx:507 msgid "Discard draft?" -msgstr "" +msgstr "下書きを削除しますか?" #: src/screens/Moderation/index.tsx:520 #: src/screens/Moderation/index.tsx:524 @@ -1341,11 +1341,11 @@ msgstr "新しいカスタムフィードを見つける" #: src/view/screens/Feeds.tsx:473 #~ msgid "Discover new feeds" -#~ msgstr "新しいフィードを見つける" +#~ msgstr "新しいフィードを探す" #: src/view/screens/Feeds.tsx:689 msgid "Discover New Feeds" -msgstr "" +msgstr "新しいフィードを探す" #: src/view/com/modals/EditProfile.tsx:192 msgid "Display name" @@ -1357,15 +1357,15 @@ msgstr "表示名" #: src/view/com/modals/ChangeHandle.tsx:398 msgid "DNS Panel" -msgstr "" +msgstr "DNSパネルがある場合" #: src/lib/moderation/useGlobalLabelStrings.ts:39 msgid "Does not include nudity." -msgstr "" +msgstr "ヌードは含まれません。" #: src/view/com/modals/ChangeHandle.tsx:482 msgid "Domain Value" -msgstr "" +msgstr "ドメインの値" #: src/view/com/modals/ChangeHandle.tsx:489 msgid "Domain verified!" @@ -1414,12 +1414,12 @@ msgstr "ダブルタップでサインイン" #: src/view/screens/Settings/index.tsx:755 #~ msgid "Download Bluesky account data (repository)" -#~ msgstr "" +#~ msgstr "Blueskyのアカウントのデータ(リポジトリ)をダウンロード" #: src/view/screens/Settings/ExportCarDialog.tsx:59 #: src/view/screens/Settings/ExportCarDialog.tsx:63 msgid "Download CAR file" -msgstr "" +msgstr "CARファイルをダウンロード" #: src/view/com/composer/text-input/TextInput.web.tsx:249 msgid "Drop to add images" @@ -1431,7 +1431,7 @@ msgstr "Appleのポリシーにより、成人向けコンテンツはサイン #: src/view/com/modals/ChangeHandle.tsx:257 msgid "e.g. alice" -msgstr "" +msgstr "例:太郎" #: src/view/com/modals/EditProfile.tsx:185 msgid "e.g. Alice Roberts" @@ -1439,7 +1439,7 @@ msgstr "例:山田 太郎" #: src/view/com/modals/ChangeHandle.tsx:381 msgid "e.g. alice.com" -msgstr "" +msgstr "例:taro.com" #: src/view/com/modals/EditProfile.tsx:203 msgid "e.g. Artist, dog-lover, and avid reader." @@ -1447,7 +1447,7 @@ msgstr "例:アーティスト、犬好き、熱烈な読書愛好家。" #: src/lib/moderation/useGlobalLabelStrings.ts:43 msgid "E.g. artistic nudes." -msgstr "" +msgstr "例:芸術的なヌード。" #: src/view/com/modals/CreateOrEditList.tsx:283 msgid "e.g. Great Posters" @@ -1477,7 +1477,7 @@ msgstr "編集" #: src/view/com/util/UserAvatar.tsx:299 #: src/view/com/util/UserBanner.tsx:85 msgid "Edit avatar" -msgstr "" +msgstr "アバターを編集" #: src/view/com/composer/photos/Gallery.tsx:144 #: src/view/com/modals/EditImage.tsx:207 @@ -1567,7 +1567,7 @@ msgstr "{0}のみ有効にする" #: src/screens/Moderation/index.tsx:331 msgid "Enable adult content" -msgstr "" +msgstr "成人向けコンテンツを有効にする" #: src/screens/Onboarding/StepModeration/AdultContentEnabledPref.tsx:94 msgid "Enable Adult Content" @@ -1592,7 +1592,7 @@ msgstr "この設定を有効にすると、自分がフォローしているユ #: src/screens/Moderation/index.tsx:341 msgid "Enabled" -msgstr "" +msgstr "有効" #: src/screens/Profile/Sections/Feed.tsx:84 msgid "End of feed" @@ -1605,7 +1605,7 @@ msgstr "このアプリパスワードの名前を入力" #: src/components/dialogs/MutedWords.tsx:100 #: src/components/dialogs/MutedWords.tsx:101 msgid "Enter a word or tag" -msgstr "" +msgstr "ワードまたはタグを入力" #: src/view/com/modals/VerifyEmail.tsx:105 msgid "Enter Confirmation Code" @@ -1658,7 +1658,7 @@ msgstr "ユーザー名とパスワードを入力してください" #: src/view/com/auth/create/Step3.tsx:67 msgid "Error receiving captcha response." -msgstr "" +msgstr "Captchaレスポンスの受信中にエラーが発生しました。" #: src/view/screens/Search/Search.tsx:110 msgid "Error:" @@ -1670,11 +1670,11 @@ msgstr "全員" #: src/lib/moderation/useReportOptions.ts:66 msgid "Excessive mentions or replies" -msgstr "" +msgstr "過剰なメンションや返信" #: src/view/com/modals/DeleteAccount.tsx:231 msgid "Exits account deletion process" -msgstr "" +msgstr "アカウントの削除処理を終了" #: src/view/com/modals/ChangeHandle.tsx:150 msgid "Exits handle change process" @@ -1682,7 +1682,7 @@ msgstr "ハンドルの変更を終了" #: src/view/com/modals/crop-image/CropImage.web.tsx:135 msgid "Exits image cropping process" -msgstr "" +msgstr "画像の切り抜き処理を終了" #: src/view/com/lightbox/Lightbox.web.tsx:130 msgid "Exits image view" @@ -1708,11 +1708,11 @@ msgstr "返信する投稿全体を展開または折りたたむ" #: src/lib/moderation/useGlobalLabelStrings.ts:47 msgid "Explicit or potentially disturbing media." -msgstr "" +msgstr "露骨な、または不愉快になる可能性のあるメディア。" #: src/lib/moderation/useGlobalLabelStrings.ts:35 msgid "Explicit sexual images." -msgstr "" +msgstr "露骨な性的画像。" #: src/view/screens/Settings/index.tsx:777 msgid "Export my data" @@ -1758,11 +1758,11 @@ msgstr "投稿の削除に失敗しました。もう一度お試しください #: src/view/com/auth/onboarding/RecommendedFeeds.tsx:109 #: src/view/com/auth/onboarding/RecommendedFeeds.tsx:141 msgid "Failed to load recommended feeds" -msgstr "おすすめのフィードのロードに失敗しました" +msgstr "おすすめのフィードの読み込みに失敗しました" #: src/view/com/lightbox/Lightbox.tsx:83 msgid "Failed to save image: {0}" -msgstr "" +msgstr "画像の保存に失敗しました:{0}" #: src/Navigation.tsx:196 msgid "Feed" @@ -1818,11 +1818,11 @@ msgstr "フィードには特定の話題に焦点を当てたものもありま #: src/view/com/modals/ChangeHandle.tsx:482 msgid "File Contents" -msgstr "" +msgstr "ファイルのコンテンツ" #: src/lib/moderation/useLabelBehaviorDescription.ts:66 msgid "Filter from feeds" -msgstr "" +msgstr "フィードからのフィルター" #: src/screens/Onboarding/StepFinished.tsx:151 msgid "Finalizing" @@ -1848,7 +1848,7 @@ msgstr "似ているアカウントを検索中..." #: src/view/screens/PreferencesFollowingFeed.tsx:111 msgid "Fine-tune the content you see on your Following feed." -msgstr "" +msgstr "Followingフィードに表示されるコンテンツを調整します。" #: src/view/screens/PreferencesHomeFeed.tsx:111 #~ msgid "Fine-tune the content you see on your home screen." @@ -1897,7 +1897,7 @@ msgstr "{0}をフォロー" #: src/view/com/profile/ProfileMenu.tsx:242 #: src/view/com/profile/ProfileMenu.tsx:253 msgid "Follow Account" -msgstr "" +msgstr "アカウントをフォロー" #: src/screens/Onboarding/StepSuggestedAccounts/index.tsx:179 msgid "Follow All" @@ -1953,7 +1953,7 @@ msgstr "{0}をフォローしています" #: src/view/screens/Settings/index.tsx:553 msgid "Following feed preferences" -msgstr "" +msgstr "Followingフィードの設定" #: src/Navigation.tsx:262 #: src/view/com/home/HomeHeaderLayout.web.tsx:50 @@ -1961,7 +1961,7 @@ msgstr "" #: src/view/screens/PreferencesFollowingFeed.tsx:104 #: src/view/screens/Settings/index.tsx:562 msgid "Following Feed Preferences" -msgstr "" +msgstr "Followingフィードの設定" #: src/screens/Profile/Header/Handle.tsx:24 msgid "Follows you" @@ -1998,12 +1998,12 @@ msgstr "パスワードを忘れた" #: src/lib/moderation/useReportOptions.ts:52 msgid "Frequently Posts Unwanted Content" -msgstr "" +msgstr "望ましくないコンテンツを頻繁に投稿" #: src/screens/Hashtag.tsx:108 #: src/screens/Hashtag.tsx:148 msgid "From @{sanitizedAuthor}" -msgstr "" +msgstr "@{sanitizedAuthor}による" #: src/view/com/posts/FeedItem.tsx:179 msgctxt "from-feed" @@ -2021,7 +2021,7 @@ msgstr "開始" #: src/lib/moderation/useReportOptions.ts:37 msgid "Glaring violations of law or terms of service" -msgstr "" +msgstr "法律または利用規約への明らかな違反" #: src/components/moderation/ScreenHider.tsx:144 #: src/components/moderation/ScreenHider.tsx:153 @@ -2051,11 +2051,11 @@ msgstr "前のステップに戻る" #: src/view/screens/NotFound.tsx:55 msgid "Go home" -msgstr "" +msgstr "ホームへ" #: src/view/screens/NotFound.tsx:54 msgid "Go Home" -msgstr "" +msgstr "ホームへ" #: src/view/screens/Search/Search.tsx:748 #: src/view/shell/desktop/Search.tsx:263 @@ -2072,7 +2072,7 @@ msgstr "次へ" #: src/lib/moderation/useGlobalLabelStrings.ts:46 msgid "Graphic Media" -msgstr "" +msgstr "生々しいメディア" #: src/view/com/modals/ChangeHandle.tsx:265 msgid "Handle" @@ -2080,23 +2080,23 @@ msgstr "ハンドル" #: src/lib/moderation/useReportOptions.ts:32 msgid "Harassment, trolling, or intolerance" -msgstr "" +msgstr "嫌がらせ、荒らし、不寛容" #: src/Navigation.tsx:282 msgid "Hashtag" -msgstr "" +msgstr "ハッシュタグ" #: src/components/RichText.tsx:188 #~ msgid "Hashtag: {tag}" -#~ msgstr "" +#~ msgstr "ハッシュタグ:{tag}" #: src/components/RichText.tsx:190 msgid "Hashtag: #{tag}" -msgstr "" +msgstr "ハッシュタグ:#{tag}" #: src/view/com/auth/create/CreateAccount.tsx:208 msgid "Having trouble?" -msgstr "何か問題が発生しましたか?" +msgstr "なにか問題が発生しましたか?" #: src/view/shell/desktop/RightNav.tsx:90 #: src/view/shell/Drawer.tsx:324 @@ -2117,7 +2117,7 @@ msgstr "人気のあるフィードを紹介します。好きなだけフォロ #: src/screens/Onboarding/StepTopicalFeeds.tsx:80 msgid "Here are some topical feeds based on your interests: {interestsText}. You can choose to follow as many as you like." -msgstr "「{interestsText}」への興味に基づいたおすすめです。好きなだけフォローすることができます。" +msgstr "{interestsText}への興味に基づいたおすすめです。好きなだけフォローすることができます。" #: src/view/com/modals/AddAppPasswords.tsx:153 msgid "Here is your app password." @@ -2185,11 +2185,11 @@ msgstr "このフィードが見つからないようです。もしかしたら #: src/screens/Moderation/index.tsx:61 msgid "Hmmmm, it seems we're having trouble loading this data. See below for more details. If this issue persists, please contact us." -msgstr "" +msgstr "このデータの読み込みに問題があるようです。詳細は以下をご覧ください。この問題が解決しない場合は、サポートにご連絡ください。" #: src/screens/Profile/ErrorState.tsx:31 msgid "Hmmmm, we couldn't load that moderation service." -msgstr "" +msgstr "そのモデレーションサービスを読み込めませんでした。" #: src/Navigation.tsx:454 #: src/view/shell/bottom-bar/BottomBar.tsx:139 @@ -2208,7 +2208,7 @@ msgstr "ホーム" #: src/view/com/modals/ChangeHandle.tsx:421 msgid "Host:" -msgstr "" +msgstr "ホスト:" #: src/view/com/auth/create/Step1.tsx:75 #: src/view/com/auth/login/ForgotPasswordForm.tsx:120 @@ -2243,19 +2243,19 @@ msgstr "ALTテキストが長い場合、ALTテキストの展開状態を切り #: src/view/com/modals/SelfLabel.tsx:127 msgid "If none are selected, suitable for all ages." -msgstr "何も選択しない場合は、全年齢対象です。" +msgstr "なにも選択しない場合は、全年齢対象です。" #: src/view/com/auth/create/Policies.tsx:91 msgid "If you are not yet an adult according to the laws of your country, your parent or legal guardian must read these Terms on your behalf." -msgstr "" +msgstr "あなたがお住いの国の法律においてまだ成人していない場合は、親権者または法定後見人があなたに代わって本規約をお読みください。" #: src/view/screens/ProfileList.tsx:610 msgid "If you delete this list, you won't be able to recover it." -msgstr "" +msgstr "このリストを削除すると、復元できなくなります。" #: src/view/com/util/forms/PostDropdownBtn.tsx:316 msgid "If you remove this post, you won't be able to recover it." -msgstr "" +msgstr "この投稿を削除すると、復元できなくなります。" #: src/view/com/modals/ChangePassword.tsx:148 msgid "If you want to change your password, we will send you a code to verify that this is your account." @@ -2263,7 +2263,7 @@ msgstr "パスワードを変更する場合は、あなたのアカウントで #: src/lib/moderation/useReportOptions.ts:36 msgid "Illegal and Urgent" -msgstr "" +msgstr "違法かつ緊急" #: src/view/com/util/images/Gallery.tsx:38 msgid "Image" @@ -2280,7 +2280,7 @@ msgstr "画像のALTテキスト" #: src/lib/moderation/useReportOptions.ts:47 msgid "Impersonation or false claims about identity or affiliation" -msgstr "" +msgstr "なりすまし、または身元もしくは所属に関する虚偽の主張" #: src/view/com/auth/login/SetNewPasswordForm.tsx:138 msgid "Input code sent to your email for password reset" @@ -2344,7 +2344,7 @@ msgstr "あなたのパスワードを入力" #: src/view/com/modals/ChangeHandle.tsx:390 msgid "Input your preferred hosting provider" -msgstr "" +msgstr "ご希望のホスティングプロバイダーを入力" #: src/view/com/auth/create/Step2.tsx:80 msgid "Input your user handle" @@ -2381,7 +2381,7 @@ msgstr "招待コード:{0}個使用可能" #: src/view/shell/Drawer.tsx:645 #~ msgid "Invite codes: {invitesAvailable} available" -#~ msgstr "使用可能な招待コード: {invitesAvailable} 個" +#~ msgstr "使用可能な招待コード:{invitesAvailable}個" #: src/view/com/modals/InviteCodes.tsx:169 msgid "Invite codes: 1 available" @@ -2415,35 +2415,35 @@ msgstr "報道" #: src/components/moderation/LabelsOnMe.tsx:59 msgid "label has been placed on this {labelTarget}" -msgstr "" +msgstr "個のラベルがこの{labelTarget}に貼られました" #: src/components/moderation/ContentHider.tsx:144 msgid "Labeled by {0}." -msgstr "" +msgstr "{0}によるラベル" #: src/components/moderation/ContentHider.tsx:142 msgid "Labeled by the author." -msgstr "" +msgstr "投稿者によるラベル。" #: src/view/screens/Profile.tsx:186 msgid "Labels" -msgstr "" +msgstr "ラベル" #: src/screens/Profile/Sections/Labels.tsx:143 msgid "Labels are annotations on users and content. They can be used to hide, warn, and categorize the network." -msgstr "" +msgstr "ラベルは、ユーザーやコンテンツに対する注釈です。ラベルはネットワークを隠したり、警告したり、分類したりするのに使われます。" #: src/components/moderation/LabelsOnMe.tsx:61 msgid "labels have been placed on this {labelTarget}" -msgstr "" +msgstr "個のラベルがこの{labelTarget}に貼られました" #: src/components/moderation/LabelsOnMeDialog.tsx:63 msgid "Labels on your account" -msgstr "" +msgstr "あなたのアカウントのラベル" #: src/components/moderation/LabelsOnMeDialog.tsx:65 msgid "Labels on your content" -msgstr "" +msgstr "あなたのコンテンツのラベル" #: src/view/com/composer/select-language/SelectLangBtn.tsx:104 msgid "Language selection" @@ -2477,7 +2477,7 @@ msgstr "詳細" #: src/components/moderation/ContentHider.tsx:65 #: src/components/moderation/ContentHider.tsx:128 msgid "Learn more about the moderation applied to this content." -msgstr "" +msgstr "このコンテンツに適用されるモデレーションはこちらを参照してください。" #: src/components/moderation/PostHider.tsx:85 #: src/components/moderation/ScreenHider.tsx:126 @@ -2490,7 +2490,7 @@ msgstr "Blueskyで公開されている内容はこちらを参照してくだ #: src/components/moderation/ContentHider.tsx:152 msgid "Learn more." -msgstr "" +msgstr "詳細。" #: src/view/com/modals/lang-settings/ContentLanguagesSettings.tsx:82 msgid "Leave them all unchecked to see any language." @@ -2553,7 +2553,7 @@ msgstr "{0} {1}にいいねされました" #: src/components/LabelingServiceCard/index.tsx:72 msgid "Liked by {count} {0}" -msgstr "" +msgstr "{count} {0}にいいねされました" #: src/screens/Profile/Header/ProfileHeaderLabeler.tsx:277 #: src/screens/Profile/Header/ProfileHeaderLabeler.tsx:291 @@ -2637,22 +2637,22 @@ msgstr "リスト" #: src/view/com/post-thread/PostThread.tsx:333 #: src/view/com/post-thread/PostThread.tsx:341 #~ msgid "Load more posts" -#~ msgstr "投稿をさらにロード" +#~ msgstr "投稿をさらに読み込む" #: src/view/screens/Notifications.tsx:159 msgid "Load new notifications" -msgstr "最新の通知をロード" +msgstr "最新の通知を読み込む" #: src/screens/Profile/Sections/Feed.tsx:70 #: src/view/com/feeds/FeedPage.tsx:124 #: src/view/screens/ProfileFeed.tsx:495 #: src/view/screens/ProfileList.tsx:695 msgid "Load new posts" -msgstr "最新の投稿をロード" +msgstr "最新の投稿を読み込む" #: src/view/com/composer/text-input/mobile/Autocomplete.tsx:99 msgid "Loading..." -msgstr "ロード中..." +msgstr "読み込み中..." #: src/view/com/modals/ServerInput.tsx:50 #~ msgid "Local dev server" @@ -2691,15 +2691,15 @@ msgstr "意図した場所であることを確認してください!" #: src/components/dialogs/MutedWords.tsx:83 msgid "Manage your muted words and tags" -msgstr "" +msgstr "ミュートしたワードとタグの管理" #: src/view/com/auth/create/Step2.tsx:118 msgid "May not be longer than 253 characters" -msgstr "" +msgstr "253文字より長くはできません" #: src/view/com/auth/create/Step2.tsx:109 msgid "May only contain letters and numbers" -msgstr "" +msgstr "英字と数字のみ使用可能です" #: src/view/screens/Profile.tsx:190 msgid "Media" @@ -2728,7 +2728,7 @@ msgstr "サーバーからのメッセージ:{0}" #: src/lib/moderation/useReportOptions.ts:45 msgid "Misleading Account" -msgstr "" +msgstr "誤解を招くアカウント" #: src/Navigation.tsx:119 #: src/screens/Moderation/index.tsx:106 @@ -2741,7 +2741,7 @@ msgstr "モデレーション" #: src/components/moderation/ModerationDetailsDialog.tsx:113 msgid "Moderation details" -msgstr "" +msgstr "モデレーションの詳細" #: src/view/com/lists/ListCard.tsx:93 #: src/view/com/modals/UserAddRemoveLists.tsx:206 @@ -2781,11 +2781,11 @@ msgstr "モデレーションの設定" #: src/Navigation.tsx:216 msgid "Moderation states" -msgstr "" +msgstr "モデレーションのステータス" #: src/screens/Moderation/index.tsx:217 msgid "Moderation tools" -msgstr "" +msgstr "モデレーションのツール" #: src/components/moderation/ModerationDetailsDialog.tsx:49 #: src/lib/moderation/useModerationCauseDescription.ts:40 @@ -2794,7 +2794,7 @@ msgstr "モデレーターによりコンテンツに一般的な警告が設定 #: src/view/com/post-thread/PostThreadItem.tsx:541 msgid "More" -msgstr "" +msgstr "さらに" #: src/view/shell/desktop/Feeds.tsx:65 msgid "More feeds" @@ -2806,7 +2806,7 @@ msgstr "その他のオプション" #: src/view/com/util/forms/PostDropdownBtn.tsx:315 #~ msgid "More post options" -#~ msgstr "そのほかの投稿のオプション" +#~ msgstr "その他の投稿のオプション" #: src/view/screens/PreferencesThreads.tsx:82 msgid "Most-liked replies first" @@ -2814,15 +2814,15 @@ msgstr "いいねの数が多い順に返信を表示" #: src/view/com/auth/create/Step2.tsx:122 msgid "Must be at least 3 characters" -msgstr "" +msgstr "最低でも3文字以上にしてください" #: src/components/TagMenu/index.tsx:249 msgid "Mute" -msgstr "" +msgstr "ミュート" #: src/components/TagMenu/index.web.tsx:105 msgid "Mute {truncatedTag}" -msgstr "" +msgstr "{truncatedTag}をミュート" #: src/view/com/profile/ProfileMenu.tsx:279 #: src/view/com/profile/ProfileMenu.tsx:286 @@ -2835,19 +2835,19 @@ msgstr "アカウントをミュート" #: src/components/TagMenu/index.tsx:209 msgid "Mute all {displayTag} posts" -msgstr "" +msgstr "{displayTag}のすべての投稿をミュート" #: src/components/TagMenu/index.tsx:211 #~ msgid "Mute all {tag} posts" -#~ msgstr "" +#~ msgstr "{tag}のすべての投稿をミュート" #: src/components/dialogs/MutedWords.tsx:149 msgid "Mute in tags only" -msgstr "" +msgstr "タグのみをミュート" #: src/components/dialogs/MutedWords.tsx:134 msgid "Mute in text & tags" -msgstr "" +msgstr "テキストとタグをミュート" #: src/view/screens/ProfileList.tsx:461 #: src/view/screens/ProfileList.tsx:624 @@ -2864,11 +2864,11 @@ msgstr "これらのアカウントをミュートしますか?" #: src/components/dialogs/MutedWords.tsx:127 msgid "Mute this word in post text and tags" -msgstr "" +msgstr "投稿のテキストやタグでこのワードをミュート" #: src/components/dialogs/MutedWords.tsx:142 msgid "Mute this word in tags only" -msgstr "" +msgstr "タグのみでこのワードをミュート" #: src/view/com/util/forms/PostDropdownBtn.tsx:251 #: src/view/com/util/forms/PostDropdownBtn.tsx:257 @@ -2878,7 +2878,7 @@ msgstr "スレッドをミュート" #: src/view/com/util/forms/PostDropdownBtn.tsx:267 #: src/view/com/util/forms/PostDropdownBtn.tsx:269 msgid "Mute words & tags" -msgstr "" +msgstr "ワードとタグをミュート" #: src/view/com/lists/ListCard.tsx:102 msgid "Muted" @@ -2899,11 +2899,11 @@ msgstr "ミュート中のアカウントの投稿は、フィードや通知か #: src/lib/moderation/useModerationCauseDescription.ts:85 msgid "Muted by \"{0}\"" -msgstr "" +msgstr "「{0}」によってミュート中" #: src/screens/Moderation/index.tsx:233 msgid "Muted words & tags" -msgstr "" +msgstr "ミュートしたワードとタグ" #: src/view/screens/ProfileList.tsx:621 msgid "Muting is private. Muted accounts can interact with you, but you will not see their posts or receive notifications from them." @@ -2924,7 +2924,7 @@ msgstr "マイプロフィール" #: src/view/screens/Settings/index.tsx:596 msgid "My saved feeds" -msgstr "" +msgstr "保存されたフィード" #: src/view/screens/Settings/index.tsx:602 msgid "My Saved Feeds" @@ -2932,7 +2932,7 @@ msgstr "保存されたフィード" #: src/view/com/auth/server-input/index.tsx:118 #~ msgid "my-server.com" -#~ msgstr "" +#~ msgstr "my-server.com" #: src/view/com/modals/AddAppPasswords.tsx:179 #: src/view/com/modals/CreateOrEditList.tsx:290 @@ -2947,7 +2947,7 @@ msgstr "名前は必須です" #: src/lib/moderation/useReportOptions.ts:78 #: src/lib/moderation/useReportOptions.ts:86 msgid "Name or Description Violates Community Standards" -msgstr "" +msgstr "名前または説明がコミュニティ基準に違反" #: src/screens/Onboarding/index.tsx:25 msgid "Nature" @@ -2967,7 +2967,7 @@ msgstr "あなたのプロフィールに移動します" #: src/components/ReportDialog/SelectReportOptionView.tsx:124 msgid "Need to report a copyright violation?" -msgstr "" +msgstr "著作権違反を報告する必要がありますか?" #: src/view/com/modals/EmbedConsent.tsx:107 #: src/view/com/modals/EmbedConsent.tsx:123 @@ -2985,11 +2985,11 @@ msgstr "フォロワーやデータへのアクセスを失うことはありま #: src/components/dialogs/MutedWords.tsx:293 #~ msgid "Nevermind" -#~ msgstr "" +#~ msgstr "やめておく" #: src/view/com/modals/ChangeHandle.tsx:520 msgid "Nevermind, create a handle for me" -msgstr "" +msgstr "気にせずにハンドルを作成" #: src/view/screens/Lists.tsx:76 msgctxt "action" @@ -3086,7 +3086,7 @@ msgstr "説明はありません" #: src/view/com/modals/ChangeHandle.tsx:406 msgid "No DNS Panel" -msgstr "" +msgstr "DNSパネルがない場合" #: src/screens/Profile/Header/ProfileHeaderStandard.tsx:111 msgid "No longer following {0}" @@ -3103,7 +3103,7 @@ msgstr "結果はありません" #: src/components/Lists.tsx:189 msgid "No results found" -msgstr "" +msgstr "結果は見つかりません" #: src/view/screens/Feeds.tsx:495 msgid "No results found for \"{query}\"" @@ -3126,11 +3126,11 @@ msgstr "返信不可" #: src/components/LikedByList.tsx:102 #: src/components/LikesDialog.tsx:99 msgid "Nobody has liked this yet. Maybe you should be the first!" -msgstr "" +msgstr "まだ誰もこれをいいねしていません。あなたが最初になるべきかもしれません!" #: src/lib/moderation/useGlobalLabelStrings.ts:42 msgid "Non-sexual Nudity" -msgstr "" +msgstr "性的ではないヌード" #: src/view/com/modals/SelfLabel.tsx:135 msgid "Not Applicable." @@ -3149,7 +3149,7 @@ msgstr "今はしない" #: src/view/com/profile/ProfileMenu.tsx:368 #: src/view/com/util/forms/PostDropdownBtn.tsx:342 msgid "Note about sharing" -msgstr "" +msgstr "共有についての注意事項" #: src/view/screens/Moderation.tsx:227 #~ msgid "Note: Bluesky is an open and public network, and enabling this will not make your profile private or limit the ability of logged in users to see your posts. This setting only limits the visibility of posts on the Bluesky app and website; third-party apps that display Bluesky content may not respect this setting, and could show your content to logged-out users." @@ -3175,11 +3175,11 @@ msgstr "ヌード" #: src/lib/moderation/useReportOptions.ts:71 msgid "Nudity or pornography not labeled as such" -msgstr "" +msgstr "ヌードもしくはポルノと表示されていないもの" #: src/lib/moderation/useLabelBehaviorDescription.ts:11 msgid "Off" -msgstr "" +msgstr "オフ" #: src/view/com/util/ErrorBoundary.tsx:49 msgid "Oh no!" @@ -3187,11 +3187,11 @@ msgstr "ちょっと!" #: src/screens/Onboarding/StepInterests/index.tsx:128 msgid "Oh no! Something went wrong." -msgstr "ちょっと!何かがおかしいです。" +msgstr "ちょっと!なにかがおかしいです。" #: src/screens/Onboarding/StepModeration/AdultContentEnabledPref.tsx:127 msgid "OK" -msgstr "" +msgstr "OK" #: src/view/com/auth/login/PasswordUpdatedForm.tsx:41 msgid "Okay" @@ -3207,7 +3207,7 @@ msgstr "オンボーディングのリセット" #: src/view/com/composer/Composer.tsx:391 msgid "One or more images is missing alt text." -msgstr "1つもしくは複数の画像にALTテキストがありません。" +msgstr "1つもしくは複数の画像にALTテキストがありません。" #: src/view/com/threadgate/WhoCanReply.tsx:100 msgid "Only {0} can reply." @@ -3215,7 +3215,7 @@ msgstr "{0}のみ返信可能" #: src/components/Lists.tsx:83 msgid "Oops, something went wrong!" -msgstr "" +msgstr "おっと、なにかが間違っているようです!" #: src/components/Lists.tsx:157 #: src/view/screens/AppPasswords.tsx:67 @@ -3229,7 +3229,7 @@ msgstr "開かれています" #: src/view/screens/Moderation.tsx:75 #~ msgid "Open content filtering settings" -#~ msgstr "" +#~ msgstr "コンテンツのフィルタリング設定を開く" #: src/view/com/composer/Composer.tsx:490 #: src/view/com/composer/Composer.tsx:491 @@ -3238,7 +3238,7 @@ msgstr "絵文字を入力" #: src/view/screens/ProfileFeed.tsx:299 msgid "Open feed options menu" -msgstr "" +msgstr "フィードの設定メニューを開く" #: src/view/screens/Settings/index.tsx:734 msgid "Open links with in-app browser" @@ -3246,11 +3246,11 @@ msgstr "アプリ内ブラウザーでリンクを開く" #: src/screens/Moderation/index.tsx:229 msgid "Open muted words and tags settings" -msgstr "" +msgstr "ミュートしたワードとタグの設定を開く" #: src/view/screens/Moderation.tsx:92 #~ msgid "Open muted words settings" -#~ msgstr "" +#~ msgstr "ミュートしたワードの設定を開く" #: src/view/com/home/HomeHeaderLayoutMobile.tsx:50 msgid "Open navigation" @@ -3258,7 +3258,7 @@ msgstr "ナビゲーションを開く" #: src/view/com/util/forms/PostDropdownBtn.tsx:183 msgid "Open post options menu" -msgstr "" +msgstr "投稿のオプションを開く" #: src/view/screens/Settings/index.tsx:828 #: src/view/screens/Settings/index.tsx:838 @@ -3267,7 +3267,7 @@ msgstr "絵本のページを開く" #: src/view/screens/Settings/index.tsx:816 msgid "Open system log" -msgstr "" +msgstr "システムのログを開く" #: src/view/com/util/forms/DropdownButton.tsx:154 msgid "Opens {numItems} options" @@ -3308,12 +3308,12 @@ msgstr "外部コンテンツの埋め込みの設定を開く" #: src/view/com/auth/HomeLoggedOutCTA.tsx:56 #: src/view/com/auth/SplashScreen.tsx:70 msgid "Opens flow to create a new Bluesky account" -msgstr "" +msgstr "新しいBlueskyのアカウントを作成するフローを開く" #: src/view/com/auth/HomeLoggedOutCTA.tsx:74 #: src/view/com/auth/SplashScreen.tsx:83 msgid "Opens flow to sign into your existing Bluesky account" -msgstr "" +msgstr "既存のBlueskyアカウントにサインインするフローを開く" #: src/view/com/profile/ProfileHeader.tsx:575 #~ msgid "Opens followers list" @@ -3333,7 +3333,7 @@ msgstr "招待コードのリストを開く" #: src/view/screens/Settings/index.tsx:798 msgid "Opens modal for account deletion confirmation. Requires email code" -msgstr "" +msgstr "アカウントの削除確認用の表示を開きます。メールアドレスのコードが必要です" #: src/view/screens/Settings/index.tsx:774 #~ msgid "Opens modal for account deletion confirmation. Requires email code." @@ -3341,19 +3341,19 @@ msgstr "" #: src/view/screens/Settings/index.tsx:756 msgid "Opens modal for changing your Bluesky password" -msgstr "" +msgstr "Blueskyのパスワードを変更するためのモーダルを開く" #: src/view/screens/Settings/index.tsx:718 msgid "Opens modal for choosing a new Bluesky handle" -msgstr "" +msgstr "新しいBlueskyのハンドルを選択するためのモーダルを開く" #: src/view/screens/Settings/index.tsx:779 msgid "Opens modal for downloading your Bluesky account data (repository)" -msgstr "" +msgstr "Blueskyのアカウントのデータ(リポジトリ)をダウンロードするためのモーダルを開く" #: src/view/screens/Settings/index.tsx:970 msgid "Opens modal for email verification" -msgstr "" +msgstr "メールアドレスの認証のためのモーダルを開く" #: src/view/com/modals/ChangeHandle.tsx:281 msgid "Opens modal for using custom domain" @@ -3378,7 +3378,7 @@ msgstr "保存されたすべてのフィードで画面を開く" #: src/view/screens/Settings/index.tsx:696 msgid "Opens the app password settings" -msgstr "" +msgstr "アプリパスワードの設定を開く" #: src/view/screens/Settings/index.tsx:676 #~ msgid "Opens the app password settings page" @@ -3386,7 +3386,7 @@ msgstr "" #: src/view/screens/Settings/index.tsx:554 msgid "Opens the Following feed preferences" -msgstr "" +msgstr "Followingフィードの設定を開く" #: src/view/screens/Settings/index.tsx:535 #~ msgid "Opens the home feed preferences" @@ -3394,7 +3394,7 @@ msgstr "" #: src/view/com/modals/LinkWarning.tsx:76 msgid "Opens the linked website" -msgstr "" +msgstr "リンク先のウェブサイトを開く" #: src/view/screens/Settings/index.tsx:829 #: src/view/screens/Settings/index.tsx:839 @@ -3415,7 +3415,7 @@ msgstr "{numItems}個中{0}目のオプション" #: src/components/ReportDialog/SubmitView.tsx:162 msgid "Optionally provide additional information below:" -msgstr "" +msgstr "オプションとして、以下に追加情報をご記入ください:" #: src/view/com/modals/Threadgate.tsx:89 msgid "Or combine these options:" @@ -3427,7 +3427,7 @@ msgstr "または以下のオプションを組み合わせてください:" #: src/lib/moderation/useReportOptions.ts:25 msgid "Other" -msgstr "" +msgstr "その他" #: src/view/com/auth/login/ChooseAccountForm.tsx:147 msgid "Other account" @@ -3462,7 +3462,7 @@ msgstr "パスワード" #: src/view/com/modals/ChangePassword.tsx:142 msgid "Password Changed" -msgstr "" +msgstr "パスワードが変更されました" #: src/view/com/auth/login/Login.tsx:157 msgid "Password updated" @@ -3507,7 +3507,7 @@ msgstr "ホームにピン留め" #: src/view/screens/ProfileFeed.tsx:294 msgid "Pin to Home" -msgstr "" +msgstr "ホームにピン留め" #: src/view/screens/SavedFeeds.tsx:88 msgid "Pinned Feeds" @@ -3536,7 +3536,7 @@ msgstr "パスワードを選択してください。" #: src/view/com/auth/create/state.ts:131 msgid "Please complete the verification captcha." -msgstr "" +msgstr "Captcha認証を完了してください。" #: src/view/com/modals/ChangeEmail.tsx:67 msgid "Please confirm your email before changing it. This is a temporary requirement while email-updating tools are added, and it will soon be removed." @@ -3556,7 +3556,7 @@ msgstr "このアプリパスワードに固有の名前を入力するか、ラ #: src/components/dialogs/MutedWords.tsx:68 msgid "Please enter a valid word, tag, or phrase to mute" -msgstr "" +msgstr "ミュートにする有効な単語、タグ、フレーズを入力してください" #: src/view/com/auth/create/state.ts:170 #~ msgid "Please enter the code you received by SMS." @@ -3576,7 +3576,7 @@ msgstr "パスワードも入力してください:" #: src/components/moderation/LabelsOnMeDialog.tsx:222 msgid "Please explain why you think this label was incorrectly applied by {0}" -msgstr "" +msgstr "{0}によって貼られたこのラベルが誤って適用されたと思われる理由を説明してください" #: src/view/com/modals/AppealLabel.tsx:72 #: src/view/com/modals/AppealLabel.tsx:75 @@ -3594,7 +3594,7 @@ msgstr "メールアドレスを確認してください" #: src/view/com/composer/Composer.tsx:221 msgid "Please wait for your link card to finish loading" -msgstr "リンクカードがロードされるまでお待ちください" +msgstr "リンクカードが読み込まれるまでお待ちください" #: src/screens/Onboarding/index.tsx:37 msgid "Politics" @@ -3606,7 +3606,7 @@ msgstr "ポルノ" #: src/lib/moderation/useGlobalLabelStrings.ts:34 msgid "Pornography" -msgstr "" +msgstr "ポルノグラフィ" #: src/view/com/composer/Composer.tsx:366 #: src/view/com/composer/Composer.tsx:374 @@ -3646,12 +3646,12 @@ msgstr "投稿を非表示" #: src/components/moderation/ModerationDetailsDialog.tsx:98 #: src/lib/moderation/useModerationCauseDescription.ts:99 msgid "Post Hidden by Muted Word" -msgstr "" +msgstr "ミュートしたワードによって投稿が表示されません" #: src/components/moderation/ModerationDetailsDialog.tsx:101 #: src/lib/moderation/useModerationCauseDescription.ts:108 msgid "Post Hidden by You" -msgstr "" +msgstr "あなたが非表示にした投稿" #: src/view/com/composer/select-language/SelectLangBtn.tsx:87 msgid "Post language" @@ -3668,7 +3668,7 @@ msgstr "投稿が見つかりません" #: src/components/TagMenu/index.tsx:253 msgid "posts" -msgstr "" +msgstr "投稿" #: src/view/screens/Profile.tsx:188 msgid "Posts" @@ -3676,7 +3676,7 @@ msgstr "投稿" #: src/components/dialogs/MutedWords.tsx:90 msgid "Posts can be muted based on their text, their tags, or both." -msgstr "" +msgstr "投稿はテキスト、タグ、またはその両方に基づいてミュートできます。" #: src/view/com/posts/FeedErrorMessage.tsx:64 msgid "Posts hidden" @@ -3688,7 +3688,7 @@ msgstr "誤解を招く可能性のあるリンク" #: src/components/Lists.tsx:88 msgid "Press to retry" -msgstr "" +msgstr "再実行する" #: src/view/com/lightbox/Lightbox.web.tsx:150 msgid "Previous image" @@ -3722,7 +3722,7 @@ msgstr "処理中..." #: src/view/screens/DebugMod.tsx:888 #: src/view/screens/Profile.tsx:340 msgid "profile" -msgstr "" +msgstr "プロフィール" #: src/view/shell/bottom-bar/BottomBar.tsx:251 #: src/view/shell/desktop/LeftNav.tsx:419 @@ -3788,7 +3788,7 @@ msgstr "比率" #: src/view/screens/Search/Search.tsx:776 msgid "Recent Searches" -msgstr "" +msgstr "検索履歴" #: src/view/com/auth/onboarding/RecommendedFeeds.tsx:116 msgid "Recommended Feeds" @@ -3817,11 +3817,11 @@ msgstr "アカウントを削除" #: src/view/com/util/UserAvatar.tsx:358 msgid "Remove Avatar" -msgstr "" +msgstr "アバターを削除" #: src/view/com/util/UserBanner.tsx:148 msgid "Remove Banner" -msgstr "" +msgstr "バナーを削除" #: src/view/com/posts/FeedErrorMessage.tsx:160 msgid "Remove feed" @@ -3829,7 +3829,7 @@ msgstr "フィードを削除" #: src/view/com/posts/FeedErrorMessage.tsx:201 msgid "Remove feed?" -msgstr "" +msgstr "フィードを削除しますか?" #: src/view/com/feeds/FeedSourceCard.tsx:173 #: src/view/com/feeds/FeedSourceCard.tsx:233 @@ -3840,7 +3840,7 @@ msgstr "マイフィードから削除" #: src/view/com/feeds/FeedSourceCard.tsx:278 msgid "Remove from my feeds?" -msgstr "" +msgstr "マイフィードから削除しますか?" #: src/view/com/composer/photos/Gallery.tsx:167 msgid "Remove image" @@ -3852,7 +3852,7 @@ msgstr "イメージプレビューを削除" #: src/components/dialogs/MutedWords.tsx:330 msgid "Remove mute word from your list" -msgstr "" +msgstr "リストからミュートワードを削除" #: src/view/com/modals/Repost.tsx:47 msgid "Remove repost" @@ -3864,7 +3864,7 @@ msgstr "リポストを削除" #: src/view/com/posts/FeedErrorMessage.tsx:202 msgid "Remove this feed from your saved feeds" -msgstr "" +msgstr "保存したフィードからこのフィードを削除" #: src/view/com/posts/FeedErrorMessage.tsx:132 #~ msgid "Remove this feed from your saved feeds?" @@ -3881,7 +3881,7 @@ msgstr "フィードから削除しました" #: src/view/screens/ProfileFeed.tsx:208 msgid "Removed from your feeds" -msgstr "" +msgstr "あなたのフィードから削除しました" #: src/view/com/composer/ExternalEmbed.tsx:71 msgid "Removes default thumbnail from {0}" @@ -3935,23 +3935,23 @@ msgstr "投稿を報告" #: src/components/ReportDialog/SelectReportOptionView.tsx:43 msgid "Report this content" -msgstr "" +msgstr "このコンテンツを報告" #: src/components/ReportDialog/SelectReportOptionView.tsx:56 msgid "Report this feed" -msgstr "" +msgstr "このフィードを報告" #: src/components/ReportDialog/SelectReportOptionView.tsx:53 msgid "Report this list" -msgstr "" +msgstr "このリストを報告" #: src/components/ReportDialog/SelectReportOptionView.tsx:50 msgid "Report this post" -msgstr "" +msgstr "この投稿を報告" #: src/components/ReportDialog/SelectReportOptionView.tsx:47 msgid "Report this user" -msgstr "" +msgstr "このユーザーを報告" #: src/view/com/modals/Repost.tsx:43 #: src/view/com/modals/Repost.tsx:48 @@ -4091,12 +4091,12 @@ msgstr "前のページに戻る" #: src/view/screens/NotFound.tsx:59 msgid "Returns to home page" -msgstr "" +msgstr "ホームページに戻る" #: src/view/screens/NotFound.tsx:58 #: src/view/screens/ProfileFeed.tsx:112 msgid "Returns to previous page" -msgstr "" +msgstr "前のページに戻る" #: src/view/shell/desktop/RightNav.tsx:55 #~ msgid "SANDBOX. Posts and accounts are not permanent." @@ -4121,7 +4121,7 @@ msgstr "ALTテキストを保存" #: src/components/dialogs/BirthDateSettings.tsx:119 msgid "Save birthday" -msgstr "" +msgstr "誕生日を保存" #: src/view/com/modals/EditProfile.tsx:232 msgid "Save Changes" @@ -4138,7 +4138,7 @@ msgstr "画像の切り抜きを保存" #: src/view/screens/ProfileFeed.tsx:335 #: src/view/screens/ProfileFeed.tsx:341 msgid "Save to my feeds" -msgstr "" +msgstr "マイフィードに保存" #: src/view/screens/SavedFeeds.tsx:122 msgid "Saved Feeds" @@ -4146,11 +4146,11 @@ msgstr "保存されたフィード" #: src/view/com/lightbox/Lightbox.tsx:81 msgid "Saved to your camera roll." -msgstr "" +msgstr "カメラロールに保存しました。" #: src/view/screens/ProfileFeed.tsx:212 msgid "Saved to your feeds" -msgstr "" +msgstr "フィードを保存しました" #: src/view/com/modals/EditProfile.tsx:225 msgid "Saves any changes to your profile" @@ -4162,7 +4162,7 @@ msgstr "{handle}へのハンドルの変更を保存" #: src/view/com/modals/crop-image/CropImage.web.tsx:145 msgid "Saves image crop settings" -msgstr "" +msgstr "画像の切り抜き設定を保存" #: src/screens/Onboarding/index.tsx:36 msgid "Science" @@ -4196,19 +4196,19 @@ msgstr "「{query}」を検索" #: src/components/TagMenu/index.tsx:145 msgid "Search for all posts by @{authorHandle} with tag {displayTag}" -msgstr "" +msgstr "{displayTag}のすべての投稿を検索(@{authorHandle}のみ)" #: src/components/TagMenu/index.tsx:145 #~ msgid "Search for all posts by @{authorHandle} with tag {tag}" -#~ msgstr "" +#~ msgstr "{tag}のすべての投稿を検索(@{authorHandle}のみ)" #: src/components/TagMenu/index.tsx:94 msgid "Search for all posts with tag {displayTag}" -msgstr "" +msgstr "{displayTag}のすべての投稿を検索(すべてのユーザー)" #: src/components/TagMenu/index.tsx:90 #~ msgid "Search for all posts with tag {tag}" -#~ msgstr "" +#~ msgstr "{tag}のすべての投稿を検索(すべてのユーザー)" #: src/view/screens/Search/Search.tsx:390 #~ msgid "Search for posts and users." @@ -4226,27 +4226,27 @@ msgstr "必要なセキュリティの手順" #: src/components/TagMenu/index.web.tsx:66 msgid "See {truncatedTag} posts" -msgstr "" +msgstr "{truncatedTag}の投稿を表示(すべてのユーザー)" #: src/components/TagMenu/index.web.tsx:83 msgid "See {truncatedTag} posts by user" -msgstr "" +msgstr "{truncatedTag}の投稿を表示(このユーザーのみ)" #: src/components/TagMenu/index.tsx:128 msgid "See <0>{displayTag} posts" -msgstr "" +msgstr "<0>{displayTag}の投稿を表示(すべてのユーザー)" #: src/components/TagMenu/index.tsx:187 msgid "See <0>{displayTag} posts by this user" -msgstr "" +msgstr "<0>{displayTag}の投稿を表示(このユーザーのみ)" #: src/components/TagMenu/index.tsx:128 #~ msgid "See <0>{tag} posts" -#~ msgstr "" +#~ msgstr "<0>{tag}の投稿を表示(すべてのユーザー)" #: src/components/TagMenu/index.tsx:189 #~ msgid "See <0>{tag} posts by this user" -#~ msgstr "" +#~ msgstr "<0>{tag}の投稿を表示(このユーザーのみ)" #: src/view/screens/SavedFeeds.tsx:163 msgid "See this guide" @@ -4270,11 +4270,11 @@ msgstr "既存のアカウントから選択" #: src/view/screens/LanguageSettings.tsx:299 msgid "Select languages" -msgstr "" +msgstr "言語を選択" #: src/components/ReportDialog/SelectLabelerView.tsx:32 msgid "Select moderator" -msgstr "" +msgstr "モデレーターを選択" #: src/view/com/util/Selector.tsx:107 msgid "Select option {i} of {numItems}" @@ -4291,7 +4291,7 @@ msgstr "次のアカウントを選択してフォローしてください" #: src/components/ReportDialog/SubmitView.tsx:135 msgid "Select the moderation service(s) to report to" -msgstr "" +msgstr "報告先のモデレーションサービスを選んでください" #: src/view/com/auth/server-input/index.tsx:82 msgid "Select the service that hosts your data." @@ -4319,7 +4319,7 @@ msgstr "登録されたフィードに含める言語を選択します。選択 #: src/view/screens/LanguageSettings.tsx:98 msgid "Select your app language for the default text to display in the app." -msgstr "" +msgstr "アプリに表示されるデフォルトのテキストの言語を選択" #: src/screens/Onboarding/StepInterests/index.tsx:196 msgid "Select your interests from the options below" @@ -4335,11 +4335,11 @@ msgstr "フィード内の翻訳に使用する言語を選択します。" #: src/screens/Onboarding/StepAlgoFeeds/index.tsx:116 msgid "Select your primary algorithmic feeds" -msgstr "1番目のフィードのアルゴリズムを選択してください" +msgstr "1番目のフィードのアルゴリズムを選択してください" #: src/screens/Onboarding/StepAlgoFeeds/index.tsx:142 msgid "Select your secondary algorithmic feeds" -msgstr "2番目のフィードのアルゴリズムを選択してください" +msgstr "2番目のフィードのアルゴリズムを選択してください" #: src/view/com/modals/VerifyEmail.tsx:202 #: src/view/com/modals/VerifyEmail.tsx:204 @@ -4367,7 +4367,7 @@ msgstr "フィードバックを送信" #: src/components/ReportDialog/SubmitView.tsx:214 #: src/components/ReportDialog/SubmitView.tsx:218 msgid "Send report" -msgstr "" +msgstr "報告を送信" #: src/view/com/modals/report/SendReportButton.tsx:45 #~ msgid "Send Report" @@ -4375,7 +4375,7 @@ msgstr "" #: src/components/ReportDialog/SelectLabelerView.tsx:46 msgid "Send report to {0}" -msgstr "" +msgstr "{0}に報告を送信" #: src/view/com/modals/DeleteAccount.tsx:133 msgid "Sends email with confirmation code for account deletion" @@ -4383,7 +4383,7 @@ msgstr "アカウントの削除の確認コードをメールに送信" #: src/view/com/auth/server-input/index.tsx:110 msgid "Server address" -msgstr "" +msgstr "サーバーアドレス" #: src/view/com/modals/ContentFilteringSettings.tsx:311 #~ msgid "Set {value} for {labelGroup} content moderation policy" @@ -4397,11 +4397,11 @@ msgstr "" #: src/screens/Moderation/index.tsx:306 msgid "Set birthdate" -msgstr "" +msgstr "生年月日を設定" #: src/view/screens/Settings/index.tsx:488 #~ msgid "Set color theme to dark" -#~ msgstr "カラーテーマを暗いものに設定します" +#~ msgstr "カラーテーマをダークに設定します" #: src/view/screens/Settings/index.tsx:481 #~ msgid "Set color theme to light" @@ -4413,7 +4413,7 @@ msgstr "" #: src/view/screens/Settings/index.tsx:514 #~ msgid "Set dark theme to the dark theme" -#~ msgstr "ダークテーマをダークに設定します" +#~ msgstr "ダークテーマを暗いものに設定します" #: src/view/screens/Settings/index.tsx:507 #~ msgid "Set dark theme to the dim theme" @@ -4449,7 +4449,7 @@ msgstr "スレッド表示で返信を表示するには、この設定を「は #: src/view/screens/PreferencesFollowingFeed.tsx:261 msgid "Set this setting to \"Yes\" to show samples of your saved feeds in your Following feed. This is an experimental feature." -msgstr "" +msgstr "保存されたフィードから投稿を抽出してFollowingフィードに表示するには、この設定を「はい」にします。これは実験的な機能です。" #: src/screens/Onboarding/Layout.tsx:50 msgid "Set up your account" @@ -4461,23 +4461,23 @@ msgstr "Blueskyのユーザーネームを設定" #: src/view/screens/Settings/index.tsx:507 msgid "Sets color theme to dark" -msgstr "" +msgstr "カラーテーマをダークに設定します" #: src/view/screens/Settings/index.tsx:500 msgid "Sets color theme to light" -msgstr "" +msgstr "カラーテーマをライトに設定します" #: src/view/screens/Settings/index.tsx:494 msgid "Sets color theme to system setting" -msgstr "" +msgstr "デバイスで設定したカラーテーマを使用するように設定します" #: src/view/screens/Settings/index.tsx:533 msgid "Sets dark theme to the dark theme" -msgstr "" +msgstr "ダークテーマを暗いものに設定します" #: src/view/screens/Settings/index.tsx:526 msgid "Sets dark theme to the dim theme" -msgstr "" +msgstr "ダークテーマを薄暗いものに設定します" #: src/view/com/auth/login/ForgotPasswordForm.tsx:157 msgid "Sets email for password reset" @@ -4493,15 +4493,15 @@ msgstr "パスワードをリセットするためのホスティングプロバ #: src/view/com/modals/crop-image/CropImage.web.tsx:123 msgid "Sets image aspect ratio to square" -msgstr "" +msgstr "画像のアスペクト比を正方形に設定" #: src/view/com/modals/crop-image/CropImage.web.tsx:113 msgid "Sets image aspect ratio to tall" -msgstr "" +msgstr "画像のアスペクト比を縦長に設定" #: src/view/com/modals/crop-image/CropImage.web.tsx:103 msgid "Sets image aspect ratio to wide" -msgstr "" +msgstr "画像のアスペクト比をワイドに設定" #: src/view/com/auth/create/Step1.tsx:97 #: src/view/com/auth/login/LoginForm.tsx:154 @@ -4522,7 +4522,7 @@ msgstr "性的行為または性的なヌード。" #: src/lib/moderation/useGlobalLabelStrings.ts:38 msgid "Sexually Suggestive" -msgstr "" +msgstr "性的にきわどい" #: src/view/com/lightbox/Lightbox.tsx:141 msgctxt "action" @@ -4541,7 +4541,7 @@ msgstr "共有" #: src/view/com/profile/ProfileMenu.tsx:373 #: src/view/com/util/forms/PostDropdownBtn.tsx:347 msgid "Share anyway" -msgstr "" +msgstr "とにかく共有" #: src/view/screens/ProfileFeed.tsx:361 #: src/view/screens/ProfileFeed.tsx:363 @@ -4568,11 +4568,11 @@ msgstr "とにかく表示" #: src/lib/moderation/useLabelBehaviorDescription.ts:27 #: src/lib/moderation/useLabelBehaviorDescription.ts:63 msgid "Show badge" -msgstr "" +msgstr "バッジを表示" #: src/lib/moderation/useLabelBehaviorDescription.ts:61 msgid "Show badge and filter from feeds" -msgstr "" +msgstr "バッジの表示とフィードからのフィルタリング" #: src/view/com/modals/EmbedConsent.tsx:87 msgid "Show embeds from {0}" @@ -4647,11 +4647,11 @@ msgstr "ユーザーを表示" #: src/lib/moderation/useLabelBehaviorDescription.ts:58 msgid "Show warning" -msgstr "" +msgstr "警告を表示" #: src/lib/moderation/useLabelBehaviorDescription.ts:56 msgid "Show warning and filter from feeds" -msgstr "" +msgstr "警告の表示とフィードからのフィルタリング" #: src/view/com/profile/ProfileHeader.tsx:462 #~ msgid "Shows a list of users similar to this user." @@ -4755,17 +4755,17 @@ msgstr "ソフトウェア開発" #: src/view/com/modals/ProfilePreview.tsx:62 #~ msgid "Something went wrong and we're not sure what." -#~ msgstr "何かの問題が起きましたが、それが何なのかわかりません。" +#~ msgstr "何かの問題が起きましたが、それがなんなのかわかりません。" #: src/components/ReportDialog/index.tsx:52 #: src/screens/Moderation/index.tsx:116 #: src/screens/Profile/Sections/Labels.tsx:77 msgid "Something went wrong, please try again." -msgstr "" +msgstr "なにか間違っているようなので、もう一度お試しください。" #: src/components/Lists.tsx:203 #~ msgid "Something went wrong!" -#~ msgstr "" +#~ msgstr "なにかが間違っているようです!" #: src/view/com/modals/Waitlist.tsx:51 #~ msgid "Something went wrong. Check your email and try again." @@ -4773,7 +4773,7 @@ msgstr "" #: src/App.native.tsx:71 msgid "Sorry! Your session expired. Please log in again." -msgstr "申し訳ありません!セッションの有効期限が切れました。もう一度ログインしてください。" +msgstr "大変申し訳ありません!セッションの有効期限が切れました。もう一度ログインしてください。" #: src/view/screens/PreferencesThreads.tsx:69 msgid "Sort Replies" @@ -4785,15 +4785,15 @@ msgstr "次の方法で同じ投稿への返信を並び替えます。" #: src/components/moderation/LabelsOnMeDialog.tsx:147 msgid "Source:" -msgstr "" +msgstr "ソース:" #: src/lib/moderation/useReportOptions.ts:65 msgid "Spam" -msgstr "" +msgstr "スパム" #: src/lib/moderation/useReportOptions.ts:53 msgid "Spam; excessive mentions or replies" -msgstr "" +msgstr "スパム、過剰なメンションや返信" #: src/screens/Onboarding/index.tsx:30 msgid "Sports" @@ -4839,20 +4839,20 @@ msgstr "登録" #: src/screens/Profile/Sections/Labels.tsx:181 msgid "Subscribe to @{0} to use these labels:" -msgstr "" +msgstr "これらのラベルを使用するには@{0}を登録してください:" #: src/screens/Profile/Header/ProfileHeaderLabeler.tsx:222 msgid "Subscribe to Labeler" -msgstr "" +msgstr "ラベラーを登録する" #: src/screens/Onboarding/StepAlgoFeeds/FeedCard.tsx:173 #: src/screens/Onboarding/StepAlgoFeeds/FeedCard.tsx:308 msgid "Subscribe to the {0} feed" -msgstr "「{0}」フィードを登録" +msgstr "{0} フィードを登録" #: src/screens/Profile/Header/ProfileHeaderLabeler.tsx:185 msgid "Subscribe to this labeler" -msgstr "" +msgstr "このラベラーを登録" #: src/view/screens/ProfileList.tsx:586 msgid "Subscribe to this list" @@ -4908,15 +4908,15 @@ msgstr "システムログ" #: src/components/dialogs/MutedWords.tsx:324 msgid "tag" -msgstr "" +msgstr "タグ" #: src/components/TagMenu/index.tsx:78 msgid "Tag menu: {displayTag}" -msgstr "" +msgstr "タグメニュー:{displayTag}" #: src/components/TagMenu/index.tsx:74 #~ msgid "Tag menu: {tag}" -#~ msgstr "" +#~ msgstr "タグメニュー:{tag}" #: src/view/com/modals/crop-image/CropImage.web.tsx:112 msgid "Tall" @@ -4946,11 +4946,11 @@ msgstr "利用規約" #: src/lib/moderation/useReportOptions.ts:79 #: src/lib/moderation/useReportOptions.ts:87 msgid "Terms used violate community standards" -msgstr "" +msgstr "使用されている用語がコミュニティ基準に違反している" #: src/components/dialogs/MutedWords.tsx:324 msgid "text" -msgstr "" +msgstr "テキスト" #: src/components/moderation/LabelsOnMeDialog.tsx:220 msgid "Text input field" @@ -4958,15 +4958,15 @@ msgstr "テキストの入力フィールド" #: src/components/ReportDialog/SubmitView.tsx:78 msgid "Thank you. Your report has been sent." -msgstr "" +msgstr "ありがとうございます。あなたの報告は送信されました。" #: src/view/com/modals/ChangeHandle.tsx:466 msgid "That contains the following:" -msgstr "" +msgstr "その内容は以下の通りです:" #: src/view/com/auth/create/CreateAccount.tsx:94 msgid "That handle is already taken." -msgstr "" +msgstr "そのハンドルはすでに使用されています。" #: src/screens/Profile/Header/ProfileHeaderStandard.tsx:274 #: src/view/com/profile/ProfileMenu.tsx:349 @@ -4975,7 +4975,7 @@ msgstr "このアカウントは、ブロック解除後にあなたとやり取 #: src/components/moderation/ModerationDetailsDialog.tsx:128 msgid "the author" -msgstr "" +msgstr "投稿者" #: src/view/screens/CommunityGuidelines.tsx:36 msgid "The Community Guidelines have been moved to <0/>" @@ -4987,11 +4987,11 @@ msgstr "著作権ポリシーは<0/>に移動しました" #: src/components/moderation/LabelsOnMeDialog.tsx:49 msgid "The following labels were applied to your account." -msgstr "" +msgstr "以下のラベルがあなたのアカウントに適用されました。" #: src/components/moderation/LabelsOnMeDialog.tsx:50 msgid "The following labels were applied to your content." -msgstr "" +msgstr "以下のラベルがあなたのコンテンツに適用されました。" #: src/screens/Onboarding/Layout.tsx:60 msgid "The following steps will help customize your Bluesky experience." @@ -5008,11 +5008,11 @@ msgstr "プライバシーポリシーは<0/>に移動しました" #: src/view/screens/Support.tsx:36 msgid "The support form has been moved. If you need help, please <0/> or visit {HELP_DESK_URL} to get in touch with us." -msgstr "サポートフォームは移動しました。サポートが必要な場合は、<0/>、または{HELP_DESK_URL}にアクセスしてご連絡ください。" +msgstr "サポートフォームは移動しました。サポートが必要な場合は、<0/>または{HELP_DESK_URL}にアクセスしてご連絡ください。" #: src/view/screens/Support.tsx:36 #~ msgid "The support form has been moved. If you need help, please<0/> or visit {HELP_DESK_URL} to get in touch with us." -#~ msgstr "サポートフォームは移動しました。サポートが必要な場合は、<0/>、または{HELP_DESK_URL}にアクセスしてご連絡ください。" +#~ msgstr "サポートフォームは移動しました。サポートが必要な場合は、<0/>または{HELP_DESK_URL}にアクセスしてご連絡ください。" #: src/view/screens/TermsOfService.tsx:33 msgid "The Terms of Service have been moved to" @@ -5069,7 +5069,7 @@ msgstr "リストの取得中に問題が発生しました。もう一度試す #: src/components/ReportDialog/SubmitView.tsx:83 msgid "There was an issue sending your report. Please check your internet connection." -msgstr "" +msgstr "報告の送信に問題が発生しました。インターネットの接続を確認してください。" #: src/screens/Onboarding/StepModeration/AdultContentEnabledPref.tsx:65 msgid "There was an issue syncing your preferences with the server" @@ -5091,7 +5091,7 @@ msgstr "アプリパスワードの取得中に問題が発生しました" #: src/view/com/profile/ProfileMenu.tsx:157 #: src/view/com/profile/ProfileMenu.tsx:170 msgid "There was an issue! {0}" -msgstr "問題が発生しました!{0}" +msgstr "問題が発生しました! {0}" #: src/view/screens/ProfileList.tsx:288 #: src/view/screens/ProfileList.tsx:302 @@ -5134,15 +5134,15 @@ msgstr "このアカウントを閲覧するためにはサインインが必要 #: src/components/moderation/LabelsOnMeDialog.tsx:205 msgid "This appeal will be sent to <0>{0}." -msgstr "" +msgstr "この申し立ては<0>{0}に送られます。" #: src/lib/moderation/useGlobalLabelStrings.ts:19 msgid "This content has been hidden by the moderators." -msgstr "" +msgstr "このコンテンツはモデレーターによって非表示になっています。" #: src/lib/moderation/useGlobalLabelStrings.ts:24 msgid "This content has received a general warning from moderators." -msgstr "" +msgstr "このコンテンツはモデレーターから一般的な警告を受けています。" #: src/view/com/modals/EmbedConsent.tsx:68 msgid "This content is hosted by {0}. Do you want to enable external media?" @@ -5159,11 +5159,11 @@ msgstr "このコンテンツはBlueskyのアカウントがないと閲覧で #: src/view/screens/Settings/ExportCarDialog.tsx:75 #~ msgid "This feature is in beta. You can read more about repository exports in <0>this blogpost." -#~ msgstr "この機能はベータ版です。 リポジトリのエクスポートの詳細については、以下を参照してください。<0>このブログ投稿" +#~ msgstr "この機能はベータ版です。リポジトリのエクスポートの詳細については、<0>このブログ投稿を参照してください。" #: src/view/screens/Settings/ExportCarDialog.tsx:75 msgid "This feature is in beta. You can read more about repository exports in <0>this blogpost." -msgstr "" +msgstr "この機能はベータ版です。リポジトリのエクスポートの詳細については、<0>このブログ投稿を参照してください。" #: src/view/com/posts/FeedErrorMessage.tsx:114 msgid "This feed is currently receiving high traffic and is temporarily unavailable. Please try again later." @@ -5193,11 +5193,11 @@ msgstr "これは、メールアドレスの変更やパスワードのリセッ #: src/components/moderation/ModerationDetailsDialog.tsx:125 msgid "This label was applied by {0}." -msgstr "" +msgstr "{0}によって適用されたラベルです。" #: src/screens/Profile/Sections/Labels.tsx:168 msgid "This labeler hasn't declared what labels it publishes, and may not be active." -msgstr "" +msgstr "このラベラーはどのようなラベルを発行しているか宣言しておらず、活動していない可能性もあります。" #: src/view/com/modals/LinkWarning.tsx:58 msgid "This link is taking you to the following website:" @@ -5209,7 +5209,7 @@ msgstr "このリストは空です!" #: src/screens/Profile/ErrorState.tsx:40 msgid "This moderation service is unavailable. See below for more details. If this issue persists, contact us." -msgstr "" +msgstr "このモデレーションのサービスはご利用できません。詳細は以下をご覧ください。この問題が解決しない場合は、サポートへお問い合わせください。" #: src/view/com/modals/AddAppPasswords.tsx:106 msgid "This name is already in use" @@ -5221,27 +5221,27 @@ msgstr "この投稿は削除されました。" #: src/view/com/util/forms/PostDropdownBtn.tsx:344 msgid "This post is only visible to logged-in users. It won't be visible to people who aren't logged in." -msgstr "" +msgstr "この投稿はログインしているユーザーにのみ表示されます。ログインしていない方には見えません。" #: src/view/com/util/forms/PostDropdownBtn.tsx:326 msgid "This post will be hidden from feeds." -msgstr "" +msgstr "この投稿はフィードから非表示になります。" #: src/view/com/profile/ProfileMenu.tsx:370 msgid "This profile is only visible to logged-in users. It won't be visible to people who aren't logged in." -msgstr "" +msgstr "このプロフィールはログインしているユーザーにのみ表示されます。ログインしていない方には見えません。" #: src/view/com/auth/create/Policies.tsx:46 msgid "This service has not provided terms of service or a privacy policy." -msgstr "" +msgstr "このサービスには、利用規約もプライバシーポリシーもありません。" #: src/view/com/modals/ChangeHandle.tsx:446 msgid "This should create a domain record at:" -msgstr "" +msgstr "右記にドメインレコードを作成されるはずです:" #: src/view/com/profile/ProfileFollowers.tsx:95 msgid "This user doesn't have any followers." -msgstr "" +msgstr "このユーザーにはフォロワーがいません。" #: src/components/moderation/ModerationDetailsDialog.tsx:73 #: src/lib/moderation/useModerationCauseDescription.ts:68 @@ -5250,7 +5250,7 @@ msgstr "このユーザーはあなたをブロックしているため、あな #: src/lib/moderation/useGlobalLabelStrings.ts:30 msgid "This user has requested that their content only be shown to signed-in users." -msgstr "" +msgstr "このユーザーは自分のコンテンツをサインインしたユーザーにのみ表示するように求めています。" #: src/view/com/modals/ModerationDetails.tsx:42 #~ msgid "This user is included in the <0/> list which you have blocked." @@ -5262,11 +5262,11 @@ msgstr "" #: src/components/moderation/ModerationDetailsDialog.tsx:56 msgid "This user is included in the <0>{0} list which you have blocked." -msgstr "" +msgstr "このユーザーはブロックした<0>{0}リストに含まれています。" #: src/components/moderation/ModerationDetailsDialog.tsx:85 msgid "This user is included in the <0>{0} list which you have muted." -msgstr "" +msgstr "このユーザーはミュートした<0>{0}リストに含まれています。" #: src/view/com/modals/ModerationDetails.tsx:74 #~ msgid "This user is included the <0/> list which you have muted." @@ -5274,7 +5274,7 @@ msgstr "" #: src/view/com/profile/ProfileFollows.tsx:94 msgid "This user isn't following anyone." -msgstr "" +msgstr "このユーザーは誰もフォローしていません。" #: src/view/com/modals/SelfLabel.tsx:137 msgid "This warning is only available for posts with media attached." @@ -5282,7 +5282,7 @@ msgstr "この警告は、メディアが添付されている投稿にのみ使 #: src/components/dialogs/MutedWords.tsx:284 msgid "This will delete {0} from your muted words. You can always add it back later." -msgstr "" +msgstr "ミュートしたワードから{0}が削除されます。あとでいつでも戻すことができます。" #: src/view/com/util/forms/PostDropdownBtn.tsx:282 #~ msgid "This will hide this post from your feeds." @@ -5290,7 +5290,7 @@ msgstr "" #: src/view/screens/Settings/index.tsx:574 msgid "Thread preferences" -msgstr "" +msgstr "スレッドの設定" #: src/view/screens/PreferencesThreads.tsx:53 #: src/view/screens/Settings/index.tsx:584 @@ -5307,11 +5307,11 @@ msgstr "スレッドの設定" #: src/components/ReportDialog/SelectLabelerView.tsx:35 msgid "To whom would you like to send this report?" -msgstr "" +msgstr "この報告を誰に送りたいですか?" #: src/components/dialogs/MutedWords.tsx:113 msgid "Toggle between muted word options." -msgstr "" +msgstr "ミュートしたワードのオプションを切り替えます。" #: src/view/com/util/forms/DropdownButton.tsx:246 msgid "Toggle dropdown" @@ -5319,7 +5319,7 @@ msgstr "ドロップダウンをトグル" #: src/screens/Moderation/index.tsx:334 msgid "Toggle to enable or disable adult content" -msgstr "" +msgstr "成人向けコンテンツの有効もしくは無効の切り替え" #: src/view/com/modals/EditImage.tsx:271 msgid "Transformations" @@ -5343,7 +5343,7 @@ msgstr "再試行" #: src/view/com/modals/ChangeHandle.tsx:429 msgid "Type:" -msgstr "" +msgstr "タイプ:" #: src/view/screens/ProfileList.tsx:478 msgid "Un-block list" @@ -5381,7 +5381,7 @@ msgstr "アカウントのブロックを解除" #: src/screens/Profile/Header/ProfileHeaderStandard.tsx:272 #: src/view/com/profile/ProfileMenu.tsx:343 msgid "Unblock Account?" -msgstr "" +msgstr "アカウントのブロックを解除しますか?" #: src/view/com/modals/Repost.tsx:42 #: src/view/com/modals/Repost.tsx:55 @@ -5393,12 +5393,12 @@ msgstr "リポストを元に戻す" #: src/view/com/auth/onboarding/RecommendedFollowsItem.tsx:141 #: src/view/com/profile/ProfileHeaderSuggestedFollows.tsx:246 msgid "Unfollow" -msgstr "" +msgstr "フォローを解除" #: src/view/com/profile/FollowButton.tsx:60 msgctxt "action" msgid "Unfollow" -msgstr "フォローをやめる" +msgstr "フォローを解除" #: src/screens/Profile/Header/ProfileHeaderStandard.tsx:213 msgid "Unfollow {0}" @@ -5407,7 +5407,7 @@ msgstr "{0}のフォローを解除" #: src/view/com/profile/ProfileMenu.tsx:241 #: src/view/com/profile/ProfileMenu.tsx:251 msgid "Unfollow Account" -msgstr "" +msgstr "アカウントのフォローを解除" #: src/view/com/auth/create/state.ts:262 msgid "Unfortunately, you do not meet the requirements to create an account." @@ -5419,7 +5419,7 @@ msgstr "いいねを外す" #: src/view/screens/ProfileFeed.tsx:572 msgid "Unlike this feed" -msgstr "" +msgstr "このフィードからいいねを外す" #: src/components/TagMenu/index.tsx:249 #: src/view/screens/ProfileList.tsx:579 @@ -5428,7 +5428,7 @@ msgstr "ミュートを解除" #: src/components/TagMenu/index.web.tsx:104 msgid "Unmute {truncatedTag}" -msgstr "" +msgstr "{truncatedTag}のミュートを解除" #: src/view/com/profile/ProfileMenu.tsx:278 #: src/view/com/profile/ProfileMenu.tsx:284 @@ -5437,11 +5437,11 @@ msgstr "アカウントのミュートを解除" #: src/components/TagMenu/index.tsx:208 msgid "Unmute all {displayTag} posts" -msgstr "" +msgstr "{displayTag}のすべての投稿のミュートを解除" #: src/components/TagMenu/index.tsx:210 #~ msgid "Unmute all {tag} posts" -#~ msgstr "" +#~ msgstr "{tag}のすべての投稿のミュートを解除" #: src/view/com/util/forms/PostDropdownBtn.tsx:251 #: src/view/com/util/forms/PostDropdownBtn.tsx:256 @@ -5455,7 +5455,7 @@ msgstr "ピン留めを解除" #: src/view/screens/ProfileFeed.tsx:291 msgid "Unpin from home" -msgstr "" +msgstr "ホームからピン留めを解除" #: src/view/screens/ProfileList.tsx:444 msgid "Unpin moderation list" @@ -5467,15 +5467,15 @@ msgstr "モデレーションリストのピン留めを解除" #: src/screens/Profile/Header/ProfileHeaderLabeler.tsx:220 msgid "Unsubscribe" -msgstr "" +msgstr "登録を解除" #: src/screens/Profile/Header/ProfileHeaderLabeler.tsx:184 msgid "Unsubscribe from this labeler" -msgstr "" +msgstr "このラベラーの登録を解除" #: src/lib/moderation/useReportOptions.ts:70 msgid "Unwanted Sexual Content" -msgstr "" +msgstr "望まない性的なコンテンツ" #: src/view/com/modals/UserAddRemoveLists.tsx:70 msgid "Update {displayName} in Lists" @@ -5487,7 +5487,7 @@ msgstr "リストの{displayName}を更新" #: src/view/com/modals/ChangeHandle.tsx:509 msgid "Update to {handle}" -msgstr "" +msgstr "{handle}に更新" #: src/view/com/auth/login/SetNewPasswordForm.tsx:204 msgid "Updating..." @@ -5502,23 +5502,23 @@ msgstr "テキストファイルのアップロード先:" #: src/view/com/util/UserBanner.tsx:116 #: src/view/com/util/UserBanner.tsx:119 msgid "Upload from Camera" -msgstr "" +msgstr "カメラからアップロード" #: src/view/com/util/UserAvatar.tsx:343 #: src/view/com/util/UserBanner.tsx:133 msgid "Upload from Files" -msgstr "" +msgstr "ファイルからアップロード" #: src/view/com/util/UserAvatar.tsx:337 #: src/view/com/util/UserAvatar.tsx:341 #: src/view/com/util/UserBanner.tsx:127 #: src/view/com/util/UserBanner.tsx:131 msgid "Upload from Library" -msgstr "" +msgstr "ライブラリーからアップロード" #: src/view/com/modals/ChangeHandle.tsx:409 msgid "Use a file on your server" -msgstr "" +msgstr "あなたのサーバーのファイルを使用" #: src/view/screens/AppPasswords.tsx:197 msgid "Use app passwords to login to other Bluesky clients without giving full access to your account or password." @@ -5526,7 +5526,7 @@ msgstr "他のBlueskyクライアントにアカウントやパスワードに #: src/view/com/modals/ChangeHandle.tsx:518 msgid "Use bsky.social as hosting provider" -msgstr "" +msgstr "ホスティングプロバイダーとしてbsky.socialを使用" #: src/view/com/modals/ChangeHandle.tsx:517 msgid "Use default provider" @@ -5544,7 +5544,7 @@ msgstr "デフォルトのブラウザーを使用" #: src/view/com/modals/ChangeHandle.tsx:401 msgid "Use the DNS panel" -msgstr "" +msgstr "DNSパネルを使用" #: src/view/com/modals/AddAppPasswords.tsx:155 msgid "Use this to sign into the other app along with your handle." @@ -5565,7 +5565,7 @@ msgstr "ブロック中のユーザー" #: src/lib/moderation/useModerationCauseDescription.ts:48 msgid "User Blocked by \"{0}\"" -msgstr "" +msgstr "「{0}」によってブロックされたユーザー" #: src/components/moderation/ModerationDetailsDialog.tsx:54 msgid "User Blocked by List" @@ -5573,7 +5573,7 @@ msgstr "リストによってブロック中のユーザー" #: src/lib/moderation/useModerationCauseDescription.ts:66 msgid "User Blocking You" -msgstr "" +msgstr "あなたがブロック中のユーザー" #: src/components/moderation/ModerationDetailsDialog.tsx:71 msgid "User Blocks You" @@ -5629,11 +5629,11 @@ msgstr "{0}のユーザー" #: src/components/LikesDialog.tsx:85 msgid "Users that have liked this content or profile" -msgstr "" +msgstr "このコンテンツやプロフィールにいいねをしているユーザー" #: src/view/com/modals/ChangeHandle.tsx:437 msgid "Value:" -msgstr "" +msgstr "値:" #: src/view/com/auth/create/Step2.tsx:243 #~ msgid "Verification code" @@ -5641,7 +5641,7 @@ msgstr "" #: src/view/com/modals/ChangeHandle.tsx:510 msgid "Verify {0}" -msgstr "" +msgstr "{0}で認証" #: src/view/screens/Settings/index.tsx:944 msgid "Verify email" @@ -5678,11 +5678,11 @@ msgstr "デバッグエントリーを表示" #: src/components/ReportDialog/SelectReportOptionView.tsx:133 msgid "View details" -msgstr "" +msgstr "詳細を表示" #: src/components/ReportDialog/SelectReportOptionView.tsx:128 msgid "View details for reporting a copyright violation" -msgstr "" +msgstr "著作権侵害の報告の詳細を見る" #: src/view/com/posts/FeedSlice.tsx:99 msgid "View full thread" @@ -5690,7 +5690,7 @@ msgstr "スレッドをすべて表示" #: src/components/moderation/LabelsOnMe.tsx:51 msgid "View information about these labels" -msgstr "" +msgstr "これらのラベルに関する情報を見る" #: src/view/com/posts/FeedErrorMessage.tsx:166 msgid "View profile" @@ -5702,11 +5702,11 @@ msgstr "アバターを表示" #: src/components/LabelingServiceCard/index.tsx:140 msgid "View the labeling service provided by @{0}" -msgstr "" +msgstr "@{0}によって提供されるラベリングサービスを見る" #: src/view/screens/ProfileFeed.tsx:584 msgid "View users who like this feed" -msgstr "" +msgstr "このフィードにいいねしたユーザーを見る" #: src/view/com/modals/LinkWarning.tsx:75 #: src/view/com/modals/LinkWarning.tsx:77 @@ -5722,11 +5722,11 @@ msgstr "警告" #: src/lib/moderation/useLabelBehaviorDescription.ts:48 msgid "Warn content" -msgstr "" +msgstr "コンテンツの警告" #: src/lib/moderation/useLabelBehaviorDescription.ts:46 msgid "Warn content and filter from feeds" -msgstr "" +msgstr "コンテンツの警告とフィードからのフィルタリング" #: src/screens/Onboarding/StepAlgoFeeds/index.tsx:134 msgid "We also think you'll like \"For You\" by Skygaze:" @@ -5734,7 +5734,7 @@ msgstr "Skygazeによる「For You」フィードもおすすめ:" #: src/screens/Hashtag.tsx:132 msgid "We couldn't find any results for that hashtag." -msgstr "" +msgstr "そのハッシュタグの検索結果は見つかりませんでした。" #: src/screens/Deactivated.tsx:133 msgid "We estimate {estimatedTime} until your account is ready." @@ -5742,7 +5742,7 @@ msgstr "あなたのアカウントが準備できるまで{estimatedTime}ほど #: src/screens/Onboarding/StepFinished.tsx:93 msgid "We hope you have a wonderful time. Remember, Bluesky is:" -msgstr "素敵なひとときをお過ごしください。 覚えておいてください、Blueskyは:" +msgstr "素敵なひとときをお過ごしください。覚えておいてください、Blueskyは:" #: src/view/com/posts/DiscoverFallbackHeader.tsx:29 #~ msgid "We ran out of posts from your follows. Here's the latest from" @@ -5758,7 +5758,7 @@ msgstr "あなたのフォロー中のユーザーの投稿を読み終わりま #: src/components/dialogs/MutedWords.tsx:204 msgid "We recommend avoiding common words that appear in many posts, since it can result in no posts being shown." -msgstr "" +msgstr "投稿が表示されなくなる可能性があるため、多くの投稿に使われる一般的なワードは避けることをおすすめします。" #: src/screens/Onboarding/StepAlgoFeeds/index.tsx:124 msgid "We recommend our \"Discover\" feed:" @@ -5766,11 +5766,11 @@ msgstr "我々の「Discover」フィードがおすすめ:" #: src/components/dialogs/BirthDateSettings.tsx:52 msgid "We were unable to load your birth date preferences. Please try again." -msgstr "" +msgstr "生年月日の設定を読み込むことはできませんでした。もう一度お試しください。" #: src/screens/Moderation/index.tsx:387 msgid "We were unable to load your configured labelers at this time." -msgstr "" +msgstr "現在設定されたラベラーを読み込めません。" #: src/screens/Onboarding/StepInterests/index.tsx:133 msgid "We weren't able to connect. Please try again to continue setting up your account. If it continues to fail, you can skip this flow." @@ -5798,7 +5798,7 @@ msgstr "大変申し訳ありませんが、このリストを解決できませ #: src/components/dialogs/MutedWords.tsx:230 msgid "We're sorry, but we weren't able to load your muted words at this time. Please try again." -msgstr "" +msgstr "大変申し訳ありませんが、現在ミュートされたワードを読み込むことができませんでした。もう一度お試しください。" #: src/view/screens/Search/Search.tsx:255 msgid "We're sorry, but your search could not be completed. Please try again in a few minutes." @@ -5807,11 +5807,11 @@ msgstr "大変申し訳ありませんが、検索を完了できませんでし #: src/components/Lists.tsx:194 #: src/view/screens/NotFound.tsx:48 msgid "We're sorry! We can't find the page you were looking for." -msgstr "大変申し訳ありません!お探しのページが見つかりません。" +msgstr "大変申し訳ありません!お探しのページは見つかりません。" #: src/screens/Profile/Header/ProfileHeaderLabeler.tsx:319 msgid "We're sorry! You can only subscribe to ten labelers, and you've reached your limit of ten." -msgstr "" +msgstr "大変申し訳ありません!ラベラーは10までしか登録できず、すでに上限に達しています。" #: src/view/com/auth/onboarding/WelcomeMobile.tsx:48 msgid "Welcome to <0>Bluesky" @@ -5819,11 +5819,11 @@ msgstr "<0>Blueskyへようこそ" #: src/screens/Onboarding/StepInterests/index.tsx:130 msgid "What are your interests?" -msgstr "何に興味がありますか?" +msgstr "なにに興味がありますか?" #: src/view/com/modals/report/Modal.tsx:169 #~ msgid "What is the issue with this {collectionName}?" -#~ msgstr "この{collectionName}の問題は何ですか?" +#~ msgstr "この{collectionName}の問題はなんですか?" #: src/view/com/auth/SplashScreen.tsx:59 #: src/view/com/composer/Composer.tsx:295 @@ -5845,23 +5845,23 @@ msgstr "返信できるユーザー" #: src/components/ReportDialog/SelectReportOptionView.tsx:44 msgid "Why should this content be reviewed?" -msgstr "" +msgstr "なぜこのコンテンツをレビューする必要がありますか?" #: src/components/ReportDialog/SelectReportOptionView.tsx:57 msgid "Why should this feed be reviewed?" -msgstr "" +msgstr "なぜこのフィードをレビューする必要がありますか?" #: src/components/ReportDialog/SelectReportOptionView.tsx:54 msgid "Why should this list be reviewed?" -msgstr "" +msgstr "なぜこのリストをレビューする必要がありますか?" #: src/components/ReportDialog/SelectReportOptionView.tsx:51 msgid "Why should this post be reviewed?" -msgstr "" +msgstr "なぜこの投稿をレビューする必要がありますか?" #: src/components/ReportDialog/SelectReportOptionView.tsx:48 msgid "Why should this user be reviewed?" -msgstr "" +msgstr "なぜこのユーザーをレビューする必要がありますか?" #: src/view/com/modals/crop-image/CropImage.web.tsx:102 msgid "Wide" @@ -5904,7 +5904,7 @@ msgstr "あなたは並んでいます。" #: src/view/com/profile/ProfileFollows.tsx:93 msgid "You are not following anyone." -msgstr "" +msgstr "あなたはまだだれもフォローしていません。" #: src/view/com/posts/FollowingEmptyState.tsx:67 #: src/view/com/posts/FollowingEndOfFeed.tsx:68 @@ -5930,7 +5930,7 @@ msgstr "新しいパスワードでサインインできるようになりまし #: src/view/com/profile/ProfileFollowers.tsx:94 msgid "You do not have any followers." -msgstr "" +msgstr "あなたはまだだれもフォロワーがいません。" #: src/view/com/modals/InviteCodes.tsx:66 msgid "You don't have any invite codes yet! We'll send you some when you've been on Bluesky for a little longer." @@ -5967,20 +5967,20 @@ msgstr "無効なコードが入力されました。それはXXXXX-XXXXXのよ #: src/lib/moderation/useModerationCauseDescription.ts:109 msgid "You have hidden this post" -msgstr "" +msgstr "この投稿を非表示にしました" #: src/components/moderation/ModerationDetailsDialog.tsx:102 msgid "You have hidden this post." -msgstr "" +msgstr "この投稿を非表示にしました。" #: src/components/moderation/ModerationDetailsDialog.tsx:95 #: src/lib/moderation/useModerationCauseDescription.ts:92 msgid "You have muted this account." -msgstr "" +msgstr "このアカウントをミュートしました。" #: src/lib/moderation/useModerationCauseDescription.ts:86 msgid "You have muted this user" -msgstr "" +msgstr "このユーザーをミュートしました" #: src/view/com/modals/ModerationDetails.tsx:87 #~ msgid "You have muted this user." @@ -5997,7 +5997,7 @@ msgstr "リストがありません。" #: src/view/screens/ModerationBlockedAccounts.tsx:132 msgid "You have not blocked any accounts yet. To block an account, go to their profile and select \"Block account\" from the menu on their account." -msgstr "" +msgstr "ブロック中のアカウントはまだありません。アカウントをブロックするには、ユーザーのプロフィールに移動し、アカウントメニューから「アカウントをブロック」を選択します。" #: src/view/screens/ModerationBlockedAccounts.tsx:132 #~ msgid "You have not blocked any accounts yet. To block an account, go to their profile and selected \"Block account\" from the menu on their account." @@ -6009,7 +6009,7 @@ msgstr "アプリパスワードはまだ作成されていません。下のボ #: src/view/screens/ModerationMutedAccounts.tsx:131 msgid "You have not muted any accounts yet. To mute an account, go to their profile and select \"Mute account\" from the menu on their account." -msgstr "" +msgstr "ミュートしているアカウントはまだありません。アカウントをミュートするには、プロフィールに移動し、アカウントメニューから「アカウントをミュート」を選択します。" #: src/view/screens/ModerationMutedAccounts.tsx:131 #~ msgid "You have not muted any accounts yet. To mute an account, go to their profile and selected \"Mute account\" from the menu on their account." @@ -6017,11 +6017,11 @@ msgstr "" #: src/components/dialogs/MutedWords.tsx:250 msgid "You haven't muted any words or tags yet" -msgstr "" +msgstr "まだワードやタグをミュートしていません" #: src/components/moderation/LabelsOnMeDialog.tsx:69 msgid "You may appeal these labels if you feel they were placed in error." -msgstr "" +msgstr "これらのラベルが誤って貼られたと思った場合は、異議申し立てを行うことができます。" #: src/view/com/modals/ContentFilteringSettings.tsx:175 #~ msgid "You must be 18 or older to enable adult content." @@ -6033,7 +6033,7 @@ msgstr "成人向けコンテンツを有効にするには、18歳以上であ #: src/components/ReportDialog/SubmitView.tsx:205 msgid "You must select at least one labeler for a report" -msgstr "" +msgstr "報告をするには少なくとも1つのラベラーを選択する必要があります" #: src/view/com/util/forms/PostDropdownBtn.tsx:144 msgid "You will no longer receive notifications for this thread" @@ -6064,7 +6064,7 @@ msgstr "準備ができました!" #: src/components/moderation/ModerationDetailsDialog.tsx:99 #: src/lib/moderation/useModerationCauseDescription.ts:101 msgid "You've chosen to hide a word or tag within this post." -msgstr "" +msgstr "この投稿でワードまたはタグを隠すことを選択しました。" #: src/view/com/posts/FollowingEndOfFeed.tsx:48 msgid "You've reached the end of your feed! Find some more accounts to follow." @@ -6080,7 +6080,7 @@ msgstr "あなたのアカウントは削除されました" #: src/view/screens/Settings/ExportCarDialog.tsx:47 msgid "Your account repository, containing all public data records, can be downloaded as a \"CAR\" file. This file does not include media embeds, such as images, or your private data, which must be fetched separately." -msgstr "" +msgstr "あなたのアカウントの公開データの全記録を含むリポジトリは、「CAR」ファイルとしてダウンロードできます。このファイルには、画像などのメディア埋め込み、また非公開のデータは含まれていないため、それらは個別に取得する必要があります。" #: src/view/com/auth/create/Step1.tsx:215 msgid "Your birth date" @@ -6088,7 +6088,7 @@ msgstr "生年月日" #: src/view/com/modals/InAppBrowserConsent.tsx:47 msgid "Your choice will be saved, but can be changed later in settings." -msgstr "ここで選択した内容は保存されますが、後から設定で変更できます。" +msgstr "ここで選択した内容は保存されますが、あとから設定で変更できます。" #: src/screens/Onboarding/StepFollowingFeed.tsx:61 msgid "Your default feed is \"Following\"" @@ -6136,7 +6136,7 @@ msgstr "フルハンドルは<0>@{0}になります" #: src/components/dialogs/MutedWords.tsx:221 msgid "Your muted words" -msgstr "" +msgstr "ミュートしたワード" #: src/view/com/modals/ChangePassword.tsx:157 msgid "Your password has been changed successfully!" diff --git a/src/locale/locales/ko/messages.po b/src/locale/locales/ko/messages.po index 9888881c2b..db97f1d7f5 100644 --- a/src/locale/locales/ko/messages.po +++ b/src/locale/locales/ko/messages.po @@ -9,7 +9,7 @@ msgstr "" "Project-Id-Version: \n" "Report-Msgid-Bugs-To: \n" "PO-Revision-Date: \n" -"Last-Translator: heartade\n" +"Last-Translator: quiple\n" "Language-Team: quiple, lens0021, HaruChanHeart, hazzzi, heartade\n" "Plural-Forms: \n" @@ -31,7 +31,7 @@ msgstr "<0/>의 멤버" #: src/view/shell/Drawer.tsx:97 msgid "<0>{0} following" -msgstr "" +msgstr "<0>{0} 팔로우 중" #: src/screens/Profile/Header/Metrics.tsx:46 msgid "<0>{following} <1>following" @@ -51,10 +51,10 @@ msgstr "<1>Bluesky<0>에 오신 것을 환영합니다" #: src/screens/Profile/Header/Handle.tsx:42 msgid "⚠Invalid Handle" -msgstr "⚠ 잘못된 핸들" +msgstr "⚠잘못된 핸들" #: src/view/com/util/ViewHeader.tsx:89 -#: src/view/screens/Search/Search.tsx:648 +#: src/view/screens/Search/Search.tsx:649 msgid "Access navigation links and settings" msgstr "탐색 링크 및 설정으로 이동합니다" @@ -71,7 +71,7 @@ msgstr "접근성" msgid "account" msgstr "계정" -#: src/view/com/auth/login/LoginForm.tsx:169 +#: src/screens/Login/LoginForm.tsx:144 #: src/view/screens/Settings/index.tsx:327 #: src/view/screens/Settings/index.tsx:743 msgid "Account" @@ -152,11 +152,11 @@ msgstr "대체 텍스트 추가하기" msgid "Add App Password" msgstr "앱 비밀번호 추가" -#: src/view/com/composer/Composer.tsx:466 +#: src/view/com/composer/Composer.tsx:467 msgid "Add link card" msgstr "링크 카드 추가" -#: src/view/com/composer/Composer.tsx:471 +#: src/view/com/composer/Composer.tsx:472 msgid "Add link card:" msgstr "링크 카드 추가:" @@ -203,11 +203,11 @@ msgstr "답글이 피드에 표시되기 위해 필요한 좋아요 수를 조 msgid "Adult Content" msgstr "성인 콘텐츠" -#: src/components/moderation/ModerationLabelPref.tsx:114 +#: src/components/moderation/LabelPreference.tsx:242 msgid "Adult content is disabled." msgstr "성인 콘텐츠가 비활성화되어 있습니다." -#: src/screens/Moderation/index.tsx:377 +#: src/screens/Moderation/index.tsx:375 #: src/view/screens/Settings/index.tsx:684 msgid "Advanced" msgstr "고급" @@ -216,12 +216,12 @@ msgstr "고급" msgid "All the feeds you've saved, right in one place." msgstr "저장한 모든 피드를 한 곳에서 확인하세요." -#: src/view/com/auth/login/ForgotPasswordForm.tsx:221 +#: src/screens/Login/ForgotPasswordForm.tsx:178 #: src/view/com/modals/ChangePassword.tsx:170 msgid "Already have a code?" msgstr "이미 코드가 있나요?" -#: src/view/com/auth/login/ChooseAccountForm.tsx:103 +#: src/screens/Login/ChooseAccountForm.tsx:101 msgid "Already signed in as @{0}" msgstr "이미 @{0}(으)로 로그인했습니다" @@ -320,7 +320,7 @@ msgstr "앱 비밀번호 \"{name}\"을(를) 삭제하시겠습니까?" msgid "Are you sure you want to remove {0} from your feeds?" msgstr "피드에서 {0}을(를) 제거하시겠습니까?" -#: src/view/com/composer/Composer.tsx:508 +#: src/view/com/composer/Composer.tsx:509 msgid "Are you sure you'd like to discard this draft?" msgstr "이 초안을 삭제하시겠습니까?" @@ -340,24 +340,27 @@ msgstr "예술" msgid "Artistic or non-erotic nudity." msgstr "선정적이지 않거나 예술적인 노출." +#: src/screens/Signup/StepHandle.tsx:118 +msgid "At least 3 characters" +msgstr "3자 이상" + #: src/components/moderation/LabelsOnMeDialog.tsx:247 #: src/components/moderation/LabelsOnMeDialog.tsx:248 +#: src/screens/Login/ChooseAccountForm.tsx:177 +#: src/screens/Login/ChooseAccountForm.tsx:182 +#: src/screens/Login/ForgotPasswordForm.tsx:129 +#: src/screens/Login/ForgotPasswordForm.tsx:135 +#: src/screens/Login/LoginForm.tsx:221 +#: src/screens/Login/LoginForm.tsx:227 +#: src/screens/Login/SetNewPasswordForm.tsx:160 +#: src/screens/Login/SetNewPasswordForm.tsx:166 #: src/screens/Profile/Header/Shell.tsx:97 -#: src/view/com/auth/create/CreateAccount.tsx:158 -#: src/view/com/auth/login/ChooseAccountForm.tsx:160 -#: src/view/com/auth/login/ForgotPasswordForm.tsx:174 -#: src/view/com/auth/login/LoginForm.tsx:262 -#: src/view/com/auth/login/SetNewPasswordForm.tsx:179 +#: src/screens/Signup/index.tsx:179 #: src/view/com/util/ViewHeader.tsx:87 msgid "Back" msgstr "뒤로" -#: src/view/com/post-thread/PostThread.tsx:481 -#~ msgctxt "action" -#~ msgid "Back" -#~ msgstr "뒤로" - -#: src/screens/Onboarding/StepSuggestedAccounts/index.tsx:136 +#: src/screens/Onboarding/StepSuggestedAccounts/index.tsx:144 msgid "Based on your interest in {interestsText}" msgstr "{interestsText}에 대한 관심사 기반" @@ -366,7 +369,6 @@ msgid "Basics" msgstr "기본" #: src/components/dialogs/BirthDateSettings.tsx:107 -#: src/view/com/auth/create/Step1.tsx:227 msgid "Birthday" msgstr "생년월일" @@ -406,7 +408,7 @@ msgstr "이 계정들을 차단하시겠습니까?" msgid "Blocked" msgstr "차단됨" -#: src/screens/Moderation/index.tsx:269 +#: src/screens/Moderation/index.tsx:267 msgid "Blocked accounts" msgstr "차단한 계정" @@ -427,7 +429,7 @@ msgstr "차단한 계정은 내 스레드에 답글을 달거나 나를 멘션 msgid "Blocked post." msgstr "차단된 게시물." -#: src/screens/Profile/Sections/Labels.tsx:153 +#: src/screens/Profile/Sections/Labels.tsx:152 msgid "Blocking does not prevent this labeler from placing labels on your account." msgstr "차단하더라도 이 라벨러가 내 계정에 라벨을 붙이는 것을 막지는 못합니다." @@ -439,12 +441,12 @@ msgstr "차단 목록은 공개됩니다. 차단한 계정은 내 스레드에 msgid "Blocking will not prevent labels from being applied on your account, but it will stop this account from replying in your threads or interacting with you." msgstr "차단하더라도 내 계정에 라벨이 붙는 것은 막지 못하지만, 이 계정이 내 스레드에 답글을 달거나 나와 상호작용하는 것은 중지됩니다." -#: src/view/com/auth/HomeLoggedOutCTA.tsx:97 -#: src/view/com/auth/SplashScreen.web.tsx:133 +#: src/view/com/auth/HomeLoggedOutCTA.tsx:98 +#: src/view/com/auth/SplashScreen.web.tsx:169 msgid "Blog" msgstr "블로그" -#: src/view/com/auth/HomeLoggedOutCTA.tsx:31 +#: src/view/com/auth/HomeLoggedOutCTA.tsx:32 #: src/view/com/auth/server-input/index.tsx:89 #: src/view/com/auth/server-input/index.tsx:90 msgid "Bluesky" @@ -469,7 +471,7 @@ msgstr "Bluesky는 열려 있습니다." msgid "Bluesky is public." msgstr "Bluesky는 공개적입니다." -#: src/screens/Moderation/index.tsx:535 +#: src/screens/Moderation/index.tsx:533 msgid "Bluesky will not show your profile and posts to logged-out users. Other apps may not honor this request. This does not make your account private." msgstr "로그아웃한 사용자에게 내 프로필과 게시물을 표시하지 않습니다. 다른 앱에서는 이 설정을 따르지 않을 수 있습니다. 내 계정을 비공개로 전환하지는 않습니다." @@ -486,11 +488,11 @@ msgid "Books" msgstr "책" #: src/view/screens/Settings/index.tsx:893 -msgid "Build version {0} {1}" -msgstr "빌드 버전 {0} {1}" +#~ msgid "Build version {0} {1}" +#~ msgstr "빌드 버전 {0} {1}" -#: src/view/com/auth/HomeLoggedOutCTA.tsx:91 -#: src/view/com/auth/SplashScreen.web.tsx:128 +#: src/view/com/auth/HomeLoggedOutCTA.tsx:92 +#: src/view/com/auth/SplashScreen.web.tsx:166 msgid "Business" msgstr "비즈니스" @@ -510,9 +512,9 @@ msgstr "{0} 님이 만듦" msgid "by <0/>" msgstr "<0/> 님이 만듦" -#: src/view/com/auth/create/Policies.tsx:87 +#: src/screens/Signup/StepInfo/Policies.tsx:74 msgid "By creating an account you agree to the {els}." -msgstr "" +msgstr "계정을 만들면 {els}에 동의하는 것입니다." #: src/view/com/profile/ProfileSubpageHeader.tsx:159 msgid "by you" @@ -527,11 +529,11 @@ msgid "Can only contain letters, numbers, spaces, dashes, and underscores. Must msgstr "글자, 숫자, 공백, 대시, 밑줄만 포함할 수 있습니다. 길이는 4자 이상이어야 하고 32자를 넘지 않아야 합니다." #: src/components/Menu/index.tsx:213 -#: src/components/Prompt.tsx:116 -#: src/components/Prompt.tsx:118 +#: src/components/Prompt.tsx:115 +#: src/components/Prompt.tsx:117 #: src/components/TagMenu/index.tsx:268 -#: src/view/com/composer/Composer.tsx:316 -#: src/view/com/composer/Composer.tsx:321 +#: src/view/com/composer/Composer.tsx:317 +#: src/view/com/composer/Composer.tsx:322 #: src/view/com/modals/ChangeEmail.tsx:218 #: src/view/com/modals/ChangeEmail.tsx:220 #: src/view/com/modals/ChangeHandle.tsx:153 @@ -548,20 +550,20 @@ msgstr "글자, 숫자, 공백, 대시, 밑줄만 포함할 수 있습니다. #: src/view/com/modals/Repost.tsx:87 #: src/view/com/modals/VerifyEmail.tsx:247 #: src/view/com/modals/VerifyEmail.tsx:253 -#: src/view/screens/Search/Search.tsx:717 +#: src/view/screens/Search/Search.tsx:718 #: src/view/shell/desktop/Search.tsx:239 msgid "Cancel" msgstr "취소" #: src/view/com/modals/CreateOrEditList.tsx:360 -#: src/view/com/modals/DeleteAccount.tsx:156 -#: src/view/com/modals/DeleteAccount.tsx:234 +#: src/view/com/modals/DeleteAccount.tsx:155 +#: src/view/com/modals/DeleteAccount.tsx:233 msgctxt "action" msgid "Cancel" msgstr "취소" -#: src/view/com/modals/DeleteAccount.tsx:152 -#: src/view/com/modals/DeleteAccount.tsx:230 +#: src/view/com/modals/DeleteAccount.tsx:151 +#: src/view/com/modals/DeleteAccount.tsx:229 msgid "Cancel account deletion" msgstr "계정 삭제 취소" @@ -588,11 +590,11 @@ msgstr "검색 취소" #: src/view/com/modals/LinkWarning.tsx:88 msgid "Cancels opening the linked website" -msgstr "" +msgstr "연결된 웹사이트를 여는 것을 취소합니다" #: src/view/com/modals/VerifyEmail.tsx:152 msgid "Change" -msgstr "" +msgstr "변경" #: src/view/screens/Settings/index.tsx:353 msgctxt "action" @@ -625,10 +627,6 @@ msgstr "비밀번호 변경" msgid "Change post language to {0}" msgstr "게시물 언어를 {0}(으)로 변경" -#: src/view/screens/Settings/index.tsx:751 -#~ msgid "Change your Bluesky password" -#~ msgstr "내 Bluesky 비밀번호를 변경합니다" - #: src/view/com/modals/ChangeEmail.tsx:109 msgid "Change Your Email" msgstr "이메일 변경" @@ -646,7 +644,7 @@ msgstr "몇 가지 추천 피드를 확인하세요. +를 탭하여 고정된 msgid "Check out some recommended users. Follow them to see similar users." msgstr "추천 사용자를 확인하세요. 해당 사용자를 팔로우하여 비슷한 사용자를 만날 수 있습니다." -#: src/view/com/modals/DeleteAccount.tsx:169 +#: src/view/com/modals/DeleteAccount.tsx:168 msgid "Check your inbox for an email with the confirmation code to enter below:" msgstr "받은 편지함에서 아래에 입력하는 확인 코드가 포함된 이메일이 있는지 확인하세요:" @@ -654,15 +652,11 @@ msgstr "받은 편지함에서 아래에 입력하는 확인 코드가 포함된 msgid "Choose \"Everybody\" or \"Nobody\"" msgstr "\"모두\" 또는 \"없음\"을 선택하세요." -#: src/view/screens/Settings/index.tsx:715 -#~ msgid "Choose a new Bluesky username or create" -#~ msgstr "새 Bluesky 사용자 이름을 선택하거나 만듭니다" - #: src/view/com/auth/server-input/index.tsx:79 msgid "Choose Service" msgstr "서비스 선택" -#: src/screens/Onboarding/StepFinished.tsx:135 +#: src/screens/Onboarding/StepFinished.tsx:136 msgid "Choose the algorithms that power your custom feeds." msgstr "맞춤 피드를 구동할 알고리즘을 선택하세요." @@ -671,11 +665,11 @@ msgstr "맞춤 피드를 구동할 알고리즘을 선택하세요." msgid "Choose the algorithms that power your experience with custom feeds." msgstr "맞춤 피드를 통해 사용자 경험을 강화하는 알고리즘을 선택하세요." -#: src/screens/Onboarding/StepAlgoFeeds/index.tsx:103 +#: src/screens/Onboarding/StepAlgoFeeds/index.tsx:109 msgid "Choose your main feeds" msgstr "기본 피드 선택" -#: src/view/com/auth/create/Step1.tsx:196 +#: src/screens/Signup/StepInfo/index.tsx:112 msgid "Choose your password" msgstr "비밀번호를 입력하세요" @@ -696,17 +690,17 @@ msgid "Clear all storage data (restart after this)" msgstr "모든 스토리지 데이터 지우기 (이후 다시 시작)" #: src/view/com/util/forms/SearchInput.tsx:88 -#: src/view/screens/Search/Search.tsx:698 +#: src/view/screens/Search/Search.tsx:699 msgid "Clear search query" msgstr "검색어 지우기" #: src/view/screens/Settings/index.tsx:869 msgid "Clears all legacy storage data" -msgstr "" +msgstr "모든 레거시 스토리지 데이터를 지웁니다" #: src/view/screens/Settings/index.tsx:881 msgid "Clears all storage data" -msgstr "" +msgstr "모든 스토리지 데이터를 지웁니다" #: src/view/screens/Support.tsx:40 msgid "click here" @@ -734,7 +728,7 @@ msgstr "닫기" msgid "Close active dialog" msgstr "열려 있는 대화 상자 닫기" -#: src/view/com/auth/login/PasswordUpdatedForm.tsx:38 +#: src/screens/Login/PasswordUpdatedForm.tsx:38 msgid "Close alert" msgstr "알림 닫기" @@ -763,11 +757,11 @@ msgstr "이 대화 상자 닫기" msgid "Closes bottom navigation bar" msgstr "하단 탐색 막대를 닫습니다" -#: src/view/com/auth/login/PasswordUpdatedForm.tsx:39 +#: src/screens/Login/PasswordUpdatedForm.tsx:39 msgid "Closes password update alert" msgstr "비밀번호 변경 알림을 닫습니다" -#: src/view/com/composer/Composer.tsx:318 +#: src/view/com/composer/Composer.tsx:319 msgid "Closes post composer and discards post draft" msgstr "게시물 작성 상자를 닫고 게시물 초안을 삭제합니다" @@ -792,15 +786,15 @@ msgstr "만화" msgid "Community Guidelines" msgstr "커뮤니티 가이드라인" -#: src/screens/Onboarding/StepFinished.tsx:148 +#: src/screens/Onboarding/StepFinished.tsx:149 msgid "Complete onboarding and start using your account" msgstr "온보딩 완료 후 계정 사용 시작" -#: src/view/com/auth/create/Step3.tsx:73 +#: src/screens/Signup/index.tsx:154 msgid "Complete the challenge" msgstr "챌린지 완료하기" -#: src/view/com/composer/Composer.tsx:437 +#: src/view/com/composer/Composer.tsx:438 msgid "Compose posts up to {MAX_GRAPHEME_LENGTH} characters in length" msgstr "최대 {MAX_GRAPHEME_LENGTH}자 길이까지 글을 작성할 수 있습니다" @@ -808,18 +802,20 @@ msgstr "최대 {MAX_GRAPHEME_LENGTH}자 길이까지 글을 작성할 수 있습 msgid "Compose reply" msgstr "답글 작성하기" -#: src/components/moderation/GlobalModerationLabelPref.tsx:69 -#: src/components/moderation/ModerationLabelPref.tsx:149 #: src/screens/Onboarding/StepModeration/ModerationOption.tsx:81 msgid "Configure content filtering setting for category: {0}" -msgstr "{0} 카테고리에 대한 콘텐츠 필터링 설정 구성" +msgstr "{0} 카테고리에 대한 콘텐츠 필터링 설정을 구성합니다." + +#: src/components/moderation/LabelPreference.tsx:81 +msgid "Configure content filtering setting for category: {name}" +msgstr "{name} 카테고리에 대한 콘텐츠 필터링 설정을 구성합니다." -#: src/components/moderation/ModerationLabelPref.tsx:116 +#: src/components/moderation/LabelPreference.tsx:244 msgid "Configured in <0>moderation settings." msgstr "<0>검토 설정에서 설정합니다." -#: src/components/Prompt.tsx:152 -#: src/components/Prompt.tsx:155 +#: src/components/Prompt.tsx:151 +#: src/components/Prompt.tsx:154 #: src/view/com/modals/SelfLabel.tsx:154 #: src/view/com/modals/VerifyEmail.tsx:231 #: src/view/com/modals/VerifyEmail.tsx:233 @@ -837,31 +833,30 @@ msgstr "변경 확인" msgid "Confirm content language settings" msgstr "콘텐츠 언어 설정 확인" -#: src/view/com/modals/DeleteAccount.tsx:220 +#: src/view/com/modals/DeleteAccount.tsx:219 msgid "Confirm delete account" msgstr "계정 삭제 확인" -#: src/screens/Moderation/index.tsx:303 +#: src/screens/Moderation/index.tsx:301 msgid "Confirm your age:" msgstr "나이를 확인하세요:" -#: src/screens/Moderation/index.tsx:294 +#: src/screens/Moderation/index.tsx:292 msgid "Confirm your birthdate" msgstr "생년월일 확인" #: src/view/com/modals/ChangeEmail.tsx:157 -#: src/view/com/modals/DeleteAccount.tsx:176 -#: src/view/com/modals/DeleteAccount.tsx:182 +#: src/view/com/modals/DeleteAccount.tsx:175 +#: src/view/com/modals/DeleteAccount.tsx:181 #: src/view/com/modals/VerifyEmail.tsx:165 msgid "Confirmation code" msgstr "확인 코드" -#: src/view/com/auth/create/CreateAccount.tsx:193 -#: src/view/com/auth/login/LoginForm.tsx:281 +#: src/screens/Login/LoginForm.tsx:246 msgid "Connecting..." msgstr "연결 중…" -#: src/view/com/auth/create/CreateAccount.tsx:213 +#: src/screens/Signup/index.tsx:219 msgid "Contact support" msgstr "지원에 연락하기" @@ -873,7 +868,7 @@ msgstr "콘텐츠" msgid "Content Blocked" msgstr "콘텐츠 차단됨" -#: src/screens/Moderation/index.tsx:287 +#: src/screens/Moderation/index.tsx:285 msgid "Content filters" msgstr "콘텐츠 필터" @@ -888,7 +883,7 @@ msgid "Content Not Available" msgstr "콘텐츠를 사용할 수 없음" #: src/components/moderation/ModerationDetailsDialog.tsx:47 -#: src/components/moderation/ScreenHider.tsx:100 +#: src/components/moderation/ScreenHider.tsx:99 #: src/lib/moderation/useGlobalLabelStrings.ts:22 #: src/lib/moderation/useModerationCauseDescription.ts:38 msgid "Content Warning" @@ -902,29 +897,34 @@ msgstr "콘텐츠 경고" msgid "Context menu backdrop, click to close the menu." msgstr "컨텍스트 메뉴 배경을 클릭하여 메뉴를 닫습니다." -#: src/screens/Onboarding/StepAlgoFeeds/index.tsx:170 -#: src/screens/Onboarding/StepFollowingFeed.tsx:153 -#: src/screens/Onboarding/StepInterests/index.tsx:248 -#: src/screens/Onboarding/StepModeration/index.tsx:102 -#: src/screens/Onboarding/StepTopicalFeeds.tsx:114 +#: src/screens/Onboarding/StepAlgoFeeds/index.tsx:176 +#: src/screens/Onboarding/StepFollowingFeed.tsx:154 +#: src/screens/Onboarding/StepInterests/index.tsx:252 +#: src/screens/Onboarding/StepModeration/index.tsx:103 +#: src/screens/Onboarding/StepTopicalFeeds.tsx:118 #: src/view/com/auth/onboarding/RecommendedFeeds.tsx:148 #: src/view/com/auth/onboarding/RecommendedFollows.tsx:209 #: src/view/com/auth/onboarding/WelcomeMobile.tsx:96 msgid "Continue" msgstr "계속" -#: src/screens/Onboarding/StepFollowingFeed.tsx:150 -#: src/screens/Onboarding/StepInterests/index.tsx:245 -#: src/screens/Onboarding/StepModeration/index.tsx:99 -#: src/screens/Onboarding/StepTopicalFeeds.tsx:111 +#: src/screens/Login/ChooseAccountForm.tsx:47 +msgid "Continue as {0} (currently signed in)" +msgstr "{0}(으)로 계속하기 (현재 로그인)" + +#: src/screens/Onboarding/StepFollowingFeed.tsx:151 +#: src/screens/Onboarding/StepInterests/index.tsx:249 +#: src/screens/Onboarding/StepModeration/index.tsx:100 +#: src/screens/Onboarding/StepTopicalFeeds.tsx:115 +#: src/screens/Signup/index.tsx:198 msgid "Continue to next step" msgstr "다음 단계로 계속하기" -#: src/screens/Onboarding/StepAlgoFeeds/index.tsx:167 +#: src/screens/Onboarding/StepAlgoFeeds/index.tsx:173 msgid "Continue to the next step" msgstr "다음 단계로 계속하기" -#: src/screens/Onboarding/StepSuggestedAccounts/index.tsx:191 +#: src/screens/Onboarding/StepSuggestedAccounts/index.tsx:199 msgid "Continue to the next step without following any accounts" msgstr "계정을 팔로우하지 않고 다음 단계로 계속하기" @@ -958,7 +958,7 @@ msgstr "복사" #: src/view/com/modals/ChangeHandle.tsx:481 msgid "Copy {0}" -msgstr "" +msgstr "{0} 복사" #: src/view/screens/ProfileList.tsx:388 msgid "Copy link to list" @@ -987,9 +987,9 @@ msgstr "피드를 불러올 수 없습니다" msgid "Could not load list" msgstr "리스트를 불러올 수 없습니다" -#: src/view/com/auth/HomeLoggedOutCTA.tsx:64 -#: src/view/com/auth/SplashScreen.tsx:73 -#: src/view/com/auth/SplashScreen.web.tsx:81 +#: src/view/com/auth/HomeLoggedOutCTA.tsx:65 +#: src/view/com/auth/SplashScreen.tsx:75 +#: src/view/com/auth/SplashScreen.web.tsx:104 msgid "Create a new account" msgstr "새 계정 만들기" @@ -997,7 +997,7 @@ msgstr "새 계정 만들기" msgid "Create a new Bluesky account" msgstr "새 Bluesky 계정을 만듭니다" -#: src/view/com/auth/create/CreateAccount.tsx:133 +#: src/screens/Signup/index.tsx:129 msgid "Create Account" msgstr "계정 만들기" @@ -1005,12 +1005,13 @@ msgstr "계정 만들기" msgid "Create App Password" msgstr "앱 비밀번호 만들기" -#: src/view/com/auth/HomeLoggedOutCTA.tsx:54 -#: src/view/com/auth/SplashScreen.tsx:68 +#: src/view/com/auth/HomeLoggedOutCTA.tsx:55 +#: src/view/com/auth/SplashScreen.tsx:66 +#: src/view/com/auth/SplashScreen.web.tsx:95 msgid "Create new account" msgstr "새 계정 만들기" -#: src/components/ReportDialog/SelectReportOptionView.tsx:94 +#: src/components/ReportDialog/SelectReportOptionView.tsx:93 msgid "Create report for {0}" msgstr "{0}에 대한 신고 작성하기" @@ -1018,7 +1019,7 @@ msgstr "{0}에 대한 신고 작성하기" msgid "Created {0}" msgstr "{0}에 생성됨" -#: src/view/com/composer/Composer.tsx:468 +#: src/view/com/composer/Composer.tsx:469 msgid "Creates a card with a thumbnail. The card links to {url}" msgstr "미리보기 이미지가 있는 카드를 만듭니다. 카드가 {url}(으)로 연결됩니다" @@ -1035,7 +1036,7 @@ msgstr "사용자 지정" msgid "Custom domain" msgstr "사용자 지정 도메인" -#: src/screens/Onboarding/StepAlgoFeeds/index.tsx:106 +#: src/screens/Onboarding/StepAlgoFeeds/index.tsx:112 #: src/view/screens/Feeds.tsx:692 msgid "Custom feeds built by the community bring you new experiences and help you find the content you love." msgstr "커뮤니티에서 구축한 맞춤 피드는 새로운 경험을 제공하고 좋아하는 콘텐츠를 찾을 수 있도록 도와줍니다." @@ -1057,6 +1058,10 @@ msgstr "어두운 모드" msgid "Dark Theme" msgstr "어두운 테마" +#: src/screens/Signup/StepInfo/index.tsx:132 +msgid "Date of birth" +msgstr "생년월일" + #: src/view/screens/Settings/index.tsx:841 msgid "Debug Moderation" msgstr "검토 디버그" @@ -1075,7 +1080,7 @@ msgstr "삭제" msgid "Delete account" msgstr "계정 삭제" -#: src/view/com/modals/DeleteAccount.tsx:87 +#: src/view/com/modals/DeleteAccount.tsx:86 msgid "Delete Account" msgstr "계정 삭제" @@ -1091,7 +1096,7 @@ msgstr "앱 비밀번호를 삭제하시겠습니까?" msgid "Delete List" msgstr "리스트 삭제" -#: src/view/com/modals/DeleteAccount.tsx:223 +#: src/view/com/modals/DeleteAccount.tsx:222 msgid "Delete my account" msgstr "내 계정 삭제" @@ -1127,7 +1132,7 @@ msgstr "삭제된 게시물." msgid "Description" msgstr "설명" -#: src/view/com/composer/Composer.tsx:217 +#: src/view/com/composer/Composer.tsx:218 msgid "Did you want to say anything?" msgstr "하고 싶은 말이 있나요?" @@ -1138,20 +1143,20 @@ msgstr "어둑함" #: src/lib/moderation/useLabelBehaviorDescription.ts:32 #: src/lib/moderation/useLabelBehaviorDescription.ts:42 #: src/lib/moderation/useLabelBehaviorDescription.ts:68 -#: src/screens/Moderation/index.tsx:343 +#: src/screens/Moderation/index.tsx:341 msgid "Disabled" msgstr "비활성화됨" -#: src/view/com/composer/Composer.tsx:510 +#: src/view/com/composer/Composer.tsx:517 msgid "Discard" msgstr "삭제" -#: src/view/com/composer/Composer.tsx:507 +#: src/view/com/composer/Composer.tsx:508 msgid "Discard draft?" msgstr "초안 삭제" -#: src/screens/Moderation/index.tsx:520 -#: src/screens/Moderation/index.tsx:524 +#: src/screens/Moderation/index.tsx:518 +#: src/screens/Moderation/index.tsx:522 msgid "Discourage apps from showing my account to logged-out users" msgstr "앱이 로그아웃한 사용자에게 내 계정을 표시하지 않도록 설정하기" @@ -1174,15 +1179,19 @@ msgstr "표시 이름" #: src/view/com/modals/ChangeHandle.tsx:398 msgid "DNS Panel" -msgstr "" +msgstr "DNS 패널" #: src/lib/moderation/useGlobalLabelStrings.ts:39 msgid "Does not include nudity." -msgstr "노출을 포함하지 않음." +msgstr "노출을 포함하지 않습니다." + +#: src/screens/Signup/StepHandle.tsx:104 +msgid "Doesn't begin or end with a hyphen" +msgstr "하이픈으로 시작하거나 끝나지 않음" #: src/view/com/modals/ChangeHandle.tsx:482 msgid "Domain Value" -msgstr "" +msgstr "도메인 값" #: src/view/com/modals/ChangeHandle.tsx:489 msgid "Domain verified!" @@ -1190,6 +1199,8 @@ msgstr "도메인을 확인했습니다." #: src/components/dialogs/BirthDateSettings.tsx:119 #: src/components/dialogs/BirthDateSettings.tsx:125 +#: src/components/forms/DateField/index.tsx:74 +#: src/components/forms/DateField/index.tsx:80 #: src/view/com/auth/server-input/index.tsx:165 #: src/view/com/auth/server-input/index.tsx:166 #: src/view/com/modals/AddAppPasswords.tsx:226 @@ -1221,14 +1232,6 @@ msgstr "완료" msgid "Done{extraText}" msgstr "완료{extraText}" -#: src/view/com/auth/login/ChooseAccountForm.tsx:46 -msgid "Double tap to sign in" -msgstr "두 번 탭하여 로그인합니다" - -#: src/view/screens/Settings/index.tsx:773 -#~ msgid "Download Bluesky account data (repository)" -#~ msgstr "Bluesky 계정 데이터를 다운로드합니다 (저장소)" - #: src/view/screens/Settings/ExportCarDialog.tsx:59 #: src/view/screens/Settings/ExportCarDialog.tsx:63 msgid "Download CAR file" @@ -1244,7 +1247,7 @@ msgstr "Apple 정책으로 인해 성인 콘텐츠는 가입을 완료한 후에 #: src/view/com/modals/ChangeHandle.tsx:257 msgid "e.g. alice" -msgstr "" +msgstr "예: alice" #: src/view/com/modals/EditProfile.tsx:185 msgid "e.g. Alice Roberts" @@ -1252,7 +1255,7 @@ msgstr "예: 앨리스 로버츠" #: src/view/com/modals/ChangeHandle.tsx:381 msgid "e.g. alice.com" -msgstr "" +msgstr "예: alice.com" #: src/view/com/modals/EditProfile.tsx:203 msgid "e.g. Artist, dog-lover, and avid reader." @@ -1315,12 +1318,12 @@ msgstr "내 피드 편집" msgid "Edit my profile" msgstr "내 프로필 편집" -#: src/screens/Profile/Header/ProfileHeaderLabeler.tsx:172 +#: src/screens/Profile/Header/ProfileHeaderLabeler.tsx:171 #: src/screens/Profile/Header/ProfileHeaderStandard.tsx:161 msgid "Edit profile" msgstr "프로필 편집" -#: src/screens/Profile/Header/ProfileHeaderLabeler.tsx:175 +#: src/screens/Profile/Header/ProfileHeaderLabeler.tsx:174 #: src/screens/Profile/Header/ProfileHeaderStandard.tsx:164 msgid "Edit Profile" msgstr "프로필 편집" @@ -1346,14 +1349,12 @@ msgstr "내 프로필 설명 편집" msgid "Education" msgstr "교육" -#: src/view/com/auth/create/Step1.tsx:176 -#: src/view/com/auth/login/ForgotPasswordForm.tsx:156 +#: src/screens/Signup/StepInfo/index.tsx:80 #: src/view/com/modals/ChangeEmail.tsx:141 msgid "Email" msgstr "이메일" -#: src/view/com/auth/create/Step1.tsx:167 -#: src/view/com/auth/login/ForgotPasswordForm.tsx:147 +#: src/screens/Login/ForgotPasswordForm.tsx:99 msgid "Email address" msgstr "이메일 주소" @@ -1378,13 +1379,13 @@ msgstr "이메일:" msgid "Enable {0} only" msgstr "{0}만 사용" -#: src/screens/Moderation/index.tsx:331 +#: src/screens/Moderation/index.tsx:329 msgid "Enable adult content" msgstr "성인 콘텐츠 활성화" #: src/screens/Onboarding/StepModeration/AdultContentEnabledPref.tsx:94 msgid "Enable Adult Content" -msgstr "" +msgstr "성인 콘텐츠 활성화" #: src/screens/Onboarding/StepModeration/AdultContentEnabledPref.tsx:78 #: src/screens/Onboarding/StepModeration/AdultContentEnabledPref.tsx:79 @@ -1403,7 +1404,7 @@ msgstr "미디어 플레이어를 사용할 외부 사이트" msgid "Enable this setting to only see replies between people you follow." msgstr "내가 팔로우하는 사람들 간의 답글만 표시합니다." -#: src/screens/Moderation/index.tsx:341 +#: src/screens/Moderation/index.tsx:339 msgid "Enabled" msgstr "활성화됨" @@ -1413,7 +1414,11 @@ msgstr "피드 끝" #: src/view/com/modals/AddAppPasswords.tsx:166 msgid "Enter a name for this App Password" -msgstr "이 앱 비밀번호의 이름을 입력하세요" +msgstr "이 앱 비밀번호의 이름 입력" + +#: src/screens/Login/SetNewPasswordForm.tsx:139 +msgid "Enter a password" +msgstr "비밀번호 입력" #: src/components/dialogs/MutedWords.tsx:100 #: src/components/dialogs/MutedWords.tsx:101 @@ -1432,16 +1437,16 @@ msgstr "비밀번호를 변경하려면 받은 코드를 입력하세요." msgid "Enter the domain you want to use" msgstr "사용할 도메인 입력" -#: src/view/com/auth/login/ForgotPasswordForm.tsx:107 +#: src/screens/Login/ForgotPasswordForm.tsx:119 msgid "Enter the email you used to create your account. We'll send you a \"reset code\" so you can set a new password." msgstr "계정을 만들 때 사용한 이메일을 입력하세요. 새 비밀번호를 설정할 수 있도록 \"재설정 코드\"를 보내드립니다." #: src/components/dialogs/BirthDateSettings.tsx:108 -#: src/view/com/auth/create/Step1.tsx:228 msgid "Enter your birth date" msgstr "생년월일을 입력하세요" -#: src/view/com/auth/create/Step1.tsx:172 +#: src/screens/Login/ForgotPasswordForm.tsx:105 +#: src/screens/Signup/StepInfo/index.tsx:91 msgid "Enter your email address" msgstr "이메일 주소를 입력하세요" @@ -1453,15 +1458,15 @@ msgstr "새 이메일을 입력하세요" msgid "Enter your new email address below." msgstr "아래에 새 이메일 주소를 입력하세요." -#: src/view/com/auth/login/Login.tsx:99 +#: src/screens/Login/index.tsx:101 msgid "Enter your username and password" msgstr "사용자 이름 및 비밀번호 입력" -#: src/view/com/auth/create/Step3.tsx:67 +#: src/screens/Signup/StepCaptcha/index.tsx:49 msgid "Error receiving captcha response." msgstr "캡차 응답을 수신하는 동안 오류가 발생했습니다." -#: src/view/screens/Search/Search.tsx:110 +#: src/view/screens/Search/Search.tsx:111 msgid "Error:" msgstr "오류:" @@ -1473,9 +1478,9 @@ msgstr "모두" msgid "Excessive mentions or replies" msgstr "과도한 멘션 또는 답글" -#: src/view/com/modals/DeleteAccount.tsx:231 +#: src/view/com/modals/DeleteAccount.tsx:230 msgid "Exits account deletion process" -msgstr "" +msgstr "계정 삭제 프로세스를 종료합니다" #: src/view/com/modals/ChangeHandle.tsx:150 msgid "Exits handle change process" @@ -1483,7 +1488,7 @@ msgstr "핸들 변경 프로세스를 종료합니다" #: src/view/com/modals/crop-image/CropImage.web.tsx:135 msgid "Exits image cropping process" -msgstr "" +msgstr "이미지 자르기 프로세스를 종료합니다" #: src/view/com/lightbox/Lightbox.web.tsx:130 msgid "Exits image view" @@ -1559,7 +1564,7 @@ msgstr "추천 피드를 불러오지 못했습니다" #: src/view/com/lightbox/Lightbox.tsx:83 msgid "Failed to save image: {0}" -msgstr "" +msgstr "이미지를 저장하지 못함: {0}" #: src/Navigation.tsx:196 msgid "Feed" @@ -1581,7 +1586,7 @@ msgstr "피드백" #: src/Navigation.tsx:464 #: src/view/screens/Feeds.tsx:419 #: src/view/screens/Feeds.tsx:524 -#: src/view/screens/Profile.tsx:192 +#: src/view/screens/Profile.tsx:194 #: src/view/shell/bottom-bar/BottomBar.tsx:183 #: src/view/shell/desktop/LeftNav.tsx:346 #: src/view/shell/Drawer.tsx:479 @@ -1597,19 +1602,19 @@ msgstr "피드는 콘텐츠를 큐레이션하기 위해 사용자에 의해 만 msgid "Feeds are custom algorithms that users build with a little coding expertise. <0/> for more information." msgstr "피드는 사용자가 약간의 코딩 전문 지식만으로 구축할 수 있는 맞춤 알고리즘입니다. <0/>에서 자세한 내용을 확인하세요." -#: src/screens/Onboarding/StepTopicalFeeds.tsx:76 +#: src/screens/Onboarding/StepTopicalFeeds.tsx:80 msgid "Feeds can be topical as well!" msgstr "주제 기반 피드도 있습니다!" #: src/view/com/modals/ChangeHandle.tsx:482 msgid "File Contents" -msgstr "" +msgstr "파일 콘텐츠" #: src/lib/moderation/useLabelBehaviorDescription.ts:66 msgid "Filter from feeds" msgstr "피드에서 필터링" -#: src/screens/Onboarding/StepFinished.tsx:151 +#: src/screens/Onboarding/StepFinished.tsx:152 msgid "Finalizing" msgstr "마무리 중" @@ -1619,11 +1624,11 @@ msgstr "마무리 중" msgid "Find accounts to follow" msgstr "팔로우할 계정 찾아보기" -#: src/view/screens/Search/Search.tsx:441 +#: src/view/screens/Search/Search.tsx:442 msgid "Find users on Bluesky" msgstr "Bluesky에서 사용자 찾기" -#: src/view/screens/Search/Search.tsx:439 +#: src/view/screens/Search/Search.tsx:440 msgid "Find users with the search tool on the right" msgstr "오른쪽의 검색 도구로 사용자 찾기" @@ -1643,7 +1648,7 @@ msgstr "대화 스레드를 미세 조정합니다." msgid "Fitness" msgstr "건강" -#: src/screens/Onboarding/StepFinished.tsx:131 +#: src/screens/Onboarding/StepFinished.tsx:132 msgid "Flexible" msgstr "유연성" @@ -1656,7 +1661,7 @@ msgstr "가로로 뒤집기" msgid "Flip vertically" msgstr "세로로 뒤집기" -#: src/screens/Onboarding/StepSuggestedAccounts/index.tsx:181 +#: src/screens/Onboarding/StepSuggestedAccounts/index.tsx:189 #: src/screens/Profile/Header/ProfileHeaderStandard.tsx:229 #: src/view/com/auth/onboarding/RecommendedFollowsItem.tsx:141 #: src/view/com/post-thread/PostThreadFollowBtn.tsx:139 @@ -1680,11 +1685,11 @@ msgstr "{0} 님을 팔로우" msgid "Follow Account" msgstr "계정 팔로우" -#: src/screens/Onboarding/StepSuggestedAccounts/index.tsx:179 +#: src/screens/Onboarding/StepSuggestedAccounts/index.tsx:187 msgid "Follow All" msgstr "모두 팔로우" -#: src/screens/Onboarding/StepSuggestedAccounts/index.tsx:174 +#: src/screens/Onboarding/StepSuggestedAccounts/index.tsx:182 msgid "Follow selected accounts and continue to the next step" msgstr "선택한 계정을 팔로우하고 다음 단계를 계속 진행합니다" @@ -1726,7 +1731,7 @@ msgstr "{0} 님을 팔로우했습니다" #: src/view/screens/Settings/index.tsx:553 msgid "Following feed preferences" -msgstr "" +msgstr "팔로우 중 피드 설정" #: src/Navigation.tsx:262 #: src/view/com/home/HomeHeaderLayout.web.tsx:50 @@ -1748,7 +1753,7 @@ msgstr "나를 팔로우함" msgid "Food" msgstr "음식" -#: src/view/com/modals/DeleteAccount.tsx:111 +#: src/view/com/modals/DeleteAccount.tsx:110 msgid "For security reasons, we'll need to send a confirmation code to your email address." msgstr "보안상의 이유로 이메일 주소로 확인 코드를 보내야 합니다." @@ -1756,19 +1761,19 @@ msgstr "보안상의 이유로 이메일 주소로 확인 코드를 보내야 msgid "For security reasons, you won't be able to view this again. If you lose this password, you'll need to generate a new one." msgstr "보안상의 이유로 이 비밀번호는 다시 볼 수 없습니다. 이 비밀번호를 분실한 경우 새 비밀번호를 생성해야 합니다." -#: src/view/com/auth/login/LoginForm.tsx:244 -msgid "Forgot" -msgstr "분실" - -#: src/view/com/auth/login/LoginForm.tsx:241 -msgid "Forgot password" -msgstr "비밀번호 분실" - -#: src/view/com/auth/login/Login.tsx:127 -#: src/view/com/auth/login/Login.tsx:143 +#: src/screens/Login/index.tsx:129 +#: src/screens/Login/index.tsx:144 msgid "Forgot Password" msgstr "비밀번호 분실" +#: src/screens/Login/LoginForm.tsx:201 +msgid "Forgot password?" +msgstr "비밀번호를 잊으셨나요?" + +#: src/screens/Login/LoginForm.tsx:212 +msgid "Forgot?" +msgstr "분실" + #: src/lib/moderation/useReportOptions.ts:52 msgid "Frequently Posts Unwanted Content" msgstr "잦은 원치 않는 콘텐츠 게시" @@ -1794,12 +1799,12 @@ msgstr "시작하기" #: src/lib/moderation/useReportOptions.ts:37 msgid "Glaring violations of law or terms of service" -msgstr "명백한 법률 또는 서비스 약관 위반 행위" +msgstr "명백한 법률 또는 서비스 이용약관 위반 행위" -#: src/components/moderation/ScreenHider.tsx:144 -#: src/components/moderation/ScreenHider.tsx:153 -#: src/view/com/auth/LoggedOut.tsx:81 +#: src/components/moderation/ScreenHider.tsx:151 +#: src/components/moderation/ScreenHider.tsx:160 #: src/view/com/auth/LoggedOut.tsx:82 +#: src/view/com/auth/LoggedOut.tsx:83 #: src/view/screens/NotFound.tsx:55 #: src/view/screens/ProfileFeed.tsx:111 #: src/view/screens/ProfileList.tsx:916 @@ -1807,6 +1812,7 @@ msgstr "명백한 법률 또는 서비스 약관 위반 행위" msgid "Go back" msgstr "뒤로" +#: src/components/Error.tsx:91 #: src/screens/Profile/ErrorState.tsx:62 #: src/screens/Profile/ErrorState.tsx:66 #: src/view/screens/NotFound.tsx:54 @@ -1815,30 +1821,28 @@ msgstr "뒤로" msgid "Go Back" msgstr "뒤로" -#: src/components/ReportDialog/SelectReportOptionView.tsx:74 +#: src/components/ReportDialog/SelectReportOptionView.tsx:73 #: src/components/ReportDialog/SubmitView.tsx:104 #: src/screens/Onboarding/Layout.tsx:104 #: src/screens/Onboarding/Layout.tsx:193 +#: src/screens/Signup/index.tsx:173 msgid "Go back to previous step" msgstr "이전 단계로 돌아가기" #: src/view/screens/NotFound.tsx:55 msgid "Go home" -msgstr "" +msgstr "홈으로 이동" #: src/view/screens/NotFound.tsx:54 msgid "Go Home" -msgstr "" +msgstr "홈으로 이동" -#: src/view/screens/Search/Search.tsx:748 +#: src/view/screens/Search/Search.tsx:749 #: src/view/shell/desktop/Search.tsx:263 msgid "Go to @{queryMaybeHandle}" msgstr "@{queryMaybeHandle}(으)로 이동" -#: src/view/com/auth/login/ForgotPasswordForm.tsx:189 -#: src/view/com/auth/login/ForgotPasswordForm.tsx:218 -#: src/view/com/auth/login/LoginForm.tsx:291 -#: src/view/com/auth/login/SetNewPasswordForm.tsx:195 +#: src/screens/Login/ForgotPasswordForm.tsx:172 #: src/view/com/modals/ChangePassword.tsx:167 msgid "Go to next" msgstr "다음" @@ -1863,7 +1867,7 @@ msgstr "해시태그" msgid "Hashtag: #{tag}" msgstr "해시태그: #{tag}" -#: src/view/com/auth/create/CreateAccount.tsx:208 +#: src/screens/Signup/index.tsx:217 msgid "Having trouble?" msgstr "문제가 있나요?" @@ -1872,15 +1876,15 @@ msgstr "문제가 있나요?" msgid "Help" msgstr "도움말" -#: src/screens/Onboarding/StepSuggestedAccounts/index.tsx:132 +#: src/screens/Onboarding/StepSuggestedAccounts/index.tsx:140 msgid "Here are some accounts for you to follow" msgstr "팔로우할 만한 계정" -#: src/screens/Onboarding/StepTopicalFeeds.tsx:85 +#: src/screens/Onboarding/StepTopicalFeeds.tsx:89 msgid "Here are some popular topical feeds. You can choose to follow as many as you like." msgstr "다음은 인기 있는 화제 피드입니다. 원하는 만큼 피드를 팔로우할 수 있습니다." -#: src/screens/Onboarding/StepTopicalFeeds.tsx:80 +#: src/screens/Onboarding/StepTopicalFeeds.tsx:84 msgid "Here are some topical feeds based on your interests: {interestsText}. You can choose to follow as many as you like." msgstr "다음은 사용자의 관심사를 기반으로 한 몇 가지 주제별 피드입니다: {interestsText}. 원하는 만큼 많은 피드를 팔로우할 수 있습니다." @@ -1889,7 +1893,7 @@ msgid "Here is your app password." msgstr "앱 비밀번호입니다." #: src/components/moderation/ContentHider.tsx:115 -#: src/components/moderation/GlobalModerationLabelPref.tsx:43 +#: src/components/moderation/LabelPreference.tsx:134 #: src/components/moderation/PostHider.tsx:107 #: src/lib/moderation/useLabelBehaviorDescription.ts:15 #: src/lib/moderation/useLabelBehaviorDescription.ts:20 @@ -1944,7 +1948,7 @@ msgstr "피드 서버에서 잘못된 응답을 보냈습니다. 피드 소유 msgid "Hmm, we're having trouble finding this feed. It may have been deleted." msgstr "이 피드를 찾는 데 문제가 있습니다. 피드가 삭제되었을 수 있습니다." -#: src/screens/Moderation/index.tsx:61 +#: src/screens/Moderation/index.tsx:59 msgid "Hmmmm, it seems we're having trouble loading this data. See below for more details. If this issue persists, please contact us." msgstr "이 데이터를 불러오는 데 문제가 있는 것 같습니다. 자세한 내용은 아래를 참조하세요. 이 문제가 지속되면 문의해 주세요." @@ -1962,10 +1966,11 @@ msgstr "홈" #: src/view/com/modals/ChangeHandle.tsx:421 msgid "Host:" -msgstr "" +msgstr "호스트:" -#: src/view/com/auth/create/Step1.tsx:75 -#: src/view/com/auth/login/ForgotPasswordForm.tsx:120 +#: src/screens/Login/ForgotPasswordForm.tsx:89 +#: src/screens/Login/LoginForm.tsx:134 +#: src/screens/Signup/StepInfo/index.tsx:40 #: src/view/com/modals/ChangeHandle.tsx:280 msgid "Hosting provider" msgstr "호스팅 제공자" @@ -1994,9 +1999,9 @@ msgstr "대체 텍스트가 긴 경우 대체 텍스트 확장 상태를 전환 msgid "If none are selected, suitable for all ages." msgstr "아무것도 선택하지 않으면 모든 연령대에 적합하다는 뜻입니다." -#: src/view/com/auth/create/Policies.tsx:91 +#: src/screens/Signup/StepInfo/Policies.tsx:83 msgid "If you are not yet an adult according to the laws of your country, your parent or legal guardian must read these Terms on your behalf." -msgstr "" +msgstr "해당 국가의 법률에 따라 아직 성인이 아닌 경우, 부모 또는 법적 보호자가 대신 이 약관을 읽어야 합니다." #: src/view/screens/ProfileList.tsx:610 msgid "If you delete this list, you won't be able to recover it." @@ -2026,51 +2031,43 @@ msgstr "이미지 대체 텍스트" msgid "Impersonation or false claims about identity or affiliation" msgstr "신원 또는 소속에 대한 사칭 또는 허위 주장" -#: src/view/com/auth/login/SetNewPasswordForm.tsx:138 +#: src/screens/Login/SetNewPasswordForm.tsx:127 msgid "Input code sent to your email for password reset" msgstr "비밀번호 재설정을 위해 이메일로 전송된 코드를 입력합니다" -#: src/view/com/modals/DeleteAccount.tsx:184 +#: src/view/com/modals/DeleteAccount.tsx:183 msgid "Input confirmation code for account deletion" msgstr "계정 삭제를 위한 확인 코드를 입력합니다" -#: src/view/com/auth/create/Step1.tsx:177 -msgid "Input email for Bluesky account" -msgstr "Bluesky 계정에 사용할 이메일을 입력합니다" - -#: src/view/com/auth/create/Step1.tsx:151 -msgid "Input invite code to proceed" -msgstr "진행하기 위해 초대 코드를 입력합니다" - #: src/view/com/modals/AddAppPasswords.tsx:180 msgid "Input name for app password" msgstr "앱 비밀번호의 이름을 입력합니다" -#: src/view/com/auth/login/SetNewPasswordForm.tsx:162 +#: src/screens/Login/SetNewPasswordForm.tsx:151 msgid "Input new password" msgstr "새 비밀번호를 입력합니다" -#: src/view/com/modals/DeleteAccount.tsx:203 +#: src/view/com/modals/DeleteAccount.tsx:202 msgid "Input password for account deletion" msgstr "계정을 삭제하기 위해 비밀번호를 입력합니다" -#: src/view/com/auth/login/LoginForm.tsx:233 +#: src/screens/Login/LoginForm.tsx:195 msgid "Input the password tied to {identifier}" msgstr "{identifier}에 연결된 비밀번호를 입력합니다" -#: src/view/com/auth/login/LoginForm.tsx:200 +#: src/screens/Login/LoginForm.tsx:168 msgid "Input the username or email address you used at signup" msgstr "가입 시 사용한 사용자 이름 또는 이메일 주소를 입력합니다" -#: src/view/com/auth/login/LoginForm.tsx:232 +#: src/screens/Login/LoginForm.tsx:194 msgid "Input your password" msgstr "비밀번호를 입력합니다" #: src/view/com/modals/ChangeHandle.tsx:390 msgid "Input your preferred hosting provider" -msgstr "" +msgstr "선호하는 호스팅 제공자를 입력합니다" -#: src/view/com/auth/create/Step2.tsx:80 +#: src/screens/Signup/StepHandle.tsx:62 msgid "Input your user handle" msgstr "사용자 핸들을 입력합니다" @@ -2078,7 +2075,7 @@ msgstr "사용자 핸들을 입력합니다" msgid "Invalid or unsupported post record" msgstr "유효하지 않거나 지원되지 않는 게시물 기록" -#: src/view/com/auth/login/LoginForm.tsx:116 +#: src/screens/Login/LoginForm.tsx:114 msgid "Invalid username or password" msgstr "잘못된 사용자 이름 또는 비밀번호" @@ -2086,12 +2083,11 @@ msgstr "잘못된 사용자 이름 또는 비밀번호" msgid "Invite a Friend" msgstr "친구 초대하기" -#: src/view/com/auth/create/Step1.tsx:141 -#: src/view/com/auth/create/Step1.tsx:150 +#: src/screens/Signup/StepInfo/index.tsx:58 msgid "Invite code" msgstr "초대 코드" -#: src/view/com/auth/create/state.ts:158 +#: src/screens/Signup/state.ts:278 msgid "Invite code not accepted. Check that you input it correctly and try again." msgstr "초대 코드가 올바르지 않습니다. 코드를 올바르게 입력했는지 확인한 후 다시 시도하세요." @@ -2103,12 +2099,12 @@ msgstr "초대 코드: {0}개 사용 가능" msgid "Invite codes: 1 available" msgstr "초대 코드: 1개 사용 가능" -#: src/screens/Onboarding/StepFollowingFeed.tsx:64 +#: src/screens/Onboarding/StepFollowingFeed.tsx:65 msgid "It shows posts from the people you follow as they happen." msgstr "내가 팔로우하는 사람들의 게시물이 올라오는 대로 표시됩니다." -#: src/view/com/auth/HomeLoggedOutCTA.tsx:103 -#: src/view/com/auth/SplashScreen.web.tsx:138 +#: src/view/com/auth/HomeLoggedOutCTA.tsx:104 +#: src/view/com/auth/SplashScreen.web.tsx:172 msgid "Jobs" msgstr "채용" @@ -2128,11 +2124,11 @@ msgstr "{0} 님이 라벨 지정함." msgid "Labeled by the author." msgstr "작성자가 라벨 지정함." -#: src/view/screens/Profile.tsx:186 +#: src/view/screens/Profile.tsx:188 msgid "Labels" msgstr "라벨" -#: src/screens/Profile/Sections/Labels.tsx:143 +#: src/screens/Profile/Sections/Labels.tsx:142 msgid "Labels are annotations on users and content. They can be used to hide, warn, and categorize the network." msgstr "라벨은 사용자 및 콘텐츠에 대한 주석입니다. 네트워크를 숨기고, 경고하고, 분류하는 데 사용할 수 있습니다." @@ -2165,11 +2161,7 @@ msgstr "언어 설정" msgid "Languages" msgstr "언어" -#: src/view/com/auth/create/StepHeader.tsx:20 -msgid "Last step!" -msgstr "마지막 단계예요!" - -#: src/components/moderation/ScreenHider.tsx:129 +#: src/components/moderation/ScreenHider.tsx:136 msgid "Learn More" msgstr "더 알아보기" @@ -2179,11 +2171,11 @@ msgid "Learn more about the moderation applied to this content." msgstr "이 콘텐츠에 적용된 검토 설정에 대해 자세히 알아보세요." #: src/components/moderation/PostHider.tsx:85 -#: src/components/moderation/ScreenHider.tsx:126 +#: src/components/moderation/ScreenHider.tsx:125 msgid "Learn more about this warning" msgstr "이 경고에 대해 더 알아보기" -#: src/screens/Moderation/index.tsx:551 +#: src/screens/Moderation/index.tsx:549 msgid "Learn more about what is public on Bluesky." msgstr "Bluesky에서 공개되는 항목에 대해 자세히 알아보세요." @@ -2207,12 +2199,12 @@ msgstr "명 남았습니다." msgid "Legacy storage cleared, you need to restart the app now." msgstr "레거시 스토리지가 지워졌으며 지금 앱을 다시 시작해야 합니다." -#: src/view/com/auth/login/Login.tsx:128 -#: src/view/com/auth/login/Login.tsx:144 +#: src/screens/Login/index.tsx:130 +#: src/screens/Login/index.tsx:145 msgid "Let's get your password reset!" msgstr "비밀번호를 재설정해 봅시다!" -#: src/screens/Onboarding/StepFinished.tsx:151 +#: src/screens/Onboarding/StepFinished.tsx:152 msgid "Let's go!" msgstr "출발!" @@ -2220,11 +2212,11 @@ msgstr "출발!" msgid "Light" msgstr "밝음" -#: src/view/com/util/post-ctrls/PostCtrls.tsx:185 +#: src/view/com/util/post-ctrls/PostCtrls.tsx:195 msgid "Like" msgstr "좋아요" -#: src/screens/Profile/Header/ProfileHeaderLabeler.tsx:257 +#: src/screens/Profile/Header/ProfileHeaderLabeler.tsx:256 #: src/view/screens/ProfileFeed.tsx:572 msgid "Like this feed" msgstr "이 피드에 좋아요 표시" @@ -2249,8 +2241,8 @@ msgstr "{0}명의 사용자가 좋아함" msgid "Liked by {count} {0}" msgstr "{count}명의 사용자가 좋아함" -#: src/screens/Profile/Header/ProfileHeaderLabeler.tsx:277 -#: src/screens/Profile/Header/ProfileHeaderLabeler.tsx:291 +#: src/screens/Profile/Header/ProfileHeaderLabeler.tsx:276 +#: src/screens/Profile/Header/ProfileHeaderLabeler.tsx:290 #: src/view/screens/ProfileFeed.tsx:587 msgid "Liked by {likeCount} {0}" msgstr "{likeCount}명의 사용자가 좋아함" @@ -2263,7 +2255,7 @@ msgstr "님이 내 맞춤 피드를 좋아합니다" msgid "liked your post" msgstr "님이 내 게시물을 좋아합니다" -#: src/view/screens/Profile.tsx:191 +#: src/view/screens/Profile.tsx:193 msgid "Likes" msgstr "좋아요" @@ -2308,19 +2300,14 @@ msgid "List unmuted" msgstr "리스트 언뮤트됨" #: src/Navigation.tsx:114 -#: src/view/screens/Profile.tsx:187 -#: src/view/screens/Profile.tsx:193 +#: src/view/screens/Profile.tsx:189 +#: src/view/screens/Profile.tsx:195 #: src/view/shell/desktop/LeftNav.tsx:383 #: src/view/shell/Drawer.tsx:495 #: src/view/shell/Drawer.tsx:496 msgid "Lists" msgstr "리스트" -#: src/view/com/post-thread/PostThread.tsx:334 -#: src/view/com/post-thread/PostThread.tsx:342 -#~ msgid "Load more posts" -#~ msgstr "더 많은 게시물 불러오기" - #: src/view/screens/Notifications.tsx:159 msgid "Load new notifications" msgstr "새 알림 불러오기" @@ -2347,14 +2334,18 @@ msgstr "로그" msgid "Log out" msgstr "로그아웃" -#: src/screens/Moderation/index.tsx:444 +#: src/screens/Moderation/index.tsx:442 msgid "Logged-out visibility" msgstr "로그아웃 표시" -#: src/view/com/auth/login/ChooseAccountForm.tsx:142 +#: src/screens/Login/ChooseAccountForm.tsx:149 msgid "Login to account that is not listed" msgstr "목록에 없는 계정으로 로그인" +#: src/screens/Login/SetNewPasswordForm.tsx:116 +msgid "Looks like XXXXX-XXXXX" +msgstr "XXXXX-XXXXX 형식" + #: src/view/com/modals/LinkWarning.tsx:65 msgid "Make sure this is where you intend to go!" msgstr "이곳이 당신이 가고자 하는 곳인지 확인하세요!" @@ -2363,15 +2354,7 @@ msgstr "이곳이 당신이 가고자 하는 곳인지 확인하세요!" msgid "Manage your muted words and tags" msgstr "뮤트한 단어 및 태그 관리" -#: src/view/com/auth/create/Step2.tsx:118 -msgid "May not be longer than 253 characters" -msgstr "253자를 넘을 수 없습니다" - -#: src/view/com/auth/create/Step2.tsx:109 -msgid "May only contain letters and numbers" -msgstr "문자와 숫자만 입력할 수 있습니다" - -#: src/view/screens/Profile.tsx:190 +#: src/view/screens/Profile.tsx:192 msgid "Media" msgstr "미디어" @@ -2384,7 +2367,7 @@ msgid "Mentioned users" msgstr "멘션한 사용자" #: src/view/com/util/ViewHeader.tsx:87 -#: src/view/screens/Search/Search.tsx:647 +#: src/view/screens/Search/Search.tsx:648 msgid "Menu" msgstr "메뉴" @@ -2397,7 +2380,7 @@ msgid "Misleading Account" msgstr "오해의 소지가 있는 계정" #: src/Navigation.tsx:119 -#: src/screens/Moderation/index.tsx:106 +#: src/screens/Moderation/index.tsx:104 #: src/view/screens/Settings/index.tsx:645 #: src/view/shell/desktop/LeftNav.tsx:401 #: src/view/shell/Drawer.tsx:514 @@ -2432,7 +2415,7 @@ msgstr "검토 리스트 생성됨" msgid "Moderation list updated" msgstr "검토 리스트 업데이트됨" -#: src/screens/Moderation/index.tsx:245 +#: src/screens/Moderation/index.tsx:243 msgid "Moderation lists" msgstr "검토 리스트" @@ -2449,18 +2432,18 @@ msgstr "검토 설정" msgid "Moderation states" msgstr "검토 상태" -#: src/screens/Moderation/index.tsx:217 +#: src/screens/Moderation/index.tsx:215 msgid "Moderation tools" msgstr "검토 도구" #: src/components/moderation/ModerationDetailsDialog.tsx:49 #: src/lib/moderation/useModerationCauseDescription.ts:40 msgid "Moderator has chosen to set a general warning on the content." -msgstr "관리자가 콘텐츠에 일반 경고를 설정했습니다." +msgstr "검토자가 콘텐츠에 일반 경고를 설정했습니다." #: src/view/com/post-thread/PostThreadItem.tsx:541 msgid "More" -msgstr "" +msgstr "더 보기" #: src/view/shell/desktop/Feeds.tsx:65 msgid "More feeds" @@ -2474,10 +2457,6 @@ msgstr "옵션 더 보기" msgid "Most-liked replies first" msgstr "좋아요 많은 순" -#: src/view/com/auth/create/Step2.tsx:122 -msgid "Must be at least 3 characters" -msgstr "최소 3자 이상이어야 합니다" - #: src/components/TagMenu/index.tsx:249 msgid "Mute" msgstr "뮤트" @@ -2538,7 +2517,7 @@ msgstr "단어 및 태그 뮤트" msgid "Muted" msgstr "뮤트됨" -#: src/screens/Moderation/index.tsx:257 +#: src/screens/Moderation/index.tsx:255 msgid "Muted accounts" msgstr "뮤트한 계정" @@ -2555,7 +2534,7 @@ msgstr "계정을 뮤트하면 피드와 알림에서 해당 계정의 게시물 msgid "Muted by \"{0}\"" msgstr "\"{0}\" 님이 뮤트함" -#: src/screens/Moderation/index.tsx:233 +#: src/screens/Moderation/index.tsx:231 msgid "Muted words & tags" msgstr "뮤트한 단어 및 태그" @@ -2578,16 +2557,12 @@ msgstr "내 프로필" #: src/view/screens/Settings/index.tsx:596 msgid "My saved feeds" -msgstr "" +msgstr "내 저장된 피드" #: src/view/screens/Settings/index.tsx:602 msgid "My Saved Feeds" msgstr "내 저장된 피드" -#: src/view/com/auth/server-input/index.tsx:118 -#~ msgid "my-server.com" -#~ msgstr "my-server.com" - #: src/view/com/modals/AddAppPasswords.tsx:179 #: src/view/com/modals/CreateOrEditList.tsx:290 msgid "Name" @@ -2607,10 +2582,8 @@ msgstr "이름 또는 설명이 커뮤니티 기준을 위반함" msgid "Nature" msgstr "자연" -#: src/view/com/auth/login/ForgotPasswordForm.tsx:190 -#: src/view/com/auth/login/ForgotPasswordForm.tsx:219 -#: src/view/com/auth/login/LoginForm.tsx:292 -#: src/view/com/auth/login/SetNewPasswordForm.tsx:196 +#: src/screens/Login/ForgotPasswordForm.tsx:173 +#: src/screens/Login/LoginForm.tsx:252 #: src/view/com/modals/ChangePassword.tsx:168 msgid "Navigates to the next screen" msgstr "다음 화면으로 이동합니다" @@ -2619,7 +2592,7 @@ msgstr "다음 화면으로 이동합니다" msgid "Navigates to your profile" msgstr "내 프로필로 이동합니다" -#: src/components/ReportDialog/SelectReportOptionView.tsx:124 +#: src/components/ReportDialog/SelectReportOptionView.tsx:122 msgid "Need to report a copyright violation?" msgstr "저작권 위반을 신고해야 하나요?" @@ -2633,13 +2606,13 @@ msgstr "{0}에서 임베드를 불러오지 않습니다" msgid "Never lose access to your followers and data." msgstr "팔로워와 데이터에 대한 접근 권한을 잃지 마세요." -#: src/screens/Onboarding/StepFinished.tsx:119 +#: src/screens/Onboarding/StepFinished.tsx:120 msgid "Never lose access to your followers or data." msgstr "팔로워 또는 데이터에 대한 접근 권한을 잃지 마세요." #: src/view/com/modals/ChangeHandle.tsx:520 msgid "Nevermind, create a handle for me" -msgstr "" +msgstr "취소하고 내 핸들 만들기" #: src/view/screens/Lists.tsx:76 msgctxt "action" @@ -2654,7 +2627,6 @@ msgstr "새로 만들기" msgid "New Moderation List" msgstr "새 검토 리스트" -#: src/view/com/auth/login/SetNewPasswordForm.tsx:150 #: src/view/com/modals/ChangePassword.tsx:212 msgid "New password" msgstr "새 비밀번호" @@ -2670,7 +2642,7 @@ msgstr "새 게시물" #: src/view/screens/Feeds.tsx:555 #: src/view/screens/Notifications.tsx:168 -#: src/view/screens/Profile.tsx:450 +#: src/view/screens/Profile.tsx:452 #: src/view/screens/ProfileFeed.tsx:433 #: src/view/screens/ProfileList.tsx:199 #: src/view/screens/ProfileList.tsx:227 @@ -2695,12 +2667,13 @@ msgstr "새로운 순" msgid "News" msgstr "뉴스" -#: src/view/com/auth/create/CreateAccount.tsx:172 -#: src/view/com/auth/login/ForgotPasswordForm.tsx:182 -#: src/view/com/auth/login/ForgotPasswordForm.tsx:192 -#: src/view/com/auth/login/LoginForm.tsx:294 -#: src/view/com/auth/login/SetNewPasswordForm.tsx:187 -#: src/view/com/auth/login/SetNewPasswordForm.tsx:198 +#: src/screens/Login/ForgotPasswordForm.tsx:143 +#: src/screens/Login/ForgotPasswordForm.tsx:150 +#: src/screens/Login/LoginForm.tsx:251 +#: src/screens/Login/LoginForm.tsx:258 +#: src/screens/Login/SetNewPasswordForm.tsx:174 +#: src/screens/Login/SetNewPasswordForm.tsx:180 +#: src/screens/Signup/index.tsx:205 #: src/view/com/auth/onboarding/RecommendedFeeds.tsx:79 #: src/view/com/modals/ChangePassword.tsx:253 #: src/view/com/modals/ChangePassword.tsx:255 @@ -2732,12 +2705,16 @@ msgstr "설명 없음" #: src/view/com/modals/ChangeHandle.tsx:406 msgid "No DNS Panel" -msgstr "" +msgstr "DNS 패널 없음" #: src/screens/Profile/Header/ProfileHeaderStandard.tsx:111 msgid "No longer following {0}" msgstr "더 이상 {0} 님을 팔로우하지 않음" +#: src/screens/Signup/StepHandle.tsx:114 +msgid "No longer than 253 characters" +msgstr "253자를 초과하지 않음" + #: src/view/com/notifications/Feed.tsx:109 msgid "No notifications yet!" msgstr "아직 알림이 없습니다." @@ -2756,14 +2733,14 @@ msgid "No results found for \"{query}\"" msgstr "\"{query}\"에 대한 결과를 찾을 수 없습니다" #: src/view/com/modals/ListAddRemoveUsers.tsx:127 -#: src/view/screens/Search/Search.tsx:282 -#: src/view/screens/Search/Search.tsx:310 +#: src/view/screens/Search/Search.tsx:283 +#: src/view/screens/Search/Search.tsx:311 msgid "No results found for {query}" msgstr "{query}에 대한 결과를 찾을 수 없습니다" #: src/view/com/modals/EmbedConsent.tsx:129 msgid "No thanks" -msgstr "괜찮습니다" +msgstr "사용하지 않음" #: src/view/com/modals/Threadgate.tsx:82 msgid "Nobody" @@ -2783,7 +2760,7 @@ msgid "Not Applicable." msgstr "해당 없음." #: src/Navigation.tsx:109 -#: src/view/screens/Profile.tsx:97 +#: src/view/screens/Profile.tsx:99 msgid "Not Found" msgstr "찾을 수 없음" @@ -2794,10 +2771,11 @@ msgstr "나중에 하기" #: src/view/com/profile/ProfileMenu.tsx:368 #: src/view/com/util/forms/PostDropdownBtn.tsx:342 +#: src/view/com/util/post-ctrls/PostCtrls.tsx:246 msgid "Note about sharing" msgstr "공유 관련 참고 사항" -#: src/screens/Moderation/index.tsx:542 +#: src/screens/Moderation/index.tsx:540 msgid "Note: Bluesky is an open and public network. This setting only limits the visibility of your content on the Bluesky app and website, and other apps may not respect this setting. Your content may still be shown to logged-out users by other apps and websites." msgstr "참고: Bluesky는 개방형 공개 네트워크입니다. 이 설정은 Bluesky 앱과 웹사이트에서만 내 콘텐츠가 표시되는 것을 제한하며, 다른 앱에서는 이 설정을 준수하지 않을 수 있습니다. 다른 앱과 웹사이트에서는 로그아웃한 사용자에게 내 콘텐츠가 계속 표시될 수 있습니다." @@ -2819,6 +2797,10 @@ msgstr "노출" msgid "Nudity or pornography not labeled as such" msgstr "누드 또는 음란물로 설정되지 않은 콘텐츠" +#: src/screens/Signup/index.tsx:142 +msgid "of" +msgstr "" + #: src/lib/moderation/useLabelBehaviorDescription.ts:11 msgid "Off" msgstr "끄기" @@ -2827,15 +2809,16 @@ msgstr "끄기" msgid "Oh no!" msgstr "이런!" -#: src/screens/Onboarding/StepInterests/index.tsx:128 +#: src/screens/Onboarding/StepInterests/index.tsx:132 msgid "Oh no! Something went wrong." msgstr "이런! 뭔가 잘못되었습니다." -#: src/screens/Onboarding/StepModeration/AdultContentEnabledPref.tsx:127 +#: src/screens/Onboarding/StepModeration/AdultContentEnabledPref.tsx:126 +#: src/screens/Profile/Header/ProfileHeaderLabeler.tsx:325 msgid "OK" -msgstr "" +msgstr "확인" -#: src/view/com/auth/login/PasswordUpdatedForm.tsx:41 +#: src/screens/Login/PasswordUpdatedForm.tsx:44 msgid "Okay" msgstr "확인" @@ -2847,7 +2830,7 @@ msgstr "오래된 순" msgid "Onboarding reset" msgstr "온보딩 재설정" -#: src/view/com/composer/Composer.tsx:391 +#: src/view/com/composer/Composer.tsx:392 msgid "One or more images is missing alt text." msgstr "하나 이상의 이미지에 대체 텍스트가 누락되었습니다." @@ -2855,22 +2838,26 @@ msgstr "하나 이상의 이미지에 대체 텍스트가 누락되었습니다. msgid "Only {0} can reply." msgstr "{0}만 답글을 달 수 있습니다." +#: src/screens/Signup/StepHandle.tsx:97 +msgid "Only contains letters, numbers, and hyphens" +msgstr "문자, 숫자, 하이픈만 포함" + #: src/components/Lists.tsx:83 msgid "Oops, something went wrong!" msgstr "이런, 뭔가 잘못되었습니다!" #: src/components/Lists.tsx:157 #: src/view/screens/AppPasswords.tsx:67 -#: src/view/screens/Profile.tsx:97 +#: src/view/screens/Profile.tsx:99 msgid "Oops!" msgstr "이런!" -#: src/screens/Onboarding/StepFinished.tsx:115 +#: src/screens/Onboarding/StepFinished.tsx:116 msgid "Open" msgstr "공개성" -#: src/view/com/composer/Composer.tsx:490 #: src/view/com/composer/Composer.tsx:491 +#: src/view/com/composer/Composer.tsx:492 msgid "Open emoji picker" msgstr "이모티콘 선택기 열기" @@ -2882,7 +2869,7 @@ msgstr "피드 옵션 메뉴 열기" msgid "Open links with in-app browser" msgstr "링크를 인앱 브라우저로 열기" -#: src/screens/Moderation/index.tsx:229 +#: src/screens/Moderation/index.tsx:227 msgid "Open muted words and tags settings" msgstr "뮤트한 단어 및 태그 설정 열기" @@ -2901,7 +2888,7 @@ msgstr "스토리북 페이지 열기" #: src/view/screens/Settings/index.tsx:816 msgid "Open system log" -msgstr "" +msgstr "시스템 로그 열기" #: src/view/com/util/forms/DropdownButton.tsx:154 msgid "Opens {numItems} options" @@ -2935,15 +2922,17 @@ msgstr "기기의 사진 갤러리를 엽니다" msgid "Opens external embeds settings" msgstr "외부 임베드 설정을 엽니다" -#: src/view/com/auth/HomeLoggedOutCTA.tsx:56 -#: src/view/com/auth/SplashScreen.tsx:70 +#: src/view/com/auth/HomeLoggedOutCTA.tsx:57 +#: src/view/com/auth/SplashScreen.tsx:68 +#: src/view/com/auth/SplashScreen.web.tsx:97 msgid "Opens flow to create a new Bluesky account" -msgstr "" +msgstr "새 Bluesky 계정을 만드는 플로를 엽니다" -#: src/view/com/auth/HomeLoggedOutCTA.tsx:74 +#: src/view/com/auth/HomeLoggedOutCTA.tsx:75 #: src/view/com/auth/SplashScreen.tsx:83 +#: src/view/com/auth/SplashScreen.web.tsx:112 msgid "Opens flow to sign into your existing Bluesky account" -msgstr "" +msgstr "존재하는 Bluesky 계정에 로그인하는 플로를 엽니다" #: src/view/com/modals/InviteCodes.tsx:172 msgid "Opens list of invite codes" @@ -2951,27 +2940,23 @@ msgstr "초대 코드 목록을 엽니다" #: src/view/screens/Settings/index.tsx:798 msgid "Opens modal for account deletion confirmation. Requires email code" -msgstr "" - -#: src/view/screens/Settings/index.tsx:792 -#~ msgid "Opens modal for account deletion confirmation. Requires email code." -#~ msgstr "계정 삭제 확인을 위한 대화 상자를 엽니다. 이메일 코드가 필요합니다" +msgstr "계정 삭제 확인을 위한 대화 상자를 엽니다. 이메일 코드가 필요합니다" #: src/view/screens/Settings/index.tsx:756 msgid "Opens modal for changing your Bluesky password" -msgstr "" +msgstr "Bluesky 비밀번호 변경을 위한 대화 상자를 엽니다" #: src/view/screens/Settings/index.tsx:718 msgid "Opens modal for choosing a new Bluesky handle" -msgstr "" +msgstr "새로운 Bluesky 핸들을 선택하기 위한 대화 상자를 엽니다" #: src/view/screens/Settings/index.tsx:779 msgid "Opens modal for downloading your Bluesky account data (repository)" -msgstr "" +msgstr "Bluesky 계정 데이터(저장소)를 다운로드하기 위한 대화 상자를 엽니다" -#: src/view/screens/Settings/index.tsx:970 +#: src/view/screens/Settings/index.tsx:968 msgid "Opens modal for email verification" -msgstr "" +msgstr "이메일 인증을 위한 대화 상자를 엽니다" #: src/view/com/modals/ChangeHandle.tsx:281 msgid "Opens modal for using custom domain" @@ -2981,7 +2966,7 @@ msgstr "사용자 지정 도메인을 사용하기 위한 대화 상자를 엽 msgid "Opens moderation settings" msgstr "검토 설정을 엽니다" -#: src/view/com/auth/login/LoginForm.tsx:242 +#: src/screens/Login/LoginForm.tsx:202 msgid "Opens password reset form" msgstr "비밀번호 재설정 양식을 엽니다" @@ -2996,23 +2981,15 @@ msgstr "모든 저장된 피드 화면을 엽니다" #: src/view/screens/Settings/index.tsx:696 msgid "Opens the app password settings" -msgstr "" - -#: src/view/screens/Settings/index.tsx:694 -#~ msgid "Opens the app password settings page" -#~ msgstr "비밀번호 설정 페이지를 엽니다" +msgstr "비밀번호 설정을 엽니다" #: src/view/screens/Settings/index.tsx:554 msgid "Opens the Following feed preferences" -msgstr "" - -#: src/view/screens/Settings/index.tsx:553 -#~ msgid "Opens the home feed preferences" -#~ msgstr "홈 피드 설정을 엽니다" +msgstr "팔로우 중 피드 설정을 엽니다" #: src/view/com/modals/LinkWarning.tsx:76 msgid "Opens the linked website" -msgstr "" +msgstr "연결된 웹사이트를 엽니다" #: src/view/screens/Settings/index.tsx:829 #: src/view/screens/Settings/index.tsx:839 @@ -3033,7 +3010,7 @@ msgstr "{numItems}개 중 {0}번째 옵션" #: src/components/ReportDialog/SubmitView.tsx:162 msgid "Optionally provide additional information below:" -msgstr "선택 사항으로 아래에 추가 정보를 입력합니다:" +msgstr "선택 사항으로 아래에 추가 정보를 입력하세요:" #: src/view/com/modals/Threadgate.tsx:89 msgid "Or combine these options:" @@ -3043,7 +3020,7 @@ msgstr "또는 다음 옵션을 결합하세요:" msgid "Other" msgstr "기타" -#: src/view/com/auth/login/ChooseAccountForm.tsx:147 +#: src/screens/Login/ChooseAccountForm.tsx:167 msgid "Other account" msgstr "다른 계정" @@ -3060,25 +3037,22 @@ msgstr "페이지를 찾을 수 없음" msgid "Page Not Found" msgstr "페이지를 찾을 수 없음" -#: src/view/com/auth/create/Step1.tsx:191 -#: src/view/com/auth/create/Step1.tsx:201 -#: src/view/com/auth/login/LoginForm.tsx:213 -#: src/view/com/auth/login/LoginForm.tsx:229 -#: src/view/com/auth/login/SetNewPasswordForm.tsx:161 -#: src/view/com/modals/DeleteAccount.tsx:195 -#: src/view/com/modals/DeleteAccount.tsx:202 +#: src/screens/Login/LoginForm.tsx:178 +#: src/screens/Signup/StepInfo/index.tsx:101 +#: src/view/com/modals/DeleteAccount.tsx:194 +#: src/view/com/modals/DeleteAccount.tsx:201 msgid "Password" msgstr "비밀번호" #: src/view/com/modals/ChangePassword.tsx:142 msgid "Password Changed" -msgstr "" +msgstr "비밀번호 변경됨" -#: src/view/com/auth/login/Login.tsx:157 +#: src/screens/Login/index.tsx:157 msgid "Password updated" msgstr "비밀번호 변경됨" -#: src/view/com/auth/login/PasswordUpdatedForm.tsx:28 +#: src/screens/Login/PasswordUpdatedForm.tsx:30 msgid "Password updated!" msgstr "비밀번호 변경됨" @@ -3132,15 +3106,15 @@ msgstr "동영상 재생" msgid "Plays the GIF" msgstr "GIF를 재생합니다" -#: src/view/com/auth/create/state.ts:124 +#: src/screens/Signup/state.ts:241 msgid "Please choose your handle." msgstr "핸들을 입력하세요." -#: src/view/com/auth/create/state.ts:117 +#: src/screens/Signup/state.ts:234 msgid "Please choose your password." msgstr "비밀번호를 입력하세요." -#: src/view/com/auth/create/state.ts:131 +#: src/screens/Signup/state.ts:251 msgid "Please complete the verification captcha." msgstr "인증 캡차를 완료해 주세요." @@ -3160,23 +3134,23 @@ msgstr "이 앱 비밀번호에 대해 고유한 이름을 입력하거나 무 msgid "Please enter a valid word, tag, or phrase to mute" msgstr "뮤트할 단어나 태그 또는 문구를 입력하세요" -#: src/view/com/auth/create/state.ts:103 +#: src/screens/Signup/state.ts:220 msgid "Please enter your email." msgstr "이메일을 입력하세요." -#: src/view/com/modals/DeleteAccount.tsx:191 +#: src/view/com/modals/DeleteAccount.tsx:190 msgid "Please enter your password as well:" msgstr "비밀번호도 입력해 주세요:" #: src/components/moderation/LabelsOnMeDialog.tsx:222 msgid "Please explain why you think this label was incorrectly applied by {0}" -msgstr "{0}이(가) 이 라벨을 잘못 적용했다고 생각하는 이유를 설명해 주세요" +msgstr "{0} 님이 이 라벨을 잘못 적용했다고 생각하는 이유를 설명해 주세요" #: src/view/com/modals/VerifyEmail.tsx:101 msgid "Please Verify Your Email" msgstr "이메일 인증하기" -#: src/view/com/composer/Composer.tsx:221 +#: src/view/com/composer/Composer.tsx:222 msgid "Please wait for your link card to finish loading" msgstr "링크 카드를 완전히 불러올 때까지 기다려주세요" @@ -3192,8 +3166,8 @@ msgstr "음란물" msgid "Pornography" msgstr "음란물" -#: src/view/com/composer/Composer.tsx:366 -#: src/view/com/composer/Composer.tsx:374 +#: src/view/com/composer/Composer.tsx:367 +#: src/view/com/composer/Composer.tsx:375 msgctxt "action" msgid "Post" msgstr "게시하기" @@ -3248,7 +3222,7 @@ msgstr "게시물을 찾을 수 없음" msgid "posts" msgstr "게시물" -#: src/view/screens/Profile.tsx:188 +#: src/view/screens/Profile.tsx:190 msgid "Posts" msgstr "게시물" @@ -3264,9 +3238,15 @@ msgstr "게시물 숨겨짐" msgid "Potentially Misleading Link" msgstr "오해의 소지가 있는 링크" +#: src/components/forms/HostingProvider.tsx:45 +msgid "Press to change hosting provider" +msgstr "호스팅 제공자를 변경하려면 누릅니다" + +#: src/components/Error.tsx:74 #: src/components/Lists.tsx:88 +#: src/screens/Signup/index.tsx:186 msgid "Press to retry" -msgstr "" +msgstr "눌러서 다시 시도하기" #: src/view/com/lightbox/Lightbox.web.tsx:150 msgid "Previous image" @@ -3286,19 +3266,19 @@ msgid "Privacy" msgstr "개인정보" #: src/Navigation.tsx:231 -#: src/view/com/auth/create/Policies.tsx:69 +#: src/screens/Signup/StepInfo/Policies.tsx:56 #: src/view/screens/PrivacyPolicy.tsx:29 -#: src/view/screens/Settings/index.tsx:925 +#: src/view/screens/Settings/index.tsx:923 #: src/view/shell/Drawer.tsx:265 msgid "Privacy Policy" msgstr "개인정보 처리방침" -#: src/view/com/auth/login/ForgotPasswordForm.tsx:198 +#: src/screens/Login/ForgotPasswordForm.tsx:156 msgid "Processing..." msgstr "처리 중…" #: src/view/screens/DebugMod.tsx:888 -#: src/view/screens/Profile.tsx:340 +#: src/view/screens/Profile.tsx:342 msgid "profile" msgstr "프로필" @@ -3314,11 +3294,11 @@ msgstr "프로필" msgid "Profile updated" msgstr "프로필 업데이트됨" -#: src/view/screens/Settings/index.tsx:983 +#: src/view/screens/Settings/index.tsx:981 msgid "Protect your account by verifying your email." msgstr "이메일을 인증하여 계정을 보호하세요." -#: src/screens/Onboarding/StepFinished.tsx:101 +#: src/screens/Onboarding/StepFinished.tsx:102 msgid "Public" msgstr "공공성" @@ -3330,11 +3310,11 @@ msgstr "일괄 뮤트하거나 차단할 수 있는 공개적이고 공유 가 msgid "Public, shareable lists which can drive feeds." msgstr "피드를 탐색할 수 있는 공개적이고 공유 가능한 목록입니다." -#: src/view/com/composer/Composer.tsx:351 +#: src/view/com/composer/Composer.tsx:352 msgid "Publish post" msgstr "게시물 게시하기" -#: src/view/com/composer/Composer.tsx:351 +#: src/view/com/composer/Composer.tsx:352 msgid "Publish reply" msgstr "답글 게시하기" @@ -3360,9 +3340,9 @@ msgstr "무작위" msgid "Ratios" msgstr "비율" -#: src/view/screens/Search/Search.tsx:776 +#: src/view/screens/Search/Search.tsx:777 msgid "Recent Searches" -msgstr "" +msgstr "최근 검색" #: src/view/com/auth/onboarding/RecommendedFeeds.tsx:116 msgid "Recommended Feeds" @@ -3449,7 +3429,7 @@ msgstr "내 피드에서 제거됨" msgid "Removes default thumbnail from {0}" msgstr "{0}에서 기본 미리보기 이미지를 제거합니다" -#: src/view/screens/Profile.tsx:189 +#: src/view/screens/Profile.tsx:191 msgid "Replies" msgstr "답글" @@ -3457,7 +3437,7 @@ msgstr "답글" msgid "Replies to this thread are disabled" msgstr "이 스레드에 대한 답글이 비활성화됩니다." -#: src/view/com/composer/Composer.tsx:364 +#: src/view/com/composer/Composer.tsx:365 msgctxt "action" msgid "Reply" msgstr "답글" @@ -3477,6 +3457,10 @@ msgstr "<0/> 님에게 보내는 답글" msgid "Report Account" msgstr "계정 신고" +#: src/components/ReportDialog/index.tsx:49 +msgid "Report dialog" +msgstr "신고 대화 상자" + #: src/view/screens/ProfileFeed.tsx:351 #: src/view/screens/ProfileFeed.tsx:353 msgid "Report feed" @@ -3491,23 +3475,23 @@ msgstr "리스트 신고" msgid "Report post" msgstr "게시물 신고" -#: src/components/ReportDialog/SelectReportOptionView.tsx:43 +#: src/components/ReportDialog/SelectReportOptionView.tsx:42 msgid "Report this content" msgstr "이 콘텐츠 신고하기" -#: src/components/ReportDialog/SelectReportOptionView.tsx:56 +#: src/components/ReportDialog/SelectReportOptionView.tsx:55 msgid "Report this feed" msgstr "이 피드 신고하기" -#: src/components/ReportDialog/SelectReportOptionView.tsx:53 +#: src/components/ReportDialog/SelectReportOptionView.tsx:52 msgid "Report this list" msgstr "이 리스트 신고하기" -#: src/components/ReportDialog/SelectReportOptionView.tsx:50 +#: src/components/ReportDialog/SelectReportOptionView.tsx:49 msgid "Report this post" msgstr "이 게시물 신고하기" -#: src/components/ReportDialog/SelectReportOptionView.tsx:47 +#: src/components/ReportDialog/SelectReportOptionView.tsx:46 msgid "Report this user" msgstr "이 사용자 신고하기" @@ -3562,12 +3546,10 @@ msgstr "코드 요청" msgid "Require alt text before posting" msgstr "게시하기 전 대체 텍스트 필수" -#: src/view/com/auth/create/Step1.tsx:146 +#: src/screens/Signup/StepInfo/index.tsx:69 msgid "Required for this provider" msgstr "이 제공자에서 필수" -#: src/view/com/auth/login/SetNewPasswordForm.tsx:124 -#: src/view/com/auth/login/SetNewPasswordForm.tsx:136 #: src/view/com/modals/ChangePassword.tsx:185 msgid "Reset code" msgstr "재설정 코드" @@ -3576,23 +3558,15 @@ msgstr "재설정 코드" msgid "Reset Code" msgstr "재설정 코드" -#: src/view/screens/Settings/index.tsx:852 -#~ msgid "Reset onboarding" -#~ msgstr "온보딩 초기화" - #: src/view/screens/Settings/index.tsx:858 #: src/view/screens/Settings/index.tsx:861 msgid "Reset onboarding state" msgstr "온보딩 상태 초기화" -#: src/view/com/auth/login/ForgotPasswordForm.tsx:104 +#: src/screens/Login/ForgotPasswordForm.tsx:86 msgid "Reset password" msgstr "비밀번호 재설정" -#: src/view/screens/Settings/index.tsx:842 -#~ msgid "Reset preferences" -#~ msgstr "설정 초기화" - #: src/view/screens/Settings/index.tsx:848 #: src/view/screens/Settings/index.tsx:851 msgid "Reset preferences state" @@ -3606,7 +3580,7 @@ msgstr "온보딩 상태 초기화" msgid "Resets the preferences state" msgstr "설정 상태 초기화" -#: src/view/com/auth/login/LoginForm.tsx:272 +#: src/screens/Login/LoginForm.tsx:235 msgid "Retries login" msgstr "로그인을 다시 시도합니다" @@ -3615,30 +3589,31 @@ msgstr "로그인을 다시 시도합니다" msgid "Retries the last action, which errored out" msgstr "오류가 발생한 마지막 작업을 다시 시도합니다" +#: src/components/Error.tsx:79 #: src/components/Lists.tsx:98 -#: src/screens/Onboarding/StepInterests/index.tsx:221 -#: src/screens/Onboarding/StepInterests/index.tsx:224 -#: src/view/com/auth/create/CreateAccount.tsx:181 -#: src/view/com/auth/create/CreateAccount.tsx:186 -#: src/view/com/auth/login/LoginForm.tsx:271 -#: src/view/com/auth/login/LoginForm.tsx:274 +#: src/screens/Login/LoginForm.tsx:234 +#: src/screens/Login/LoginForm.tsx:240 +#: src/screens/Onboarding/StepInterests/index.tsx:225 +#: src/screens/Onboarding/StepInterests/index.tsx:228 +#: src/screens/Signup/index.tsx:193 #: src/view/com/util/error/ErrorMessage.tsx:55 #: src/view/com/util/error/ErrorScreen.tsx:72 msgid "Retry" msgstr "다시 시도" +#: src/components/Error.tsx:86 #: src/view/screens/ProfileList.tsx:917 msgid "Return to previous page" msgstr "이전 페이지로 돌아갑니다" #: src/view/screens/NotFound.tsx:59 msgid "Returns to home page" -msgstr "" +msgstr "홈 페이지로 돌아갑니다" #: src/view/screens/NotFound.tsx:58 #: src/view/screens/ProfileFeed.tsx:112 msgid "Returns to previous page" -msgstr "" +msgstr "이전 페이지로 돌아갑니다" #: src/components/dialogs/BirthDateSettings.tsx:125 #: src/view/com/modals/ChangeHandle.tsx:173 @@ -3684,7 +3659,7 @@ msgstr "저장된 피드" #: src/view/com/lightbox/Lightbox.tsx:81 msgid "Saved to your camera roll." -msgstr "" +msgstr "내 앨범에 저장됨" #: src/view/screens/ProfileFeed.tsx:212 msgid "Saved to your feeds" @@ -3700,7 +3675,7 @@ msgstr "핸들을 {handle}(으)로 변경합니다" #: src/view/com/modals/crop-image/CropImage.web.tsx:145 msgid "Saves image crop settings" -msgstr "" +msgstr "이미지 자르기 설정을 저장합니다" #: src/screens/Onboarding/index.tsx:36 msgid "Science" @@ -3711,13 +3686,13 @@ msgid "Scroll to top" msgstr "맨 위로 스크롤" #: src/Navigation.tsx:459 -#: src/view/com/auth/LoggedOut.tsx:122 +#: src/view/com/auth/LoggedOut.tsx:123 #: src/view/com/modals/ListAddRemoveUsers.tsx:75 #: src/view/com/util/forms/SearchInput.tsx:67 #: src/view/com/util/forms/SearchInput.tsx:79 -#: src/view/screens/Search/Search.tsx:420 -#: src/view/screens/Search/Search.tsx:669 -#: src/view/screens/Search/Search.tsx:687 +#: src/view/screens/Search/Search.tsx:421 +#: src/view/screens/Search/Search.tsx:670 +#: src/view/screens/Search/Search.tsx:688 #: src/view/shell/bottom-bar/BottomBar.tsx:161 #: src/view/shell/desktop/LeftNav.tsx:328 #: src/view/shell/desktop/Search.tsx:215 @@ -3727,7 +3702,7 @@ msgstr "맨 위로 스크롤" msgid "Search" msgstr "검색" -#: src/view/screens/Search/Search.tsx:736 +#: src/view/screens/Search/Search.tsx:737 #: src/view/shell/desktop/Search.tsx:256 msgid "Search for \"{query}\"" msgstr "\"{query}\"에 대한 검색 결과" @@ -3740,8 +3715,8 @@ msgstr "{displayTag} 태그를 사용한 @{authorHandle} 님의 모든 게시물 msgid "Search for all posts with tag {displayTag}" msgstr "{displayTag} 태그를 사용한 모든 게시물 검색" -#: src/view/com/auth/LoggedOut.tsx:104 #: src/view/com/auth/LoggedOut.tsx:105 +#: src/view/com/auth/LoggedOut.tsx:106 #: src/view/com/modals/ListAddRemoveUsers.tsx:70 msgid "Search for users" msgstr "사용자 검색하기" @@ -3770,7 +3745,7 @@ msgstr "이 사용자의 <0>{displayTag} 게시물 보기" msgid "See this guide" msgstr "이 가이드" -#: src/view/com/auth/HomeLoggedOutCTA.tsx:39 +#: src/view/com/auth/HomeLoggedOutCTA.tsx:40 msgid "See what's next" msgstr "See what's next" @@ -3778,48 +3753,43 @@ msgstr "See what's next" msgid "Select {item}" msgstr "{item} 선택" -#: src/view/com/auth/login/Login.tsx:117 +#: src/screens/Login/ChooseAccountForm.tsx:123 +msgid "Select account" +msgstr "계정 선택" + +#: src/screens/Login/index.tsx:120 msgid "Select from an existing account" msgstr "기존 계정에서 선택" #: src/view/screens/LanguageSettings.tsx:299 msgid "Select languages" -msgstr "" +msgstr "언어 선택" #: src/components/ReportDialog/SelectLabelerView.tsx:30 -#~ msgid "Select moderation service" -#~ msgstr "검토 서비스 선택하기" - -#: src/components/ReportDialog/SelectLabelerView.tsx:32 msgid "Select moderator" -msgstr "" +msgstr "검토자 선택" #: src/view/com/util/Selector.tsx:107 msgid "Select option {i} of {numItems}" msgstr "{numItems}개 중 {i}번째 옵션을 선택합니다" -#: src/view/com/auth/create/Step1.tsx:96 -#: src/view/com/auth/login/LoginForm.tsx:153 -msgid "Select service" -msgstr "서비스 선택" - #: src/screens/Onboarding/StepSuggestedAccounts/index.tsx:52 msgid "Select some accounts below to follow" msgstr "아래에서 팔로우할 계정을 선택하세요" #: src/components/ReportDialog/SubmitView.tsx:135 msgid "Select the moderation service(s) to report to" -msgstr "신고할 검토 서비스를 선택합니다." +msgstr "신고할 검토 서비스를 선택하세요." #: src/view/com/auth/server-input/index.tsx:82 msgid "Select the service that hosts your data." msgstr "데이터를 호스팅할 서비스를 선택하세요." -#: src/screens/Onboarding/StepTopicalFeeds.tsx:96 +#: src/screens/Onboarding/StepTopicalFeeds.tsx:100 msgid "Select topical feeds to follow from the list below" msgstr "아래 목록에서 팔로우할 화제 피드를 선택하세요" -#: src/screens/Onboarding/StepModeration/index.tsx:62 +#: src/screens/Onboarding/StepModeration/index.tsx:63 msgid "Select what you want to see (or not see), and we’ll handle the rest." msgstr "보고 싶거나 보고 싶지 않은 항목을 선택하면 나머지는 알아서 처리해 드립니다." @@ -3827,15 +3797,15 @@ msgstr "보고 싶거나 보고 싶지 않은 항목을 선택하면 나머지 msgid "Select which languages you want your subscribed feeds to include. If none are selected, all languages will be shown." msgstr "구독하는 피드에 포함할 언어를 선택합니다. 선택하지 않으면 모든 언어가 표시됩니다." -#: src/view/screens/LanguageSettings.tsx:98 -#~ msgid "Select your app language for the default text to display in the app" -#~ msgstr "앱에 표시되는 기본 텍스트 언어를 선택합니다." - #: src/view/screens/LanguageSettings.tsx:98 msgid "Select your app language for the default text to display in the app." -msgstr "" +msgstr "앱에 표시되는 기본 텍스트 언어를 선택합니다." + +#: src/screens/Signup/StepInfo/index.tsx:133 +msgid "Select your date of birth" +msgstr "생년월일을 선택하세요" -#: src/screens/Onboarding/StepInterests/index.tsx:196 +#: src/screens/Onboarding/StepInterests/index.tsx:200 msgid "Select your interests from the options below" msgstr "아래 옵션에서 관심사를 선택하세요" @@ -3843,11 +3813,11 @@ msgstr "아래 옵션에서 관심사를 선택하세요" msgid "Select your preferred language for translations in your feed." msgstr "피드에서 번역을 위해 선호하는 언어를 선택합니다." -#: src/screens/Onboarding/StepAlgoFeeds/index.tsx:116 +#: src/screens/Onboarding/StepAlgoFeeds/index.tsx:122 msgid "Select your primary algorithmic feeds" msgstr "기본 알고리즘 피드를 선택하세요" -#: src/screens/Onboarding/StepAlgoFeeds/index.tsx:142 +#: src/screens/Onboarding/StepAlgoFeeds/index.tsx:148 msgid "Select your secondary algorithmic feeds" msgstr "보조 알고리즘 피드를 선택하세요" @@ -3856,11 +3826,11 @@ msgstr "보조 알고리즘 피드를 선택하세요" msgid "Send Confirmation Email" msgstr "확인 이메일 보내기" -#: src/view/com/modals/DeleteAccount.tsx:131 +#: src/view/com/modals/DeleteAccount.tsx:130 msgid "Send email" msgstr "이메일 보내기" -#: src/view/com/modals/DeleteAccount.tsx:144 +#: src/view/com/modals/DeleteAccount.tsx:143 msgctxt "action" msgid "Send Email" msgstr "이메일 보내기" @@ -3875,11 +3845,11 @@ msgstr "피드백 보내기" msgid "Send report" msgstr "신고 보내기" -#: src/components/ReportDialog/SelectLabelerView.tsx:46 +#: src/components/ReportDialog/SelectLabelerView.tsx:44 msgid "Send report to {0}" msgstr "{0} 님에게 신고 보내기" -#: src/view/com/modals/DeleteAccount.tsx:133 +#: src/view/com/modals/DeleteAccount.tsx:132 msgid "Sends email with confirmation code for account deletion" msgstr "계정 삭제를 위한 확인 코드가 포함된 이메일을 전송합니다" @@ -3887,38 +3857,14 @@ msgstr "계정 삭제를 위한 확인 코드가 포함된 이메일을 전송 msgid "Server address" msgstr "서버 주소" -#: src/screens/Moderation/index.tsx:306 +#: src/screens/Moderation/index.tsx:304 msgid "Set birthdate" msgstr "생년월일 설정" -#: src/view/screens/Settings/index.tsx:506 -#~ msgid "Set color theme to dark" -#~ msgstr "색상 테마를 어두움으로 설정합니다" - -#: src/view/screens/Settings/index.tsx:499 -#~ msgid "Set color theme to light" -#~ msgstr "색상 테마를 밝음으로 설정합니다" - -#: src/view/screens/Settings/index.tsx:493 -#~ msgid "Set color theme to system setting" -#~ msgstr "색상 테마를 시스템 설정에 맞춥니다" - -#: src/view/screens/Settings/index.tsx:532 -#~ msgid "Set dark theme to the dark theme" -#~ msgstr "어두운 테마를 완전히 어둡게 설정합니다" - -#: src/view/screens/Settings/index.tsx:525 -#~ msgid "Set dark theme to the dim theme" -#~ msgstr "어두운 테마를 살짝 밝게 설정합니다" - -#: src/view/com/auth/login/SetNewPasswordForm.tsx:104 +#: src/screens/Login/SetNewPasswordForm.tsx:102 msgid "Set new password" msgstr "새 비밀번호 설정" -#: src/view/com/auth/create/Step1.tsx:202 -msgid "Set password" -msgstr "비밀번호 설정" - #: src/view/screens/PreferencesFollowingFeed.tsx:225 msgid "Set this setting to \"No\" to hide all quote posts from your feed. Reposts will still be visible." msgstr "피드에서 모든 인용 게시물을 숨기려면 이 설정을 \"아니요\"로 설정합니다. 재게시는 계속 표시됩니다." @@ -3949,48 +3895,39 @@ msgstr "Bluesky 사용자 이름을 설정합니다" #: src/view/screens/Settings/index.tsx:507 msgid "Sets color theme to dark" -msgstr "" +msgstr "색상 테마를 어두움으로 설정합니다" #: src/view/screens/Settings/index.tsx:500 msgid "Sets color theme to light" -msgstr "" +msgstr "색상 테마를 밝음으로 설정합니다" #: src/view/screens/Settings/index.tsx:494 msgid "Sets color theme to system setting" -msgstr "" +msgstr "색상 테마를 시스템 설정에 맞춥니다" #: src/view/screens/Settings/index.tsx:533 msgid "Sets dark theme to the dark theme" -msgstr "" +msgstr "어두운 테마를 완전히 어둡게 설정합니다" #: src/view/screens/Settings/index.tsx:526 msgid "Sets dark theme to the dim theme" -msgstr "" +msgstr "어두운 테마를 살짝 밝게 설정합니다" -#: src/view/com/auth/login/ForgotPasswordForm.tsx:157 +#: src/screens/Login/ForgotPasswordForm.tsx:113 msgid "Sets email for password reset" msgstr "비밀번호 재설정을 위한 이메일을 설정합니다" -#: src/view/com/auth/login/ForgotPasswordForm.tsx:122 -msgid "Sets hosting provider for password reset" -msgstr "비밀번호 재설정을 위한 호스팅 제공자를 설정합니다" - #: src/view/com/modals/crop-image/CropImage.web.tsx:123 msgid "Sets image aspect ratio to square" -msgstr "" +msgstr "이미지 비율을 정사각형으로 설정합니다" #: src/view/com/modals/crop-image/CropImage.web.tsx:113 msgid "Sets image aspect ratio to tall" -msgstr "" +msgstr "이미지 비율을 세로로 길게 설정합니다" #: src/view/com/modals/crop-image/CropImage.web.tsx:103 msgid "Sets image aspect ratio to wide" -msgstr "" - -#: src/view/com/auth/create/Step1.tsx:97 -#: src/view/com/auth/login/LoginForm.tsx:154 -msgid "Sets server for the Bluesky client" -msgstr "Bluesky 클라이언트를 위한 서버를 설정합니다" +msgstr "이미지 비율을 가로로 길게 설정합니다" #: src/Navigation.tsx:139 #: src/view/screens/Settings/index.tsx:313 @@ -4017,13 +3954,14 @@ msgstr "공유" #: src/view/com/profile/ProfileMenu.tsx:224 #: src/view/com/util/forms/PostDropdownBtn.tsx:228 #: src/view/com/util/forms/PostDropdownBtn.tsx:237 -#: src/view/com/util/post-ctrls/PostCtrls.tsx:218 +#: src/view/com/util/post-ctrls/PostCtrls.tsx:235 #: src/view/screens/ProfileList.tsx:388 msgid "Share" msgstr "공유" #: src/view/com/profile/ProfileMenu.tsx:373 #: src/view/com/util/forms/PostDropdownBtn.tsx:347 +#: src/view/com/util/post-ctrls/PostCtrls.tsx:251 msgid "Share anyway" msgstr "무시하고 공유" @@ -4033,7 +3971,7 @@ msgid "Share feed" msgstr "피드 공유" #: src/components/moderation/ContentHider.tsx:115 -#: src/components/moderation/GlobalModerationLabelPref.tsx:45 +#: src/components/moderation/LabelPreference.tsx:136 #: src/components/moderation/PostHider.tsx:107 #: src/screens/Onboarding/StepModeration/ModerationOption.tsx:54 #: src/view/screens/Settings/index.tsx:363 @@ -4044,8 +3982,8 @@ msgstr "표시" msgid "Show all replies" msgstr "모든 답글 표시" -#: src/components/moderation/ScreenHider.tsx:162 -#: src/components/moderation/ScreenHider.tsx:165 +#: src/components/moderation/ScreenHider.tsx:169 +#: src/components/moderation/ScreenHider.tsx:172 msgid "Show anyway" msgstr "무시하고 표시" @@ -4080,15 +4018,15 @@ msgstr "내 피드에서 게시물 표시" msgid "Show Quote Posts" msgstr "인용 게시물 표시" -#: src/screens/Onboarding/StepFollowingFeed.tsx:118 +#: src/screens/Onboarding/StepFollowingFeed.tsx:119 msgid "Show quote-posts in Following feed" msgstr "팔로우 중 피드에 인용 게시물 표시" -#: src/screens/Onboarding/StepFollowingFeed.tsx:134 +#: src/screens/Onboarding/StepFollowingFeed.tsx:135 msgid "Show quotes in Following" msgstr "팔로우 중 피드에 인용 표시" -#: src/screens/Onboarding/StepFollowingFeed.tsx:94 +#: src/screens/Onboarding/StepFollowingFeed.tsx:95 msgid "Show re-posts in Following feed" msgstr "팔로우 중 피드에 재게시 표시" @@ -4100,11 +4038,11 @@ msgstr "답글 표시" msgid "Show replies by people you follow before all other replies." msgstr "내가 팔로우하는 사람들의 답글을 다른 모든 답글보다 먼저 표시합니다." -#: src/screens/Onboarding/StepFollowingFeed.tsx:86 +#: src/screens/Onboarding/StepFollowingFeed.tsx:87 msgid "Show replies in Following" msgstr "팔로우 중 피드에 답글 표시" -#: src/screens/Onboarding/StepFollowingFeed.tsx:70 +#: src/screens/Onboarding/StepFollowingFeed.tsx:71 msgid "Show replies in Following feed" msgstr "팔로우 중 피드에 답글 표시" @@ -4116,7 +4054,7 @@ msgstr "좋아요가 {value}개 이상인 답글 표시" msgid "Show Reposts" msgstr "재게시 표시" -#: src/screens/Onboarding/StepFollowingFeed.tsx:110 +#: src/screens/Onboarding/StepFollowingFeed.tsx:111 msgid "Show reposts in Following" msgstr "팔로우 중 피드에 재게시 표시" @@ -4141,9 +4079,15 @@ msgstr "경고 표시 및 피드에서 필터링" msgid "Shows posts from {0} in your feed" msgstr "피드에 {0} 님의 게시물을 표시합니다" -#: src/view/com/auth/HomeLoggedOutCTA.tsx:72 -#: src/view/com/auth/login/Login.tsx:98 +#: src/screens/Login/index.tsx:100 +#: src/screens/Login/index.tsx:119 +#: src/screens/Login/LoginForm.tsx:131 +#: src/view/com/auth/HomeLoggedOutCTA.tsx:73 +#: src/view/com/auth/HomeLoggedOutCTA.tsx:83 #: src/view/com/auth/SplashScreen.tsx:81 +#: src/view/com/auth/SplashScreen.tsx:90 +#: src/view/com/auth/SplashScreen.web.tsx:110 +#: src/view/com/auth/SplashScreen.web.tsx:119 #: src/view/shell/bottom-bar/BottomBar.tsx:289 #: src/view/shell/bottom-bar/BottomBar.tsx:290 #: src/view/shell/bottom-bar/BottomBar.tsx:292 @@ -4157,26 +4101,21 @@ msgid "Sign in" msgstr "로그인" #: src/view/com/auth/HomeLoggedOutCTA.tsx:82 -#: src/view/com/auth/SplashScreen.tsx:86 -#: src/view/com/auth/SplashScreen.web.tsx:91 -msgid "Sign In" -msgstr "로그인" +#: src/view/com/auth/SplashScreen.tsx:90 +#: src/view/com/auth/SplashScreen.web.tsx:118 +#~ msgid "Sign In" +#~ msgstr "로그인" -#: src/view/com/auth/login/ChooseAccountForm.tsx:45 +#: src/screens/Login/ChooseAccountForm.tsx:48 msgid "Sign in as {0}" msgstr "{0}(으)로 로그인" -#: src/view/com/auth/login/ChooseAccountForm.tsx:127 -#: src/view/com/auth/login/Login.tsx:116 +#: src/screens/Login/ChooseAccountForm.tsx:126 msgid "Sign in as..." msgstr "로그인" -#: src/view/com/auth/login/LoginForm.tsx:140 -msgid "Sign into" -msgstr "로그인" - -#: src/view/com/modals/SwitchAccount.tsx:68 -#: src/view/com/modals/SwitchAccount.tsx:73 +#: src/view/com/modals/SwitchAccount.tsx:69 +#: src/view/com/modals/SwitchAccount.tsx:74 #: src/view/screens/Settings/index.tsx:107 #: src/view/screens/Settings/index.tsx:110 msgid "Sign out" @@ -4198,7 +4137,7 @@ msgstr "가입하기" msgid "Sign up or sign in to join the conversation" msgstr "가입 또는 로그인하여 대화에 참여하세요" -#: src/components/moderation/ScreenHider.tsx:98 +#: src/components/moderation/ScreenHider.tsx:97 #: src/lib/moderation/useGlobalLabelStrings.ts:28 msgid "Sign-in Required" msgstr "로그인 필요" @@ -4207,21 +4146,21 @@ msgstr "로그인 필요" msgid "Signed in as" msgstr "로그인한 계정" -#: src/view/com/auth/login/ChooseAccountForm.tsx:112 +#: src/screens/Login/ChooseAccountForm.tsx:110 msgid "Signed in as @{0}" msgstr "@{0}(으)로 로그인했습니다" -#: src/view/com/modals/SwitchAccount.tsx:70 +#: src/view/com/modals/SwitchAccount.tsx:71 msgid "Signs {0} out of Bluesky" msgstr "Bluesky에서 {0}을(를) 로그아웃합니다" -#: src/screens/Onboarding/StepInterests/index.tsx:235 -#: src/screens/Onboarding/StepSuggestedAccounts/index.tsx:195 +#: src/screens/Onboarding/StepInterests/index.tsx:239 +#: src/screens/Onboarding/StepSuggestedAccounts/index.tsx:203 #: src/view/com/auth/onboarding/WelcomeMobile.tsx:35 msgid "Skip" msgstr "건너뛰기" -#: src/screens/Onboarding/StepInterests/index.tsx:232 +#: src/screens/Onboarding/StepInterests/index.tsx:236 msgid "Skip this flow" msgstr "이 단계 건너뛰기" @@ -4229,17 +4168,13 @@ msgstr "이 단계 건너뛰기" msgid "Software Dev" msgstr "소프트웨어 개발" -#: src/components/ReportDialog/index.tsx:52 -#: src/screens/Moderation/index.tsx:116 -#: src/screens/Profile/Sections/Labels.tsx:77 +#: src/components/ReportDialog/index.tsx:59 +#: src/screens/Moderation/index.tsx:114 +#: src/screens/Profile/Sections/Labels.tsx:76 msgid "Something went wrong, please try again." msgstr "뭔가 잘못되었습니다. 다시 시도해 주세요." -#: src/components/Lists.tsx:202 -#~ msgid "Something went wrong!" -#~ msgstr "뭔가 잘못되었습니다!" - -#: src/App.native.tsx:71 +#: src/App.native.tsx:68 msgid "Sorry! Your session expired. Please log in again." msgstr "죄송합니다. 세션이 만료되었습니다. 다시 로그인해 주세요." @@ -4271,13 +4206,13 @@ msgstr "스포츠" msgid "Square" msgstr "정사각형" -#: src/view/screens/Settings/index.tsx:905 +#: src/view/screens/Settings/index.tsx:903 msgid "Status page" msgstr "상태 페이지" -#: src/view/com/auth/create/StepHeader.tsx:22 -msgid "Step {0} of {numSteps}" -msgstr "{numSteps}단계 중 {0}단계" +#: src/screens/Signup/index.tsx:142 +msgid "Step" +msgstr "" #: src/view/screens/Settings/index.tsx:292 msgid "Storage cleared, you need to restart the app now." @@ -4297,11 +4232,11 @@ msgstr "확인" msgid "Subscribe" msgstr "구독" -#: src/screens/Profile/Sections/Labels.tsx:181 +#: src/screens/Profile/Sections/Labels.tsx:180 msgid "Subscribe to @{0} to use these labels:" msgstr "이 라벨을 사용하려면 @{0} 님을 구독하세요:" -#: src/screens/Profile/Header/ProfileHeaderLabeler.tsx:222 +#: src/screens/Profile/Header/ProfileHeaderLabeler.tsx:221 msgid "Subscribe to Labeler" msgstr "라벨러 구독" @@ -4310,7 +4245,7 @@ msgstr "라벨러 구독" msgid "Subscribe to the {0} feed" msgstr "{0} 피드 구독하기" -#: src/screens/Profile/Header/ProfileHeaderLabeler.tsx:185 +#: src/screens/Profile/Header/ProfileHeaderLabeler.tsx:184 msgid "Subscribe to this labeler" msgstr "이 라벨러 구독하기" @@ -4318,7 +4253,7 @@ msgstr "이 라벨러 구독하기" msgid "Subscribe to this list" msgstr "이 리스트 구독하기" -#: src/view/screens/Search/Search.tsx:375 +#: src/view/screens/Search/Search.tsx:376 msgid "Suggested Follows" msgstr "팔로우 추천" @@ -4336,16 +4271,16 @@ msgstr "외설적" msgid "Support" msgstr "지원" -#: src/view/com/modals/SwitchAccount.tsx:123 +#: src/view/com/modals/SwitchAccount.tsx:124 msgid "Switch Account" msgstr "계정 전환" -#: src/view/com/modals/SwitchAccount.tsx:103 +#: src/view/com/modals/SwitchAccount.tsx:104 #: src/view/screens/Settings/index.tsx:139 msgid "Switch to {0}" msgstr "{0}(으)로 전환" -#: src/view/com/modals/SwitchAccount.tsx:104 +#: src/view/com/modals/SwitchAccount.tsx:105 #: src/view/screens/Settings/index.tsx:140 msgid "Switches the account you are logged in to" msgstr "로그인한 계정을 전환합니다" @@ -4383,8 +4318,8 @@ msgid "Terms" msgstr "이용약관" #: src/Navigation.tsx:236 -#: src/view/com/auth/create/Policies.tsx:59 -#: src/view/screens/Settings/index.tsx:919 +#: src/screens/Signup/StepInfo/Policies.tsx:49 +#: src/view/screens/Settings/index.tsx:917 #: src/view/screens/TermsOfService.tsx:29 #: src/view/shell/Drawer.tsx:259 msgid "Terms of Service" @@ -4410,9 +4345,9 @@ msgstr "감사합니다. 신고를 전송했습니다." #: src/view/com/modals/ChangeHandle.tsx:466 msgid "That contains the following:" -msgstr "" +msgstr "텍스트 파일 내용:" -#: src/view/com/auth/create/CreateAccount.tsx:94 +#: src/screens/Signup/index.tsx:84 msgid "That handle is already taken." msgstr "이 핸들은 이미 사용 중입니다." @@ -4462,11 +4397,11 @@ msgstr "지원 양식을 이동했습니다. 도움이 필요하다면 <0/>하 msgid "The Terms of Service have been moved to" msgstr "서비스 이용약관을 다음으로 이동했습니다:" -#: src/screens/Onboarding/StepAlgoFeeds/index.tsx:150 +#: src/screens/Onboarding/StepAlgoFeeds/index.tsx:156 msgid "There are many feeds to try:" msgstr "시도해 볼 만한 피드:" -#: src/screens/Profile/Header/ProfileHeaderLabeler.tsx:113 +#: src/screens/Profile/Header/ProfileHeaderLabeler.tsx:112 #: src/view/screens/ProfileFeed.tsx:543 msgid "There was an an issue contacting the server, please check your internet connection and try again." msgstr "서버에 연결하는 동안 문제가 발생했습니다. 인터넷 연결을 확인한 후 다시 시도하세요." @@ -4552,15 +4487,15 @@ msgstr "애플리케이션에 예기치 않은 문제가 발생했습니다. 이 msgid "There's been a rush of new users to Bluesky! We'll activate your account as soon as we can." msgstr "Bluesky에 신규 사용자가 몰리고 있습니다! 최대한 빨리 계정을 활성화해 드리겠습니다." -#: src/screens/Onboarding/StepSuggestedAccounts/index.tsx:138 +#: src/screens/Onboarding/StepSuggestedAccounts/index.tsx:146 msgid "These are popular accounts you might like:" msgstr "내가 좋아할 만한 인기 계정입니다:" -#: src/components/moderation/ScreenHider.tsx:117 +#: src/components/moderation/ScreenHider.tsx:116 msgid "This {screenDescription} has been flagged:" msgstr "이 {screenDescription}에 다음 플래그가 지정되었습니다:" -#: src/components/moderation/ScreenHider.tsx:112 +#: src/components/moderation/ScreenHider.tsx:111 msgid "This account has requested that users sign in to view their profile." msgstr "이 계정의 프로필을 보려면 로그인해야 합니다." @@ -4570,11 +4505,11 @@ msgstr "이 이의신청은 <0>{0}에게 보내집니다." #: src/lib/moderation/useGlobalLabelStrings.ts:19 msgid "This content has been hidden by the moderators." -msgstr "이 콘텐츠는 관리자에 의해 숨겨졌습니다." +msgstr "이 콘텐츠는 검토자에 의해 숨겨졌습니다." #: src/lib/moderation/useGlobalLabelStrings.ts:24 msgid "This content has received a general warning from moderators." -msgstr "이 콘텐츠는 관리자로부터 일반 경고를 받았습니다." +msgstr "이 콘텐츠는 검토자로부터 일반 경고를 받았습니다." #: src/view/com/modals/EmbedConsent.tsx:68 msgid "This content is hosted by {0}. Do you want to enable external media?" @@ -4589,13 +4524,9 @@ msgstr "관련 사용자 중 한 명이 다른 사용자를 차단했기 때문 msgid "This content is not viewable without a Bluesky account." msgstr "이 콘텐츠는 Bluesky 계정이 없으면 볼 수 없습니다." -#: src/view/screens/Settings/ExportCarDialog.tsx:75 -#~ msgid "This feature is in beta. You can read more about repository exports in <0>this blogpost." -#~ msgstr "이 기능은 베타 버전입니다. 저장소 내보내기에 대한 자세한 내용은 <0>이 블로그 글에서 확인할 수 있습니다." - #: src/view/screens/Settings/ExportCarDialog.tsx:75 msgid "This feature is in beta. You can read more about repository exports in <0>this blogpost." -msgstr "" +msgstr "이 기능은 베타 버전입니다. 저장소 내보내기에 대한 자세한 내용은 <0>이 블로그 글에서 확인할 수 있습니다." #: src/view/com/posts/FeedErrorMessage.tsx:114 msgid "This feed is currently receiving high traffic and is temporarily unavailable. Please try again later." @@ -4623,7 +4554,7 @@ msgstr "이는 이메일을 변경하거나 비밀번호를 재설정해야 할 msgid "This label was applied by {0}." msgstr "이 라벨은 {0}이(가) 적용했습니다." -#: src/screens/Profile/Sections/Labels.tsx:168 +#: src/screens/Profile/Sections/Labels.tsx:167 msgid "This labeler hasn't declared what labels it publishes, and may not be active." msgstr "이 라벨러는 라벨을 게시하지 않았으며 활성화되어 있지 않을 수 있습니다." @@ -4648,6 +4579,7 @@ msgid "This post has been deleted." msgstr "이 게시물은 삭제되었습니다." #: src/view/com/util/forms/PostDropdownBtn.tsx:344 +#: src/view/com/util/post-ctrls/PostCtrls.tsx:248 msgid "This post is only visible to logged-in users. It won't be visible to people who aren't logged in." msgstr "이 게시물은 로그인한 사용자에게만 표시됩니다. 로그인하지 않은 사용자에게는 표시되지 않습니다." @@ -4659,17 +4591,17 @@ msgstr "이 게시물을 피드에서 숨깁니다." msgid "This profile is only visible to logged-in users. It won't be visible to people who aren't logged in." msgstr "이 프로필은 로그인한 사용자에게만 표시됩니다. 로그인하지 않은 사용자에게는 표시되지 않습니다." -#: src/view/com/auth/create/Policies.tsx:46 +#: src/screens/Signup/StepInfo/Policies.tsx:37 msgid "This service has not provided terms of service or a privacy policy." -msgstr "" +msgstr "이 서비스는 서비스 이용약관이나 개인정보 처리방침을 제공하지 않습니다." #: src/view/com/modals/ChangeHandle.tsx:446 msgid "This should create a domain record at:" -msgstr "" +msgstr "이 도메인에 레코드가 추가됩니다:" #: src/view/com/profile/ProfileFollowers.tsx:95 msgid "This user doesn't have any followers." -msgstr "" +msgstr "이 사용자는 팔로워가 없습니다." #: src/components/moderation/ModerationDetailsDialog.tsx:73 #: src/lib/moderation/useModerationCauseDescription.ts:68 @@ -4690,7 +4622,7 @@ msgstr "이 사용자는 내가 뮤트한 <0>{0} 리스트에 포함되어 #: src/view/com/profile/ProfileFollows.tsx:94 msgid "This user isn't following anyone." -msgstr "" +msgstr "이 사용자는 아무도 팔로우하지 않았습니다." #: src/view/com/modals/SelfLabel.tsx:137 msgid "This warning is only available for posts with media attached." @@ -4702,7 +4634,7 @@ msgstr "뮤트한 단어에서 {0}이(가) 삭제됩니다. 나중에 언제든 #: src/view/screens/Settings/index.tsx:574 msgid "Thread preferences" -msgstr "" +msgstr "스레드 설정" #: src/view/screens/PreferencesThreads.tsx:53 #: src/view/screens/Settings/index.tsx:584 @@ -4717,9 +4649,9 @@ msgstr "스레드 모드" msgid "Threads Preferences" msgstr "스레드 설정" -#: src/components/ReportDialog/SelectLabelerView.tsx:35 +#: src/components/ReportDialog/SelectLabelerView.tsx:33 msgid "To whom would you like to send this report?" -msgstr "" +msgstr "이 신고를 누구에게 보내시겠습니까?" #: src/components/dialogs/MutedWords.tsx:113 msgid "Toggle between muted word options." @@ -4729,7 +4661,7 @@ msgstr "뮤트한 단어 옵션 사이를 전환합니다." msgid "Toggle dropdown" msgstr "드롭다운 열기 및 닫기" -#: src/screens/Moderation/index.tsx:334 +#: src/screens/Moderation/index.tsx:332 msgid "Toggle to enable or disable adult content" msgstr "성인 콘텐츠 활성화 또는 비활성화 전환" @@ -4751,7 +4683,7 @@ msgstr "다시 시도" #: src/view/com/modals/ChangeHandle.tsx:429 msgid "Type:" -msgstr "" +msgstr "유형:" #: src/view/screens/ProfileList.tsx:478 msgid "Un-block list" @@ -4761,10 +4693,11 @@ msgstr "리스트 차단 해제" msgid "Un-mute list" msgstr "리스트 언뮤트" -#: src/view/com/auth/create/CreateAccount.tsx:58 -#: src/view/com/auth/login/ForgotPasswordForm.tsx:87 -#: src/view/com/auth/login/Login.tsx:76 -#: src/view/com/auth/login/LoginForm.tsx:121 +#: src/screens/Login/ForgotPasswordForm.tsx:74 +#: src/screens/Login/index.tsx:78 +#: src/screens/Login/LoginForm.tsx:119 +#: src/screens/Login/SetNewPasswordForm.tsx:77 +#: src/screens/Signup/index.tsx:63 #: src/view/com/modals/ChangePassword.tsx:70 msgid "Unable to contact your service. Please check your Internet connection." msgstr "서비스에 연결할 수 없습니다. 인터넷 연결을 확인하세요." @@ -4801,7 +4734,7 @@ msgstr "재게시 취소" #: src/view/com/auth/onboarding/RecommendedFollowsItem.tsx:141 #: src/view/com/profile/ProfileHeaderSuggestedFollows.tsx:246 msgid "Unfollow" -msgstr "" +msgstr "언팔로우" #: src/view/com/profile/FollowButton.tsx:60 msgctxt "action" @@ -4817,11 +4750,7 @@ msgstr "{0} 님을 언팔로우" msgid "Unfollow Account" msgstr "계정 언팔로우" -#: src/view/com/auth/create/state.ts:262 -msgid "Unfortunately, you do not meet the requirements to create an account." -msgstr "아쉽지만 계정을 만들 수 있는 요건을 충족하지 못했습니다." - -#: src/view/com/util/post-ctrls/PostCtrls.tsx:185 +#: src/view/com/util/post-ctrls/PostCtrls.tsx:195 msgid "Unlike" msgstr "좋아요 취소" @@ -4865,11 +4794,11 @@ msgstr "홈에서 고정 해제" msgid "Unpin moderation list" msgstr "검토 리스트 고정 해제" -#: src/screens/Profile/Header/ProfileHeaderLabeler.tsx:220 +#: src/screens/Profile/Header/ProfileHeaderLabeler.tsx:219 msgid "Unsubscribe" msgstr "구독 취소" -#: src/screens/Profile/Header/ProfileHeaderLabeler.tsx:184 +#: src/screens/Profile/Header/ProfileHeaderLabeler.tsx:183 msgid "Unsubscribe from this labeler" msgstr "이 라벨러 구독 취소하기" @@ -4883,9 +4812,9 @@ msgstr "리스트에서 {displayName} 업데이트" #: src/view/com/modals/ChangeHandle.tsx:509 msgid "Update to {handle}" -msgstr "" +msgstr "{handle}로 변경" -#: src/view/com/auth/login/SetNewPasswordForm.tsx:204 +#: src/screens/Login/SetNewPasswordForm.tsx:186 msgid "Updating..." msgstr "업데이트 중…" @@ -4914,7 +4843,7 @@ msgstr "라이브러리에서 업로드" #: src/view/com/modals/ChangeHandle.tsx:409 msgid "Use a file on your server" -msgstr "" +msgstr "서버에 있는 파일을 사용합니다" #: src/view/screens/AppPasswords.tsx:197 msgid "Use app passwords to login to other Bluesky clients without giving full access to your account or password." @@ -4922,7 +4851,7 @@ msgstr "앱 비밀번호를 사용하면 계정이나 비밀번호에 대한 전 #: src/view/com/modals/ChangeHandle.tsx:518 msgid "Use bsky.social as hosting provider" -msgstr "" +msgstr "호스팅 제공자로 bsky.social을 사용합니다" #: src/view/com/modals/ChangeHandle.tsx:517 msgid "Use default provider" @@ -4940,7 +4869,7 @@ msgstr "내 기본 브라우저 사용" #: src/view/com/modals/ChangeHandle.tsx:401 msgid "Use the DNS panel" -msgstr "" +msgstr "DNS 패널을 사용합니다" #: src/view/com/modals/AddAppPasswords.tsx:155 msgid "Use this to sign into the other app along with your handle." @@ -4971,10 +4900,6 @@ msgstr "나를 차단한 사용자" msgid "User Blocks You" msgstr "나를 차단한 사용자" -#: src/view/com/auth/create/Step2.tsx:79 -msgid "User handle" -msgstr "사용자 핸들" - #: src/view/com/lists/ListCard.tsx:85 #: src/view/com/modals/UserAddRemoveLists.tsx:198 msgid "User list by {0}" @@ -5002,8 +4927,7 @@ msgstr "사용자 리스트 업데이트됨" msgid "User Lists" msgstr "사용자 리스트" -#: src/view/com/auth/login/LoginForm.tsx:180 -#: src/view/com/auth/login/LoginForm.tsx:198 +#: src/screens/Login/LoginForm.tsx:151 msgid "Username or email address" msgstr "사용자 이름 또는 이메일 주소" @@ -5025,21 +4949,21 @@ msgstr "이 콘텐츠 또는 프로필을 좋아하는 사용자" #: src/view/com/modals/ChangeHandle.tsx:437 msgid "Value:" -msgstr "" +msgstr "값:" #: src/view/com/modals/ChangeHandle.tsx:510 msgid "Verify {0}" -msgstr "" +msgstr "{0} 확인" -#: src/view/screens/Settings/index.tsx:944 +#: src/view/screens/Settings/index.tsx:942 msgid "Verify email" msgstr "이메일 인증" -#: src/view/screens/Settings/index.tsx:969 +#: src/view/screens/Settings/index.tsx:967 msgid "Verify my email" msgstr "내 이메일 인증하기" -#: src/view/screens/Settings/index.tsx:978 +#: src/view/screens/Settings/index.tsx:976 msgid "Verify My Email" msgstr "내 이메일 인증하기" @@ -5052,6 +4976,10 @@ msgstr "새 이메일 인증" msgid "Verify Your Email" msgstr "이메일 인증하기" +#: src/view/screens/Settings/index.tsx:893 +msgid "Version {0}" +msgstr "버전 {0}" + #: src/screens/Onboarding/index.tsx:42 msgid "Video Games" msgstr "비디오 게임" @@ -5064,11 +4992,11 @@ msgstr "{0} 님의 아바타를 봅니다" msgid "View debug entry" msgstr "디버그 항목 보기" -#: src/components/ReportDialog/SelectReportOptionView.tsx:133 +#: src/components/ReportDialog/SelectReportOptionView.tsx:131 msgid "View details" msgstr "세부 정보 보기" -#: src/components/ReportDialog/SelectReportOptionView.tsx:128 +#: src/components/ReportDialog/SelectReportOptionView.tsx:126 msgid "View details for reporting a copyright violation" msgstr "저작권 위반 신고에 대한 세부 정보 보기" @@ -5101,7 +5029,7 @@ msgstr "이 피드를 좋아하는 사용자 보기" msgid "Visit Site" msgstr "사이트 방문" -#: src/components/moderation/GlobalModerationLabelPref.tsx:44 +#: src/components/moderation/LabelPreference.tsx:135 #: src/lib/moderation/useLabelBehaviorDescription.ts:17 #: src/lib/moderation/useLabelBehaviorDescription.ts:22 #: src/screens/Onboarding/StepModeration/ModerationOption.tsx:53 @@ -5116,7 +5044,7 @@ msgstr "콘텐츠 경고" msgid "Warn content and filter from feeds" msgstr "콘텐츠 경고 및 피드에서 필터링" -#: src/screens/Onboarding/StepAlgoFeeds/index.tsx:134 +#: src/screens/Onboarding/StepAlgoFeeds/index.tsx:140 msgid "We also think you'll like \"For You\" by Skygaze:" msgstr "Skygaze의 \"For You\"를 사용해 볼 수도 있습니다:" @@ -5128,7 +5056,7 @@ msgstr "해당 해시태그에 대한 결과를 찾을 수 없습니다." msgid "We estimate {estimatedTime} until your account is ready." msgstr "계정이 준비될 때까지 {estimatedTime}이(가) 걸릴 것으로 예상됩니다." -#: src/screens/Onboarding/StepFinished.tsx:93 +#: src/screens/Onboarding/StepFinished.tsx:94 msgid "We hope you have a wonderful time. Remember, Bluesky is:" msgstr "즐거운 시간 되시기 바랍니다. Bluesky의 다음 특징을 기억하세요:" @@ -5140,19 +5068,19 @@ msgstr "팔로우한 사용자의 게시물이 부족합니다. 대신 <0/>의 msgid "We recommend avoiding common words that appear in many posts, since it can result in no posts being shown." msgstr "게시물이 표시되지 않을 수 있으므로 많은 게시물에 자주 등장하는 단어는 피하는 것이 좋습니다." -#: src/screens/Onboarding/StepAlgoFeeds/index.tsx:124 +#: src/screens/Onboarding/StepAlgoFeeds/index.tsx:130 msgid "We recommend our \"Discover\" feed:" msgstr "\"Discover\" 피드를 권장합니다:" #: src/components/dialogs/BirthDateSettings.tsx:52 msgid "We were unable to load your birth date preferences. Please try again." -msgstr "" +msgstr "생년월일 설정을 불러올 수 없습니다. 다시 시도해 주세요." -#: src/screens/Moderation/index.tsx:387 +#: src/screens/Moderation/index.tsx:385 msgid "We were unable to load your configured labelers at this time." msgstr "현재 구성된 라벨러를 불러올 수 없습니다." -#: src/screens/Onboarding/StepInterests/index.tsx:133 +#: src/screens/Onboarding/StepInterests/index.tsx:137 msgid "We weren't able to connect. Please try again to continue setting up your account. If it continues to fail, you can skip this flow." msgstr "연결하지 못했습니다. 계정 설정을 계속하려면 다시 시도해 주세요. 계속 실패하면 이 과정을 건너뛸 수 있습니다." @@ -5160,11 +5088,11 @@ msgstr "연결하지 못했습니다. 계정 설정을 계속하려면 다시 msgid "We will let you know when your account is ready." msgstr "계정이 준비되면 알려드리겠습니다." -#: src/screens/Onboarding/StepInterests/index.tsx:138 +#: src/screens/Onboarding/StepInterests/index.tsx:142 msgid "We'll use this to help customize your experience." msgstr "이를 통해 사용자 환경을 맞춤 설정할 수 있습니다." -#: src/view/com/auth/create/CreateAccount.tsx:134 +#: src/screens/Signup/index.tsx:130 msgid "We're so excited to have you join us!" msgstr "함께하게 되어 정말 기뻐요!" @@ -5176,7 +5104,7 @@ msgstr "죄송하지만 이 리스트를 불러올 수 없습니다. 이 문제 msgid "We're sorry, but we weren't able to load your muted words at this time. Please try again." msgstr "죄송하지만 현재 뮤트한 단어를 불러올 수 없습니다. 다시 시도해 주세요." -#: src/view/screens/Search/Search.tsx:255 +#: src/view/screens/Search/Search.tsx:256 msgid "We're sorry, but your search could not be completed. Please try again in a few minutes." msgstr "죄송하지만 검색을 완료할 수 없습니다. 몇 분 후에 다시 시도해 주세요." @@ -5193,12 +5121,13 @@ msgstr "죄송합니다. 라벨러는 10개까지만 구독할 수 있으며 10 msgid "Welcome to <0>Bluesky" msgstr "<0>Bluesky에 오신 것을 환영합니다" -#: src/screens/Onboarding/StepInterests/index.tsx:130 +#: src/screens/Onboarding/StepInterests/index.tsx:134 msgid "What are your interests?" msgstr "어떤 관심사가 있으신가요?" -#: src/view/com/auth/SplashScreen.tsx:59 -#: src/view/com/composer/Composer.tsx:295 +#: src/view/com/auth/SplashScreen.tsx:58 +#: src/view/com/auth/SplashScreen.web.tsx:84 +#: src/view/com/composer/Composer.tsx:296 msgid "What's up?" msgstr "무슨 일이 일어나고 있나요?" @@ -5215,27 +5144,23 @@ msgstr "알고리즘 피드에 어떤 언어를 표시하시겠습니까?" msgid "Who can reply" msgstr "답글을 달 수 있는 사람" -#: src/components/ReportDialog/SelectLabelerView.tsx:33 -#~ msgid "Who do you want to send this report to?" -#~ msgstr "이 신고를 누구에게 보내시겠습니까?" - -#: src/components/ReportDialog/SelectReportOptionView.tsx:44 +#: src/components/ReportDialog/SelectReportOptionView.tsx:43 msgid "Why should this content be reviewed?" msgstr "이 콘텐츠를 검토해야 하는 이유는 무엇인가요?" -#: src/components/ReportDialog/SelectReportOptionView.tsx:57 +#: src/components/ReportDialog/SelectReportOptionView.tsx:56 msgid "Why should this feed be reviewed?" msgstr "이 피드를 검토해야 하는 이유는 무엇인가요?" -#: src/components/ReportDialog/SelectReportOptionView.tsx:54 +#: src/components/ReportDialog/SelectReportOptionView.tsx:53 msgid "Why should this list be reviewed?" msgstr "이 리스트를 검토해야 하는 이유는 무엇인가요?" -#: src/components/ReportDialog/SelectReportOptionView.tsx:51 +#: src/components/ReportDialog/SelectReportOptionView.tsx:50 msgid "Why should this post be reviewed?" msgstr "이 게시물을 검토해야 하는 이유는 무엇인가요?" -#: src/components/ReportDialog/SelectReportOptionView.tsx:48 +#: src/components/ReportDialog/SelectReportOptionView.tsx:47 msgid "Why should this user be reviewed?" msgstr "이 사용자를 검토해야 하는 이유는 무엇인가요?" @@ -5243,11 +5168,11 @@ msgstr "이 사용자를 검토해야 하는 이유는 무엇인가요?" msgid "Wide" msgstr "가로" -#: src/view/com/composer/Composer.tsx:435 +#: src/view/com/composer/Composer.tsx:436 msgid "Write post" msgstr "게시물 작성" -#: src/view/com/composer/Composer.tsx:294 +#: src/view/com/composer/Composer.tsx:295 #: src/view/com/composer/Prompt.tsx:37 msgid "Write your reply" msgstr "답글 작성하기" @@ -5272,25 +5197,25 @@ msgstr "대기 중입니다." #: src/view/com/profile/ProfileFollows.tsx:93 msgid "You are not following anyone." -msgstr "" +msgstr "아무도 팔로우하지 않았습니다." #: src/view/com/posts/FollowingEmptyState.tsx:67 #: src/view/com/posts/FollowingEndOfFeed.tsx:68 msgid "You can also discover new Custom Feeds to follow." msgstr "팔로우할 새로운 맞춤 피드를 찾을 수도 있습니다." -#: src/screens/Onboarding/StepFollowingFeed.tsx:142 +#: src/screens/Onboarding/StepFollowingFeed.tsx:143 msgid "You can change these settings later." msgstr "이 설정은 나중에 변경할 수 있습니다." -#: src/view/com/auth/login/Login.tsx:158 -#: src/view/com/auth/login/PasswordUpdatedForm.tsx:31 +#: src/screens/Login/index.tsx:158 +#: src/screens/Login/PasswordUpdatedForm.tsx:33 msgid "You can now sign in with your new password." msgstr "이제 새 비밀번호로 로그인할 수 있습니다." #: src/view/com/profile/ProfileFollowers.tsx:94 msgid "You do not have any followers." -msgstr "" +msgstr "팔로워가 없습니다." #: src/view/com/modals/InviteCodes.tsx:66 msgid "You don't have any invite codes yet! We'll send you some when you've been on Bluesky for a little longer." @@ -5318,8 +5243,8 @@ msgstr "작성자를 차단했거나 작성자가 나를 차단했습니다." msgid "You have blocked this user. You cannot view their content." msgstr "이 사용자를 차단했습니다. 해당 사용자의 콘텐츠를 볼 수 없습니다." -#: src/view/com/auth/login/SetNewPasswordForm.tsx:57 -#: src/view/com/auth/login/SetNewPasswordForm.tsx:92 +#: src/screens/Login/SetNewPasswordForm.tsx:54 +#: src/screens/Login/SetNewPasswordForm.tsx:91 #: src/view/com/modals/ChangePassword.tsx:87 #: src/view/com/modals/ChangePassword.tsx:121 msgid "You have entered an invalid code. It should look like XXXXX-XXXXX." @@ -5353,11 +5278,7 @@ msgstr "리스트가 없습니다." #: src/view/screens/ModerationBlockedAccounts.tsx:132 msgid "You have not blocked any accounts yet. To block an account, go to their profile and select \"Block account\" from the menu on their account." -msgstr "" - -#: src/view/screens/ModerationBlockedAccounts.tsx:132 -#~ msgid "You have not blocked any accounts yet. To block an account, go to their profile and selected \"Block account\" from the menu on their account." -#~ msgstr "아직 어떤 계정도 차단하지 않았습니다. 계정을 차단하려면 해당 계정의 프로필로 이동하여 계정 메뉴에서 \"계정 차단\"을 선택하세요." +msgstr "아직 어떤 계정도 차단하지 않았습니다. 계정을 차단하려면 해당 계정의 프로필로 이동하여 계정 메뉴에서 \"계정 차단\"을 선택하세요." #: src/view/screens/AppPasswords.tsx:89 msgid "You have not created any app passwords yet. You can create one by pressing the button below." @@ -5365,11 +5286,7 @@ msgstr "아직 앱 비밀번호를 생성하지 않았습니다. 아래 버튼 #: src/view/screens/ModerationMutedAccounts.tsx:131 msgid "You have not muted any accounts yet. To mute an account, go to their profile and select \"Mute account\" from the menu on their account." -msgstr "" - -#: src/view/screens/ModerationMutedAccounts.tsx:131 -#~ msgid "You have not muted any accounts yet. To mute an account, go to their profile and selected \"Mute account\" from the menu on their account." -#~ msgstr "아직 어떤 계정도 뮤트하지 않았습니다. 계정을 뮤트하려면 해당 계정의 프로필로 이동하여 계정 메뉴에서 \"계정 뮤트\"를 선택하세요." +msgstr "아직 어떤 계정도 뮤트하지 않았습니다. 계정을 뮤트하려면 해당 계정의 프로필로 이동하여 계정 메뉴에서 \"계정 뮤트\"를 선택하세요." #: src/components/dialogs/MutedWords.tsx:250 msgid "You haven't muted any words or tags yet" @@ -5379,6 +5296,10 @@ msgstr "아직 어떤 단어나 태그도 뮤트하지 않았습니다" msgid "You may appeal these labels if you feel they were placed in error." msgstr "이 라벨이 잘못 지정되었다고 생각되면 이의신청할 수 있습니다." +#: src/screens/Signup/StepInfo/Policies.tsx:79 +msgid "You must be 13 years of age or older to sign up." +msgstr "가입하려면 만 13세 이상이어야 합니다." + #: src/screens/Onboarding/StepModeration/AdultContentEnabledPref.tsx:110 msgid "You must be 18 years or older to enable adult content" msgstr "성인 콘텐츠를 사용하려면 만 18세 이상이어야 합니다." @@ -5395,11 +5316,11 @@ msgstr "이 스레드에 대한 알림을 더 이상 받지 않습니다" msgid "You will now receive notifications for this thread" msgstr "이제 이 스레드에 대한 알림을 받습니다" -#: src/view/com/auth/login/SetNewPasswordForm.tsx:107 +#: src/screens/Login/SetNewPasswordForm.tsx:104 msgid "You will receive an email with a \"reset code.\" Enter that code here, then enter your new password." msgstr "\"재설정 코드\"가 포함된 이메일을 받게 되면 여기에 해당 코드를 입력한 다음 새 비밀번호를 입력합니다." -#: src/screens/Onboarding/StepModeration/index.tsx:59 +#: src/screens/Onboarding/StepModeration/index.tsx:60 msgid "You're in control" msgstr "직접 제어하세요" @@ -5409,7 +5330,7 @@ msgstr "직접 제어하세요" msgid "You're in line" msgstr "대기 중입니다" -#: src/screens/Onboarding/StepFinished.tsx:90 +#: src/screens/Onboarding/StepFinished.tsx:91 msgid "You're ready to go!" msgstr "준비가 끝났습니다!" @@ -5422,11 +5343,11 @@ msgstr "이 글에서 단어 또는 태그를 숨기도록 설정했습니다." msgid "You've reached the end of your feed! Find some more accounts to follow." msgstr "피드 끝에 도달했습니다! 팔로우할 계정을 더 찾아보세요." -#: src/view/com/auth/create/Step1.tsx:67 +#: src/screens/Signup/index.tsx:150 msgid "Your account" msgstr "내 계정" -#: src/view/com/modals/DeleteAccount.tsx:67 +#: src/view/com/modals/DeleteAccount.tsx:68 msgid "Your account has been deleted" msgstr "계정을 삭제했습니다" @@ -5434,7 +5355,7 @@ msgstr "계정을 삭제했습니다" msgid "Your account repository, containing all public data records, can be downloaded as a \"CAR\" file. This file does not include media embeds, such as images, or your private data, which must be fetched separately." msgstr "모든 공개 데이터 레코드가 포함된 계정 저장소를 \"CAR\" 파일로 다운로드할 수 있습니다. 이 파일에는 이미지와 같은 미디어 임베드나 별도로 가져와야 하는 비공개 데이터는 포함되지 않습니다." -#: src/view/com/auth/create/Step1.tsx:215 +#: src/screens/Signup/StepInfo/index.tsx:121 msgid "Your birth date" msgstr "생년월일" @@ -5442,12 +5363,12 @@ msgstr "생년월일" msgid "Your choice will be saved, but can be changed later in settings." msgstr "선택 사항은 저장되며 나중에 설정에서 변경할 수 있습니다." -#: src/screens/Onboarding/StepFollowingFeed.tsx:61 +#: src/screens/Onboarding/StepFollowingFeed.tsx:62 msgid "Your default feed is \"Following\"" msgstr "기본 피드는 \"팔로우 중\"입니다" -#: src/view/com/auth/create/state.ts:110 -#: src/view/com/auth/login/ForgotPasswordForm.tsx:70 +#: src/screens/Login/ForgotPasswordForm.tsx:57 +#: src/screens/Signup/state.ts:227 #: src/view/com/modals/ChangePassword.tsx:54 msgid "Your email appears to be invalid." msgstr "이메일이 잘못된 것 같습니다." @@ -5464,7 +5385,7 @@ msgstr "이메일이 아직 인증되지 않았습니다. 이는 중요한 보 msgid "Your following feed is empty! Follow more users to see what's happening." msgstr "팔로우 중 피드가 비어 있습니다! 더 많은 사용자를 팔로우하여 무슨 일이 일어나고 있는지 확인하세요." -#: src/view/com/auth/create/Step2.tsx:83 +#: src/screens/Signup/StepHandle.tsx:72 msgid "Your full handle will be" msgstr "내 전체 핸들:" @@ -5480,25 +5401,25 @@ msgstr "뮤트한 단어" msgid "Your password has been changed successfully!" msgstr "비밀번호를 성공적으로 변경했습니다." -#: src/view/com/composer/Composer.tsx:283 +#: src/view/com/composer/Composer.tsx:284 msgid "Your post has been published" msgstr "게시물을 게시했습니다" -#: src/screens/Onboarding/StepFinished.tsx:105 +#: src/screens/Onboarding/StepFinished.tsx:106 #: src/view/com/auth/onboarding/WelcomeDesktop.tsx:59 #: src/view/com/auth/onboarding/WelcomeMobile.tsx:61 msgid "Your posts, likes, and blocks are public. Mutes are private." msgstr "게시물, 좋아요, 차단 목록은 공개됩니다. 뮤트 목록은 공개되지 않습니다." -#: src/view/com/modals/SwitchAccount.tsx:88 +#: src/view/com/modals/SwitchAccount.tsx:89 #: src/view/screens/Settings/index.tsx:125 msgid "Your profile" msgstr "내 프로필" -#: src/view/com/composer/Composer.tsx:282 +#: src/view/com/composer/Composer.tsx:283 msgid "Your reply has been published" msgstr "내 답글을 게시했습니다" -#: src/view/com/auth/create/Step2.tsx:65 +#: src/screens/Signup/index.tsx:152 msgid "Your user handle" msgstr "내 사용자 핸들" diff --git a/src/locale/locales/pt-BR/messages.po b/src/locale/locales/pt-BR/messages.po index 81b063a205..98c6789140 100644 --- a/src/locale/locales/pt-BR/messages.po +++ b/src/locale/locales/pt-BR/messages.po @@ -8,7 +8,7 @@ msgstr "" "Language: pt-BR\n" "Project-Id-Version: \n" "Report-Msgid-Bugs-To: \n" -"PO-Revision-Date: 2024-03-12 11:36\n" +"PO-Revision-Date: 2024-03-22 11:51\n" "Last-Translator: gildaswise\n" "Language-Team: maisondasilva, MightyLoggor, gildaswise, gleydson, faeriarum\n" "Plural-Forms: nplurals=2; plural=(n != 1);\n" @@ -31,7 +31,7 @@ msgstr "<0/> membros" #: src/view/shell/Drawer.tsx:97 msgid "<0>{0} following" -msgstr "" +msgstr "<0>{0} seguindo" #: src/screens/Profile/Header/Metrics.tsx:46 msgid "<0>{following} <1>following" @@ -77,7 +77,7 @@ msgstr "Acessibilidade" #: src/components/moderation/LabelsOnMe.tsx:42 msgid "account" -msgstr "" +msgstr "conta" #: src/view/com/auth/login/LoginForm.tsx:169 #: src/view/screens/Settings/index.tsx:327 @@ -91,7 +91,7 @@ msgstr "Conta bloqueada" #: src/view/com/profile/ProfileMenu.tsx:153 msgid "Account followed" -msgstr "" +msgstr "Você está seguindo esta conta" #: src/view/com/profile/ProfileMenu.tsx:113 msgid "Account muted" @@ -121,7 +121,7 @@ msgstr "Conta desbloqueada" #: src/view/com/profile/ProfileMenu.tsx:166 msgid "Account unfollowed" -msgstr "" +msgstr "Você não segue mais esta conta" #: src/view/com/profile/ProfileMenu.tsx:102 msgid "Account unmuted" @@ -226,7 +226,7 @@ msgstr "Conteúdo Adulto" #: src/components/moderation/ModerationLabelPref.tsx:114 msgid "Adult content is disabled." -msgstr "" +msgstr "O conteúdo adulto está desabilitado." #: src/screens/Moderation/index.tsx:377 #: src/view/screens/Settings/index.tsx:684 @@ -244,7 +244,7 @@ msgstr "Já tem um código?" #: src/view/com/auth/login/ChooseAccountForm.tsx:103 msgid "Already signed in as @{0}" -msgstr "Já logado como @{0}" +msgstr "Já autenticado como @{0}" #: src/view/com/composer/photos/Gallery.tsx:130 msgid "ALT" @@ -268,7 +268,7 @@ msgstr "Um email foi enviado para seu email anterior, {0}. Ele inclui um código #: src/lib/moderation/useReportOptions.ts:26 msgid "An issue not included in these options" -msgstr "" +msgstr "Outro problema" #: src/view/com/profile/FollowButton.tsx:35 #: src/view/com/profile/FollowButton.tsx:45 @@ -288,7 +288,7 @@ msgstr "Animais" #: src/lib/moderation/useReportOptions.ts:31 msgid "Anti-Social Behavior" -msgstr "" +msgstr "Comportamento anti-social" #: src/view/screens/LanguageSettings.tsx:95 msgid "App Language" @@ -323,11 +323,11 @@ msgstr "Senhas de Aplicativos" #: src/components/moderation/LabelsOnMeDialog.tsx:134 #: src/components/moderation/LabelsOnMeDialog.tsx:137 msgid "Appeal" -msgstr "" +msgstr "Contestar" #: src/components/moderation/LabelsOnMeDialog.tsx:202 msgid "Appeal \"{0}\" label" -msgstr "" +msgstr "Contestar rótulo \"{0}\"" #: src/view/com/util/forms/PostDropdownBtn.tsx:337 #: src/view/com/util/forms/PostDropdownBtn.tsx:346 @@ -340,7 +340,7 @@ msgstr "" #: src/components/moderation/LabelsOnMeDialog.tsx:193 msgid "Appeal submitted." -msgstr "" +msgstr "Contestação enviada." #: src/view/com/util/moderation/LabelInfo.tsx:52 #~ msgid "Appeal this decision" @@ -360,7 +360,7 @@ msgstr "Tem certeza de que deseja excluir a senha do aplicativo \"{name}\"?" #: src/view/com/feeds/FeedSourceCard.tsx:280 msgid "Are you sure you want to remove {0} from your feeds?" -msgstr "" +msgstr "Tem certeza que deseja remover {0} dos seus feeds?" #: src/view/com/composer/Composer.tsx:508 msgid "Are you sure you'd like to discard this draft?" @@ -423,7 +423,7 @@ msgstr "Aniversário:" #: src/screens/Profile/Header/ProfileHeaderStandard.tsx:278 #: src/view/com/profile/ProfileMenu.tsx:361 msgid "Block" -msgstr "" +msgstr "Bloquear" #: src/view/com/profile/ProfileMenu.tsx:300 #: src/view/com/profile/ProfileMenu.tsx:307 @@ -432,7 +432,7 @@ msgstr "Bloquear Conta" #: src/view/com/profile/ProfileMenu.tsx:344 msgid "Block Account?" -msgstr "" +msgstr "Bloquear Conta?" #: src/view/screens/ProfileList.tsx:530 msgid "Block accounts" @@ -479,7 +479,7 @@ msgstr "Post bloqueado." #: src/screens/Profile/Sections/Labels.tsx:153 msgid "Blocking does not prevent this labeler from placing labels on your account." -msgstr "" +msgstr "Bloquear não previne este rotulador de rotular a sua conta." #: src/view/screens/ProfileList.tsx:631 msgid "Blocking is public. Blocked accounts cannot reply in your threads, mention you, or otherwise interact with you." @@ -487,7 +487,7 @@ msgstr "Bloqueios são públicos. Contas bloqueadas não podem te responder, men #: src/view/com/profile/ProfileMenu.tsx:353 msgid "Blocking will not prevent labels from being applied on your account, but it will stop this account from replying in your threads or interacting with you." -msgstr "" +msgstr "Bloquear não previne rótulos de serem aplicados na sua conta, mas vai impedir esta conta de interagir com você." #: src/view/com/auth/HomeLoggedOutCTA.tsx:97 #: src/view/com/auth/SplashScreen.web.tsx:133 @@ -529,11 +529,11 @@ msgstr "O Bluesky não mostrará seu perfil e publicações para usuários desco #: src/lib/moderation/useLabelBehaviorDescription.ts:53 msgid "Blur images" -msgstr "" +msgstr "Desfocar imagens" #: src/lib/moderation/useLabelBehaviorDescription.ts:51 msgid "Blur images and filter from feeds" -msgstr "" +msgstr "Desfocar imagens e filtrar dos feeds" #: src/screens/Onboarding/index.tsx:33 msgid "Books" @@ -558,7 +558,7 @@ msgstr "por {0}" #: src/components/LabelingServiceCard/index.tsx:57 msgid "By {0}" -msgstr "" +msgstr "Por {0}" #: src/view/com/profile/ProfileSubpageHeader.tsx:161 msgid "by <0/>" @@ -566,7 +566,7 @@ msgstr "por <0/>" #: src/view/com/auth/create/Policies.tsx:87 msgid "By creating an account you agree to the {els}." -msgstr "" +msgstr "Ao criar uma conta, você concorda com os {els}." #: src/view/com/profile/ProfileSubpageHeader.tsx:159 msgid "by you" @@ -646,11 +646,11 @@ msgstr "Cancelar busca" #: src/view/com/modals/LinkWarning.tsx:88 msgid "Cancels opening the linked website" -msgstr "" +msgstr "Cancela a abertura do link" #: src/view/com/modals/VerifyEmail.tsx:152 msgid "Change" -msgstr "" +msgstr "Trocar" #: src/view/screens/Settings/index.tsx:353 msgctxt "action" @@ -760,11 +760,11 @@ msgstr "Limpar busca" #: src/view/screens/Settings/index.tsx:869 msgid "Clears all legacy storage data" -msgstr "" +msgstr "Limpa todos os dados antigos" #: src/view/screens/Settings/index.tsx:881 msgid "Clears all storage data" -msgstr "" +msgstr "Limpa todos os dados antigos" #: src/view/screens/Support.tsx:40 msgid "click here" @@ -874,7 +874,7 @@ msgstr "Configure o filtro de conteúdo por categoria: {0}" #: src/components/moderation/ModerationLabelPref.tsx:116 msgid "Configured in <0>moderation settings." -msgstr "" +msgstr "Configure no <0>painel de moderação." #: src/components/Prompt.tsx:152 #: src/components/Prompt.tsx:155 @@ -911,11 +911,11 @@ msgstr "Confirmar a exclusão da conta" #: src/screens/Moderation/index.tsx:303 msgid "Confirm your age:" -msgstr "" +msgstr "Confirme sua idade:" #: src/screens/Moderation/index.tsx:294 msgid "Confirm your birthdate" -msgstr "" +msgstr "Confirme sua data de nascimento" #: src/view/com/modals/ChangeEmail.tsx:157 #: src/view/com/modals/DeleteAccount.tsx:176 @@ -939,11 +939,11 @@ msgstr "Contatar suporte" #: src/components/moderation/LabelsOnMe.tsx:42 msgid "content" -msgstr "" +msgstr "conteúdo" #: src/lib/moderation/useGlobalLabelStrings.ts:18 msgid "Content Blocked" -msgstr "" +msgstr "Conteúdo bloqueado" #: src/view/screens/Moderation.tsx:83 #~ msgid "Content filtering" @@ -955,7 +955,7 @@ msgstr "" #: src/screens/Moderation/index.tsx:287 msgid "Content filters" -msgstr "" +msgstr "Filtros de conteúdo" #: src/view/com/modals/lang-settings/ContentLanguagesSettings.tsx:74 #: src/view/screens/LanguageSettings.tsx:278 @@ -980,7 +980,7 @@ msgstr "Avisos de conteúdo" #: src/components/Menu/index.web.tsx:84 msgid "Context menu backdrop, click to close the menu." -msgstr "" +msgstr "Fundo do menu, clique para fechá-lo." #: src/screens/Onboarding/StepAlgoFeeds/index.tsx:170 #: src/screens/Onboarding/StepFollowingFeed.tsx:153 @@ -1038,7 +1038,7 @@ msgstr "Copiar" #: src/view/com/modals/ChangeHandle.tsx:481 msgid "Copy {0}" -msgstr "" +msgstr "Copiar {0}" #: src/view/screens/ProfileList.tsx:388 msgid "Copy link to list" @@ -1096,7 +1096,7 @@ msgstr "Criar uma nova conta" #: src/components/ReportDialog/SelectReportOptionView.tsx:94 msgid "Create report for {0}" -msgstr "" +msgstr "Criar denúncia para {0}" #: src/view/screens/AppPasswords.tsx:246 msgid "Created {0}" @@ -1151,7 +1151,7 @@ msgstr "Modo Escuro" #: src/view/screens/Settings/index.tsx:841 msgid "Debug Moderation" -msgstr "" +msgstr "Testar Moderação" #: src/view/screens/Debug.tsx:83 msgid "Debug panel" @@ -1161,7 +1161,7 @@ msgstr "Painel de depuração" #: src/view/screens/AppPasswords.tsx:268 #: src/view/screens/ProfileList.tsx:613 msgid "Delete" -msgstr "" +msgstr "Excluir" #: src/view/screens/Settings/index.tsx:796 msgid "Delete account" @@ -1177,7 +1177,7 @@ msgstr "Excluir senha de aplicativo" #: src/view/screens/AppPasswords.tsx:263 msgid "Delete app password?" -msgstr "" +msgstr "Excluir senha de aplicativo?" #: src/view/screens/ProfileList.tsx:415 msgid "Delete List" @@ -1198,7 +1198,7 @@ msgstr "Excluir post" #: src/view/screens/ProfileList.tsx:608 msgid "Delete this list?" -msgstr "" +msgstr "Excluir esta lista?" #: src/view/com/util/forms/PostDropdownBtn.tsx:314 msgid "Delete this post?" @@ -1236,7 +1236,7 @@ msgstr "Menos escuro" #: src/lib/moderation/useLabelBehaviorDescription.ts:68 #: src/screens/Moderation/index.tsx:343 msgid "Disabled" -msgstr "" +msgstr "Desabilitado" #: src/view/com/composer/Composer.tsx:510 msgid "Discard" @@ -1248,12 +1248,12 @@ msgstr "Descartar" #: src/view/com/composer/Composer.tsx:507 msgid "Discard draft?" -msgstr "" +msgstr "Descartar rascunho?" #: src/screens/Moderation/index.tsx:520 #: src/screens/Moderation/index.tsx:524 msgid "Discourage apps from showing my account to logged-out users" -msgstr "Desencorajar aplicativos a mostrar minha conta para usuários deslogados" +msgstr "Desencorajar aplicativos a mostrar minha conta para usuários desautenticados" #: src/view/com/posts/FollowingEmptyState.tsx:74 #: src/view/com/posts/FollowingEndOfFeed.tsx:75 @@ -1274,15 +1274,15 @@ msgstr "Nome de Exibição" #: src/view/com/modals/ChangeHandle.tsx:398 msgid "DNS Panel" -msgstr "" +msgstr "Painel DNS" #: src/lib/moderation/useGlobalLabelStrings.ts:39 msgid "Does not include nudity." -msgstr "" +msgstr "Não inclui nudez." #: src/view/com/modals/ChangeHandle.tsx:482 msgid "Domain Value" -msgstr "" +msgstr "Domínio" #: src/view/com/modals/ChangeHandle.tsx:489 msgid "Domain verified!" @@ -1348,7 +1348,7 @@ msgstr "Devido a políticas da Apple, o conteúdo adulto só pode ser habilitado #: src/view/com/modals/ChangeHandle.tsx:257 msgid "e.g. alice" -msgstr "" +msgstr "ex. alice" #: src/view/com/modals/EditProfile.tsx:185 msgid "e.g. Alice Roberts" @@ -1356,7 +1356,7 @@ msgstr "ex. Alice Roberts" #: src/view/com/modals/ChangeHandle.tsx:381 msgid "e.g. alice.com" -msgstr "" +msgstr "ex. alice.com" #: src/view/com/modals/EditProfile.tsx:203 msgid "e.g. Artist, dog-lover, and avid reader." @@ -1364,7 +1364,7 @@ msgstr "ex. Artista, amo cachorros, leitora ávida." #: src/lib/moderation/useGlobalLabelStrings.ts:43 msgid "E.g. artistic nudes." -msgstr "" +msgstr "Ex. nudez artística." #: src/view/com/modals/CreateOrEditList.tsx:283 msgid "e.g. Great Posters" @@ -1394,7 +1394,7 @@ msgstr "Editar" #: src/view/com/util/UserAvatar.tsx:299 #: src/view/com/util/UserBanner.tsx:85 msgid "Edit avatar" -msgstr "" +msgstr "Editar avatar" #: src/view/com/composer/photos/Gallery.tsx:144 #: src/view/com/modals/EditImage.tsx:207 @@ -1484,7 +1484,7 @@ msgstr "Habilitar somente {0}" #: src/screens/Moderation/index.tsx:331 msgid "Enable adult content" -msgstr "" +msgstr "Habilitar conteúdo adulto" #: src/screens/Onboarding/StepModeration/AdultContentEnabledPref.tsx:94 msgid "Enable Adult Content" @@ -1509,7 +1509,7 @@ msgstr "Ative esta configuração para ver respostas apenas entre as pessoas que #: src/screens/Moderation/index.tsx:341 msgid "Enabled" -msgstr "" +msgstr "Habilitado" #: src/screens/Profile/Sections/Feed.tsx:84 msgid "End of feed" @@ -1579,11 +1579,11 @@ msgstr "Todos" #: src/lib/moderation/useReportOptions.ts:66 msgid "Excessive mentions or replies" -msgstr "" +msgstr "Menções ou respostas excessivas" #: src/view/com/modals/DeleteAccount.tsx:231 msgid "Exits account deletion process" -msgstr "" +msgstr "Sair do processo de deleção da conta" #: src/view/com/modals/ChangeHandle.tsx:150 msgid "Exits handle change process" @@ -1591,7 +1591,7 @@ msgstr "Sair do processo de trocar usuário" #: src/view/com/modals/crop-image/CropImage.web.tsx:135 msgid "Exits image cropping process" -msgstr "" +msgstr "Sair do processo de cortar imagem" #: src/view/com/lightbox/Lightbox.web.tsx:130 msgid "Exits image view" @@ -1617,11 +1617,11 @@ msgstr "Mostrar ou esconder o post a que você está respondendo" #: src/lib/moderation/useGlobalLabelStrings.ts:47 msgid "Explicit or potentially disturbing media." -msgstr "" +msgstr "Imagens explícitas ou potencialmente perturbadoras." #: src/lib/moderation/useGlobalLabelStrings.ts:35 msgid "Explicit sexual images." -msgstr "" +msgstr "Imagens sexualmente explícitas." #: src/view/screens/Settings/index.tsx:777 msgid "Export my data" @@ -1671,7 +1671,7 @@ msgstr "Falha ao carregar feeds recomendados" #: src/view/com/lightbox/Lightbox.tsx:83 msgid "Failed to save image: {0}" -msgstr "" +msgstr "Não foi possível salvar a imagem: {0}" #: src/Navigation.tsx:196 msgid "Feed" @@ -1719,11 +1719,11 @@ msgstr "Feeds podem ser de assuntos específicos também!" #: src/view/com/modals/ChangeHandle.tsx:482 msgid "File Contents" -msgstr "" +msgstr "Conteúdo do arquivo" #: src/lib/moderation/useLabelBehaviorDescription.ts:66 msgid "Filter from feeds" -msgstr "" +msgstr "Filtrar dos feeds" #: src/screens/Onboarding/StepFinished.tsx:151 msgid "Finalizing" @@ -1794,7 +1794,7 @@ msgstr "Seguir {0}" #: src/view/com/profile/ProfileMenu.tsx:242 #: src/view/com/profile/ProfileMenu.tsx:253 msgid "Follow Account" -msgstr "" +msgstr "Seguir Conta" #: src/screens/Onboarding/StepSuggestedAccounts/index.tsx:179 msgid "Follow All" @@ -1842,7 +1842,7 @@ msgstr "Seguindo {0}" #: src/view/screens/Settings/index.tsx:553 msgid "Following feed preferences" -msgstr "" +msgstr "Configurações do feed principal" #: src/Navigation.tsx:262 #: src/view/com/home/HomeHeaderLayout.web.tsx:50 @@ -1887,7 +1887,7 @@ msgstr "Esqueci a Senha" #: src/lib/moderation/useReportOptions.ts:52 msgid "Frequently Posts Unwanted Content" -msgstr "" +msgstr "Frequentemente Posta Conteúdo Indesejado" #: src/screens/Hashtag.tsx:108 #: src/screens/Hashtag.tsx:148 @@ -1910,7 +1910,7 @@ msgstr "Vamos começar" #: src/lib/moderation/useReportOptions.ts:37 msgid "Glaring violations of law or terms of service" -msgstr "" +msgstr "Violações flagrantes da lei ou dos termos de serviço" #: src/components/moderation/ScreenHider.tsx:144 #: src/components/moderation/ScreenHider.tsx:153 @@ -1940,11 +1940,11 @@ msgstr "Voltar para o passo anterior" #: src/view/screens/NotFound.tsx:55 msgid "Go home" -msgstr "" +msgstr "Voltar para a tela inicial" #: src/view/screens/NotFound.tsx:54 msgid "Go Home" -msgstr "" +msgstr "Voltar para a tela inicial" #: src/view/screens/Search/Search.tsx:748 #: src/view/shell/desktop/Search.tsx:263 @@ -1961,7 +1961,7 @@ msgstr "Próximo" #: src/lib/moderation/useGlobalLabelStrings.ts:46 msgid "Graphic Media" -msgstr "" +msgstr "Conteúdo Gráfico" #: src/view/com/modals/ChangeHandle.tsx:265 msgid "Handle" @@ -1969,7 +1969,7 @@ msgstr "Usuário" #: src/lib/moderation/useReportOptions.ts:32 msgid "Harassment, trolling, or intolerance" -msgstr "" +msgstr "Assédio, intolerância ou \"trollagem\"" #: src/Navigation.tsx:282 msgid "Hashtag" @@ -2070,11 +2070,11 @@ msgstr "Hmm, estamos com problemas para encontrar este feed. Ele pode ter sido e #: src/screens/Moderation/index.tsx:61 msgid "Hmmmm, it seems we're having trouble loading this data. See below for more details. If this issue persists, please contact us." -msgstr "" +msgstr "Hmmmm, parece que estamos com problemas pra carregar isso. Veja mais detalhes abaixo. Se o problema continuar, por favor, entre em contato." #: src/screens/Profile/ErrorState.tsx:31 msgid "Hmmmm, we couldn't load that moderation service." -msgstr "" +msgstr "Hmmmm, não foi possível carregar este serviço de moderação." #: src/Navigation.tsx:454 #: src/view/shell/bottom-bar/BottomBar.tsx:139 @@ -2093,7 +2093,7 @@ msgstr "Página Inicial" #: src/view/com/modals/ChangeHandle.tsx:421 msgid "Host:" -msgstr "" +msgstr "Host:" #: src/view/com/auth/create/Step1.tsx:75 #: src/view/com/auth/login/ForgotPasswordForm.tsx:120 @@ -2127,15 +2127,15 @@ msgstr "Se nenhum for selecionado, adequado para todas as idades." #: src/view/com/auth/create/Policies.tsx:91 msgid "If you are not yet an adult according to the laws of your country, your parent or legal guardian must read these Terms on your behalf." -msgstr "" +msgstr "Se você ainda não é um adulto de acordo com as leis do seu país, seu responsável ou guardião legal deve ler estes Termos por você." #: src/view/screens/ProfileList.tsx:610 msgid "If you delete this list, you won't be able to recover it." -msgstr "" +msgstr "Se você deletar esta lista, você não poderá recuperá-la." #: src/view/com/util/forms/PostDropdownBtn.tsx:316 msgid "If you remove this post, you won't be able to recover it." -msgstr "" +msgstr "Se você remover este post, você não poderá recuperá-la." #: src/view/com/modals/ChangePassword.tsx:148 msgid "If you want to change your password, we will send you a code to verify that this is your account." @@ -2143,7 +2143,7 @@ msgstr "Se você quiser alterar sua senha, enviaremos um código que para verifi #: src/lib/moderation/useReportOptions.ts:36 msgid "Illegal and Urgent" -msgstr "" +msgstr "Ilegal e Urgente" #: src/view/com/util/images/Gallery.tsx:38 msgid "Image" @@ -2160,7 +2160,7 @@ msgstr "Texto alternativo da imagem" #: src/lib/moderation/useReportOptions.ts:47 msgid "Impersonation or false claims about identity or affiliation" -msgstr "" +msgstr "Falsificação de identidade ou alegações falsas sobre identidade ou filiação" #: src/view/com/auth/login/SetNewPasswordForm.tsx:138 msgid "Input code sent to your email for password reset" @@ -2208,7 +2208,7 @@ msgstr "Insira sua senha" #: src/view/com/modals/ChangeHandle.tsx:390 msgid "Input your preferred hosting provider" -msgstr "" +msgstr "Insira seu provedor de hospedagem" #: src/view/com/auth/create/Step2.tsx:80 msgid "Input your user handle" @@ -2271,35 +2271,35 @@ msgstr "Jornalismo" #: src/components/moderation/LabelsOnMe.tsx:59 msgid "label has been placed on this {labelTarget}" -msgstr "" +msgstr "rótulo aplicado neste {labelTarget}" #: src/components/moderation/ContentHider.tsx:144 msgid "Labeled by {0}." -msgstr "" +msgstr "Rotulado por {0}." #: src/components/moderation/ContentHider.tsx:142 msgid "Labeled by the author." -msgstr "" +msgstr "Rotulado pelo autor." #: src/view/screens/Profile.tsx:186 msgid "Labels" -msgstr "" +msgstr "Rótulos" #: src/screens/Profile/Sections/Labels.tsx:143 msgid "Labels are annotations on users and content. They can be used to hide, warn, and categorize the network." -msgstr "" +msgstr "Rótulos são identificações aplicadas sobre perfis e conteúdos. Eles são utilizados para esconder, avisar e categorizar o conteúdo da rede." #: src/components/moderation/LabelsOnMe.tsx:61 msgid "labels have been placed on this {labelTarget}" -msgstr "" +msgstr "rótulos foram aplicados neste {labelTarget}" #: src/components/moderation/LabelsOnMeDialog.tsx:63 msgid "Labels on your account" -msgstr "" +msgstr "Rótulos sobre sua conta" #: src/components/moderation/LabelsOnMeDialog.tsx:65 msgid "Labels on your content" -msgstr "" +msgstr "Rótulos sobre seu conteúdo" #: src/view/com/composer/select-language/SelectLangBtn.tsx:104 msgid "Language selection" @@ -2333,7 +2333,7 @@ msgstr "Saiba Mais" #: src/components/moderation/ContentHider.tsx:65 #: src/components/moderation/ContentHider.tsx:128 msgid "Learn more about the moderation applied to this content." -msgstr "" +msgstr "Saiba mais sobre a decisão de moderação aplicada neste conteúdo." #: src/components/moderation/PostHider.tsx:85 #: src/components/moderation/ScreenHider.tsx:126 @@ -2346,7 +2346,7 @@ msgstr "Saiba mais sobre o que é público no Bluesky." #: src/components/moderation/ContentHider.tsx:152 msgid "Learn more." -msgstr "" +msgstr "Saiba mais." #: src/view/com/modals/lang-settings/ContentLanguagesSettings.tsx:82 msgid "Leave them all unchecked to see any language." @@ -2409,7 +2409,7 @@ msgstr "Curtido por {0} {1}" #: src/components/LabelingServiceCard/index.tsx:72 msgid "Liked by {count} {0}" -msgstr "" +msgstr "Curtido por {count} {0}" #: src/screens/Profile/Header/ProfileHeaderLabeler.tsx:277 #: src/screens/Profile/Header/ProfileHeaderLabeler.tsx:291 @@ -2560,7 +2560,7 @@ msgstr "Mensagem do servidor: {0}" #: src/lib/moderation/useReportOptions.ts:45 msgid "Misleading Account" -msgstr "" +msgstr "Conta Enganosa" #: src/Navigation.tsx:119 #: src/screens/Moderation/index.tsx:106 @@ -2573,7 +2573,7 @@ msgstr "Moderação" #: src/components/moderation/ModerationDetailsDialog.tsx:113 msgid "Moderation details" -msgstr "" +msgstr "Detalhes da moderação" #: src/view/com/lists/ListCard.tsx:93 #: src/view/com/modals/UserAddRemoveLists.tsx:206 @@ -2613,11 +2613,11 @@ msgstr "Moderação" #: src/Navigation.tsx:216 msgid "Moderation states" -msgstr "" +msgstr "Moderação" #: src/screens/Moderation/index.tsx:217 msgid "Moderation tools" -msgstr "" +msgstr "Ferramentas de moderação" #: src/components/moderation/ModerationDetailsDialog.tsx:49 #: src/lib/moderation/useModerationCauseDescription.ts:40 @@ -2626,7 +2626,7 @@ msgstr "O moderador escolheu um aviso geral neste conteúdo." #: src/view/com/post-thread/PostThreadItem.tsx:541 msgid "More" -msgstr "" +msgstr "Mais" #: src/view/shell/desktop/Feeds.tsx:65 msgid "More feeds" @@ -2731,7 +2731,7 @@ msgstr "Contas silenciadas não aparecem no seu feed ou nas suas notificações. #: src/lib/moderation/useModerationCauseDescription.ts:85 msgid "Muted by \"{0}\"" -msgstr "" +msgstr "Silenciado por \"{0}\"" #: src/screens/Moderation/index.tsx:233 msgid "Muted words & tags" @@ -2756,7 +2756,7 @@ msgstr "Meu Perfil" #: src/view/screens/Settings/index.tsx:596 msgid "My saved feeds" -msgstr "" +msgstr "Meus feeds salvos" #: src/view/screens/Settings/index.tsx:602 msgid "My Saved Feeds" @@ -2779,7 +2779,7 @@ msgstr "Nome é obrigatório" #: src/lib/moderation/useReportOptions.ts:78 #: src/lib/moderation/useReportOptions.ts:86 msgid "Name or Description Violates Community Standards" -msgstr "" +msgstr "Nome ou Descrição Viola os Padrões da Comunidade" #: src/screens/Onboarding/index.tsx:25 msgid "Nature" @@ -2799,7 +2799,7 @@ msgstr "Navega para seu perfil" #: src/components/ReportDialog/SelectReportOptionView.tsx:124 msgid "Need to report a copyright violation?" -msgstr "" +msgstr "Precisa denunciar uma violação de copyright?" #: src/view/com/modals/EmbedConsent.tsx:107 #: src/view/com/modals/EmbedConsent.tsx:123 @@ -2821,7 +2821,7 @@ msgstr "Nunca perca o acesso aos seus seguidores ou dados." #: src/view/com/modals/ChangeHandle.tsx:520 msgid "Nevermind, create a handle for me" -msgstr "" +msgstr "Deixa pra lá, crie um usuário pra mim" #: src/view/screens/Lists.tsx:76 msgctxt "action" @@ -2914,7 +2914,7 @@ msgstr "Sem descrição" #: src/view/com/modals/ChangeHandle.tsx:406 msgid "No DNS Panel" -msgstr "" +msgstr "Não tenho painel de DNS" #: src/screens/Profile/Header/ProfileHeaderStandard.tsx:111 msgid "No longer following {0}" @@ -2954,11 +2954,11 @@ msgstr "Ninguém" #: src/components/LikedByList.tsx:102 #: src/components/LikesDialog.tsx:99 msgid "Nobody has liked this yet. Maybe you should be the first!" -msgstr "" +msgstr "Ninguém curtiu isso ainda. Você pode ser o primeiro!" #: src/lib/moderation/useGlobalLabelStrings.ts:42 msgid "Non-sexual Nudity" -msgstr "" +msgstr "Nudez não-erótica" #: src/view/com/modals/SelfLabel.tsx:135 msgid "Not Applicable." @@ -2977,11 +2977,11 @@ msgstr "Agora não" #: src/view/com/profile/ProfileMenu.tsx:368 #: src/view/com/util/forms/PostDropdownBtn.tsx:342 msgid "Note about sharing" -msgstr "" +msgstr "Nota sobre compartilhamento" #: src/screens/Moderation/index.tsx:542 msgid "Note: Bluesky is an open and public network. This setting only limits the visibility of your content on the Bluesky app and website, and other apps may not respect this setting. Your content may still be shown to logged-out users by other apps and websites." -msgstr "Nota: o Bluesky é uma rede aberta e pública. Esta configuração limita somente a visibilidade do seu conteúdo no site e aplicativo do Bluesky, e outros aplicativos podem não respeitar esta configuração. Seu conteúdo ainda poderá ser exibido para usuários deslogados por outros aplicativos e sites." +msgstr "Nota: o Bluesky é uma rede aberta e pública. Esta configuração limita somente a visibilidade do seu conteúdo no site e aplicativo do Bluesky, e outros aplicativos podem não respeitar esta configuração. Seu conteúdo ainda poderá ser exibido para usuários não autenticados por outros aplicativos e sites." #: src/Navigation.tsx:469 #: src/view/screens/Notifications.tsx:124 @@ -2999,11 +2999,11 @@ msgstr "Nudez" #: src/lib/moderation/useReportOptions.ts:71 msgid "Nudity or pornography not labeled as such" -msgstr "" +msgstr "Nudez ou pornografia sem aviso aplicado" #: src/lib/moderation/useLabelBehaviorDescription.ts:11 msgid "Off" -msgstr "" +msgstr "Desligado" #: src/view/com/util/ErrorBoundary.tsx:49 msgid "Oh no!" @@ -3015,7 +3015,7 @@ msgstr "Opa! Algo deu errado." #: src/screens/Onboarding/StepModeration/AdultContentEnabledPref.tsx:127 msgid "OK" -msgstr "" +msgstr "OK" #: src/view/com/auth/login/PasswordUpdatedForm.tsx:41 msgid "Okay" @@ -3062,7 +3062,7 @@ msgstr "Abrir seletor de emojis" #: src/view/screens/ProfileFeed.tsx:299 msgid "Open feed options menu" -msgstr "" +msgstr "Abrir opções do feed" #: src/view/screens/Settings/index.tsx:734 msgid "Open links with in-app browser" @@ -3070,7 +3070,7 @@ msgstr "Abrir links no navegador interno" #: src/screens/Moderation/index.tsx:229 msgid "Open muted words and tags settings" -msgstr "" +msgstr "Abrir opções de palavras/tags silenciadas" #: src/view/screens/Moderation.tsx:92 #~ msgid "Open muted words settings" @@ -3091,7 +3091,7 @@ msgstr "Abre o storybook" #: src/view/screens/Settings/index.tsx:816 msgid "Open system log" -msgstr "" +msgstr "Abrir registros do sistema" #: src/view/com/util/forms/DropdownButton.tsx:154 msgid "Opens {numItems} options" @@ -3132,12 +3132,12 @@ msgstr "Abre as configurações de anexos externos" #: src/view/com/auth/HomeLoggedOutCTA.tsx:56 #: src/view/com/auth/SplashScreen.tsx:70 msgid "Opens flow to create a new Bluesky account" -msgstr "" +msgstr "Abre o fluxo de criação de conta do Bluesky" #: src/view/com/auth/HomeLoggedOutCTA.tsx:74 #: src/view/com/auth/SplashScreen.tsx:83 msgid "Opens flow to sign into your existing Bluesky account" -msgstr "" +msgstr "Abre o fluxo de entrar na sua conta do Bluesky" #: src/view/com/profile/ProfileHeader.tsx:575 #~ msgid "Opens followers list" @@ -3153,27 +3153,23 @@ msgstr "Abre a lista de códigos de convite" #: src/view/screens/Settings/index.tsx:798 msgid "Opens modal for account deletion confirmation. Requires email code" -msgstr "" - -#: src/view/screens/Settings/index.tsx:774 -#~ msgid "Opens modal for account deletion confirmation. Requires email code." -#~ msgstr "Abre modal para confirmar exclusão de conta. Requer código de verificação." +msgstr "Abre modal de confirmar a exclusão da conta. Requer código enviado por email" #: src/view/screens/Settings/index.tsx:756 msgid "Opens modal for changing your Bluesky password" -msgstr "" +msgstr "Abre modal para troca da sua senha do Bluesky" #: src/view/screens/Settings/index.tsx:718 msgid "Opens modal for choosing a new Bluesky handle" -msgstr "" +msgstr "Abre modal para troca do seu usuário do Bluesky" #: src/view/screens/Settings/index.tsx:779 msgid "Opens modal for downloading your Bluesky account data (repository)" -msgstr "" +msgstr "Abre modal para baixar os dados da sua conta do Bluesky" #: src/view/screens/Settings/index.tsx:970 msgid "Opens modal for email verification" -msgstr "" +msgstr "Abre modal para verificação de email" #: src/view/com/modals/ChangeHandle.tsx:281 msgid "Opens modal for using custom domain" @@ -3198,23 +3194,15 @@ msgstr "Abre a tela com todos os feeds salvos" #: src/view/screens/Settings/index.tsx:696 msgid "Opens the app password settings" -msgstr "" - -#: src/view/screens/Settings/index.tsx:676 -#~ msgid "Opens the app password settings page" -#~ msgstr "Abre a página de configurações de senha do aplicativo" +msgstr "Abre as configurações de senha do aplicativo" #: src/view/screens/Settings/index.tsx:554 msgid "Opens the Following feed preferences" -msgstr "" - -#: src/view/screens/Settings/index.tsx:535 -#~ msgid "Opens the home feed preferences" -#~ msgstr "Abre as preferências do feed inicial" +msgstr "Abre as preferências do feed inicial" #: src/view/com/modals/LinkWarning.tsx:76 msgid "Opens the linked website" -msgstr "" +msgstr "Abre o link" #: src/view/screens/Settings/index.tsx:829 #: src/view/screens/Settings/index.tsx:839 @@ -3235,7 +3223,7 @@ msgstr "Opção {0} de {numItems}" #: src/components/ReportDialog/SubmitView.tsx:162 msgid "Optionally provide additional information below:" -msgstr "" +msgstr "Se quiser adicionar mais informações, digite abaixo:" #: src/view/com/modals/Threadgate.tsx:89 msgid "Or combine these options:" @@ -3243,7 +3231,7 @@ msgstr "Ou combine estas opções:" #: src/lib/moderation/useReportOptions.ts:25 msgid "Other" -msgstr "" +msgstr "Outro" #: src/view/com/auth/login/ChooseAccountForm.tsx:147 msgid "Other account" @@ -3274,7 +3262,7 @@ msgstr "Senha" #: src/view/com/modals/ChangePassword.tsx:142 msgid "Password Changed" -msgstr "" +msgstr "Senha Atualizada" #: src/view/com/auth/login/Login.tsx:157 msgid "Password updated" @@ -3315,7 +3303,7 @@ msgstr "Fixar na tela inicial" #: src/view/screens/ProfileFeed.tsx:294 msgid "Pin to Home" -msgstr "" +msgstr "Fixar na Tela Inicial" #: src/view/screens/SavedFeeds.tsx:88 msgid "Pinned Feeds" @@ -3380,7 +3368,7 @@ msgstr "Por favor, digite sua senha também:" #: src/components/moderation/LabelsOnMeDialog.tsx:222 msgid "Please explain why you think this label was incorrectly applied by {0}" -msgstr "" +msgstr "Por favor, explique por que você acha que este rótulo foi aplicado incorrentamente por {0}" #: src/view/com/modals/AppealLabel.tsx:72 #: src/view/com/modals/AppealLabel.tsx:75 @@ -3405,7 +3393,7 @@ msgstr "Pornografia" #: src/lib/moderation/useGlobalLabelStrings.ts:34 msgid "Pornography" -msgstr "" +msgstr "Pornografia" #: src/view/com/composer/Composer.tsx:366 #: src/view/com/composer/Composer.tsx:374 @@ -3439,12 +3427,12 @@ msgstr "Post oculto" #: src/components/moderation/ModerationDetailsDialog.tsx:98 #: src/lib/moderation/useModerationCauseDescription.ts:99 msgid "Post Hidden by Muted Word" -msgstr "" +msgstr "Post Escondido por Palavra Silenciada" #: src/components/moderation/ModerationDetailsDialog.tsx:101 #: src/lib/moderation/useModerationCauseDescription.ts:108 msgid "Post Hidden by You" -msgstr "" +msgstr "Post Escondido por Você" #: src/view/com/composer/select-language/SelectLangBtn.tsx:87 msgid "Post language" @@ -3481,7 +3469,7 @@ msgstr "Link Potencialmente Enganoso" #: src/components/Lists.tsx:88 msgid "Press to retry" -msgstr "" +msgstr "Tentar novamente" #: src/view/com/lightbox/Lightbox.web.tsx:150 msgid "Previous image" @@ -3515,7 +3503,7 @@ msgstr "Processando..." #: src/view/screens/DebugMod.tsx:888 #: src/view/screens/Profile.tsx:340 msgid "profile" -msgstr "" +msgstr "perfil" #: src/view/shell/bottom-bar/BottomBar.tsx:251 #: src/view/shell/desktop/LeftNav.tsx:419 @@ -3577,7 +3565,7 @@ msgstr "Índices" #: src/view/screens/Search/Search.tsx:776 msgid "Recent Searches" -msgstr "" +msgstr "Buscas Recentes" #: src/view/com/auth/onboarding/RecommendedFeeds.tsx:116 msgid "Recommended Feeds" @@ -3606,11 +3594,11 @@ msgstr "Remover conta" #: src/view/com/util/UserAvatar.tsx:358 msgid "Remove Avatar" -msgstr "" +msgstr "Remover avatar" #: src/view/com/util/UserBanner.tsx:148 msgid "Remove Banner" -msgstr "" +msgstr "Remover banner" #: src/view/com/posts/FeedErrorMessage.tsx:160 msgid "Remove feed" @@ -3618,7 +3606,7 @@ msgstr "Remover feed" #: src/view/com/posts/FeedErrorMessage.tsx:201 msgid "Remove feed?" -msgstr "" +msgstr "Remover feed?" #: src/view/com/feeds/FeedSourceCard.tsx:173 #: src/view/com/feeds/FeedSourceCard.tsx:233 @@ -3629,7 +3617,7 @@ msgstr "Remover dos meus feeds" #: src/view/com/feeds/FeedSourceCard.tsx:278 msgid "Remove from my feeds?" -msgstr "" +msgstr "Remover dos meus feeds?" #: src/view/com/composer/photos/Gallery.tsx:167 msgid "Remove image" @@ -3653,7 +3641,7 @@ msgstr "Desfazer repost" #: src/view/com/posts/FeedErrorMessage.tsx:202 msgid "Remove this feed from your saved feeds" -msgstr "" +msgstr "Remover este feed dos feeds salvos" #: src/view/com/posts/FeedErrorMessage.tsx:132 #~ msgid "Remove this feed from your saved feeds?" @@ -3666,11 +3654,11 @@ msgstr "Removido da lista" #: src/view/com/feeds/FeedSourceCard.tsx:121 msgid "Removed from my feeds" -msgstr "Remover dos meus feeds" +msgstr "Removido dos meus feeds" #: src/view/screens/ProfileFeed.tsx:208 msgid "Removed from your feeds" -msgstr "" +msgstr "Removido dos feeds salvos" #: src/view/com/composer/ExternalEmbed.tsx:71 msgid "Removes default thumbnail from {0}" @@ -3724,23 +3712,23 @@ msgstr "Denunciar post" #: src/components/ReportDialog/SelectReportOptionView.tsx:43 msgid "Report this content" -msgstr "" +msgstr "Denunciar conteúdo" #: src/components/ReportDialog/SelectReportOptionView.tsx:56 msgid "Report this feed" -msgstr "" +msgstr "Denunciar este feed" #: src/components/ReportDialog/SelectReportOptionView.tsx:53 msgid "Report this list" -msgstr "" +msgstr "Denunciar esta lista" #: src/components/ReportDialog/SelectReportOptionView.tsx:50 msgid "Report this post" -msgstr "" +msgstr "Denunciar este post" #: src/components/ReportDialog/SelectReportOptionView.tsx:47 msgid "Report this user" -msgstr "" +msgstr "Denunciar este usuário" #: src/view/com/modals/Repost.tsx:43 #: src/view/com/modals/Repost.tsx:48 @@ -3868,12 +3856,12 @@ msgstr "Voltar para página anterior" #: src/view/screens/NotFound.tsx:59 msgid "Returns to home page" -msgstr "" +msgstr "Voltar para a tela inicial" #: src/view/screens/NotFound.tsx:58 #: src/view/screens/ProfileFeed.tsx:112 msgid "Returns to previous page" -msgstr "" +msgstr "Voltar para página anterior" #: src/components/dialogs/BirthDateSettings.tsx:125 #: src/view/com/modals/ChangeHandle.tsx:173 @@ -3894,7 +3882,7 @@ msgstr "Salvar texto alternativo" #: src/components/dialogs/BirthDateSettings.tsx:119 msgid "Save birthday" -msgstr "" +msgstr "Salvar data de nascimento" #: src/view/com/modals/EditProfile.tsx:232 msgid "Save Changes" @@ -3911,7 +3899,7 @@ msgstr "Salvar corte de imagem" #: src/view/screens/ProfileFeed.tsx:335 #: src/view/screens/ProfileFeed.tsx:341 msgid "Save to my feeds" -msgstr "" +msgstr "Salvar nos meus feeds" #: src/view/screens/SavedFeeds.tsx:122 msgid "Saved Feeds" @@ -3919,11 +3907,11 @@ msgstr "Feeds Salvos" #: src/view/com/lightbox/Lightbox.tsx:81 msgid "Saved to your camera roll." -msgstr "" +msgstr "Imagem salva na galeria." #: src/view/screens/ProfileFeed.tsx:212 msgid "Saved to your feeds" -msgstr "" +msgstr "Adicionado aos seus feeds" #: src/view/com/modals/EditProfile.tsx:225 msgid "Saves any changes to your profile" @@ -3935,7 +3923,7 @@ msgstr "Salva mudança de usuário para {handle}" #: src/view/com/modals/crop-image/CropImage.web.tsx:145 msgid "Saves image crop settings" -msgstr "" +msgstr "Salva o corte da imagem" #: src/screens/Onboarding/index.tsx:36 msgid "Science" @@ -4035,11 +4023,11 @@ msgstr "Selecionar de uma conta existente" #: src/view/screens/LanguageSettings.tsx:299 msgid "Select languages" -msgstr "" +msgstr "Selecionar idiomas" #: src/components/ReportDialog/SelectLabelerView.tsx:32 msgid "Select moderator" -msgstr "" +msgstr "Selecionar moderador" #: src/view/com/util/Selector.tsx:107 msgid "Select option {i} of {numItems}" @@ -4056,7 +4044,7 @@ msgstr "Selecione algumas contas para seguir" #: src/components/ReportDialog/SubmitView.tsx:135 msgid "Select the moderation service(s) to report to" -msgstr "" +msgstr "Selecione o(s) serviço(s) de moderação para reportar" #: src/view/com/auth/server-input/index.tsx:82 msgid "Select the service that hosts your data." @@ -4080,7 +4068,7 @@ msgstr "Selecione quais idiomas você deseja ver nos seus feeds. Se nenhum for s #: src/view/screens/LanguageSettings.tsx:98 msgid "Select your app language for the default text to display in the app." -msgstr "" +msgstr "Selecione o idioma do seu aplicativo" #: src/screens/Onboarding/StepInterests/index.tsx:196 msgid "Select your interests from the options below" @@ -4120,7 +4108,7 @@ msgstr "Enviar comentários" #: src/components/ReportDialog/SubmitView.tsx:214 #: src/components/ReportDialog/SubmitView.tsx:218 msgid "Send report" -msgstr "" +msgstr "Denunciar" #: src/view/com/modals/report/SendReportButton.tsx:45 #~ msgid "Send Report" @@ -4128,7 +4116,7 @@ msgstr "" #: src/components/ReportDialog/SelectLabelerView.tsx:46 msgid "Send report to {0}" -msgstr "" +msgstr "Denunciar via {0}" #: src/view/com/modals/DeleteAccount.tsx:133 msgid "Sends email with confirmation code for account deletion" @@ -4150,7 +4138,7 @@ msgstr "URL do servidor" #: src/screens/Moderation/index.tsx:306 msgid "Set birthdate" -msgstr "" +msgstr "Definir data de nascimento" #: src/view/screens/Settings/index.tsx:488 #~ msgid "Set color theme to dark" @@ -4210,23 +4198,23 @@ msgstr "Configura o usuário no Bluesky" #: src/view/screens/Settings/index.tsx:507 msgid "Sets color theme to dark" -msgstr "" +msgstr "Define o tema para escuro" #: src/view/screens/Settings/index.tsx:500 msgid "Sets color theme to light" -msgstr "" +msgstr "Define o tema para claro" #: src/view/screens/Settings/index.tsx:494 msgid "Sets color theme to system setting" -msgstr "" +msgstr "Define o tema para seguir o sistema" #: src/view/screens/Settings/index.tsx:533 msgid "Sets dark theme to the dark theme" -msgstr "" +msgstr "Define o tema escuro para o padrão" #: src/view/screens/Settings/index.tsx:526 msgid "Sets dark theme to the dim theme" -msgstr "" +msgstr "Define o tema escuro para o menos escuro" #: src/view/com/auth/login/ForgotPasswordForm.tsx:157 msgid "Sets email for password reset" @@ -4238,15 +4226,15 @@ msgstr "Configura o provedor de hospedagem para recuperação de senha" #: src/view/com/modals/crop-image/CropImage.web.tsx:123 msgid "Sets image aspect ratio to square" -msgstr "" +msgstr "Define a proporção da imagem para quadrada" #: src/view/com/modals/crop-image/CropImage.web.tsx:113 msgid "Sets image aspect ratio to tall" -msgstr "" +msgstr "Define a proporção da imagem para alta" #: src/view/com/modals/crop-image/CropImage.web.tsx:103 msgid "Sets image aspect ratio to wide" -msgstr "" +msgstr "Define a proporção da imagem para comprida" #: src/view/com/auth/create/Step1.tsx:97 #: src/view/com/auth/login/LoginForm.tsx:154 @@ -4267,7 +4255,7 @@ msgstr "Atividade sexual ou nudez erótica." #: src/lib/moderation/useGlobalLabelStrings.ts:38 msgid "Sexually Suggestive" -msgstr "" +msgstr "Sexualmente Sugestivo" #: src/view/com/lightbox/Lightbox.tsx:141 msgctxt "action" @@ -4286,7 +4274,7 @@ msgstr "Compartilhar" #: src/view/com/profile/ProfileMenu.tsx:373 #: src/view/com/util/forms/PostDropdownBtn.tsx:347 msgid "Share anyway" -msgstr "" +msgstr "Compartilhar assim" #: src/view/screens/ProfileFeed.tsx:361 #: src/view/screens/ProfileFeed.tsx:363 @@ -4313,11 +4301,11 @@ msgstr "Mostrar mesmo assim" #: src/lib/moderation/useLabelBehaviorDescription.ts:27 #: src/lib/moderation/useLabelBehaviorDescription.ts:63 msgid "Show badge" -msgstr "" +msgstr "Mostrar rótulo" #: src/lib/moderation/useLabelBehaviorDescription.ts:61 msgid "Show badge and filter from feeds" -msgstr "" +msgstr "Mostrar rótulo e filtrar dos feeds" #: src/view/com/modals/EmbedConsent.tsx:87 msgid "Show embeds from {0}" @@ -4392,11 +4380,11 @@ msgstr "Mostrar usuários" #: src/lib/moderation/useLabelBehaviorDescription.ts:58 msgid "Show warning" -msgstr "" +msgstr "Mostrar aviso" #: src/lib/moderation/useLabelBehaviorDescription.ts:56 msgid "Show warning and filter from feeds" -msgstr "" +msgstr "Mostrar aviso e filtrar dos feeds" #: src/view/com/profile/ProfileHeader.tsx:462 #~ msgid "Shows a list of users similar to this user." @@ -4474,7 +4462,7 @@ msgstr "Entrou como" #: src/view/com/auth/login/ChooseAccountForm.tsx:112 msgid "Signed in as @{0}" -msgstr "Logado como @{0}" +msgstr "autenticado como @{0}" #: src/view/com/modals/SwitchAccount.tsx:70 msgid "Signs {0} out of Bluesky" @@ -4502,7 +4490,7 @@ msgstr "Desenvolvimento de software" #: src/screens/Moderation/index.tsx:116 #: src/screens/Profile/Sections/Labels.tsx:77 msgid "Something went wrong, please try again." -msgstr "" +msgstr "Algo deu errado. Por favor, tente novamente." #: src/components/Lists.tsx:203 #~ msgid "Something went wrong!" @@ -4526,15 +4514,15 @@ msgstr "Classificar respostas de um post por:" #: src/components/moderation/LabelsOnMeDialog.tsx:147 msgid "Source:" -msgstr "" +msgstr "Fonte:" #: src/lib/moderation/useReportOptions.ts:65 msgid "Spam" -msgstr "" +msgstr "Spam" #: src/lib/moderation/useReportOptions.ts:53 msgid "Spam; excessive mentions or replies" -msgstr "" +msgstr "Spam; menções ou respostas excessivas" #: src/screens/Onboarding/index.tsx:30 msgid "Sports" @@ -4572,11 +4560,11 @@ msgstr "Inscrever-se" #: src/screens/Profile/Sections/Labels.tsx:181 msgid "Subscribe to @{0} to use these labels:" -msgstr "" +msgstr "Inscreva-se em @{0} para utilizar estes rótulos:" #: src/screens/Profile/Header/ProfileHeaderLabeler.tsx:222 msgid "Subscribe to Labeler" -msgstr "" +msgstr "Inscrever-se no rotulador" #: src/screens/Onboarding/StepAlgoFeeds/FeedCard.tsx:173 #: src/screens/Onboarding/StepAlgoFeeds/FeedCard.tsx:308 @@ -4585,7 +4573,7 @@ msgstr "Increver-se no feed {0}" #: src/screens/Profile/Header/ProfileHeaderLabeler.tsx:185 msgid "Subscribe to this labeler" -msgstr "" +msgstr "Inscrever-se neste rotulador" #: src/view/screens/ProfileList.tsx:586 msgid "Subscribe to this list" @@ -4621,7 +4609,7 @@ msgstr "Trocar para {0}" #: src/view/com/modals/SwitchAccount.tsx:104 #: src/view/screens/Settings/index.tsx:140 msgid "Switches the account you are logged in to" -msgstr "Troca a conta que você está logado" +msgstr "Troca a conta que você está autenticado" #: src/view/screens/Settings/index.tsx:491 msgid "System" @@ -4671,7 +4659,7 @@ msgstr "Termos de Serviço" #: src/lib/moderation/useReportOptions.ts:79 #: src/lib/moderation/useReportOptions.ts:87 msgid "Terms used violate community standards" -msgstr "" +msgstr "Termos utilizados violam as diretrizes da comunidade" #: src/components/dialogs/MutedWords.tsx:324 msgid "text" @@ -4683,11 +4671,11 @@ msgstr "Campo de entrada de texto" #: src/components/ReportDialog/SubmitView.tsx:78 msgid "Thank you. Your report has been sent." -msgstr "" +msgstr "Obrigado. Sua denúncia foi enviada." #: src/view/com/modals/ChangeHandle.tsx:466 msgid "That contains the following:" -msgstr "" +msgstr "Contém o seguinte:" #: src/view/com/auth/create/CreateAccount.tsx:94 msgid "That handle is already taken." @@ -4700,7 +4688,7 @@ msgstr "A conta poderá interagir com você após o desbloqueio." #: src/components/moderation/ModerationDetailsDialog.tsx:128 msgid "the author" -msgstr "" +msgstr "o(a) autor(a)" #: src/view/screens/CommunityGuidelines.tsx:36 msgid "The Community Guidelines have been moved to <0/>" @@ -4712,11 +4700,11 @@ msgstr "A Política de Direitos Autorais foi movida para <0/>" #: src/components/moderation/LabelsOnMeDialog.tsx:49 msgid "The following labels were applied to your account." -msgstr "" +msgstr "Os seguintes rótulos foram aplicados sobre sua conta." #: src/components/moderation/LabelsOnMeDialog.tsx:50 msgid "The following labels were applied to your content." -msgstr "" +msgstr "Os seguintes rótulos foram aplicados sobre seu conteúdo." #: src/screens/Onboarding/Layout.tsx:60 msgid "The following steps will help customize your Bluesky experience." @@ -4790,7 +4778,7 @@ msgstr "Tivemos um problema ao carregar suas listas. Toque aqui para tentar de n #: src/components/ReportDialog/SubmitView.tsx:83 msgid "There was an issue sending your report. Please check your internet connection." -msgstr "" +msgstr "Tivemos um problema ao enviar sua denúncia. Por favor, verifique sua conexão com a internet." #: src/screens/Onboarding/StepModeration/AdultContentEnabledPref.tsx:65 msgid "There was an issue syncing your preferences with the server" @@ -4843,15 +4831,15 @@ msgstr "Esta conta solicitou que os usuários fizessem login para visualizar seu #: src/components/moderation/LabelsOnMeDialog.tsx:205 msgid "This appeal will be sent to <0>{0}." -msgstr "" +msgstr "Esta contestação será enviada para <0>{0}." #: src/lib/moderation/useGlobalLabelStrings.ts:19 msgid "This content has been hidden by the moderators." -msgstr "" +msgstr "Este conteúdo foi escondido pelos moderadores." #: src/lib/moderation/useGlobalLabelStrings.ts:24 msgid "This content has received a general warning from moderators." -msgstr "" +msgstr "Este conteúdo recebeu um aviso dos moderadores." #: src/view/com/modals/EmbedConsent.tsx:68 msgid "This content is hosted by {0}. Do you want to enable external media?" @@ -4872,7 +4860,7 @@ msgstr "Este conteúdo não é visível sem uma conta do Bluesky." #: src/view/screens/Settings/ExportCarDialog.tsx:75 msgid "This feature is in beta. You can read more about repository exports in <0>this blogpost." -msgstr "" +msgstr "Esta funcionalidade está em beta. Você pode ler mais sobre exportação de repositórios <0>neste post do nosso blog." #: src/view/com/posts/FeedErrorMessage.tsx:114 msgid "This feed is currently receiving high traffic and is temporarily unavailable. Please try again later." @@ -4898,11 +4886,11 @@ msgstr "Isso é importante caso você precise alterar seu e-mail ou redefinir su #: src/components/moderation/ModerationDetailsDialog.tsx:125 msgid "This label was applied by {0}." -msgstr "" +msgstr "Este rótulo foi aplicado por {0}." #: src/screens/Profile/Sections/Labels.tsx:168 msgid "This labeler hasn't declared what labels it publishes, and may not be active." -msgstr "" +msgstr "Este rotulador não declarou quais rótulos utiliza e pode não estar funcionando ainda." #: src/view/com/modals/LinkWarning.tsx:58 msgid "This link is taking you to the following website:" @@ -4914,7 +4902,7 @@ msgstr "Esta lista está vazia!" #: src/screens/Profile/ErrorState.tsx:40 msgid "This moderation service is unavailable. See below for more details. If this issue persists, contact us." -msgstr "" +msgstr "Este serviço de moderação está indisponível. Veja mais detalhes abaixo. Se este problema persistir, entre em contato." #: src/view/com/modals/AddAppPasswords.tsx:106 msgid "This name is already in use" @@ -4926,27 +4914,27 @@ msgstr "Este post foi excluído." #: src/view/com/util/forms/PostDropdownBtn.tsx:344 msgid "This post is only visible to logged-in users. It won't be visible to people who aren't logged in." -msgstr "" +msgstr "Este post só pode ser visto por usuários autenticados e não aparecerá para pessoas que não estão autenticadas." #: src/view/com/util/forms/PostDropdownBtn.tsx:326 msgid "This post will be hidden from feeds." -msgstr "" +msgstr "Este post será escondido de todos os feeds." #: src/view/com/profile/ProfileMenu.tsx:370 msgid "This profile is only visible to logged-in users. It won't be visible to people who aren't logged in." -msgstr "" +msgstr "Este post só pode ser visto por usuários autenticados e não aparecerá para pessoas que não estão autenticadas." #: src/view/com/auth/create/Policies.tsx:46 msgid "This service has not provided terms of service or a privacy policy." -msgstr "" +msgstr "Este serviço não proveu termos de serviço ou política de privacidade." #: src/view/com/modals/ChangeHandle.tsx:446 msgid "This should create a domain record at:" -msgstr "" +msgstr "Isso deve criar um registro no domínio:" #: src/view/com/profile/ProfileFollowers.tsx:95 msgid "This user doesn't have any followers." -msgstr "" +msgstr "Este usuário não é seguido por ninguém ainda." #: src/components/moderation/ModerationDetailsDialog.tsx:73 #: src/lib/moderation/useModerationCauseDescription.ts:68 @@ -4955,7 +4943,7 @@ msgstr "Este usuário te bloqueou. Você não pode ver este conteúdo." #: src/lib/moderation/useGlobalLabelStrings.ts:30 msgid "This user has requested that their content only be shown to signed-in users." -msgstr "" +msgstr "Este usuário requisitou que seu conteúdo só seja visível para usuários autenticados." #: src/view/com/modals/ModerationDetails.tsx:42 #~ msgid "This user is included in the <0/> list which you have blocked." @@ -4967,15 +4955,15 @@ msgstr "" #: src/components/moderation/ModerationDetailsDialog.tsx:56 msgid "This user is included in the <0>{0} list which you have blocked." -msgstr "" +msgstr "Este usuário está incluído na lista <0>{0}, que você bloqueou." #: src/components/moderation/ModerationDetailsDialog.tsx:85 msgid "This user is included in the <0>{0} list which you have muted." -msgstr "" +msgstr "Este usuário está incluído na lista <0>{0}, que você silenciou." #: src/view/com/profile/ProfileFollows.tsx:94 msgid "This user isn't following anyone." -msgstr "" +msgstr "Este usuário não segue ninguém ainda." #: src/view/com/modals/SelfLabel.tsx:137 msgid "This warning is only available for posts with media attached." @@ -4991,7 +4979,7 @@ msgstr "Isso removerá {0} das suas palavras silenciadas. Você pode adicioná-l #: src/view/screens/Settings/index.tsx:574 msgid "Thread preferences" -msgstr "" +msgstr "Preferências das Threads" #: src/view/screens/PreferencesThreads.tsx:53 #: src/view/screens/Settings/index.tsx:584 @@ -5008,7 +4996,7 @@ msgstr "Preferências das Threads" #: src/components/ReportDialog/SelectLabelerView.tsx:35 msgid "To whom would you like to send this report?" -msgstr "" +msgstr "Para quem você gostaria de enviar esta denúncia?" #: src/components/dialogs/MutedWords.tsx:113 msgid "Toggle between muted word options." @@ -5020,7 +5008,7 @@ msgstr "Alternar menu suspenso" #: src/screens/Moderation/index.tsx:334 msgid "Toggle to enable or disable adult content" -msgstr "" +msgstr "Ligar ou desligar conteúdo adulto" #: src/view/com/modals/EditImage.tsx:271 msgid "Transformations" @@ -5040,7 +5028,7 @@ msgstr "Tentar novamente" #: src/view/com/modals/ChangeHandle.tsx:429 msgid "Type:" -msgstr "" +msgstr "Tipo:" #: src/view/screens/ProfileList.tsx:478 msgid "Un-block list" @@ -5078,7 +5066,7 @@ msgstr "Desbloquear Conta" #: src/screens/Profile/Header/ProfileHeaderStandard.tsx:272 #: src/view/com/profile/ProfileMenu.tsx:343 msgid "Unblock Account?" -msgstr "" +msgstr "Desbloquear Conta?" #: src/view/com/modals/Repost.tsx:42 #: src/view/com/modals/Repost.tsx:55 @@ -5090,7 +5078,7 @@ msgstr "Desfazer repost" #: src/view/com/auth/onboarding/RecommendedFollowsItem.tsx:141 #: src/view/com/profile/ProfileHeaderSuggestedFollows.tsx:246 msgid "Unfollow" -msgstr "" +msgstr "Deixar de seguir" #: src/view/com/profile/FollowButton.tsx:60 msgctxt "action" @@ -5104,7 +5092,7 @@ msgstr "Deixar de seguir {0}" #: src/view/com/profile/ProfileMenu.tsx:241 #: src/view/com/profile/ProfileMenu.tsx:251 msgid "Unfollow Account" -msgstr "" +msgstr "Deixar de seguir" #: src/view/com/auth/create/state.ts:262 msgid "Unfortunately, you do not meet the requirements to create an account." @@ -5116,7 +5104,7 @@ msgstr "Descurtir" #: src/view/screens/ProfileFeed.tsx:572 msgid "Unlike this feed" -msgstr "" +msgstr "Descurtir este feed" #: src/components/TagMenu/index.tsx:249 #: src/view/screens/ProfileList.tsx:579 @@ -5152,7 +5140,7 @@ msgstr "Desafixar" #: src/view/screens/ProfileFeed.tsx:291 msgid "Unpin from home" -msgstr "" +msgstr "Desafixar da tela inicial" #: src/view/screens/ProfileList.tsx:444 msgid "Unpin moderation list" @@ -5164,15 +5152,15 @@ msgstr "Desafixar lista de moderação" #: src/screens/Profile/Header/ProfileHeaderLabeler.tsx:220 msgid "Unsubscribe" -msgstr "" +msgstr "Desinscrever-se" #: src/screens/Profile/Header/ProfileHeaderLabeler.tsx:184 msgid "Unsubscribe from this labeler" -msgstr "" +msgstr "Desinscrever-se deste rotulador" #: src/lib/moderation/useReportOptions.ts:70 msgid "Unwanted Sexual Content" -msgstr "" +msgstr "Conteúdo Sexual Indesejado" #: src/view/com/modals/UserAddRemoveLists.tsx:70 msgid "Update {displayName} in Lists" @@ -5184,7 +5172,7 @@ msgstr "Atualizar {displayName} nas Listas" #: src/view/com/modals/ChangeHandle.tsx:509 msgid "Update to {handle}" -msgstr "" +msgstr "Alterar para {handle}" #: src/view/com/auth/login/SetNewPasswordForm.tsx:204 msgid "Updating..." @@ -5199,23 +5187,23 @@ msgstr "Carregar um arquivo de texto para:" #: src/view/com/util/UserBanner.tsx:116 #: src/view/com/util/UserBanner.tsx:119 msgid "Upload from Camera" -msgstr "" +msgstr "Tirar uma foto" #: src/view/com/util/UserAvatar.tsx:343 #: src/view/com/util/UserBanner.tsx:133 msgid "Upload from Files" -msgstr "" +msgstr "Carregar um arquivo" #: src/view/com/util/UserAvatar.tsx:337 #: src/view/com/util/UserAvatar.tsx:341 #: src/view/com/util/UserBanner.tsx:127 #: src/view/com/util/UserBanner.tsx:131 msgid "Upload from Library" -msgstr "" +msgstr "Carregar da galeria" #: src/view/com/modals/ChangeHandle.tsx:409 msgid "Use a file on your server" -msgstr "" +msgstr "Utilize um arquivo no seu servidor" #: src/view/screens/AppPasswords.tsx:197 msgid "Use app passwords to login to other Bluesky clients without giving full access to your account or password." @@ -5223,7 +5211,7 @@ msgstr "Use as senhas de aplicativos para fazer login em outros clientes do Blue #: src/view/com/modals/ChangeHandle.tsx:518 msgid "Use bsky.social as hosting provider" -msgstr "" +msgstr "Usar bsky.social como serviço de hospedagem" #: src/view/com/modals/ChangeHandle.tsx:517 msgid "Use default provider" @@ -5241,7 +5229,7 @@ msgstr "Usar o meu navegador padrão" #: src/view/com/modals/ChangeHandle.tsx:401 msgid "Use the DNS panel" -msgstr "" +msgstr "Usar o painel do meu DNS" #: src/view/com/modals/AddAppPasswords.tsx:155 msgid "Use this to sign into the other app along with your handle." @@ -5258,7 +5246,7 @@ msgstr "Usuário Bloqueado" #: src/lib/moderation/useModerationCauseDescription.ts:48 msgid "User Blocked by \"{0}\"" -msgstr "" +msgstr "Usuário Bloqueado por \"{0}\"" #: src/components/moderation/ModerationDetailsDialog.tsx:54 msgid "User Blocked by List" @@ -5266,7 +5254,7 @@ msgstr "Usuário Bloqueado Por Lista" #: src/lib/moderation/useModerationCauseDescription.ts:66 msgid "User Blocking You" -msgstr "" +msgstr "Usuário Bloqueia Você" #: src/components/moderation/ModerationDetailsDialog.tsx:71 msgid "User Blocks You" @@ -5322,15 +5310,15 @@ msgstr "Usuários em \"{0}\"" #: src/components/LikesDialog.tsx:85 msgid "Users that have liked this content or profile" -msgstr "" +msgstr "Usuários que curtiram este conteúdo ou perfil" #: src/view/com/modals/ChangeHandle.tsx:437 msgid "Value:" -msgstr "" +msgstr "Conteúdo:" #: src/view/com/modals/ChangeHandle.tsx:510 msgid "Verify {0}" -msgstr "" +msgstr "Verificar {0}" #: src/view/screens/Settings/index.tsx:944 msgid "Verify email" @@ -5367,11 +5355,11 @@ msgstr "Ver depuração" #: src/components/ReportDialog/SelectReportOptionView.tsx:133 msgid "View details" -msgstr "" +msgstr "Ver detalhes" #: src/components/ReportDialog/SelectReportOptionView.tsx:128 msgid "View details for reporting a copyright violation" -msgstr "" +msgstr "Ver detalhes para denunciar uma violação de copyright" #: src/view/com/posts/FeedSlice.tsx:99 msgid "View full thread" @@ -5379,7 +5367,7 @@ msgstr "Ver thread completa" #: src/components/moderation/LabelsOnMe.tsx:51 msgid "View information about these labels" -msgstr "" +msgstr "Ver informações sobre estes rótulos" #: src/view/com/posts/FeedErrorMessage.tsx:166 msgid "View profile" @@ -5391,11 +5379,11 @@ msgstr "Ver o avatar" #: src/components/LabelingServiceCard/index.tsx:140 msgid "View the labeling service provided by @{0}" -msgstr "" +msgstr "Ver este rotulador provido por @{0}" #: src/view/screens/ProfileFeed.tsx:584 msgid "View users who like this feed" -msgstr "" +msgstr "Ver usuários que curtiram este feed" #: src/view/com/modals/LinkWarning.tsx:75 #: src/view/com/modals/LinkWarning.tsx:77 @@ -5411,11 +5399,11 @@ msgstr "Avisar" #: src/lib/moderation/useLabelBehaviorDescription.ts:48 msgid "Warn content" -msgstr "" +msgstr "Avisar" #: src/lib/moderation/useLabelBehaviorDescription.ts:46 msgid "Warn content and filter from feeds" -msgstr "" +msgstr "Avisar e filtrar dos feeds" #: src/screens/Onboarding/StepAlgoFeeds/index.tsx:134 msgid "We also think you'll like \"For You\" by Skygaze:" @@ -5451,11 +5439,11 @@ msgstr "Recomendamos nosso feed \"Discover\":" #: src/components/dialogs/BirthDateSettings.tsx:52 msgid "We were unable to load your birth date preferences. Please try again." -msgstr "" +msgstr "Não foi possível carregar sua data de nascimento. Por favor, tente novamente." #: src/screens/Moderation/index.tsx:387 msgid "We were unable to load your configured labelers at this time." -msgstr "" +msgstr "Não foi possível carregar seus rotuladores." #: src/screens/Onboarding/StepInterests/index.tsx:133 msgid "We weren't able to connect. Please try again to continue setting up your account. If it continues to fail, you can skip this flow." @@ -5496,7 +5484,7 @@ msgstr "Sentimos muito! Não conseguimos encontrar a página que você estava pr #: src/screens/Profile/Header/ProfileHeaderLabeler.tsx:319 msgid "We're sorry! You can only subscribe to ten labelers, and you've reached your limit of ten." -msgstr "" +msgstr "Sentimos muito! Você só pode se inscrever em até dez rotuladores e você já chegou ao máximo." #: src/view/com/auth/onboarding/WelcomeMobile.tsx:48 msgid "Welcome to <0>Bluesky" @@ -5530,23 +5518,23 @@ msgstr "Quem pode responder" #: src/components/ReportDialog/SelectReportOptionView.tsx:44 msgid "Why should this content be reviewed?" -msgstr "" +msgstr "Por que este conteúdo deve ser revisado?" #: src/components/ReportDialog/SelectReportOptionView.tsx:57 msgid "Why should this feed be reviewed?" -msgstr "" +msgstr "Por que este feed deve ser revisado?" #: src/components/ReportDialog/SelectReportOptionView.tsx:54 msgid "Why should this list be reviewed?" -msgstr "" +msgstr "Por que esta lista deve ser revisada?" #: src/components/ReportDialog/SelectReportOptionView.tsx:51 msgid "Why should this post be reviewed?" -msgstr "" +msgstr "Por que este post deve ser revisado?" #: src/components/ReportDialog/SelectReportOptionView.tsx:48 msgid "Why should this user be reviewed?" -msgstr "" +msgstr "Por que este usuário deve ser revisado?" #: src/view/com/modals/crop-image/CropImage.web.tsx:102 msgid "Wide" @@ -5581,7 +5569,7 @@ msgstr "Você está na fila." #: src/view/com/profile/ProfileFollows.tsx:93 msgid "You are not following anyone." -msgstr "" +msgstr "Você não segue ninguém." #: src/view/com/posts/FollowingEmptyState.tsx:67 #: src/view/com/posts/FollowingEndOfFeed.tsx:68 @@ -5599,7 +5587,7 @@ msgstr "Agora você pode entrar com a sua nova senha." #: src/view/com/profile/ProfileFollowers.tsx:94 msgid "You do not have any followers." -msgstr "" +msgstr "Ninguém segue você ainda." #: src/view/com/modals/InviteCodes.tsx:66 msgid "You don't have any invite codes yet! We'll send you some when you've been on Bluesky for a little longer." @@ -5636,20 +5624,20 @@ msgstr "Você utilizou um código inválido. O código segue este padrão: XXXXX #: src/lib/moderation/useModerationCauseDescription.ts:109 msgid "You have hidden this post" -msgstr "" +msgstr "Você escondeu este post" #: src/components/moderation/ModerationDetailsDialog.tsx:102 msgid "You have hidden this post." -msgstr "" +msgstr "Você escondeu este post." #: src/components/moderation/ModerationDetailsDialog.tsx:95 #: src/lib/moderation/useModerationCauseDescription.ts:92 msgid "You have muted this account." -msgstr "" +msgstr "Você silenciou esta conta." #: src/lib/moderation/useModerationCauseDescription.ts:86 msgid "You have muted this user" -msgstr "" +msgstr "Você silenciou este usuário." #: src/view/com/modals/ModerationDetails.tsx:87 #~ msgid "You have muted this user." @@ -5666,7 +5654,7 @@ msgstr "Você não tem listas." #: src/view/screens/ModerationBlockedAccounts.tsx:132 msgid "You have not blocked any accounts yet. To block an account, go to their profile and select \"Block account\" from the menu on their account." -msgstr "" +msgstr "Você ainda não bloqueou nenhuma conta. Para bloquear uma conta, acesse um perfil e selecione \"Bloquear conta\" no menu." #: src/view/screens/ModerationBlockedAccounts.tsx:132 #~ msgid "You have not blocked any accounts yet. To block an account, go to their profile and selected \"Block account\" from the menu on their account." @@ -5678,7 +5666,7 @@ msgstr "Você ainda não criou nenhuma senha de aplicativo. Você pode criar uma #: src/view/screens/ModerationMutedAccounts.tsx:131 msgid "You have not muted any accounts yet. To mute an account, go to their profile and select \"Mute account\" from the menu on their account." -msgstr "" +msgstr "Você ainda não silenciou nenhuma conta. Para silenciar uma conta, acesse um perfil e selecione \"Silenciar conta\" no menu." #: src/view/screens/ModerationMutedAccounts.tsx:131 #~ msgid "You have not muted any accounts yet. To mute an account, go to their profile and selected \"Mute account\" from the menu on their account." @@ -5690,7 +5678,7 @@ msgstr "Você não silenciou nenhuma palavra ou tag ainda" #: src/components/moderation/LabelsOnMeDialog.tsx:69 msgid "You may appeal these labels if you feel they were placed in error." -msgstr "" +msgstr "Você pode contestar estes rótulos se você acha que estão errados." #: src/view/com/modals/ContentFilteringSettings.tsx:175 #~ msgid "You must be 18 or older to enable adult content." @@ -5702,7 +5690,7 @@ msgstr "Você precisa ser maior de idade para habilitar conteúdo adulto." #: src/components/ReportDialog/SubmitView.tsx:205 msgid "You must select at least one labeler for a report" -msgstr "" +msgstr "Você deve selecionar no mínimo um rotulador" #: src/view/com/util/forms/PostDropdownBtn.tsx:144 msgid "You will no longer receive notifications for this thread" @@ -5733,7 +5721,7 @@ msgstr "Tudo pronto!" #: src/components/moderation/ModerationDetailsDialog.tsx:99 #: src/lib/moderation/useModerationCauseDescription.ts:101 msgid "You've chosen to hide a word or tag within this post." -msgstr "" +msgstr "Você escolheu esconder uma palavra ou tag deste post." #: src/view/com/posts/FollowingEndOfFeed.tsx:48 msgid "You've reached the end of your feed! Find some more accounts to follow." diff --git a/src/locale/locales/tr/messages.po b/src/locale/locales/tr/messages.po new file mode 100644 index 0000000000..b9d857e1cc --- /dev/null +++ b/src/locale/locales/tr/messages.po @@ -0,0 +1,4391 @@ +msgid "" +msgstr "" +"Project-Id-Version: \n" +"Report-Msgid-Bugs-To: \n" +"POT-Creation-Date: 2023-11-05 16:01-0800\n" +"PO-Revision-Date: \n" +"Last-Translator: atiksoftware\n" +"Language-Team: atiksoftware\n" +"Language: tr\n" +"MIME-Version: 1.0\n" +"Content-Type: text/plain; charset=utf-8\n" +"Content-Transfer-Encoding: 8bit\n" +"Plural-Forms: \n" +"X-Generator: Poedit 3.4.2\n" + +#: src/view/com/modals/VerifyEmail.tsx:142 +msgid "(no email)" +msgstr "(e-posta yok)" + +#: src/view/shell/desktop/RightNav.tsx:168 +msgid "{0, plural, one {# invite code available} other {# invite codes available}}" +msgstr "{0, plural, one {# davet kodu mevcut} other {# davet kodları mevcut}}" + +#: src/view/com/profile/ProfileHeader.tsx:632 +msgid "{following} following" +msgstr "{following} takip ediliyor" + +#: src/view/shell/desktop/RightNav.tsx:151 +msgid "{invitesAvailable, plural, one {Invite codes: # available} other {Invite codes: # available}}" +msgstr "{invitesAvailable, plural, one {Davet kodları: # mevcut} other {Davet kodları: # mevcut}}" + +#: src/view/screens/Settings.tsx:435 src/view/shell/Drawer.tsx:664 +msgid "{invitesAvailable} invite code available" +msgstr "{invitesAvailable} davet kodu mevcut" + +#: src/view/screens/Settings.tsx:437 src/view/shell/Drawer.tsx:666 +msgid "{invitesAvailable} invite codes available" +msgstr "{invitesAvailable} davet kodları mevcut" + +#: src/view/shell/Drawer.tsx:443 +msgid "{numUnreadNotifications} unread" +msgstr "{numUnreadNotifications} okunmamış" + +#: src/view/com/threadgate/WhoCanReply.tsx:158 +msgid "<0/> members" +msgstr "<0/> üyeleri" + +#: src/view/com/profile/ProfileHeader.tsx:634 +msgid "<0>{following} <1>following" +msgstr "<0>{following} <1>takip ediliyor" + +#: src/view/com/auth/onboarding/RecommendedFeeds.tsx:30 +msgid "<0>Choose your<1>Recommended<2>Feeds" +msgstr "<0>Önerilen<1>Feeds<2>Seç" + +#: src/view/com/auth/onboarding/RecommendedFollows.tsx:37 +msgid "<0>Follow some<1>Recommended<2>Users" +msgstr "<0>Önerilen<1>Kullanıcıları Takip Et<2>Seç" + +#: src/view/com/auth/onboarding/WelcomeDesktop.tsx:21 +msgid "<0>Welcome to<1>Bluesky" +msgstr "<0>Bluesky'e<1>Hoşgeldiniz" + +#: src/view/com/profile/ProfileHeader.tsx:597 +msgid "⚠Invalid Handle" +msgstr "⚠Geçersiz Kullanıcı Adı" + +#: src/view/com/util/moderation/LabelInfo.tsx:45 +msgid "A content warning has been applied to this {0}." +msgstr "Bu {0} için bir içerik uyarısı uygulandı." + +#: src/lib/hooks/useOTAUpdate.ts:16 +msgid "A new version of the app is available. Please update to continue using the app." +msgstr "Uygulamanın yeni bir sürümü mevcut. Devam etmek için güncelleyin." + +#: src/view/com/util/ViewHeader.tsx:83 src/view/screens/Search/Search.tsx:624 +msgid "Access navigation links and settings" +msgstr "Gezinme bağlantılarına ve ayarlara erişin" + +#: src/view/com/pager/FeedsTabBarMobile.tsx:89 +msgid "Access profile and other navigation links" +msgstr "Profil ve diğer gezinme bağlantılarına erişin" + +#: src/view/com/modals/EditImage.tsx:299 src/view/screens/Settings.tsx:445 +msgid "Accessibility" +msgstr "Erişilebilirlik" + +#: src/view/com/auth/login/LoginForm.tsx:163 src/view/screens/Settings.tsx:308 +#: src/view/screens/Settings.tsx:715 +msgid "Account" +msgstr "Hesap" + +#: src/view/com/profile/ProfileHeader.tsx:293 +msgid "Account blocked" +msgstr "Hesap engellendi" + +#: src/view/com/profile/ProfileHeader.tsx:260 +msgid "Account muted" +msgstr "Hesap susturuldu" + +#: src/view/com/modals/ModerationDetails.tsx:86 +msgid "Account Muted" +msgstr "Hesap Susturuldu" + +#: src/view/com/modals/ModerationDetails.tsx:72 +msgid "Account Muted by List" +msgstr "Liste Tarafından Hesap Susturuldu" + +#: src/view/com/util/AccountDropdownBtn.tsx:41 +msgid "Account options" +msgstr "Hesap seçenekleri" + +#: src/view/com/util/AccountDropdownBtn.tsx:25 +msgid "Account removed from quick access" +msgstr "Hesap hızlı erişimden kaldırıldı" + +#: src/view/com/profile/ProfileHeader.tsx:315 +msgid "Account unblocked" +msgstr "Hesap engeli kaldırıldı" + +#: src/view/com/profile/ProfileHeader.tsx:273 +msgid "Account unmuted" +msgstr "Hesap susturulması kaldırıldı" + +#: src/view/com/auth/onboarding/RecommendedFeedsItem.tsx:150 +#: src/view/com/modals/ListAddRemoveUsers.tsx:264 +#: src/view/com/modals/UserAddRemoveLists.tsx:219 +#: src/view/screens/ProfileList.tsx:812 +msgid "Add" +msgstr "Ekle" + +#: src/view/com/modals/SelfLabel.tsx:56 +msgid "Add a content warning" +msgstr "Bir içerik uyarısı ekleyin" + +#: src/view/screens/ProfileList.tsx:802 +msgid "Add a user to this list" +msgstr "Bu listeye bir kullanıcı ekleyin" + +#: src/view/screens/Settings.tsx:383 src/view/screens/Settings.tsx:392 +msgid "Add account" +msgstr "Hesap ekle" + +#: src/view/com/composer/photos/Gallery.tsx:119 +#: src/view/com/composer/photos/Gallery.tsx:180 +#: src/view/com/modals/AltImage.tsx:116 +msgid "Add alt text" +msgstr "Alternatif metin ekle" + +#: src/view/screens/AppPasswords.tsx:102 src/view/screens/AppPasswords.tsx:143 +#: src/view/screens/AppPasswords.tsx:156 +msgid "Add App Password" +msgstr "Uygulama Şifresi Ekle" + +#: src/view/com/modals/report/InputIssueDetails.tsx:41 +#: src/view/com/modals/report/Modal.tsx:191 +msgid "Add details" +msgstr "Detaylar ekle" + +#: src/view/com/modals/report/Modal.tsx:194 +msgid "Add details to report" +msgstr "Rapor için detaylar ekleyin" + +#: src/view/com/composer/Composer.tsx:446 +msgid "Add link card" +msgstr "Bağlantı kartı ekle" + +#: src/view/com/composer/Composer.tsx:451 +msgid "Add link card:" +msgstr "Bağlantı kartı ekle:" + +#: src/view/com/modals/ChangeHandle.tsx:417 +msgid "Add the following DNS record to your domain:" +msgstr "Alan adınıza aşağıdaki DNS kaydını ekleyin:" + +#: src/view/com/profile/ProfileHeader.tsx:357 +msgid "Add to Lists" +msgstr "Listelere Ekle" + +#: src/view/com/feeds/FeedSourceCard.tsx:243 +#: src/view/screens/ProfileFeed.tsx:272 +msgid "Add to my feeds" +msgstr "Beslemelerime ekle" + +#: src/view/com/auth/onboarding/RecommendedFeedsItem.tsx:139 +msgid "Added" +msgstr "Eklendi" + +#: src/view/com/modals/ListAddRemoveUsers.tsx:191 +#: src/view/com/modals/UserAddRemoveLists.tsx:144 +msgid "Added to list" +msgstr "Listeye eklendi" + +#: src/view/com/feeds/FeedSourceCard.tsx:125 +msgid "Added to my feeds" +msgstr "Beslemelerime eklendi" + +#: src/view/screens/PreferencesHomeFeed.tsx:173 +msgid "Adjust the number of likes a reply must have to be shown in your feed." +msgstr "Bir yanıtın beslemenizde gösterilmesi için sahip olması gereken beğeni sayısını ayarlayın." + +#: src/view/com/modals/SelfLabel.tsx:75 +msgid "Adult Content" +msgstr "Yetişkin İçerik" + +#: src/view/com/modals/ContentFilteringSettings.tsx:137 +msgid "Adult content can only be enabled via the Web at <0/>." +msgstr "Yetişkin içeriği yalnızca Web üzerinden <0/> etkinleştirilebilir." + +#: src/view/screens/Settings.tsx:658 +msgid "Advanced" +msgstr "Gelişmiş" + +#: src/view/com/auth/login/ForgotPasswordForm.tsx:217 +#: src/view/com/modals/ChangePassword.tsx:168 +msgid "Already have a code?" +msgstr "Zaten bir kodunuz mu var?" + +#: src/view/com/auth/login/ChooseAccountForm.tsx:98 +msgid "Already signed in as @{0}" +msgstr "Zaten @{0} olarak oturum açıldı" + +#: src/view/com/composer/photos/Gallery.tsx:130 +msgid "ALT" +msgstr "ALT" + +#: src/view/com/modals/EditImage.tsx:315 +msgid "Alt text" +msgstr "Alternatif metin" + +#: src/view/com/composer/photos/Gallery.tsx:209 +msgid "Alt text describes images for blind and low-vision users, and helps give context to everyone." +msgstr "Alternatif metin, görme engelli ve düşük görme yeteneğine sahip kullanıcılar için resimleri tanımlar ve herkes için bağlam sağlamaya yardımcı olur." + +#: src/view/com/modals/VerifyEmail.tsx:124 +msgid "An email has been sent to {0}. It includes a confirmation code which you can enter below." +msgstr "{0} adresine bir e-posta gönderildi. Aşağıda girebileceğiniz bir onay kodu içerir." + +#: src/view/com/modals/ChangeEmail.tsx:119 +msgid "An email has been sent to your previous address, {0}. It includes a confirmation code which you can enter below." +msgstr "Önceki adresinize, {0} bir e-posta gönderildi. Aşağıda girebileceğiniz bir onay kodu içerir." + +#: src/view/com/profile/FollowButton.tsx:30 +#: src/view/com/profile/FollowButton.tsx:40 +msgid "An issue occurred, please try again." +msgstr "Bir sorun oluştu, lütfen tekrar deneyin." + +#: src/view/com/notifications/FeedItem.tsx:236 +#: src/view/com/threadgate/WhoCanReply.tsx:178 +msgid "and" +msgstr "ve" + +#: src/screens/Onboarding/index.tsx:32 +msgid "Animals" +msgstr "Hayvanlar" + +#: src/view/screens/LanguageSettings.tsx:95 +msgid "App Language" +msgstr "Uygulama Dili" + +#: src/view/screens/AppPasswords.tsx:228 +msgid "App password deleted" +msgstr "Uygulama şifresi silindi" + +#: src/view/com/modals/AddAppPasswords.tsx:134 +msgid "App Password names can only contain letters, numbers, spaces, dashes, and underscores." +msgstr "Uygulama Şifre adları yalnızca harfler, sayılar, boşluklar, tireler ve alt çizgiler içerebilir." + +#: src/view/com/modals/AddAppPasswords.tsx:99 +msgid "App Password names must be at least 4 characters long." +msgstr "Uygulama Şifre adları en az 4 karakter uzunluğunda olmalıdır." + +#: src/view/screens/Settings.tsx:669 +msgid "App password settings" +msgstr "Uygulama şifresi ayarları" + +#: src/Navigation.tsx:238 src/view/screens/AppPasswords.tsx:187 +#: src/view/screens/Settings.tsx:678 +msgid "App Passwords" +msgstr "Uygulama Şifreleri" + +#: src/view/com/util/forms/PostDropdownBtn.tsx:250 +msgid "Appeal content warning" +msgstr "İçerik uyarısını itiraz et" + +#: src/view/com/modals/AppealLabel.tsx:65 +msgid "Appeal Content Warning" +msgstr "İçerik Uyarısını İtiraz Et" + +#: src/view/com/util/moderation/LabelInfo.tsx:52 +msgid "Appeal this decision" +msgstr "Bu karara itiraz et" + +#: src/view/com/util/moderation/LabelInfo.tsx:56 +msgid "Appeal this decision." +msgstr "Bu karara itiraz et." + +#: src/view/screens/Settings.tsx:460 +msgid "Appearance" +msgstr "Görünüm" + +#: src/view/screens/AppPasswords.tsx:224 +msgid "Are you sure you want to delete the app password \"{name}\"?" +msgstr "\"{name}\" uygulama şifresini silmek istediğinizden emin misiniz?" + +#: src/view/com/composer/Composer.tsx:143 +msgid "Are you sure you'd like to discard this draft?" +msgstr "Bu taslağı silmek istediğinizden emin misiniz?" + +#: src/view/screens/ProfileList.tsx:364 +msgid "Are you sure?" +msgstr "Emin misiniz?" + +#: src/view/com/util/forms/PostDropdownBtn.tsx:233 +msgid "Are you sure? This cannot be undone." +msgstr "Emin misiniz? Bu geri alınamaz." + +#: src/view/com/composer/select-language/SuggestedLanguage.tsx:60 +msgid "Are you writing in <0>{0}?" +msgstr "<0>{0} dilinde mi yazıyorsunuz?" + +#: src/screens/Onboarding/index.tsx:26 +msgid "Art" +msgstr "Sanat" + +#: src/view/com/modals/SelfLabel.tsx:123 +msgid "Artistic or non-erotic nudity." +msgstr "Sanatsal veya erotik olmayan çıplaklık." + +#: src/view/com/auth/create/CreateAccount.tsx:147 +#: src/view/com/auth/login/ChooseAccountForm.tsx:151 +#: src/view/com/auth/login/ForgotPasswordForm.tsx:170 +#: src/view/com/auth/login/LoginForm.tsx:256 +#: src/view/com/auth/login/SetNewPasswordForm.tsx:179 +#: src/view/com/modals/report/InputIssueDetails.tsx:46 +#: src/view/com/post-thread/PostThread.tsx:413 +#: src/view/com/post-thread/PostThread.tsx:463 +#: src/view/com/post-thread/PostThread.tsx:471 +#: src/view/com/profile/ProfileHeader.tsx:688 +#: src/view/com/util/ViewHeader.tsx:81 +msgid "Back" +msgstr "Geri" + +#: src/view/com/post-thread/PostThread.tsx:421 +msgctxt "action" +msgid "Back" +msgstr "Geri" + +#: src/screens/Onboarding/StepSuggestedAccounts/index.tsx:136 +msgid "Based on your interest in {interestsText}" +msgstr "{interestsText} ilginize dayalı" + +#: src/view/screens/Settings.tsx:517 +msgid "Basics" +msgstr "Temel" + +#: src/view/com/auth/create/Step1.tsx:194 +#: src/view/com/modals/BirthDateSettings.tsx:73 +msgid "Birthday" +msgstr "Doğum günü" + +#: src/view/screens/Settings.tsx:340 +msgid "Birthday:" +msgstr "Doğum günü:" + +#: src/view/com/profile/ProfileHeader.tsx:286 +#: src/view/com/profile/ProfileHeader.tsx:393 +msgid "Block Account" +msgstr "Hesabı Engelle" + +#: src/view/screens/ProfileList.tsx:555 +msgid "Block accounts" +msgstr "Hesapları engelle" + +#: src/view/screens/ProfileList.tsx:505 +msgid "Block list" +msgstr "Listeyi engelle" + +#: src/view/screens/ProfileList.tsx:315 +msgid "Block these accounts?" +msgstr "Bu hesapları engelle?" + +#: src/view/screens/ProfileList.tsx:319 +msgid "Block this List" +msgstr "Bu Listeyi Engelle" + +#: src/view/com/lists/ListCard.tsx:109 +#: src/view/com/util/post-embeds/QuoteEmbed.tsx:60 +msgid "Blocked" +msgstr "Engellendi" + +#: src/view/screens/Moderation.tsx:123 +msgid "Blocked accounts" +msgstr "Engellenen hesaplar" + +#: src/Navigation.tsx:130 src/view/screens/ModerationBlockedAccounts.tsx:107 +msgid "Blocked Accounts" +msgstr "Engellenen Hesaplar" + +#: src/view/com/profile/ProfileHeader.tsx:288 +msgid "Blocked accounts cannot reply in your threads, mention you, or otherwise interact with you." +msgstr "Engellenen hesaplar, konularınıza yanıt veremez, sizi bahsedemez veya başka şekilde sizinle etkileşime giremez." + +#: src/view/screens/ModerationBlockedAccounts.tsx:115 +msgid "Blocked accounts cannot reply in your threads, mention you, or otherwise interact with you. You will not see their content and they will be prevented from seeing yours." +msgstr "Engellenen hesaplar, konularınıza yanıt veremez, sizi bahsedemez veya başka şekilde sizinle etkileşime giremez. Onların içeriğini görmeyeceksiniz ve onlar da sizinkini görmekten alıkonulacaklar." + +#: src/view/com/post-thread/PostThread.tsx:272 +msgid "Blocked post." +msgstr "Engellenen gönderi." + +#: src/view/screens/ProfileList.tsx:317 +msgid "Blocking is public. Blocked accounts cannot reply in your threads, mention you, or otherwise interact with you." +msgstr "Engelleme herkese açıktır. Engellenen hesaplar, konularınıza yanıt veremez, sizi bahsedemez veya başka şekilde sizinle etkileşime giremez." + +#: src/view/com/auth/HomeLoggedOutCTA.tsx:93 +msgid "Blog" +msgstr "Blog" + +#: src/view/com/auth/HomeLoggedOutCTA.tsx:31 +msgid "Bluesky" +msgstr "Bluesky" + +#: src/view/com/auth/onboarding/WelcomeDesktop.tsx:80 +#: src/view/com/auth/onboarding/WelcomeMobile.tsx:80 +msgid "Bluesky is flexible." +msgstr "Bluesky esnek." + +#: src/view/com/auth/onboarding/WelcomeDesktop.tsx:69 +#: src/view/com/auth/onboarding/WelcomeMobile.tsx:69 +msgid "Bluesky is open." +msgstr "Bluesky açık." + +#: src/view/com/auth/onboarding/WelcomeDesktop.tsx:56 +#: src/view/com/auth/onboarding/WelcomeMobile.tsx:56 +msgid "Bluesky is public." +msgstr "Bluesky kamusal." + +#: src/view/com/modals/Waitlist.tsx:70 +msgid "Bluesky uses invites to build a healthier community. If you don't know anybody with an invite, you can sign up for the waitlist and we'll send one soon." +msgstr "Bluesky, daha sağlıklı bir topluluk oluşturmak için davetleri kullanır. Bir daveti olan kimseyi tanımıyorsanız, bekleme listesine kaydolabilir ve yakında bir tane göndereceğiz." + +#: src/view/screens/Moderation.tsx:226 +msgid "Bluesky will not show your profile and posts to logged-out users. Other apps may not honor this request. This does not make your account private." +msgstr "Bluesky, profilinizi ve gönderilerinizi oturum açmamış kullanıcılara göstermeyecektir. Diğer uygulamalar bu isteği yerine getirmeyebilir. Bu, hesabınızı özel yapmaz." + +#: src/view/com/modals/ServerInput.tsx:78 +msgid "Bluesky.Social" +msgstr "Bluesky.Social" + +#: src/screens/Onboarding/index.tsx:33 +msgid "Books" +msgstr "Kitaplar" + +#: src/view/screens/Settings.tsx:841 +msgid "Build version {0} {1}" +msgstr "Sürüm {0} {1}" + +#: src/view/com/auth/HomeLoggedOutCTA.tsx:87 +msgid "Business" +msgstr "İş" + +#: src/view/com/modals/ServerInput.tsx:115 +msgid "Button disabled. Input custom domain to proceed." +msgstr "Button devre dışı. Devam etmek için özel alan adını girin." + +#: src/view/com/profile/ProfileSubpageHeader.tsx:157 +msgid "by —" +msgstr "tarafından —" + +#: src/view/com/auth/onboarding/RecommendedFeedsItem.tsx:100 +msgid "by {0}" +msgstr "tarafından {0}" + +#: src/view/com/profile/ProfileSubpageHeader.tsx:161 +msgid "by <0/>" +msgstr "tarafından <0/>" + +#: src/view/com/profile/ProfileSubpageHeader.tsx:159 +msgid "by you" +msgstr "siz tarafından" + +#: src/view/com/composer/photos/OpenCameraBtn.tsx:60 +#: src/view/com/util/UserAvatar.tsx:221 src/view/com/util/UserBanner.tsx:38 +msgid "Camera" +msgstr "Kamera" + +#: src/view/com/modals/AddAppPasswords.tsx:216 +msgid "Can only contain letters, numbers, spaces, dashes, and underscores. Must be at least 4 characters long, but no more than 32 characters long." +msgstr "Yalnızca harfler, sayılar, boşluklar, tireler ve alt çizgiler içerebilir. En az 4 karakter uzunluğunda, ancak 32 karakterden fazla olmamalıdır." + +#: src/components/Prompt.tsx:92 src/view/com/composer/Composer.tsx:300 +#: src/view/com/composer/Composer.tsx:305 +#: src/view/com/modals/ChangeEmail.tsx:218 +#: src/view/com/modals/ChangeEmail.tsx:220 +#: src/view/com/modals/ChangePassword.tsx:265 +#: src/view/com/modals/ChangePassword.tsx:268 +#: src/view/com/modals/CreateOrEditList.tsx:355 +#: src/view/com/modals/EditImage.tsx:323 +#: src/view/com/modals/EditProfile.tsx:249 +#: src/view/com/modals/InAppBrowserConsent.tsx:78 +#: src/view/com/modals/LinkWarning.tsx:87 src/view/com/modals/Repost.tsx:87 +#: src/view/com/modals/VerifyEmail.tsx:247 +#: src/view/com/modals/VerifyEmail.tsx:253 src/view/com/modals/Waitlist.tsx:142 +#: src/view/screens/Search/Search.tsx:693 src/view/shell/desktop/Search.tsx:238 +msgid "Cancel" +msgstr "İptal" + +#: src/view/com/modals/Confirm.tsx:88 src/view/com/modals/Confirm.tsx:91 +#: src/view/com/modals/CreateOrEditList.tsx:360 +#: src/view/com/modals/DeleteAccount.tsx:156 +#: src/view/com/modals/DeleteAccount.tsx:234 +msgctxt "action" +msgid "Cancel" +msgstr "İptal" + +#: src/view/com/modals/DeleteAccount.tsx:152 +#: src/view/com/modals/DeleteAccount.tsx:230 +msgid "Cancel account deletion" +msgstr "Hesap silmeyi iptal et" + +#: src/view/com/modals/ChangeHandle.tsx:149 +msgid "Cancel change handle" +msgstr "Kullanıcı adı değişikliğini iptal et" + +#: src/view/com/modals/crop-image/CropImage.web.tsx:134 +msgid "Cancel image crop" +msgstr "Resim kırpma işlemini iptal et" + +#: src/view/com/modals/EditProfile.tsx:244 +msgid "Cancel profile editing" +msgstr "Profil düzenlemeyi iptal et" + +#: src/view/com/modals/Repost.tsx:78 +msgid "Cancel quote post" +msgstr "Alıntı gönderiyi iptal et" + +#: src/view/com/modals/ListAddRemoveUsers.tsx:87 +#: src/view/shell/desktop/Search.tsx:234 +msgid "Cancel search" +msgstr "Aramayı iptal et" + +#: src/view/com/modals/Waitlist.tsx:136 +msgid "Cancel waitlist signup" +msgstr "Bekleme listesi kaydını iptal et" + +#: src/view/screens/Settings.tsx:334 +msgctxt "action" +msgid "Change" +msgstr "Değiştir" + +#: src/view/screens/Settings.tsx:690 +msgid "Change handle" +msgstr "Kullanıcı adını değiştir" + +#: src/view/com/modals/ChangeHandle.tsx:161 src/view/screens/Settings.tsx:699 +msgid "Change Handle" +msgstr "Kullanıcı Adını Değiştir" + +#: src/view/com/modals/VerifyEmail.tsx:147 +msgid "Change my email" +msgstr "E-postamı değiştir" + +#: src/view/screens/Settings.tsx:726 +msgid "Change password" +msgstr "Şifre değiştir" + +#: src/view/screens/Settings.tsx:735 +msgid "Change Password" +msgstr "Şifre Değiştir" + +#: src/view/com/composer/select-language/SuggestedLanguage.tsx:73 +msgid "Change post language to {0}" +msgstr "Gönderi dilini {0} olarak değiştir" + +#: src/view/screens/Settings.tsx:727 +msgid "Change your Bluesky password" +msgstr "Bluesky şifrenizi değiştirin" + +#: src/view/com/modals/ChangeEmail.tsx:109 +msgid "Change Your Email" +msgstr "E-postanızı Değiştirin" + +#: src/screens/Deactivated.tsx:73 src/screens/Deactivated.tsx:77 +msgid "Check my status" +msgstr "Durumumu kontrol et" + +#: src/view/com/auth/onboarding/RecommendedFeeds.tsx:121 +msgid "Check out some recommended feeds. Tap + to add them to your list of pinned feeds." +msgstr "Bazı önerilen beslemelere göz atın. Eklemek için + simgesine dokunun." + +#: src/view/com/auth/onboarding/RecommendedFollows.tsx:185 +msgid "Check out some recommended users. Follow them to see similar users." +msgstr "Bazı önerilen kullanıcılara göz atın. Benzer kullanıcıları görmek için onları takip edin." + +#: src/view/com/modals/DeleteAccount.tsx:169 +msgid "Check your inbox for an email with the confirmation code to enter below:" +msgstr "Aşağıya gireceğiniz onay kodu içeren bir e-posta için gelen kutunuzu kontrol edin:" + +#: src/view/com/modals/Threadgate.tsx:72 +msgid "Choose \"Everybody\" or \"Nobody\"" +msgstr "\"Herkes\" veya \"Hiç kimse\" seçin" + +#: src/view/screens/Settings.tsx:691 +msgid "Choose a new Bluesky username or create" +msgstr "Yeni bir Bluesky kullanıcı adı seçin veya oluşturun" + +#: src/view/com/modals/ServerInput.tsx:38 +msgid "Choose Service" +msgstr "Hizmet Seç" + +#: src/screens/Onboarding/StepFinished.tsx:135 +msgid "Choose the algorithms that power your custom feeds." +msgstr "Özel beslemelerinizi destekleyen algoritmaları seçin." + +#: src/view/com/auth/onboarding/WelcomeDesktop.tsx:83 +#: src/view/com/auth/onboarding/WelcomeMobile.tsx:83 +msgid "Choose the algorithms that power your experience with custom feeds." +msgstr "Özel beslemelerle deneyiminizi destekleyen algoritmaları seçin." + +#: src/screens/Onboarding/StepAlgoFeeds/index.tsx:103 +msgid "Choose your main feeds" +msgstr "Ana beslemelerinizi seçin" + +#: src/view/com/auth/create/Step1.tsx:163 +msgid "Choose your password" +msgstr "Şifrenizi seçin" + +#: src/view/screens/Settings.tsx:816 src/view/screens/Settings.tsx:817 +msgid "Clear all legacy storage data" +msgstr "Tüm eski depolama verilerini temizle" + +#: src/view/screens/Settings.tsx:819 +msgid "Clear all legacy storage data (restart after this)" +msgstr "Tüm eski depolama verilerini temizle (bundan sonra yeniden başlat)" + +#: src/view/screens/Settings.tsx:828 src/view/screens/Settings.tsx:829 +msgid "Clear all storage data" +msgstr "Tüm depolama verilerini temizle" + +#: src/view/screens/Settings.tsx:831 +msgid "Clear all storage data (restart after this)" +msgstr "Tüm depolama verilerini temizle (bundan sonra yeniden başlat)" + +#: src/view/com/util/forms/SearchInput.tsx:74 +#: src/view/screens/Search/Search.tsx:674 +msgid "Clear search query" +msgstr "Arama sorgusunu temizle" + +#: src/view/screens/Support.tsx:40 +msgid "click here" +msgstr "buraya tıklayın" + +#: src/screens/Onboarding/index.tsx:35 +msgid "Climate" +msgstr "İklim" + +#: src/view/com/modals/ChangePassword.tsx:265 +#: src/view/com/modals/ChangePassword.tsx:268 +msgid "Close" +msgstr "Kapat" + +#: src/components/Dialog/index.web.tsx:78 +msgid "Close active dialog" +msgstr "Etkin iletişim kutusunu kapat" + +#: src/view/com/auth/login/PasswordUpdatedForm.tsx:38 +msgid "Close alert" +msgstr "Uyarıyı kapat" + +#: src/view/com/util/BottomSheetCustomBackdrop.tsx:33 +msgid "Close bottom drawer" +msgstr "Alt çekmeceyi kapat" + +#: src/view/com/lightbox/ImageViewing/components/ImageDefaultHeader.tsx:26 +msgid "Close image" +msgstr "Resmi kapat" + +#: src/view/com/lightbox/Lightbox.web.tsx:119 +msgid "Close image viewer" +msgstr "Resim görüntüleyiciyi kapat" + +#: src/view/shell/index.web.tsx:49 +msgid "Close navigation footer" +msgstr "Gezinme altbilgisini kapat" + +#: src/view/shell/index.web.tsx:50 +msgid "Closes bottom navigation bar" +msgstr "Alt gezinme çubuğunu kapatır" + +#: src/view/com/auth/login/PasswordUpdatedForm.tsx:39 +msgid "Closes password update alert" +msgstr "Şifre güncelleme uyarısını kapatır" + +#: src/view/com/composer/Composer.tsx:302 +msgid "Closes post composer and discards post draft" +msgstr "Gönderi bestecisini kapatır ve gönderi taslağını siler" + +#: src/view/com/lightbox/ImageViewing/components/ImageDefaultHeader.tsx:27 +msgid "Closes viewer for header image" +msgstr "Başlık resmi görüntüleyicisini kapatır" + +#: src/view/com/notifications/FeedItem.tsx:317 +msgid "Collapses list of users for a given notification" +msgstr "Belirli bir bildirim için kullanıcı listesini daraltır" + +#: src/screens/Onboarding/index.tsx:41 +msgid "Comedy" +msgstr "Komedi" + +#: src/screens/Onboarding/index.tsx:27 +msgid "Comics" +msgstr "Çizgi romanlar" + +#: src/Navigation.tsx:228 src/view/screens/CommunityGuidelines.tsx:32 +msgid "Community Guidelines" +msgstr "Topluluk Kuralları" + +#: src/screens/Onboarding/StepFinished.tsx:148 +msgid "Complete onboarding and start using your account" +msgstr "Onboarding'i tamamlayın ve hesabınızı kullanmaya başlayın" + +#: src/view/com/composer/Composer.tsx:417 +msgid "Compose posts up to {MAX_GRAPHEME_LENGTH} characters in length" +msgstr "En fazla {MAX_GRAPHEME_LENGTH} karakter uzunluğunda gönderiler oluşturun" + +#: src/view/com/composer/Prompt.tsx:24 +msgid "Compose reply" +msgstr "Yanıt oluştur" + +#: src/screens/Onboarding/StepModeration/ModerationOption.tsx:67 +msgid "Configure content filtering setting for category: {0}" +msgstr "Kategori için içerik filtreleme ayarlarını yapılandır: {0}" + +#: src/components/Prompt.tsx:114 src/view/com/modals/AppealLabel.tsx:98 +#: src/view/com/modals/SelfLabel.tsx:154 +#: src/view/com/modals/VerifyEmail.tsx:231 +#: src/view/com/modals/VerifyEmail.tsx:233 +#: src/view/screens/PreferencesHomeFeed.tsx:308 +#: src/view/screens/PreferencesThreads.tsx:159 +msgid "Confirm" +msgstr "Onayla" + +#: src/view/com/modals/Confirm.tsx:75 src/view/com/modals/Confirm.tsx:78 +msgctxt "action" +msgid "Confirm" +msgstr "Onayla" + +#: src/view/com/modals/ChangeEmail.tsx:193 +#: src/view/com/modals/ChangeEmail.tsx:195 +msgid "Confirm Change" +msgstr "Değişikliği Onayla" + +#: src/view/com/modals/lang-settings/ConfirmLanguagesButton.tsx:34 +msgid "Confirm content language settings" +msgstr "İçerik dil ayarlarını onayla" + +#: src/view/com/modals/DeleteAccount.tsx:220 +msgid "Confirm delete account" +msgstr "Hesabı silmeyi onayla" + +#: src/view/com/modals/ContentFilteringSettings.tsx:151 +msgid "Confirm your age to enable adult content." +msgstr "Yetişkin içeriği etkinleştirmek için yaşınızı onaylayın." + +#: src/view/com/modals/ChangeEmail.tsx:157 +#: src/view/com/modals/DeleteAccount.tsx:182 +#: src/view/com/modals/VerifyEmail.tsx:165 +msgid "Confirmation code" +msgstr "Onay kodu" + +#: src/view/com/modals/Waitlist.tsx:120 +msgid "Confirms signing up {email} to the waitlist" +msgstr "{email} adresinin bekleme listesine kaydını onaylar" + +#: src/view/com/auth/create/CreateAccount.tsx:182 +#: src/view/com/auth/login/LoginForm.tsx:275 +msgid "Connecting..." +msgstr "Bağlanıyor..." + +#: src/view/com/auth/create/CreateAccount.tsx:202 +msgid "Contact support" +msgstr "Destek ile iletişime geçin" + +#: src/view/screens/Moderation.tsx:81 +msgid "Content filtering" +msgstr "İçerik filtreleme" + +#: src/view/com/modals/ContentFilteringSettings.tsx:44 +msgid "Content Filtering" +msgstr "İçerik Filtreleme" + +#: src/view/com/modals/lang-settings/ContentLanguagesSettings.tsx:74 +#: src/view/screens/LanguageSettings.tsx:278 +msgid "Content Languages" +msgstr "İçerik Dilleri" + +#: src/view/com/modals/ModerationDetails.tsx:65 +msgid "Content Not Available" +msgstr "İçerik Mevcut Değil" + +#: src/view/com/modals/ModerationDetails.tsx:33 +#: src/view/com/util/moderation/ScreenHider.tsx:78 +msgid "Content Warning" +msgstr "İçerik Uyarısı" + +#: src/view/com/composer/labels/LabelsBtn.tsx:31 +msgid "Content warnings" +msgstr "İçerik uyarıları" + +#: src/screens/Onboarding/StepAlgoFeeds/index.tsx:155 +#: src/screens/Onboarding/StepFollowingFeed.tsx:153 +#: src/screens/Onboarding/StepInterests/index.tsx:248 +#: src/screens/Onboarding/StepModeration/index.tsx:118 +#: src/screens/Onboarding/StepTopicalFeeds.tsx:108 +#: src/view/com/auth/onboarding/RecommendedFeeds.tsx:148 +#: src/view/com/auth/onboarding/RecommendedFollows.tsx:209 +msgid "Continue" +msgstr "Devam et" + +#: src/screens/Onboarding/StepFollowingFeed.tsx:150 +#: src/screens/Onboarding/StepInterests/index.tsx:245 +#: src/screens/Onboarding/StepModeration/index.tsx:115 +#: src/screens/Onboarding/StepTopicalFeeds.tsx:105 +msgid "Continue to next step" +msgstr "Sonraki adıma devam et" + +#: src/screens/Onboarding/StepAlgoFeeds/index.tsx:152 +msgid "Continue to the next step" +msgstr "Sonraki adıma devam et" + +#: src/screens/Onboarding/StepSuggestedAccounts/index.tsx:187 +msgid "Continue to the next step without following any accounts" +msgstr "Herhangi bir hesabı takip etmeden sonraki adıma devam et" + +#: src/screens/Onboarding/index.tsx:44 +msgid "Cooking" +msgstr "Yemek pişirme" + +#: src/view/com/modals/AddAppPasswords.tsx:195 +#: src/view/com/modals/InviteCodes.tsx:182 +msgid "Copied" +msgstr "Kopyalandı" + +#: src/view/screens/Settings.tsx:243 +msgid "Copied build version to clipboard" +msgstr "Sürüm numarası panoya kopyalandı" + +#: src/view/com/modals/AddAppPasswords.tsx:76 +#: src/view/com/modals/InviteCodes.tsx:152 +#: src/view/com/util/forms/PostDropdownBtn.tsx:112 +msgid "Copied to clipboard" +msgstr "Panoya kopyalandı" + +#: src/view/com/modals/AddAppPasswords.tsx:189 +msgid "Copies app password" +msgstr "Uygulama şifresini kopyalar" + +#: src/view/com/modals/AddAppPasswords.tsx:188 +msgid "Copy" +msgstr "Kopyala" + +#: src/view/screens/ProfileList.tsx:417 +msgid "Copy link to list" +msgstr "Liste bağlantısını kopyala" + +#: src/view/com/util/forms/PostDropdownBtn.tsx:153 +msgid "Copy link to post" +msgstr "Gönderi bağlantısını kopyala" + +#: src/view/com/profile/ProfileHeader.tsx:342 +msgid "Copy link to profile" +msgstr "Profili bağlantısını kopyala" + +#: src/view/com/util/forms/PostDropdownBtn.tsx:139 +msgid "Copy post text" +msgstr "Gönderi metnini kopyala" + +#: src/Navigation.tsx:233 src/view/screens/CopyrightPolicy.tsx:29 +msgid "Copyright Policy" +msgstr "Telif Hakkı Politikası" + +#: src/view/screens/ProfileFeed.tsx:96 +msgid "Could not load feed" +msgstr "Besleme yüklenemedi" + +#: src/view/screens/ProfileList.tsx:888 +msgid "Could not load list" +msgstr "Liste yüklenemedi" + +#: src/view/com/auth/create/Step2.tsx:91 +msgid "Country" +msgstr "Ülke" + +#: src/view/com/auth/HomeLoggedOutCTA.tsx:62 +#: src/view/com/auth/SplashScreen.tsx:46 +#: src/view/com/auth/SplashScreen.web.tsx:77 +msgid "Create a new account" +msgstr "Yeni bir hesap oluştur" + +#: src/view/screens/Settings.tsx:384 +msgid "Create a new Bluesky account" +msgstr "Yeni bir Bluesky hesabı oluştur" + +#: src/view/com/auth/create/CreateAccount.tsx:122 +msgid "Create Account" +msgstr "Hesap Oluştur" + +#: src/view/com/modals/AddAppPasswords.tsx:226 +msgid "Create App Password" +msgstr "Uygulama Şifresi Oluştur" + +#: src/view/com/auth/HomeLoggedOutCTA.tsx:54 +#: src/view/com/auth/SplashScreen.tsx:43 +msgid "Create new account" +msgstr "Yeni hesap oluştur" + +#: src/view/screens/AppPasswords.tsx:249 +msgid "Created {0}" +msgstr "{0} oluşturuldu" + +#: src/view/screens/ProfileFeed.tsx:616 +msgid "Created by <0/>" +msgstr "<0/> tarafından oluşturuldu" + +#: src/view/screens/ProfileFeed.tsx:614 +msgid "Created by you" +msgstr "Siz tarafından oluşturuldu" + +#: src/view/com/composer/Composer.tsx:448 +msgid "Creates a card with a thumbnail. The card links to {url}" +msgstr "Küçük resimli bir kart oluşturur. Kart, {url} bağlantısına gider" + +#: src/screens/Onboarding/index.tsx:29 +msgid "Culture" +msgstr "Kültür" + +#: src/view/com/modals/ChangeHandle.tsx:389 +#: src/view/com/modals/ServerInput.tsx:102 +msgid "Custom domain" +msgstr "Özel alan adı" + +#: src/screens/Onboarding/StepAlgoFeeds/index.tsx:106 +msgid "Custom feeds built by the community bring you new experiences and help you find the content you love." +msgstr "Topluluk tarafından oluşturulan özel beslemeler size yeni deneyimler sunar ve sevdiğiniz içeriği bulmanıza yardımcı olur." + +#: src/view/screens/PreferencesExternalEmbeds.tsx:55 +msgid "Customize media from external sites." +msgstr "Harici sitelerden medyayı özelleştirin." + +#: src/view/screens/Settings.tsx:479 src/view/screens/Settings.tsx:505 +msgid "Dark" +msgstr "Karanlık" + +#: src/view/screens/Debug.tsx:63 +msgid "Dark mode" +msgstr "Karanlık mod" + +#: src/view/screens/Settings.tsx:492 +msgid "Dark Theme" +msgstr "Karanlık Tema" + +#: src/view/screens/Debug.tsx:83 +msgid "Debug panel" +msgstr "Hata ayıklama paneli" + +#: src/view/screens/Settings.tsx:743 +msgid "Delete account" +msgstr "Hesabı sil" + +#: src/view/com/modals/DeleteAccount.tsx:87 +msgid "Delete Account" +msgstr "Hesabı Sil" + +#: src/view/screens/AppPasswords.tsx:222 src/view/screens/AppPasswords.tsx:242 +msgid "Delete app password" +msgstr "Uygulama şifresini sil" + +#: src/view/screens/ProfileList.tsx:363 src/view/screens/ProfileList.tsx:444 +msgid "Delete List" +msgstr "Listeyi Sil" + +#: src/view/com/modals/DeleteAccount.tsx:223 +msgid "Delete my account" +msgstr "Hesabımı sil" + +#: src/view/screens/Settings.tsx:755 +msgid "Delete My Account…" +msgstr "Hesabımı Sil…" + +#: src/view/com/util/forms/PostDropdownBtn.tsx:228 +msgid "Delete post" +msgstr "Gönderiyi sil" + +#: src/view/com/util/forms/PostDropdownBtn.tsx:232 +msgid "Delete this post?" +msgstr "Bu gönderiyi sil?" + +#: src/view/com/util/post-embeds/QuoteEmbed.tsx:69 +msgid "Deleted" +msgstr "Silindi" + +#: src/view/com/post-thread/PostThread.tsx:264 +msgid "Deleted post." +msgstr "Silinen gönderi." + +#: src/view/com/modals/CreateOrEditList.tsx:300 +#: src/view/com/modals/CreateOrEditList.tsx:321 +#: src/view/com/modals/EditProfile.tsx:198 +#: src/view/com/modals/EditProfile.tsx:210 +msgid "Description" +msgstr "Açıklama" + +#: src/view/screens/Settings.tsx:760 +msgid "Developer Tools" +msgstr "Geliştirici Araçları" + +#: src/view/com/composer/Composer.tsx:211 +msgid "Did you want to say anything?" +msgstr "Bir şey söylemek istediniz mi?" + +#: src/view/screens/Settings.tsx:498 +msgid "Dim" +msgstr "Karart" + +#: src/view/com/composer/Composer.tsx:144 +msgid "Discard" +msgstr "Sil" + +#: src/view/com/composer/Composer.tsx:138 +msgid "Discard draft" +msgstr "Taslağı sil" + +#: src/view/screens/Moderation.tsx:207 +msgid "Discourage apps from showing my account to logged-out users" +msgstr "Uygulamaların hesabımı oturum açmamış kullanıcılara göstermesini engelle" + +#: src/view/com/posts/FollowingEmptyState.tsx:74 +#: src/view/com/posts/FollowingEndOfFeed.tsx:75 +msgid "Discover new custom feeds" +msgstr "Yeni özel beslemeler keşfet" + +#: src/view/screens/Feeds.tsx:441 +msgid "Discover new feeds" +msgstr "Yeni beslemeler keşfet" + +#: src/view/com/modals/EditProfile.tsx:192 +msgid "Display name" +msgstr "Görünen ad" + +#: src/view/com/modals/EditProfile.tsx:180 +msgid "Display Name" +msgstr "Görünen Ad" + +#: src/view/com/modals/ChangeHandle.tsx:487 +msgid "Domain verified!" +msgstr "Alan adı doğrulandı!" + +#: src/view/com/auth/create/Step1.tsx:114 +msgid "Don't have an invite code?" +msgstr "Davet kodunuz yok mu?" + +#: src/view/com/auth/onboarding/RecommendedFollows.tsx:86 +#: src/view/com/modals/EditImage.tsx:333 +#: src/view/com/modals/ListAddRemoveUsers.tsx:144 +#: src/view/com/modals/SelfLabel.tsx:157 src/view/com/modals/Threadgate.tsx:129 +#: src/view/com/modals/Threadgate.tsx:132 +#: src/view/com/modals/UserAddRemoveLists.tsx:95 +#: src/view/com/modals/UserAddRemoveLists.tsx:98 +#: src/view/screens/PreferencesThreads.tsx:162 +msgctxt "action" +msgid "Done" +msgstr "Tamam" + +#: src/view/com/modals/AddAppPasswords.tsx:226 +#: src/view/com/modals/AltImage.tsx:139 +#: src/view/com/modals/ContentFilteringSettings.tsx:88 +#: src/view/com/modals/ContentFilteringSettings.tsx:96 +#: src/view/com/modals/crop-image/CropImage.web.tsx:152 +#: src/view/com/modals/InviteCodes.tsx:80 +#: src/view/com/modals/InviteCodes.tsx:123 +#: src/view/com/modals/ListAddRemoveUsers.tsx:142 +#: src/view/screens/PreferencesHomeFeed.tsx:311 +msgid "Done" +msgstr "Tamam" + +#: src/view/com/modals/lang-settings/ConfirmLanguagesButton.tsx:42 +msgid "Done{extraText}" +msgstr "Tamam{extraText}" + +#: src/view/com/auth/login/ChooseAccountForm.tsx:45 +msgid "Double tap to sign in" +msgstr "Oturum açmak için çift dokunun" + +#: src/view/com/composer/text-input/TextInput.web.tsx:244 +msgid "Drop to add images" +msgstr "Resim eklemek için bırakın" + +#: src/screens/Onboarding/StepModeration/AdultContentEnabledPref.tsx:111 +msgid "Due to Apple policies, adult content can only be enabled on the web after completing sign up." +msgstr "Apple politikaları gereği, yetişkin içeriği yalnızca kaydı tamamladıktan sonra web üzerinde etkinleştirilebilir." + +#: src/view/com/modals/EditProfile.tsx:185 +msgid "e.g. Alice Roberts" +msgstr "örn: Alice Roberts" + +#: src/view/com/modals/EditProfile.tsx:203 +msgid "e.g. Artist, dog-lover, and avid reader." +msgstr "örn: Sanatçı, köpek sever ve okumayı seven." + +#: src/view/com/modals/CreateOrEditList.tsx:283 +msgid "e.g. Great Posters" +msgstr "örn: Harika Göndericiler" + +#: src/view/com/modals/CreateOrEditList.tsx:284 +msgid "e.g. Spammers" +msgstr "örn: Spamcılar" + +#: src/view/com/modals/CreateOrEditList.tsx:312 +msgid "e.g. The posters who never miss." +msgstr "örn: Asla kaçırmayan göndericiler." + +#: src/view/com/modals/CreateOrEditList.tsx:313 +msgid "e.g. Users that repeatedly reply with ads." +msgstr "örn: Reklamlarla tekrar tekrar yanıt veren kullanıcılar." + +#: src/view/com/modals/InviteCodes.tsx:96 +msgid "Each code works once. You'll receive more invite codes periodically." +msgstr "Her kod bir kez çalışır. Düzenli aralıklarla daha fazla davet kodu alacaksınız." + +#: src/view/com/lists/ListMembers.tsx:149 +msgctxt "action" +msgid "Edit" +msgstr "Düzenle" + +#: src/view/com/composer/photos/Gallery.tsx:144 +#: src/view/com/modals/EditImage.tsx:207 +msgid "Edit image" +msgstr "Resmi düzenle" + +#: src/view/screens/ProfileList.tsx:432 +msgid "Edit list details" +msgstr "Liste ayrıntılarını düzenle" + +#: src/view/com/modals/CreateOrEditList.tsx:250 +msgid "Edit Moderation List" +msgstr "Düzenleme Listesini Düzenle" + +#: src/Navigation.tsx:243 src/view/screens/Feeds.tsx:403 +#: src/view/screens/SavedFeeds.tsx:84 +msgid "Edit My Feeds" +msgstr "Beslemelerimi Düzenle" + +#: src/view/com/modals/EditProfile.tsx:152 +msgid "Edit my profile" +msgstr "Profilimi düzenle" + +#: src/view/com/profile/ProfileHeader.tsx:457 +msgid "Edit profile" +msgstr "Profil düzenle" + +#: src/view/com/profile/ProfileHeader.tsx:462 +msgid "Edit Profile" +msgstr "Profil Düzenle" + +#: src/view/screens/Feeds.tsx:337 +msgid "Edit Saved Feeds" +msgstr "Kayıtlı Beslemeleri Düzenle" + +#: src/view/com/modals/CreateOrEditList.tsx:245 +msgid "Edit User List" +msgstr "Kullanıcı Listesini Düzenle" + +#: src/view/com/modals/EditProfile.tsx:193 +msgid "Edit your display name" +msgstr "Görünen adınızı düzenleyin" + +#: src/view/com/modals/EditProfile.tsx:211 +msgid "Edit your profile description" +msgstr "Profil açıklamanızı düzenleyin" + +#: src/screens/Onboarding/index.tsx:34 +msgid "Education" +msgstr "Eğitim" + +#: src/view/com/auth/create/Step1.tsx:143 +#: src/view/com/auth/create/Step2.tsx:194 +#: src/view/com/auth/create/Step2.tsx:269 +#: src/view/com/auth/login/ForgotPasswordForm.tsx:152 +#: src/view/com/modals/ChangeEmail.tsx:141 src/view/com/modals/Waitlist.tsx:88 +msgid "Email" +msgstr "E-posta" + +#: src/view/com/auth/create/Step1.tsx:134 +#: src/view/com/auth/login/ForgotPasswordForm.tsx:143 +msgid "Email address" +msgstr "E-posta adresi" + +#: src/view/com/modals/ChangeEmail.tsx:56 +#: src/view/com/modals/ChangeEmail.tsx:88 +msgid "Email updated" +msgstr "E-posta güncellendi" + +#: src/view/com/modals/ChangeEmail.tsx:111 +msgid "Email Updated" +msgstr "E-posta Güncellendi" + +#: src/view/com/modals/VerifyEmail.tsx:78 +msgid "Email verified" +msgstr "E-posta doğrulandı" + +#: src/view/screens/Settings.tsx:312 +msgid "Email:" +msgstr "E-posta:" + +#: src/view/com/modals/EmbedConsent.tsx:113 +msgid "Enable {0} only" +msgstr "Yalnızca {0} etkinleştir" + +#: src/view/com/modals/ContentFilteringSettings.tsx:162 +msgid "Enable Adult Content" +msgstr "Yetişkin İçeriği Etkinleştir" + +#: src/screens/Onboarding/StepModeration/AdultContentEnabledPref.tsx:76 +#: src/screens/Onboarding/StepModeration/AdultContentEnabledPref.tsx:77 +msgid "Enable adult content in your feeds" +msgstr "Beslemelerinizde yetişkin içeriği etkinleştirin" + +#: src/view/com/modals/EmbedConsent.tsx:97 +msgid "Enable External Media" +msgstr "Harici Medyayı Etkinleştir" + +#: src/view/screens/PreferencesExternalEmbeds.tsx:75 +msgid "Enable media players for" +msgstr "Medya oynatıcılarını etkinleştir" + +#: src/view/screens/PreferencesHomeFeed.tsx:147 +msgid "Enable this setting to only see replies between people you follow." +msgstr "Bu ayarı yalnızca takip ettiğiniz kişiler arasındaki yanıtları görmek için etkinleştirin." + +#: src/view/screens/Profile.tsx:437 +msgid "End of feed" +msgstr "Beslemenin sonu" + +#: src/view/com/modals/AddAppPasswords.tsx:166 +msgid "Enter a name for this App Password" +msgstr "Bu Uygulama Şifresi için bir ad girin" + +#: src/view/com/modals/VerifyEmail.tsx:105 +msgid "Enter Confirmation Code" +msgstr "Onay Kodunu Girin" + +#: src/view/com/modals/ChangePassword.tsx:151 +msgid "Enter the code you received to change your password." +msgstr "Şifrenizi değiştirmek için aldığınız kodu girin." + +#: src/view/com/modals/ChangeHandle.tsx:371 +msgid "Enter the domain you want to use" +msgstr "Kullanmak istediğiniz alan adını girin" + +#: src/view/com/auth/login/ForgotPasswordForm.tsx:103 +msgid "Enter the email you used to create your account. We'll send you a \"reset code\" so you can set a new password." +msgstr "Hesabınızı oluşturmak için kullandığınız e-postayı girin. Size yeni bir şifre belirlemeniz için bir \"sıfırlama kodu\" göndereceğiz." + +#: src/view/com/auth/create/Step1.tsx:195 +#: src/view/com/modals/BirthDateSettings.tsx:74 +msgid "Enter your birth date" +msgstr "Doğum tarihinizi girin" + +#: src/view/com/modals/Waitlist.tsx:78 +msgid "Enter your email" +msgstr "E-posta adresinizi girin" + +#: src/view/com/auth/create/Step1.tsx:139 +msgid "Enter your email address" +msgstr "E-posta adresinizi girin" + +#: src/view/com/modals/ChangeEmail.tsx:41 +msgid "Enter your new email above" +msgstr "Yeni e-postanızı yukarıya girin" + +#: src/view/com/modals/ChangeEmail.tsx:117 +msgid "Enter your new email address below." +msgstr "Yeni e-posta adresinizi aşağıya girin." + +#: src/view/com/auth/create/Step2.tsx:188 +msgid "Enter your phone number" +msgstr "Telefon numaranızı girin" + +#: src/view/com/auth/login/Login.tsx:99 +msgid "Enter your username and password" +msgstr "Kullanıcı adınızı ve şifrenizi girin" + +#: src/view/screens/Search/Search.tsx:109 +msgid "Error:" +msgstr "Hata:" + +#: src/view/com/modals/Threadgate.tsx:76 +msgid "Everybody" +msgstr "Herkes" + +#: src/view/com/modals/ChangeHandle.tsx:150 +msgid "Exits handle change process" +msgstr "Kullanıcı adı değişikliği sürecinden çıkar" + +#: src/view/com/lightbox/Lightbox.web.tsx:120 +msgid "Exits image view" +msgstr "Resim görünümünden çıkar" + +#: src/view/com/modals/ListAddRemoveUsers.tsx:88 +#: src/view/shell/desktop/Search.tsx:235 +msgid "Exits inputting search query" +msgstr "Arama sorgusu girişinden çıkar" + +#: src/view/com/modals/Waitlist.tsx:138 +msgid "Exits signing up for waitlist with {email}" +msgstr "{email} adresiyle bekleme listesine kaydolma işleminden çıkar" + +#: src/view/com/lightbox/Lightbox.web.tsx:163 +msgid "Expand alt text" +msgstr "Alternatif metni genişlet" + +#: src/view/com/composer/ComposerReplyTo.tsx:81 +#: src/view/com/composer/ComposerReplyTo.tsx:84 +msgid "Expand or collapse the full post you are replying to" +msgstr "Yanıt verdiğiniz tam gönderiyi genişletin veya daraltın" + +#: src/view/com/modals/EmbedConsent.tsx:64 +msgid "External Media" +msgstr "Harici Medya" + +#: src/view/com/modals/EmbedConsent.tsx:75 +#: src/view/screens/PreferencesExternalEmbeds.tsx:66 +msgid "External media may allow websites to collect information about you and your device. No information is sent or requested until you press the \"play\" button." +msgstr "Harici medya, web sitelerinin siz ve cihazınız hakkında bilgi toplamasına izin verebilir. Bilgi, \"oynat\" düğmesine basana kadar gönderilmez veya istenmez." + +#: src/Navigation.tsx:259 src/view/screens/PreferencesExternalEmbeds.tsx:52 +#: src/view/screens/Settings.tsx:651 +msgid "External Media Preferences" +msgstr "Harici Medya Tercihleri" + +#: src/view/screens/Settings.tsx:642 +msgid "External media settings" +msgstr "Harici medya ayarları" + +#: src/view/com/modals/AddAppPasswords.tsx:115 +#: src/view/com/modals/AddAppPasswords.tsx:119 +msgid "Failed to create app password." +msgstr "Uygulama şifresi oluşturulamadı." + +#: src/view/com/modals/CreateOrEditList.tsx:206 +msgid "Failed to create the list. Check your internet connection and try again." +msgstr "Liste oluşturulamadı. İnternet bağlantınızı kontrol edin ve tekrar deneyin." + +#: src/view/com/util/forms/PostDropdownBtn.tsx:88 +msgid "Failed to delete post, please try again" +msgstr "Gönderi silinemedi, lütfen tekrar deneyin" + +#: src/view/com/auth/onboarding/RecommendedFeeds.tsx:109 +#: src/view/com/auth/onboarding/RecommendedFeeds.tsx:141 +msgid "Failed to load recommended feeds" +msgstr "Önerilen beslemeler yüklenemedi" + +#: src/Navigation.tsx:193 +msgid "Feed" +msgstr "Besleme" + +#: src/view/com/feeds/FeedSourceCard.tsx:229 +msgid "Feed by {0}" +msgstr "{0} tarafından besleme" + +#: src/view/screens/Feeds.tsx:597 +msgid "Feed offline" +msgstr "Besleme çevrimdışı" + +#: src/view/com/feeds/FeedPage.tsx:143 +msgid "Feed Preferences" +msgstr "Besleme Tercihleri" + +#: src/view/shell/desktop/RightNav.tsx:73 src/view/shell/Drawer.tsx:314 +msgid "Feedback" +msgstr "Geribildirim" + +#: src/Navigation.tsx:443 src/view/screens/Feeds.tsx:514 +#: src/view/screens/Profile.tsx:175 src/view/shell/bottom-bar/BottomBar.tsx:181 +#: src/view/shell/desktop/LeftNav.tsx:342 src/view/shell/Drawer.tsx:479 +#: src/view/shell/Drawer.tsx:480 +msgid "Feeds" +msgstr "Beslemeler" + +#: src/view/com/auth/onboarding/RecommendedFeeds.tsx:57 +msgid "Feeds are created by users to curate content. Choose some feeds that you find interesting." +msgstr "Beslemeler, içerikleri düzenlemek için kullanıcılar tarafından oluşturulur. İlginizi çeken bazı beslemeler seçin." + +#: src/view/screens/SavedFeeds.tsx:156 +msgid "Feeds are custom algorithms that users build with a little coding expertise. <0/> for more information." +msgstr "Beslemeler, kullanıcıların biraz kodlama uzmanlığı ile oluşturduğu özel algoritmalardır. Daha fazla bilgi için <0/>." + +#: src/screens/Onboarding/StepTopicalFeeds.tsx:70 +msgid "Feeds can be topical as well!" +msgstr "Beslemeler aynı zamanda konusal olabilir!" + +#: src/screens/Onboarding/StepFinished.tsx:151 +msgid "Finalizing" +msgstr "Tamamlanıyor" + +#: src/view/com/posts/CustomFeedEmptyState.tsx:47 +#: src/view/com/posts/FollowingEmptyState.tsx:57 +#: src/view/com/posts/FollowingEndOfFeed.tsx:58 +msgid "Find accounts to follow" +msgstr "Takip edilecek hesaplar bul" + +#: src/view/screens/Search/Search.tsx:439 +msgid "Find users on Bluesky" +msgstr "Bluesky'da kullanıcı bul" + +#: src/view/screens/Search/Search.tsx:437 +msgid "Find users with the search tool on the right" +msgstr "Sağdaki arama aracıyla kullanıcı bul" + +#: src/view/com/auth/onboarding/RecommendedFollowsItem.tsx:150 +msgid "Finding similar accounts..." +msgstr "Benzer hesaplar bulunuyor..." + +#: src/view/screens/PreferencesHomeFeed.tsx:111 +msgid "Fine-tune the content you see on your home screen." +msgstr "Ana ekranınızda gördüğünüz içeriği ayarlayın." + +#: src/view/screens/PreferencesThreads.tsx:60 +msgid "Fine-tune the discussion threads." +msgstr "Tartışma konularını ayarlayın." + +#: src/screens/Onboarding/index.tsx:38 +msgid "Fitness" +msgstr "Fitness" + +#: src/screens/Onboarding/StepFinished.tsx:131 +msgid "Flexible" +msgstr "Esnek" + +#: src/view/com/modals/EditImage.tsx:115 +msgid "Flip horizontal" +msgstr "Yatay çevir" + +#: src/view/com/modals/EditImage.tsx:120 src/view/com/modals/EditImage.tsx:287 +msgid "Flip vertically" +msgstr "Dikey çevir" + +#: src/view/com/profile/FollowButton.tsx:64 +msgctxt "action" +msgid "Follow" +msgstr "Takip et" + +#: src/view/com/profile/ProfileHeader.tsx:552 +msgid "Follow" +msgstr "Takip et" + +#: src/screens/Onboarding/StepSuggestedAccounts/index.tsx:58 +#: src/view/com/profile/ProfileHeader.tsx:543 +msgid "Follow {0}" +msgstr "{0} takip et" + +#: src/screens/Onboarding/StepSuggestedAccounts/index.tsx:178 +msgid "Follow All" +msgstr "Hepsini Takip Et" + +#: src/screens/Onboarding/StepSuggestedAccounts/index.tsx:174 +msgid "Follow selected accounts and continue to the next step" +msgstr "Seçili hesapları takip edin ve sonraki adıma devam edin" + +#: src/view/com/auth/onboarding/RecommendedFollows.tsx:64 +msgid "Follow some users to get started. We can recommend you more users based on who you find interesting." +msgstr "Başlamak için bazı kullanıcıları takip edin. Sizi ilginç bulduğunuz kişilere dayanarak size daha fazla kullanıcı önerebiliriz." + +#: src/view/com/profile/ProfileCard.tsx:194 +msgid "Followed by {0}" +msgstr "{0} tarafından takip ediliyor" + +#: src/view/com/modals/Threadgate.tsx:98 +msgid "Followed users" +msgstr "Takip edilen kullanıcılar" + +#: src/view/screens/PreferencesHomeFeed.tsx:154 +msgid "Followed users only" +msgstr "Yalnızca takip edilen kullanıcılar" + +#: src/view/com/notifications/FeedItem.tsx:166 +msgid "followed you" +msgstr "sizi takip etti" + +#: src/view/screens/ProfileFollowers.tsx:25 +msgid "Followers" +msgstr "Takipçiler" + +#: src/view/com/profile/ProfileHeader.tsx:534 +#: src/view/screens/ProfileFollows.tsx:25 +msgid "Following" +msgstr "Takip edilenler" + +#: src/view/com/profile/ProfileHeader.tsx:196 +msgid "Following {0}" +msgstr "{0} takip ediliyor" + +#: src/view/com/profile/ProfileHeader.tsx:585 +msgid "Follows you" +msgstr "Sizi takip ediyor" + +#: src/view/com/profile/ProfileCard.tsx:141 +msgid "Follows You" +msgstr "Sizi Takip Ediyor" + +#: src/screens/Onboarding/index.tsx:43 +msgid "Food" +msgstr "Yiyecek" + +#: src/view/com/modals/DeleteAccount.tsx:111 +msgid "For security reasons, we'll need to send a confirmation code to your email address." +msgstr "Güvenlik nedeniyle, e-posta adresinize bir onay kodu göndermemiz gerekecek." + +#: src/view/com/modals/AddAppPasswords.tsx:209 +msgid "For security reasons, you won't be able to view this again. If you lose this password, you'll need to generate a new one." +msgstr "Güvenlik nedeniyle, bunu tekrar göremezsiniz. Bu şifreyi kaybederseniz, yeni bir tane oluşturmanız gerekecek." + +#: src/view/com/auth/login/LoginForm.tsx:238 +msgid "Forgot" +msgstr "Unuttum" + +#: src/view/com/auth/login/LoginForm.tsx:235 +msgid "Forgot password" +msgstr "Şifremi unuttum" + +#: src/view/com/auth/login/Login.tsx:127 src/view/com/auth/login/Login.tsx:143 +msgid "Forgot Password" +msgstr "Şifremi Unuttum" + +#: src/view/com/posts/FeedItem.tsx:189 +msgctxt "from-feed" +msgid "From <0/>" +msgstr "<0/> tarafından" + +#: src/view/com/composer/photos/SelectPhotoBtn.tsx:43 +msgid "Gallery" +msgstr "Galeri" + +#: src/view/com/modals/VerifyEmail.tsx:189 +#: src/view/com/modals/VerifyEmail.tsx:191 +msgid "Get Started" +msgstr "Başlayın" + +#: src/view/com/auth/LoggedOut.tsx:81 src/view/com/auth/LoggedOut.tsx:82 +#: src/view/com/util/moderation/ScreenHider.tsx:123 +#: src/view/shell/desktop/LeftNav.tsx:104 +msgid "Go back" +msgstr "Geri git" + +#: src/view/screens/ProfileFeed.tsx:105 src/view/screens/ProfileFeed.tsx:110 +#: src/view/screens/ProfileList.tsx:897 src/view/screens/ProfileList.tsx:902 +msgid "Go Back" +msgstr "Geri Git" + +#: src/screens/Onboarding/Layout.tsx:104 src/screens/Onboarding/Layout.tsx:193 +msgid "Go back to previous step" +msgstr "Önceki adıma geri dön" + +#: src/view/screens/Search/Search.tsx:724 src/view/shell/desktop/Search.tsx:262 +msgid "Go to @{queryMaybeHandle}" +msgstr "@{queryMaybeHandle} adresine git" + +#: src/view/com/auth/login/ForgotPasswordForm.tsx:185 +#: src/view/com/auth/login/ForgotPasswordForm.tsx:214 +#: src/view/com/auth/login/LoginForm.tsx:285 +#: src/view/com/auth/login/SetNewPasswordForm.tsx:195 +#: src/view/com/modals/ChangePassword.tsx:165 +msgid "Go to next" +msgstr "Sonrakine git" + +#: src/view/com/modals/ChangeHandle.tsx:265 +msgid "Handle" +msgstr "Kullanıcı adı" + +#: src/view/com/auth/create/CreateAccount.tsx:197 +msgid "Having trouble?" +msgstr "Sorun mu yaşıyorsunuz?" + +#: src/view/shell/desktop/RightNav.tsx:102 src/view/shell/Drawer.tsx:324 +msgid "Help" +msgstr "Yardım" + +#: src/screens/Onboarding/StepSuggestedAccounts/index.tsx:132 +msgid "Here are some accounts for you to follow" +msgstr "Takip etmeniz için size bazı hesaplar" + +#: src/screens/Onboarding/StepTopicalFeeds.tsx:79 +msgid "Here are some popular topical feeds. You can choose to follow as many as you like." +msgstr "İşte bazı popüler konusal beslemeler. İstediğiniz kadar takip etmeyi seçebilirsiniz." + +#: src/screens/Onboarding/StepTopicalFeeds.tsx:74 +msgid "Here are some topical feeds based on your interests: {interestsText}. You can choose to follow as many as you like." +msgstr "İlgi alanlarınıza dayalı olarak bazı konusal beslemeler: {interestsText}. İstediğiniz kadar takip etmeyi seçebilirsiniz." + +#: src/view/com/modals/AddAppPasswords.tsx:153 +msgid "Here is your app password." +msgstr "İşte uygulama şifreniz." + +#: src/screens/Onboarding/StepModeration/ModerationOption.tsx:41 +#: src/view/com/modals/ContentFilteringSettings.tsx:246 +#: src/view/com/util/moderation/ContentHider.tsx:105 +#: src/view/com/util/moderation/PostHider.tsx:108 +msgid "Hide" +msgstr "Gizle" + +#: src/view/com/modals/ContentFilteringSettings.tsx:219 +#: src/view/com/notifications/FeedItem.tsx:325 +msgctxt "action" +msgid "Hide" +msgstr "Gizle" + +#: src/view/com/util/forms/PostDropdownBtn.tsx:187 +msgid "Hide post" +msgstr "Gönderiyi gizle" + +#: src/view/com/util/moderation/ContentHider.tsx:67 +#: src/view/com/util/moderation/PostHider.tsx:61 +msgid "Hide the content" +msgstr "İçeriği gizle" + +#: src/view/com/util/forms/PostDropdownBtn.tsx:191 +msgid "Hide this post?" +msgstr "Bu gönderiyi gizle?" + +#: src/view/com/notifications/FeedItem.tsx:315 +msgid "Hide user list" +msgstr "Kullanıcı listesini gizle" + +#: src/view/com/profile/ProfileHeader.tsx:526 +msgid "Hides posts from {0} in your feed" +msgstr "Beslemenizdeki {0} gönderilerini gizler" + +#: src/view/com/posts/FeedErrorMessage.tsx:111 +msgid "Hmm, some kind of issue occurred when contacting the feed server. Please let the feed owner know about this issue." +msgstr "Hmm, besleme sunucusuna ulaşırken bir tür sorun oluştu. Lütfen bu konuda besleme sahibini bilgilendirin." + +#: src/view/com/posts/FeedErrorMessage.tsx:99 +msgid "Hmm, the feed server appears to be misconfigured. Please let the feed owner know about this issue." +msgstr "Hmm, besleme sunucusunun yanlış yapılandırılmış görünüyor. Lütfen bu konuda besleme sahibini bilgilendirin." + +#: src/view/com/posts/FeedErrorMessage.tsx:105 +msgid "Hmm, the feed server appears to be offline. Please let the feed owner know about this issue." +msgstr "Hmm, besleme sunucusunun çevrimdışı görünüyor. Lütfen bu konuda besleme sahibini bilgilendirin." + +#: src/view/com/posts/FeedErrorMessage.tsx:102 +msgid "Hmm, the feed server gave a bad response. Please let the feed owner know about this issue." +msgstr "Hmm, besleme sunucusu kötü bir yanıt verdi. Lütfen bu konuda besleme sahibini bilgilendirin." + +#: src/view/com/posts/FeedErrorMessage.tsx:96 +msgid "Hmm, we're having trouble finding this feed. It may have been deleted." +msgstr "Hmm, bu beslemeyi bulmakta sorun yaşıyoruz. Silinmiş olabilir." + +#: src/Navigation.tsx:433 src/view/shell/bottom-bar/BottomBar.tsx:137 +#: src/view/shell/desktop/LeftNav.tsx:306 src/view/shell/Drawer.tsx:401 +#: src/view/shell/Drawer.tsx:402 +msgid "Home" +msgstr "Ana Sayfa" + +#: src/Navigation.tsx:248 src/view/com/pager/FeedsTabBarMobile.tsx:123 +#: src/view/screens/PreferencesHomeFeed.tsx:104 +#: src/view/screens/Settings.tsx:537 +msgid "Home Feed Preferences" +msgstr "Ana Sayfa Besleme Tercihleri" + +#: src/view/com/auth/login/ForgotPasswordForm.tsx:116 +msgid "Hosting provider" +msgstr "Barındırma sağlayıcısı" + +#: src/view/com/modals/InAppBrowserConsent.tsx:44 +msgid "How should we open this link?" +msgstr "Bu bağlantıyı nasıl açmalıyız?" + +#: src/view/com/modals/VerifyEmail.tsx:214 +msgid "I have a code" +msgstr "Bir kodum var" + +#: src/view/com/modals/VerifyEmail.tsx:216 +msgid "I have a confirmation code" +msgstr "Bir onay kodum var" + +#: src/view/com/modals/ChangeHandle.tsx:283 +msgid "I have my own domain" +msgstr "Kendi alan adım var" + +#: src/view/com/lightbox/Lightbox.web.tsx:165 +msgid "If alt text is long, toggles alt text expanded state" +msgstr "Alternatif metin uzunsa, alternatif metin genişletme durumunu değiştirir" + +#: src/view/com/modals/SelfLabel.tsx:127 +msgid "If none are selected, suitable for all ages." +msgstr "Hiçbiri seçilmezse, tüm yaşlar için uygun." + +#: src/view/com/modals/ChangePassword.tsx:146 +msgid "If you want to change your password, we will send you a code to verify that this is your account." +msgstr "Şifrenizi değiştirmek istiyorsanız, size hesabınızın sizin olduğunu doğrulamak için bir kod göndereceğiz." + +#: src/view/com/util/images/Gallery.tsx:38 +msgid "Image" +msgstr "Resim" + +#: src/view/com/modals/AltImage.tsx:120 +msgid "Image alt text" +msgstr "Resim alternatif metni" + +#: src/view/com/util/UserAvatar.tsx:308 src/view/com/util/UserBanner.tsx:116 +msgid "Image options" +msgstr "Resim seçenekleri" + +#: src/view/com/auth/login/SetNewPasswordForm.tsx:138 +msgid "Input code sent to your email for password reset" +msgstr "Şifre sıfırlama için e-postanıza gönderilen kodu girin" + +#: src/view/com/modals/DeleteAccount.tsx:184 +msgid "Input confirmation code for account deletion" +msgstr "Hesap silme için onay kodunu girin" + +#: src/view/com/auth/create/Step1.tsx:144 +msgid "Input email for Bluesky account" +msgstr "Bluesky hesabı için e-posta girin" + +#: src/view/com/auth/create/Step1.tsx:102 +msgid "Input invite code to proceed" +msgstr "Devam etmek için davet kodunu girin" + +#: src/view/com/modals/AddAppPasswords.tsx:180 +msgid "Input name for app password" +msgstr "Uygulama şifresi için ad girin" + +#: src/view/com/auth/login/SetNewPasswordForm.tsx:162 +msgid "Input new password" +msgstr "Yeni şifre girin" + +#: src/view/com/modals/DeleteAccount.tsx:203 +msgid "Input password for account deletion" +msgstr "Hesap silme için şifre girin" + +#: src/view/com/auth/create/Step2.tsx:196 +msgid "Input phone number for SMS verification" +msgstr "SMS doğrulaması için telefon numarası girin" + +#: src/view/com/auth/login/LoginForm.tsx:227 +msgid "Input the password tied to {identifier}" +msgstr "{identifier} ile ilişkili şifreyi girin" + +#: src/view/com/auth/login/LoginForm.tsx:194 +msgid "Input the username or email address you used at signup" +msgstr "Kaydolurken kullandığınız kullanıcı adını veya e-posta adresini girin" + +#: src/view/com/auth/create/Step2.tsx:271 +msgid "Input the verification code we have texted to you" +msgstr "Size mesaj attığımız doğrulama kodunu girin" + +#: src/view/com/modals/Waitlist.tsx:90 +msgid "Input your email to get on the Bluesky waitlist" +msgstr "Bluesky bekleme listesine girmek için e-postanızı girin" + +#: src/view/com/auth/login/LoginForm.tsx:226 +msgid "Input your password" +msgstr "Şifrenizi girin" + +#: src/view/com/auth/create/Step3.tsx:42 +msgid "Input your user handle" +msgstr "Kullanıcı adınızı girin" + +#: src/view/com/post-thread/PostThreadItem.tsx:231 +msgid "Invalid or unsupported post record" +msgstr "Geçersiz veya desteklenmeyen gönderi kaydı" + +#: src/view/com/auth/login/LoginForm.tsx:115 +msgid "Invalid username or password" +msgstr "Geçersiz kullanıcı adı veya şifre" + +#: src/view/screens/Settings.tsx:411 +msgid "Invite" +msgstr "Davet et" + +#: src/view/com/modals/InviteCodes.tsx:93 src/view/screens/Settings.tsx:399 +msgid "Invite a Friend" +msgstr "Arkadaşını Davet Et" + +#: src/view/com/auth/create/Step1.tsx:92 src/view/com/auth/create/Step1.tsx:101 +msgid "Invite code" +msgstr "Davet kodu" + +#: src/view/com/auth/create/state.ts:199 +msgid "Invite code not accepted. Check that you input it correctly and try again." +msgstr "Davet kodu kabul edilmedi. Doğru girdiğinizden emin olun ve tekrar deneyin." + +#: src/view/com/modals/InviteCodes.tsx:170 +msgid "Invite codes: {0} available" +msgstr "Davet kodları: {0} kullanılabilir" + +#: src/view/shell/Drawer.tsx:645 +msgid "Invite codes: {invitesAvailable} available" +msgstr "Davet kodları: {invitesAvailable} kullanılabilir" + +#: src/view/com/modals/InviteCodes.tsx:169 +msgid "Invite codes: 1 available" +msgstr "Davet kodları: 1 kullanılabilir" + +#: src/screens/Onboarding/StepFollowingFeed.tsx:64 +msgid "It shows posts from the people you follow as they happen." +msgstr "Takip ettiğiniz kişilerin gönderilerini olduğu gibi gösterir." + +#: src/view/com/auth/HomeLoggedOutCTA.tsx:99 +msgid "Jobs" +msgstr "İşler" + +#: src/view/com/modals/Waitlist.tsx:67 +msgid "Join the waitlist" +msgstr "Bekleme listesine katıl" + +#: src/view/com/auth/create/Step1.tsx:118 +#: src/view/com/auth/create/Step1.tsx:122 +msgid "Join the waitlist." +msgstr "Bekleme listesine katıl." + +#: src/view/com/modals/Waitlist.tsx:128 +msgid "Join Waitlist" +msgstr "Bekleme Listesine Katıl" + +#: src/screens/Onboarding/index.tsx:24 +msgid "Journalism" +msgstr "Gazetecilik" + +#: src/view/com/composer/select-language/SelectLangBtn.tsx:104 +msgid "Language selection" +msgstr "Dil seçimi" + +#: src/view/screens/Settings.tsx:588 +msgid "Language settings" +msgstr "Dil ayarları" + +#: src/Navigation.tsx:140 src/view/screens/LanguageSettings.tsx:89 +msgid "Language Settings" +msgstr "Dil Ayarları" + +#: src/view/screens/Settings.tsx:597 +msgid "Languages" +msgstr "Diller" + +#: src/view/com/auth/create/StepHeader.tsx:20 +msgid "Last step!" +msgstr "Son adım!" + +#: src/view/com/util/moderation/ContentHider.tsx:103 +msgid "Learn more" +msgstr "Daha fazla bilgi edinin" + +#: src/view/com/util/moderation/PostAlerts.tsx:47 +#: src/view/com/util/moderation/ProfileHeaderAlerts.tsx:65 +#: src/view/com/util/moderation/ScreenHider.tsx:104 +msgid "Learn More" +msgstr "Daha Fazla Bilgi Edinin" + +#: src/view/com/util/moderation/ContentHider.tsx:85 +#: src/view/com/util/moderation/PostAlerts.tsx:40 +#: src/view/com/util/moderation/PostHider.tsx:78 +#: src/view/com/util/moderation/ProfileHeaderAlerts.tsx:49 +#: src/view/com/util/moderation/ScreenHider.tsx:101 +msgid "Learn more about this warning" +msgstr "Bu uyarı hakkında daha fazla bilgi edinin" + +#: src/view/screens/Moderation.tsx:243 +msgid "Learn more about what is public on Bluesky." +msgstr "Bluesky'da neyin herkese açık olduğu hakkında daha fazla bilgi edinin." + +#: src/view/com/modals/lang-settings/ContentLanguagesSettings.tsx:82 +msgid "Leave them all unchecked to see any language." +msgstr "Hepsini işaretlemeyin, herhangi bir dil görmek için." + +#: src/view/com/modals/LinkWarning.tsx:51 +msgid "Leaving Bluesky" +msgstr "Bluesky'dan ayrılıyor" + +#: src/screens/Deactivated.tsx:129 +msgid "left to go." +msgstr "kaldı." + +#: src/view/screens/Settings.tsx:280 +msgid "Legacy storage cleared, you need to restart the app now." +msgstr "Eski depolama temizlendi, şimdi uygulamayı yeniden başlatmanız gerekiyor." + +#: src/view/com/auth/login/Login.tsx:128 src/view/com/auth/login/Login.tsx:144 +msgid "Let's get your password reset!" +msgstr "Şifrenizi sıfırlamaya başlayalım!" + +#: src/screens/Onboarding/StepFinished.tsx:151 +msgid "Let's go!" +msgstr "Hadi gidelim!" + +#: src/view/com/util/UserAvatar.tsx:245 src/view/com/util/UserBanner.tsx:60 +msgid "Library" +msgstr "Kütüphane" + +#: src/view/screens/Settings.tsx:473 +msgid "Light" +msgstr "Açık" + +#: src/view/com/util/post-ctrls/PostCtrls.tsx:170 +msgid "Like" +msgstr "Beğen" + +#: src/view/screens/ProfileFeed.tsx:591 +msgid "Like this feed" +msgstr "Bu beslemeyi beğen" + +#: src/Navigation.tsx:198 +msgid "Liked by" +msgstr "Beğenenler" + +#: src/view/screens/PostLikedBy.tsx:27 +#: src/view/screens/ProfileFeedLikedBy.tsx:27 +msgid "Liked By" +msgstr "Beğenenler" + +#: src/view/com/feeds/FeedSourceCard.tsx:277 +msgid "Liked by {0} {1}" +msgstr "{0} {1} tarafından beğenildi" + +#: src/view/screens/ProfileFeed.tsx:606 +msgid "Liked by {likeCount} {0}" +msgstr "{likeCount} {0} tarafından beğenildi" + +#: src/view/com/notifications/FeedItem.tsx:170 +msgid "liked your custom feed" +msgstr "özel beslemenizi beğendi" + +#: src/view/com/notifications/FeedItem.tsx:155 +msgid "liked your post" +msgstr "gönderinizi beğendi" + +#: src/view/screens/Profile.tsx:174 +msgid "Likes" +msgstr "Beğeniler" + +#: src/view/com/post-thread/PostThreadItem.tsx:185 +msgid "Likes on this post" +msgstr "Bu gönderideki beğeniler" + +#: src/Navigation.tsx:167 +msgid "List" +msgstr "Liste" + +#: src/view/com/modals/CreateOrEditList.tsx:261 +msgid "List Avatar" +msgstr "Liste Avatarı" + +#: src/view/screens/ProfileList.tsx:323 +msgid "List blocked" +msgstr "Liste engellendi" + +#: src/view/com/feeds/FeedSourceCard.tsx:231 +msgid "List by {0}" +msgstr "{0} tarafından liste" + +#: src/view/screens/ProfileList.tsx:377 +msgid "List deleted" +msgstr "Liste silindi" + +#: src/view/screens/ProfileList.tsx:282 +msgid "List muted" +msgstr "Liste sessize alındı" + +#: src/view/com/modals/CreateOrEditList.tsx:275 +msgid "List Name" +msgstr "Liste Adı" + +#: src/view/screens/ProfileList.tsx:342 +msgid "List unblocked" +msgstr "Liste engeli kaldırıldı" + +#: src/view/screens/ProfileList.tsx:301 +msgid "List unmuted" +msgstr "Liste sessizden çıkarıldı" + +#: src/Navigation.tsx:110 src/view/screens/Profile.tsx:176 +#: src/view/shell/desktop/LeftNav.tsx:379 src/view/shell/Drawer.tsx:495 +#: src/view/shell/Drawer.tsx:496 +msgid "Lists" +msgstr "Listeler" + +#: src/view/com/post-thread/PostThread.tsx:281 +#: src/view/com/post-thread/PostThread.tsx:289 +msgid "Load more posts" +msgstr "Daha fazla gönderi yükle" + +#: src/view/screens/Notifications.tsx:155 +msgid "Load new notifications" +msgstr "Yeni bildirimleri yükle" + +#: src/view/com/feeds/FeedPage.tsx:190 src/view/screens/Profile.tsx:422 +#: src/view/screens/ProfileFeed.tsx:494 src/view/screens/ProfileList.tsx:680 +msgid "Load new posts" +msgstr "Yeni gönderileri yükle" + +#: src/view/com/composer/text-input/mobile/Autocomplete.tsx:95 +msgid "Loading..." +msgstr "Yükleniyor..." + +#: src/view/com/modals/ServerInput.tsx:50 +msgid "Local dev server" +msgstr "Yerel geliştirme sunucusu" + +#: src/Navigation.tsx:208 +msgid "Log" +msgstr "Log" + +#: src/screens/Deactivated.tsx:150 src/screens/Deactivated.tsx:153 +#: src/screens/Deactivated.tsx:179 src/screens/Deactivated.tsx:182 +msgid "Log out" +msgstr "Çıkış yap" + +#: src/view/screens/Moderation.tsx:136 +msgid "Logged-out visibility" +msgstr "Çıkış yapan görünürlüğü" + +#: src/view/com/auth/login/ChooseAccountForm.tsx:133 +msgid "Login to account that is not listed" +msgstr "Listelenmeyen hesaba giriş yap" + +#: src/view/com/modals/LinkWarning.tsx:65 +msgid "Make sure this is where you intend to go!" +msgstr "Bu gitmek istediğiniz yer olduğundan emin olun!" + +#: src/view/screens/Profile.tsx:173 +msgid "Media" +msgstr "Medya" + +#: src/view/com/threadgate/WhoCanReply.tsx:139 +msgid "mentioned users" +msgstr "bahsedilen kullanıcılar" + +#: src/view/com/modals/Threadgate.tsx:93 +msgid "Mentioned users" +msgstr "Bahsedilen kullanıcılar" + +#: src/view/com/util/ViewHeader.tsx:81 src/view/screens/Search/Search.tsx:623 +msgid "Menu" +msgstr "Menü" + +#: src/view/com/posts/FeedErrorMessage.tsx:197 +msgid "Message from server: {0}" +msgstr "Sunucudan mesaj: {0}" + +#: src/Navigation.tsx:115 src/view/screens/Moderation.tsx:64 +#: src/view/screens/Settings.tsx:619 src/view/shell/desktop/LeftNav.tsx:397 +#: src/view/shell/Drawer.tsx:514 src/view/shell/Drawer.tsx:515 +msgid "Moderation" +msgstr "Moderasyon" + +#: src/view/com/lists/ListCard.tsx:92 +#: src/view/com/modals/UserAddRemoveLists.tsx:206 +msgid "Moderation list by {0}" +msgstr "{0} tarafından moderasyon listesi" + +#: src/view/screens/ProfileList.tsx:774 +msgid "Moderation list by <0/>" +msgstr "<0/> tarafından moderasyon listesi" + +#: src/view/com/lists/ListCard.tsx:90 +#: src/view/com/modals/UserAddRemoveLists.tsx:204 +#: src/view/screens/ProfileList.tsx:772 +msgid "Moderation list by you" +msgstr "Sizin tarafınızdan moderasyon listesi" + +#: src/view/com/modals/CreateOrEditList.tsx:197 +msgid "Moderation list created" +msgstr "Moderasyon listesi oluşturuldu" + +#: src/view/com/modals/CreateOrEditList.tsx:183 +msgid "Moderation list updated" +msgstr "Moderasyon listesi güncellendi" + +#: src/view/screens/Moderation.tsx:95 +msgid "Moderation lists" +msgstr "Moderasyon listeleri" + +#: src/Navigation.tsx:120 src/view/screens/ModerationModlists.tsx:58 +msgid "Moderation Lists" +msgstr "Moderasyon Listeleri" + +#: src/view/screens/Settings.tsx:613 +msgid "Moderation settings" +msgstr "Moderasyon ayarları" + +#: src/view/com/modals/ModerationDetails.tsx:35 +msgid "Moderator has chosen to set a general warning on the content." +msgstr "Moderatör, içeriğe genel bir uyarı koymayı seçti." + +#: src/view/shell/desktop/Feeds.tsx:53 +msgid "More feeds" +msgstr "Daha fazla besleme" + +#: src/view/com/profile/ProfileHeader.tsx:562 +#: src/view/screens/ProfileFeed.tsx:362 src/view/screens/ProfileList.tsx:616 +msgid "More options" +msgstr "Daha fazla seçenek" + +#: src/view/com/util/forms/PostDropdownBtn.tsx:270 +msgid "More post options" +msgstr "Daha fazla gönderi seçeneği" + +#: src/view/screens/PreferencesThreads.tsx:82 +msgid "Most-liked replies first" +msgstr "En çok beğenilen yanıtlar önce" + +#: src/view/com/profile/ProfileHeader.tsx:374 +msgid "Mute Account" +msgstr "Hesabı Sessize Al" + +#: src/view/screens/ProfileList.tsx:543 +msgid "Mute accounts" +msgstr "Hesapları sessize al" + +#: src/view/screens/ProfileList.tsx:490 +msgid "Mute list" +msgstr "Listeyi sessize al" + +#: src/view/screens/ProfileList.tsx:274 +msgid "Mute these accounts?" +msgstr "Bu hesapları sessize al?" + +#: src/view/screens/ProfileList.tsx:278 +msgid "Mute this List" +msgstr "Bu Listeyi Sessize Al" + +#: src/view/com/util/forms/PostDropdownBtn.tsx:171 +msgid "Mute thread" +msgstr "Konuyu sessize al" + +#: src/view/com/lists/ListCard.tsx:101 +msgid "Muted" +msgstr "Sessize alındı" + +#: src/view/screens/Moderation.tsx:109 +msgid "Muted accounts" +msgstr "Sessize alınan hesaplar" + +#: src/Navigation.tsx:125 src/view/screens/ModerationMutedAccounts.tsx:107 +msgid "Muted Accounts" +msgstr "Sessize Alınan Hesaplar" + +#: src/view/screens/ModerationMutedAccounts.tsx:115 +msgid "Muted accounts have their posts removed from your feed and from your notifications. Mutes are completely private." +msgstr "Sessize alınan hesapların gönderileri beslemenizden ve bildirimlerinizden kaldırılır. Sessizlik tamamen özeldir." + +#: src/view/screens/ProfileList.tsx:276 +msgid "Muting is private. Muted accounts can interact with you, but you will not see their posts or receive notifications from them." +msgstr "Sessizlik özeldir. Sessize alınan hesaplar sizinle etkileşime geçebilir, ancak gönderilerini görmeyecek ve onlardan bildirim almayacaksınız." + +#: src/view/com/modals/BirthDateSettings.tsx:56 +msgid "My Birthday" +msgstr "Doğum Günüm" + +#: src/view/screens/Feeds.tsx:399 +msgid "My Feeds" +msgstr "Beslemelerim" + +#: src/view/shell/desktop/LeftNav.tsx:65 +msgid "My Profile" +msgstr "Profilim" + +#: src/view/screens/Settings.tsx:576 +msgid "My Saved Feeds" +msgstr "Kayıtlı Beslemelerim" + +#: src/view/com/modals/AddAppPasswords.tsx:179 +#: src/view/com/modals/CreateOrEditList.tsx:290 +msgid "Name" +msgstr "Ad" + +#: src/view/com/modals/CreateOrEditList.tsx:145 +msgid "Name is required" +msgstr "Ad gerekli" + +#: src/screens/Onboarding/index.tsx:25 +msgid "Nature" +msgstr "Doğa" + +#: src/view/com/auth/login/ForgotPasswordForm.tsx:186 +#: src/view/com/auth/login/ForgotPasswordForm.tsx:215 +#: src/view/com/auth/login/LoginForm.tsx:286 +#: src/view/com/auth/login/SetNewPasswordForm.tsx:196 +#: src/view/com/modals/ChangePassword.tsx:166 +msgid "Navigates to the next screen" +msgstr "Sonraki ekrana yönlendirir" + +#: src/view/shell/Drawer.tsx:73 +msgid "Navigates to your profile" +msgstr "Profilinize yönlendirir" + +#: src/view/com/modals/EmbedConsent.tsx:107 +#: src/view/com/modals/EmbedConsent.tsx:123 +msgid "Never load embeds from {0}" +msgstr "{0} adresinden gömülü içerikleri asla yükleme" + +#: src/view/com/auth/onboarding/WelcomeDesktop.tsx:72 +#: src/view/com/auth/onboarding/WelcomeMobile.tsx:72 +msgid "Never lose access to your followers and data." +msgstr "Takipçilerinize ve verilerinize asla erişimi kaybetmeyin." + +#: src/screens/Onboarding/StepFinished.tsx:119 +msgid "Never lose access to your followers or data." +msgstr "Takipçilerinize veya verilerinize asla erişimi kaybetmeyin." + +#: src/view/screens/Lists.tsx:76 +msgctxt "action" +msgid "New" +msgstr "Yeni" + +#: src/view/screens/ModerationModlists.tsx:78 +msgid "New" +msgstr "Yeni" + +#: src/view/com/modals/CreateOrEditList.tsx:252 +msgid "New Moderation List" +msgstr "Yeni Moderasyon Listesi" + +#: src/view/com/auth/login/SetNewPasswordForm.tsx:150 +msgid "New password" +msgstr "Yeni şifre" + +#: src/view/com/modals/ChangePassword.tsx:215 +msgid "New Password" +msgstr "Yeni Şifre" + +#: src/view/com/feeds/FeedPage.tsx:201 +msgctxt "action" +msgid "New post" +msgstr "Yeni gönderi" + +#: src/view/screens/Feeds.tsx:547 src/view/screens/Profile.tsx:364 +#: src/view/screens/ProfileFeed.tsx:432 src/view/screens/ProfileList.tsx:195 +#: src/view/screens/ProfileList.tsx:223 src/view/shell/desktop/LeftNav.tsx:248 +msgid "New post" +msgstr "Yeni gönderi" + +#: src/view/shell/desktop/LeftNav.tsx:258 +msgctxt "action" +msgid "New Post" +msgstr "Yeni Gönderi" + +#: src/view/com/modals/CreateOrEditList.tsx:247 +msgid "New User List" +msgstr "Yeni Kullanıcı Listesi" + +#: src/view/screens/PreferencesThreads.tsx:79 +msgid "Newest replies first" +msgstr "En yeni yanıtlar önce" + +#: src/screens/Onboarding/index.tsx:23 +msgid "News" +msgstr "Haberler" + +#: src/view/com/auth/create/CreateAccount.tsx:161 +#: src/view/com/auth/login/ForgotPasswordForm.tsx:178 +#: src/view/com/auth/login/ForgotPasswordForm.tsx:188 +#: src/view/com/auth/login/LoginForm.tsx:288 +#: src/view/com/auth/login/SetNewPasswordForm.tsx:187 +#: src/view/com/auth/login/SetNewPasswordForm.tsx:198 +#: src/view/com/auth/onboarding/RecommendedFeeds.tsx:79 +#: src/view/com/modals/ChangePassword.tsx:251 +#: src/view/com/modals/ChangePassword.tsx:253 +msgid "Next" +msgstr "İleri" + +#: src/view/com/auth/onboarding/WelcomeDesktop.tsx:103 +msgctxt "action" +msgid "Next" +msgstr "İleri" + +#: src/view/com/lightbox/Lightbox.web.tsx:149 +msgid "Next image" +msgstr "Sonraki resim" + +#: src/view/screens/PreferencesHomeFeed.tsx:129 +#: src/view/screens/PreferencesHomeFeed.tsx:200 +#: src/view/screens/PreferencesHomeFeed.tsx:235 +#: src/view/screens/PreferencesHomeFeed.tsx:272 +#: src/view/screens/PreferencesThreads.tsx:106 +#: src/view/screens/PreferencesThreads.tsx:129 +msgid "No" +msgstr "Hayır" + +#: src/view/screens/ProfileFeed.tsx:584 src/view/screens/ProfileList.tsx:754 +msgid "No description" +msgstr "Açıklama yok" + +#: src/view/com/profile/ProfileHeader.tsx:217 +msgid "No longer following {0}" +msgstr "{0} artık takip edilmiyor" + +#: src/view/com/notifications/Feed.tsx:109 +msgid "No notifications yet!" +msgstr "Henüz bildirim yok!" + +#: src/view/com/composer/text-input/mobile/Autocomplete.tsx:97 +#: src/view/com/composer/text-input/web/Autocomplete.tsx:191 +msgid "No result" +msgstr "Sonuç yok" + +#: src/view/screens/Feeds.tsx:490 +msgid "No results found for \"{query}\"" +msgstr "\"{query}\" için sonuç bulunamadı" + +#: src/view/com/modals/ListAddRemoveUsers.tsx:127 +#: src/view/screens/Search/Search.tsx:280 +#: src/view/screens/Search/Search.tsx:308 +msgid "No results found for {query}" +msgstr "{query} için sonuç bulunamadı" + +#: src/view/com/modals/EmbedConsent.tsx:129 +msgid "No thanks" +msgstr "Teşekkürler" + +#: src/view/com/modals/Threadgate.tsx:82 +msgid "Nobody" +msgstr "Hiç kimse" + +#: src/view/com/modals/SelfLabel.tsx:135 +msgid "Not Applicable." +msgstr "Uygulanamaz." + +#: src/Navigation.tsx:105 +msgid "Not Found" +msgstr "Bulunamadı" + +#: src/view/com/modals/VerifyEmail.tsx:246 +#: src/view/com/modals/VerifyEmail.tsx:252 +msgid "Not right now" +msgstr "Şu anda değil" + +#: src/view/screens/Moderation.tsx:233 +msgid "Note: Bluesky is an open and public network. This setting only limits the visibility of your content on the Bluesky app and website, and other apps may not respect this setting. Your content may still be shown to logged-out users by other apps and websites." +msgstr "Not: Bluesky açık ve kamusal bir ağdır. Bu ayar yalnızca içeriğinizin Bluesky uygulaması ve web sitesindeki görünürlüğünü sınırlar, diğer uygulamalar bu ayarı dikkate almayabilir. İçeriğiniz hala diğer uygulamalar ve web siteleri tarafından çıkış yapan kullanıcılara gösterilebilir." + +#: src/Navigation.tsx:448 src/view/screens/Notifications.tsx:120 +#: src/view/screens/Notifications.tsx:144 +#: src/view/shell/bottom-bar/BottomBar.tsx:205 +#: src/view/shell/desktop/LeftNav.tsx:361 src/view/shell/Drawer.tsx:438 +#: src/view/shell/Drawer.tsx:439 +msgid "Notifications" +msgstr "Bildirimler" + +#: src/view/com/modals/SelfLabel.tsx:103 +msgid "Nudity" +msgstr "Çıplaklık" + +#: src/view/com/util/ErrorBoundary.tsx:35 +msgid "Oh no!" +msgstr "Oh hayır!" + +#: src/screens/Onboarding/StepInterests/index.tsx:128 +msgid "Oh no! Something went wrong." +msgstr "Oh hayır! Bir şeyler yanlış gitti." + +#: src/view/com/auth/login/PasswordUpdatedForm.tsx:41 +msgid "Okay" +msgstr "Tamam" + +#: src/view/screens/PreferencesThreads.tsx:78 +msgid "Oldest replies first" +msgstr "En eski yanıtlar önce" + +#: src/view/screens/Settings.tsx:236 +msgid "Onboarding reset" +msgstr "Onboarding sıfırlama" + +#: src/view/com/composer/Composer.tsx:375 +msgid "One or more images is missing alt text." +msgstr "Bir veya daha fazla resimde alternatif metin eksik." + +#: src/view/com/threadgate/WhoCanReply.tsx:100 +msgid "Only {0} can reply." +msgstr "Yalnızca {0} yanıtlayabilir." + +#: src/view/com/modals/ProfilePreview.tsx:49 +#: src/view/com/modals/ProfilePreview.tsx:61 +#: src/view/screens/AppPasswords.tsx:65 +msgid "Oops!" +msgstr "Hata!" + +#: src/screens/Onboarding/StepFinished.tsx:115 +msgid "Open" +msgstr "Aç" + +#: src/view/com/composer/Composer.tsx:470 +#: src/view/com/composer/Composer.tsx:471 +msgid "Open emoji picker" +msgstr "Emoji seçiciyi aç" + +#: src/view/screens/Settings.tsx:706 +msgid "Open links with in-app browser" +msgstr "Uygulama içi tarayıcıda bağlantıları aç" + +#: src/view/com/pager/FeedsTabBarMobile.tsx:87 +msgid "Open navigation" +msgstr "Navigasyonu aç" + +#: src/view/screens/Settings.tsx:786 +msgid "Open storybook page" +msgstr "Storybook sayfasını aç" + +#: src/view/com/util/forms/DropdownButton.tsx:154 +msgid "Opens {numItems} options" +msgstr "{numItems} seçeneği açar" + +#: src/view/screens/Log.tsx:54 +msgid "Opens additional details for a debug entry" +msgstr "Hata ayıklama girişi için ek ayrıntıları açar" + +#: src/view/com/notifications/FeedItem.tsx:348 +msgid "Opens an expanded list of users in this notification" +msgstr "Bu bildirimdeki kullanıcıların genişletilmiş bir listesini açar" + +#: src/view/com/composer/photos/OpenCameraBtn.tsx:61 +msgid "Opens camera on device" +msgstr "Cihazdaki kamerayı açar" + +#: src/view/com/composer/Prompt.tsx:25 +msgid "Opens composer" +msgstr "Besteciyi açar" + +#: src/view/screens/Settings.tsx:589 +msgid "Opens configurable language settings" +msgstr "Yapılandırılabilir dil ayarlarını açar" + +#: src/view/com/composer/photos/SelectPhotoBtn.tsx:44 +msgid "Opens device photo gallery" +msgstr "Cihaz fotoğraf galerisini açar" + +#: src/view/com/profile/ProfileHeader.tsx:459 +msgid "Opens editor for profile display name, avatar, background image, and description" +msgstr "Profil görüntü adı, avatar, arka plan resmi ve açıklama için düzenleyiciyi açar" + +#: src/view/screens/Settings.tsx:643 +msgid "Opens external embeds settings" +msgstr "Harici gömülü ayarları açar" + +#: src/view/com/profile/ProfileHeader.tsx:614 +msgid "Opens followers list" +msgstr "Takipçi listesini açar" + +#: src/view/com/profile/ProfileHeader.tsx:633 +msgid "Opens following list" +msgstr "Takip listesini açar" + +#: src/view/screens/Settings.tsx:412 +msgid "Opens invite code list" +msgstr "Davet kodu listesini açar" + +#: src/view/com/modals/InviteCodes.tsx:172 +#: src/view/shell/desktop/RightNav.tsx:156 src/view/shell/Drawer.tsx:646 +msgid "Opens list of invite codes" +msgstr "Davet kodu listesini açar" + +#: src/view/screens/Settings.tsx:745 +msgid "Opens modal for account deletion confirmation. Requires email code." +msgstr "Hesap silme onayı için modalı açar. E-posta kodu gerektirir." + +#: src/view/com/modals/ChangeHandle.tsx:281 +msgid "Opens modal for using custom domain" +msgstr "Özel alan adı kullanımı için modalı açar" + +#: src/view/screens/Settings.tsx:614 +msgid "Opens moderation settings" +msgstr "Moderasyon ayarlarını açar" + +#: src/view/com/auth/login/LoginForm.tsx:236 +msgid "Opens password reset form" +msgstr "Şifre sıfırlama formunu açar" + +#: src/view/screens/Feeds.tsx:338 +msgid "Opens screen to edit Saved Feeds" +msgstr "Kayıtlı Beslemeleri düzenlemek için ekranı açar" + +#: src/view/screens/Settings.tsx:570 +msgid "Opens screen with all saved feeds" +msgstr "Tüm kayıtlı beslemeleri içeren ekrana açar" + +#: src/view/screens/Settings.tsx:670 +msgid "Opens the app password settings page" +msgstr "Uygulama şifre ayarları sayfasını açar" + +#: src/view/screens/Settings.tsx:529 +msgid "Opens the home feed preferences" +msgstr "Ana besleme tercihlerini açar" + +#: src/view/screens/Settings.tsx:787 +msgid "Opens the storybook page" +msgstr "Storybook sayfasını açar" + +#: src/view/screens/Settings.tsx:767 +msgid "Opens the system log page" +msgstr "Sistem log sayfasını açar" + +#: src/view/screens/Settings.tsx:550 +msgid "Opens the threads preferences" +msgstr "Konu tercihlerini açar" + +#: src/view/com/util/forms/DropdownButton.tsx:280 +msgid "Option {0} of {numItems}" +msgstr "{0} seçeneği, {numItems} seçenekten" + +#: src/view/com/modals/Threadgate.tsx:89 +msgid "Or combine these options:" +msgstr "Veya bu seçenekleri birleştirin:" + +#: src/view/com/auth/login/ChooseAccountForm.tsx:138 +msgid "Other account" +msgstr "Diğer hesap" + +#: src/view/com/modals/ServerInput.tsx:88 +msgid "Other service" +msgstr "Diğer servis" + +#: src/view/com/composer/select-language/SelectLangBtn.tsx:91 +msgid "Other..." +msgstr "Diğer..." + +#: src/view/screens/NotFound.tsx:45 +msgid "Page not found" +msgstr "Sayfa bulunamadı" + +#: src/view/screens/NotFound.tsx:42 +msgid "Page Not Found" +msgstr "Sayfa Bulunamadı" + +#: src/view/com/auth/create/Step1.tsx:158 +#: src/view/com/auth/create/Step1.tsx:168 +#: src/view/com/auth/login/LoginForm.tsx:223 +#: src/view/com/auth/login/SetNewPasswordForm.tsx:161 +#: src/view/com/modals/DeleteAccount.tsx:202 +msgid "Password" +msgstr "Şifre" + +#: src/view/com/auth/login/Login.tsx:157 +msgid "Password updated" +msgstr "Şifre güncellendi" + +#: src/view/com/auth/login/PasswordUpdatedForm.tsx:28 +msgid "Password updated!" +msgstr "Şifre güncellendi!" + +#: src/Navigation.tsx:161 +msgid "People followed by @{0}" +msgstr "@{0} tarafından takip edilenler" + +#: src/Navigation.tsx:154 +msgid "People following @{0}" +msgstr "@{0} tarafından takip edilenler" + +#: src/view/com/lightbox/Lightbox.tsx:66 +msgid "Permission to access camera roll is required." +msgstr "Kamera rulosuna erişim izni gerekiyor." + +#: src/view/com/lightbox/Lightbox.tsx:72 +msgid "Permission to access camera roll was denied. Please enable it in your system settings." +msgstr "Kamera rulosuna erişim izni reddedildi. Lütfen sistem ayarlarınızda etkinleştirin." + +#: src/screens/Onboarding/index.tsx:31 +msgid "Pets" +msgstr "Evcil Hayvanlar" + +#: src/view/com/auth/create/Step2.tsx:183 +msgid "Phone number" +msgstr "Telefon numarası" + +#: src/view/com/modals/SelfLabel.tsx:121 +msgid "Pictures meant for adults." +msgstr "Yetişkinler için resimler." + +#: src/view/screens/ProfileFeed.tsx:353 src/view/screens/ProfileList.tsx:580 +msgid "Pin to home" +msgstr "Ana ekrana sabitle" + +#: src/view/screens/SavedFeeds.tsx:88 +msgid "Pinned Feeds" +msgstr "Sabitleme Beslemeleri" + +#: src/view/com/util/post-embeds/ExternalGifEmbed.tsx:111 +msgid "Play {0}" +msgstr "{0} oynat" + +#: src/view/com/util/post-embeds/ExternalPlayerEmbed.tsx:54 +#: src/view/com/util/post-embeds/ExternalPlayerEmbed.tsx:55 +msgid "Play Video" +msgstr "Videoyu Oynat" + +#: src/view/com/util/post-embeds/ExternalGifEmbed.tsx:110 +msgid "Plays the GIF" +msgstr "GIF'i oynatır" + +#: src/view/com/auth/create/state.ts:177 +msgid "Please choose your handle." +msgstr "Kullanıcı adınızı seçin." + +#: src/view/com/auth/create/state.ts:160 +msgid "Please choose your password." +msgstr "Şifrenizi seçin." + +#: src/view/com/modals/ChangeEmail.tsx:67 +msgid "Please confirm your email before changing it. This is a temporary requirement while email-updating tools are added, and it will soon be removed." +msgstr "E-postanızı değiştirmeden önce onaylayın. Bu, e-posta güncelleme araçları eklenirken geçici bir gerekliliktir ve yakında kaldırılacaktır." + +#: src/view/com/modals/AddAppPasswords.tsx:90 +msgid "Please enter a name for your app password. All spaces is not allowed." +msgstr "Uygulama şifreniz için bir ad girin. Tüm boşluklar izin verilmez." + +#: src/view/com/auth/create/Step2.tsx:206 +msgid "Please enter a phone number that can receive SMS text messages." +msgstr "SMS metin mesajları alabilen bir telefon numarası girin." + +#: src/view/com/modals/AddAppPasswords.tsx:145 +msgid "Please enter a unique name for this App Password or use our randomly generated one." +msgstr "Bu Uygulama Şifresi için benzersiz bir ad girin veya rastgele oluşturulanı kullanın." + +#: src/view/com/auth/create/state.ts:170 +msgid "Please enter the code you received by SMS." +msgstr "SMS ile aldığınız kodu girin." + +#: src/view/com/auth/create/Step2.tsx:282 +msgid "Please enter the verification code sent to {phoneNumberFormatted}." +msgstr "{phoneNumberFormatted} numarasına gönderilen doğrulama kodunu girin." + +#: src/view/com/auth/create/state.ts:146 +msgid "Please enter your email." +msgstr "E-postanızı girin." + +#: src/view/com/modals/DeleteAccount.tsx:191 +msgid "Please enter your password as well:" +msgstr "Lütfen şifrenizi de girin:" + +#: src/view/com/modals/AppealLabel.tsx:72 +#: src/view/com/modals/AppealLabel.tsx:75 +msgid "Please tell us why you think this content warning was incorrectly applied!" +msgstr "Lütfen bu içerik uyarısının yanlış uygulandığını düşündüğünüz nedeni bize bildirin!" + +#: src/view/com/modals/VerifyEmail.tsx:101 +msgid "Please Verify Your Email" +msgstr "Lütfen E-postanızı Doğrulayın" + +#: src/view/com/composer/Composer.tsx:215 +msgid "Please wait for your link card to finish loading" +msgstr "Bağlantı kartınızın yüklenmesini bekleyin" + +#: src/screens/Onboarding/index.tsx:37 +msgid "Politics" +msgstr "Politika" + +#: src/view/com/modals/SelfLabel.tsx:111 +msgid "Porn" +msgstr "Pornografi" + +#: src/view/com/composer/Composer.tsx:350 +#: src/view/com/composer/Composer.tsx:358 +msgctxt "action" +msgid "Post" +msgstr "Gönder" + +#: src/view/com/post-thread/PostThread.tsx:251 +msgctxt "description" +msgid "Post" +msgstr "Gönderi" + +#: src/view/com/post-thread/PostThreadItem.tsx:177 +msgid "Post by {0}" +msgstr "{0} tarafından gönderi" + +#: src/Navigation.tsx:173 src/Navigation.tsx:180 src/Navigation.tsx:187 +msgid "Post by @{0}" +msgstr "@{0} tarafından gönderi" + +#: src/view/com/util/forms/PostDropdownBtn.tsx:84 +msgid "Post deleted" +msgstr "Gönderi silindi" + +#: src/view/com/post-thread/PostThread.tsx:403 +msgid "Post hidden" +msgstr "Gönderi gizlendi" + +#: src/view/com/composer/select-language/SelectLangBtn.tsx:87 +msgid "Post language" +msgstr "Gönderi dili" + +#: src/view/com/modals/lang-settings/PostLanguagesSettings.tsx:75 +msgid "Post Languages" +msgstr "Gönderi Dilleri" + +#: src/view/com/post-thread/PostThread.tsx:455 +msgid "Post not found" +msgstr "Gönderi bulunamadı" + +#: src/view/screens/Profile.tsx:171 +msgid "Posts" +msgstr "Gönderiler" + +#: src/view/com/posts/FeedErrorMessage.tsx:64 +msgid "Posts hidden" +msgstr "Gönderiler gizlendi" + +#: src/view/com/modals/LinkWarning.tsx:46 +msgid "Potentially Misleading Link" +msgstr "Potansiyel Yanıltıcı Bağlantı" + +#: src/view/com/lightbox/Lightbox.web.tsx:135 +msgid "Previous image" +msgstr "Önceki resim" + +#: src/view/screens/LanguageSettings.tsx:187 +msgid "Primary Language" +msgstr "Birincil Dil" + +#: src/view/screens/PreferencesThreads.tsx:97 +msgid "Prioritize Your Follows" +msgstr "Takipçilerinizi Önceliklendirin" + +#: src/view/screens/Settings.tsx:626 src/view/shell/desktop/RightNav.tsx:84 +msgid "Privacy" +msgstr "Gizlilik" + +#: src/Navigation.tsx:218 src/view/screens/PrivacyPolicy.tsx:29 +#: src/view/screens/Settings.tsx:873 src/view/shell/Drawer.tsx:265 +msgid "Privacy Policy" +msgstr "Gizlilik Politikası" + +#: src/view/com/auth/login/ForgotPasswordForm.tsx:194 +msgid "Processing..." +msgstr "İşleniyor..." + +#: src/view/shell/bottom-bar/BottomBar.tsx:247 +#: src/view/shell/desktop/LeftNav.tsx:415 src/view/shell/Drawer.tsx:72 +#: src/view/shell/Drawer.tsx:549 src/view/shell/Drawer.tsx:550 +msgid "Profile" +msgstr "Profil" + +#: src/view/com/modals/EditProfile.tsx:128 +msgid "Profile updated" +msgstr "Profil güncellendi" + +#: src/view/screens/Settings.tsx:931 +msgid "Protect your account by verifying your email." +msgstr "E-postanızı doğrulayarak hesabınızı koruyun." + +#: src/screens/Onboarding/StepFinished.tsx:101 +msgid "Public" +msgstr "Herkese Açık" + +#: src/view/screens/ModerationModlists.tsx:61 +msgid "Public, shareable lists of users to mute or block in bulk." +msgstr "Toplu olarak sessize almak veya engellemek için herkese açık, paylaşılabilir kullanıcı listeleri." + +#: src/view/screens/Lists.tsx:61 +msgid "Public, shareable lists which can drive feeds." +msgstr "Beslemeleri yönlendirebilen herkese açık, paylaşılabilir listeler." + +#: src/view/com/composer/Composer.tsx:335 +msgid "Publish post" +msgstr "Gönderiyi yayınla" + +#: src/view/com/composer/Composer.tsx:335 +msgid "Publish reply" +msgstr "Yanıtı yayınla" + +#: src/view/com/modals/Repost.tsx:65 +msgctxt "action" +msgid "Quote post" +msgstr "Gönderiyi alıntıla" + +#: src/view/com/util/post-ctrls/RepostButton.web.tsx:58 +msgid "Quote post" +msgstr "Gönderiyi alıntıla" + +#: src/view/com/modals/Repost.tsx:70 +msgctxt "action" +msgid "Quote Post" +msgstr "Gönderiyi Alıntıla" + +#: src/view/screens/PreferencesThreads.tsx:86 +msgid "Random (aka \"Poster's Roulette\")" +msgstr "Rastgele (yani \"Gönderenin Ruleti\")" + +#: src/view/com/modals/EditImage.tsx:236 +msgid "Ratios" +msgstr "Oranlar" + +#: src/view/com/auth/onboarding/RecommendedFeeds.tsx:116 +msgid "Recommended Feeds" +msgstr "Önerilen Beslemeler" + +#: src/view/com/auth/onboarding/RecommendedFollows.tsx:180 +msgid "Recommended Users" +msgstr "Önerilen Kullanıcılar" + +#: src/view/com/modals/ListAddRemoveUsers.tsx:264 +#: src/view/com/modals/SelfLabel.tsx:83 +#: src/view/com/modals/UserAddRemoveLists.tsx:219 +#: src/view/com/util/UserAvatar.tsx:282 src/view/com/util/UserBanner.tsx:89 +msgid "Remove" +msgstr "Kaldır" + +#: src/view/com/feeds/FeedSourceCard.tsx:106 +msgid "Remove {0} from my feeds?" +msgstr "{0} beslemelerimden kaldırılsın mı?" + +#: src/view/com/util/AccountDropdownBtn.tsx:22 +msgid "Remove account" +msgstr "Hesabı kaldır" + +#: src/view/com/posts/FeedErrorMessage.tsx:131 +#: src/view/com/posts/FeedErrorMessage.tsx:166 +msgid "Remove feed" +msgstr "Beslemeyi kaldır" + +#: src/view/com/feeds/FeedSourceCard.tsx:105 +#: src/view/com/feeds/FeedSourceCard.tsx:167 +#: src/view/com/feeds/FeedSourceCard.tsx:172 +#: src/view/com/feeds/FeedSourceCard.tsx:243 +#: src/view/screens/ProfileFeed.tsx:272 +msgid "Remove from my feeds" +msgstr "Beslemelerimden kaldır" + +#: src/view/com/composer/photos/Gallery.tsx:167 +msgid "Remove image" +msgstr "Resmi kaldır" + +#: src/view/com/composer/ExternalEmbed.tsx:70 +msgid "Remove image preview" +msgstr "Resim önizlemesini kaldır" + +#: src/view/com/modals/Repost.tsx:47 +msgid "Remove repost" +msgstr "Yeniden göndermeyi kaldır" + +#: src/view/com/feeds/FeedSourceCard.tsx:173 +msgid "Remove this feed from my feeds?" +msgstr "Bu beslemeyi beslemelerimden kaldırsın mı?" + +#: src/view/com/posts/FeedErrorMessage.tsx:132 +msgid "Remove this feed from your saved feeds?" +msgstr "Bu beslemeyi kayıtlı beslemelerinizden kaldırsın mı?" + +#: src/view/com/modals/ListAddRemoveUsers.tsx:199 +#: src/view/com/modals/UserAddRemoveLists.tsx:152 +msgid "Removed from list" +msgstr "Listeden kaldırıldı" + +#: src/view/com/feeds/FeedSourceCard.tsx:111 +#: src/view/com/feeds/FeedSourceCard.tsx:178 +msgid "Removed from my feeds" +msgstr "Beslemelerimden kaldırıldı" + +#: src/view/com/composer/ExternalEmbed.tsx:71 +msgid "Removes default thumbnail from {0}" +msgstr "{0} adresinden varsayılan küçük resmi kaldırır" + +#: src/view/screens/Profile.tsx:172 +msgid "Replies" +msgstr "Yanıtlar" + +#: src/view/com/threadgate/WhoCanReply.tsx:98 +msgid "Replies to this thread are disabled" +msgstr "Bu konuya yanıtlar devre dışı bırakıldı" + +#: src/view/com/composer/Composer.tsx:348 +msgctxt "action" +msgid "Reply" +msgstr "Yanıtla" + +#: src/view/screens/PreferencesHomeFeed.tsx:144 +msgid "Reply Filters" +msgstr "Yanıt Filtreleri" + +#: src/view/com/post/Post.tsx:166 src/view/com/posts/FeedItem.tsx:287 +msgctxt "description" +msgid "Reply to <0/>" +msgstr "<0/>'a yanıt" + +#: src/view/com/modals/report/Modal.tsx:166 +msgid "Report {collectionName}" +msgstr "{collectionName} raporla" + +#: src/view/com/profile/ProfileHeader.tsx:408 +msgid "Report Account" +msgstr "Hesabı Raporla" + +#: src/view/screens/ProfileFeed.tsx:292 +msgid "Report feed" +msgstr "Beslemeyi raporla" + +#: src/view/screens/ProfileList.tsx:458 +msgid "Report List" +msgstr "Listeyi Raporla" + +#: src/view/com/modals/report/SendReportButton.tsx:37 +#: src/view/com/util/forms/PostDropdownBtn.tsx:210 +msgid "Report post" +msgstr "Gönderiyi raporla" + +#: src/view/com/modals/Repost.tsx:43 src/view/com/modals/Repost.tsx:48 +#: src/view/com/modals/Repost.tsx:53 +#: src/view/com/util/post-ctrls/RepostButton.tsx:61 +msgctxt "action" +msgid "Repost" +msgstr "Yeniden gönder" + +#: src/view/com/util/post-ctrls/RepostButton.web.tsx:48 +msgid "Repost" +msgstr "Yeniden gönder" + +#: src/view/com/util/post-ctrls/RepostButton.web.tsx:94 +#: src/view/com/util/post-ctrls/RepostButton.web.tsx:105 +msgid "Repost or quote post" +msgstr "Gönderiyi yeniden gönder veya alıntıla" + +#: src/view/screens/PostRepostedBy.tsx:27 +msgid "Reposted By" +msgstr "Yeniden Gönderen" + +#: src/view/com/posts/FeedItem.tsx:207 +msgid "Reposted by {0}" +msgstr "{0} tarafından yeniden gönderildi" + +#: src/view/com/posts/FeedItem.tsx:224 +msgid "Reposted by <0/>" +msgstr "<0/>'a yeniden gönderildi" + +#: src/view/com/notifications/FeedItem.tsx:162 +msgid "reposted your post" +msgstr "gönderinizi yeniden gönderdi" + +#: src/view/com/post-thread/PostThreadItem.tsx:190 +msgid "Reposts of this post" +msgstr "Bu gönderinin yeniden gönderilmesi" + +#: src/view/com/modals/ChangeEmail.tsx:181 +#: src/view/com/modals/ChangeEmail.tsx:183 +msgid "Request Change" +msgstr "Değişiklik İste" + +#: src/view/com/auth/create/Step2.tsx:219 +msgid "Request code" +msgstr "Kod iste" + +#: src/view/com/modals/ChangePassword.tsx:239 +#: src/view/com/modals/ChangePassword.tsx:241 +msgid "Request Code" +msgstr "Kod İste" + +#: src/view/screens/Settings.tsx:450 +msgid "Require alt text before posting" +msgstr "Göndermeden önce alternatif metin gerektir" + +#: src/view/com/auth/create/Step1.tsx:97 +msgid "Required for this provider" +msgstr "Bu sağlayıcı için gereklidir" + +#: src/view/com/auth/login/SetNewPasswordForm.tsx:124 +#: src/view/com/auth/login/SetNewPasswordForm.tsx:136 +msgid "Reset code" +msgstr "Sıfırlama kodu" + +#: src/view/com/modals/ChangePassword.tsx:190 +msgid "Reset Code" +msgstr "Sıfırlama Kodu" + +#: src/view/screens/Settings.tsx:806 +msgid "Reset onboarding" +msgstr "Onboarding sıfırla" + +#: src/view/screens/Settings.tsx:809 +msgid "Reset onboarding state" +msgstr "Onboarding durumunu sıfırla" + +#: src/view/com/auth/login/ForgotPasswordForm.tsx:100 +msgid "Reset password" +msgstr "Şifreyi sıfırla" + +#: src/view/screens/Settings.tsx:796 +msgid "Reset preferences" +msgstr "Tercihleri sıfırla" + +#: src/view/screens/Settings.tsx:799 +msgid "Reset preferences state" +msgstr "Tercih durumunu sıfırla" + +#: src/view/screens/Settings.tsx:807 +msgid "Resets the onboarding state" +msgstr "Onboarding durumunu sıfırlar" + +#: src/view/screens/Settings.tsx:797 +msgid "Resets the preferences state" +msgstr "Tercih durumunu sıfırlar" + +#: src/view/com/auth/login/LoginForm.tsx:266 +msgid "Retries login" +msgstr "Giriş tekrar denemesi" + +#: src/view/com/util/error/ErrorMessage.tsx:57 +#: src/view/com/util/error/ErrorScreen.tsx:67 +msgid "Retries the last action, which errored out" +msgstr "Son hataya neden olan son eylemi tekrarlar" + +#: src/screens/Onboarding/StepInterests/index.tsx:221 +#: src/screens/Onboarding/StepInterests/index.tsx:224 +#: src/view/com/auth/create/CreateAccount.tsx:170 +#: src/view/com/auth/create/CreateAccount.tsx:175 +#: src/view/com/auth/create/Step2.tsx:255 +#: src/view/com/auth/login/LoginForm.tsx:265 +#: src/view/com/auth/login/LoginForm.tsx:268 +#: src/view/com/util/error/ErrorMessage.tsx:55 +#: src/view/com/util/error/ErrorScreen.tsx:65 +msgid "Retry" +msgstr "Tekrar dene" + +#: src/view/com/auth/create/Step2.tsx:247 +msgid "Retry." +msgstr "Tekrar dene." + +#: src/view/screens/ProfileList.tsx:898 +msgid "Return to previous page" +msgstr "Önceki sayfaya dön" + +#: src/view/shell/desktop/RightNav.tsx:59 +msgid "SANDBOX. Posts and accounts are not permanent." +msgstr "KUM KUTUSU. Gönderiler ve hesaplar kalıcı değildir." + +#: src/view/com/lightbox/Lightbox.tsx:132 +#: src/view/com/modals/CreateOrEditList.tsx:345 +msgctxt "action" +msgid "Save" +msgstr "Kaydet" + +#: src/view/com/modals/BirthDateSettings.tsx:94 +#: src/view/com/modals/BirthDateSettings.tsx:97 +#: src/view/com/modals/ChangeHandle.tsx:173 +#: src/view/com/modals/CreateOrEditList.tsx:337 +#: src/view/com/modals/EditProfile.tsx:224 src/view/screens/ProfileFeed.tsx:345 +msgid "Save" +msgstr "Kaydet" + +#: src/view/com/modals/AltImage.tsx:130 +msgid "Save alt text" +msgstr "Alternatif metni kaydet" + +#: src/view/com/modals/EditProfile.tsx:232 +msgid "Save Changes" +msgstr "Değişiklikleri Kaydet" + +#: src/view/com/modals/ChangeHandle.tsx:170 +msgid "Save handle change" +msgstr "Kullanıcı adı değişikliğini kaydet" + +#: src/view/com/modals/crop-image/CropImage.web.tsx:144 +msgid "Save image crop" +msgstr "Resim kırpma kaydet" + +#: src/view/screens/SavedFeeds.tsx:122 +msgid "Saved Feeds" +msgstr "Kayıtlı Beslemeler" + +#: src/view/com/modals/EditProfile.tsx:225 +msgid "Saves any changes to your profile" +msgstr "Profilinizdeki herhangi bir değişikliği kaydeder" + +#: src/view/com/modals/ChangeHandle.tsx:171 +msgid "Saves handle change to {handle}" +msgstr "{handle} kullanıcı adı değişikliğini kaydeder" + +#: src/screens/Onboarding/index.tsx:36 +msgid "Science" +msgstr "Bilim" + +#: src/view/screens/ProfileList.tsx:854 +msgid "Scroll to top" +msgstr "Başa kaydır" + +#: src/Navigation.tsx:438 src/view/com/auth/LoggedOut.tsx:122 +#: src/view/com/modals/ListAddRemoveUsers.tsx:75 +#: src/view/com/util/forms/SearchInput.tsx:53 +#: src/view/com/util/forms/SearchInput.tsx:65 +#: src/view/screens/Search/Search.tsx:418 +#: src/view/screens/Search/Search.tsx:645 +#: src/view/screens/Search/Search.tsx:663 +#: src/view/shell/bottom-bar/BottomBar.tsx:159 +#: src/view/shell/desktop/LeftNav.tsx:324 src/view/shell/desktop/Search.tsx:214 +#: src/view/shell/desktop/Search.tsx:223 src/view/shell/Drawer.tsx:365 +#: src/view/shell/Drawer.tsx:366 +msgid "Search" +msgstr "Ara" + +#: src/view/screens/Search/Search.tsx:712 src/view/shell/desktop/Search.tsx:255 +msgid "Search for \"{query}\"" +msgstr "\"{query}\" için ara" + +#: src/view/com/auth/LoggedOut.tsx:104 src/view/com/auth/LoggedOut.tsx:105 +#: src/view/com/modals/ListAddRemoveUsers.tsx:70 +msgid "Search for users" +msgstr "Kullanıcıları ara" + +#: src/view/com/modals/ChangeEmail.tsx:110 +msgid "Security Step Required" +msgstr "Güvenlik Adımı Gerekli" + +#: src/view/screens/SavedFeeds.tsx:163 +msgid "See this guide" +msgstr "Bu kılavuzu gör" + +#: src/view/com/auth/HomeLoggedOutCTA.tsx:39 +msgid "See what's next" +msgstr "Ne olduğunu gör" + +#: src/view/com/util/Selector.tsx:106 +msgid "Select {item}" +msgstr "{item} seç" + +#: src/view/com/modals/ServerInput.tsx:75 +msgid "Select Bluesky Social" +msgstr "Bluesky Social seç" + +#: src/view/com/auth/login/Login.tsx:117 +msgid "Select from an existing account" +msgstr "Mevcut bir hesaptan seç" + +#: src/view/com/util/Selector.tsx:107 +msgid "Select option {i} of {numItems}" +msgstr "{i} seçeneği, {numItems} seçenekten" + +#: src/view/com/auth/create/Step1.tsx:77 +#: src/view/com/auth/login/LoginForm.tsx:147 +msgid "Select service" +msgstr "Servis seç" + +#: src/screens/Onboarding/StepSuggestedAccounts/index.tsx:52 +msgid "Select some accounts below to follow" +msgstr "Aşağıdaki hesaplardan bazılarını takip et" + +#: src/screens/Onboarding/StepTopicalFeeds.tsx:90 +msgid "Select topical feeds to follow from the list below" +msgstr "Aşağıdaki listeden takip edilecek konu beslemelerini seçin" + +#: src/screens/Onboarding/StepModeration/index.tsx:75 +msgid "Select what you want to see (or not see), and we’ll handle the rest." +msgstr "Görmek istediğinizi (veya görmek istemediğinizi) seçin, gerisini biz hallederiz." + +#: src/view/screens/LanguageSettings.tsx:281 +msgid "Select which languages you want your subscribed feeds to include. If none are selected, all languages will be shown." +msgstr "Abone olduğunuz beslemelerin hangi dilleri içermesini istediğinizi seçin. Hiçbiri seçilmezse, tüm diller gösterilir." + +#: src/view/screens/LanguageSettings.tsx:98 +msgid "Select your app language for the default text to display in the app" +msgstr "Uygulama dilinizi seçin, uygulamada görüntülenecek varsayılan metin" + +#: src/screens/Onboarding/StepInterests/index.tsx:196 +msgid "Select your interests from the options below" +msgstr "Aşağıdaki seçeneklerden ilgi alanlarınızı seçin" + +#: src/view/com/auth/create/Step2.tsx:155 +msgid "Select your phone's country" +msgstr "Telefonunuzun ülkesini seçin" + +#: src/view/screens/LanguageSettings.tsx:190 +msgid "Select your preferred language for translations in your feed." +msgstr "Beslemenizdeki çeviriler için tercih ettiğiniz dili seçin." + +#: src/screens/Onboarding/StepAlgoFeeds/index.tsx:116 +msgid "Select your primary algorithmic feeds" +msgstr "Birincil algoritmik beslemelerinizi seçin" + +#: src/screens/Onboarding/StepAlgoFeeds/index.tsx:132 +msgid "Select your secondary algorithmic feeds" +msgstr "İkincil algoritmik beslemelerinizi seçin" + +#: src/view/com/modals/VerifyEmail.tsx:202 +#: src/view/com/modals/VerifyEmail.tsx:204 +msgid "Send Confirmation Email" +msgstr "Onay E-postası Gönder" + +#: src/view/com/modals/DeleteAccount.tsx:131 +msgid "Send email" +msgstr "E-posta gönder" + +#: src/view/com/modals/DeleteAccount.tsx:144 +msgctxt "action" +msgid "Send Email" +msgstr "E-posta Gönder" + +#: src/view/shell/Drawer.tsx:298 src/view/shell/Drawer.tsx:319 +msgid "Send feedback" +msgstr "Geribildirim gönder" + +#: src/view/com/modals/report/SendReportButton.tsx:45 +msgid "Send Report" +msgstr "Rapor Gönder" + +#: src/view/com/modals/DeleteAccount.tsx:133 +msgid "Sends email with confirmation code for account deletion" +msgstr "Hesap silme için onay kodu içeren e-posta gönderir" + +#: src/view/com/modals/ContentFilteringSettings.tsx:306 +msgid "Set {value} for {labelGroup} content moderation policy" +msgstr "{labelGroup} içerik düzenleme politikası için {value} ayarla" + +#: src/view/com/modals/ContentFilteringSettings.tsx:155 +#: src/view/com/modals/ContentFilteringSettings.tsx:174 +msgctxt "action" +msgid "Set Age" +msgstr "Yaş Ayarla" + +#: src/view/screens/Settings.tsx:482 +msgid "Set color theme to dark" +msgstr "Renk temasını koyu olarak ayarla" + +#: src/view/screens/Settings.tsx:475 +msgid "Set color theme to light" +msgstr "Renk temasını açık olarak ayarla" + +#: src/view/screens/Settings.tsx:469 +msgid "Set color theme to system setting" +msgstr "Renk temasını sistem ayarına ayarla" + +#: src/view/screens/Settings.tsx:508 +msgid "Set dark theme to the dark theme" +msgstr "Koyu teması koyu temaya ayarla" + +#: src/view/screens/Settings.tsx:501 +msgid "Set dark theme to the dim theme" +msgstr "Koyu teması loş temaya ayarla" + +#: src/view/com/auth/login/SetNewPasswordForm.tsx:104 +msgid "Set new password" +msgstr "Yeni şifre ayarla" + +#: src/view/com/auth/create/Step1.tsx:169 +msgid "Set password" +msgstr "Şifre ayarla" + +#: src/view/screens/PreferencesHomeFeed.tsx:225 +msgid "Set this setting to \"No\" to hide all quote posts from your feed. Reposts will still be visible." +msgstr "Bu ayarı \"Hayır\" olarak ayarlayarak beslemenizden tüm alıntı gönderileri gizleyebilirsiniz. Yeniden göndermeler hala görünür olacaktır." + +#: src/view/screens/PreferencesHomeFeed.tsx:122 +msgid "Set this setting to \"No\" to hide all replies from your feed." +msgstr "Bu ayarı \"Hayır\" olarak ayarlayarak beslemenizden tüm yanıtları gizleyebilirsiniz." + +#: src/view/screens/PreferencesHomeFeed.tsx:191 +msgid "Set this setting to \"No\" to hide all reposts from your feed." +msgstr "Bu ayarı \"Hayır\" olarak ayarlayarak beslemenizden tüm yeniden göndermeleri gizleyebilirsiniz." + +#: src/view/screens/PreferencesThreads.tsx:122 +msgid "Set this setting to \"Yes\" to show replies in a threaded view. This is an experimental feature." +msgstr "Bu ayarı \"Evet\" olarak ayarlayarak yanıtları konu tabanlı görüntülemek için ayarlayın. Bu deneysel bir özelliktir." + +#: src/view/screens/PreferencesHomeFeed.tsx:261 +msgid "Set this setting to \"Yes\" to show samples of your saved feeds in your following feed. This is an experimental feature." +msgstr "Bu ayarı \"Evet\" olarak ayarlayarak kayıtlı beslemelerinizin örneklerini takip ettiğiniz beslemede göstermek için ayarlayın. Bu deneysel bir özelliktir." + +#: src/screens/Onboarding/Layout.tsx:50 +msgid "Set up your account" +msgstr "Hesabınızı ayarlayın" + +#: src/view/com/modals/ChangeHandle.tsx:266 +msgid "Sets Bluesky username" +msgstr "Bluesky kullanıcı adını ayarlar" + +#: src/view/com/auth/login/ForgotPasswordForm.tsx:153 +msgid "Sets email for password reset" +msgstr "Şifre sıfırlama için e-posta ayarlar" + +#: src/view/com/auth/login/ForgotPasswordForm.tsx:118 +msgid "Sets hosting provider for password reset" +msgstr "Şifre sıfırlama için barındırma sağlayıcısını ayarlar" + +#: src/view/com/auth/create/Step1.tsx:78 +#: src/view/com/auth/login/LoginForm.tsx:148 +msgid "Sets server for the Bluesky client" +msgstr "Bluesky istemcisi için sunucuyu ayarlar" + +#: src/Navigation.tsx:135 src/view/screens/Settings.tsx:294 +#: src/view/shell/desktop/LeftNav.tsx:433 src/view/shell/Drawer.tsx:570 +#: src/view/shell/Drawer.tsx:571 +msgid "Settings" +msgstr "Ayarlar" + +#: src/view/com/modals/SelfLabel.tsx:125 +msgid "Sexual activity or erotic nudity." +msgstr "Cinsel aktivite veya erotik çıplaklık." + +#: src/view/com/lightbox/Lightbox.tsx:141 +msgctxt "action" +msgid "Share" +msgstr "Paylaş" + +#: src/view/com/profile/ProfileHeader.tsx:342 +#: src/view/com/util/forms/PostDropdownBtn.tsx:153 +#: src/view/screens/ProfileList.tsx:417 +msgid "Share" +msgstr "Paylaş" + +#: src/view/screens/ProfileFeed.tsx:304 +msgid "Share feed" +msgstr "Beslemeyi paylaş" + +#: src/screens/Onboarding/StepModeration/ModerationOption.tsx:43 +#: src/view/com/modals/ContentFilteringSettings.tsx:261 +#: src/view/com/util/moderation/ContentHider.tsx:107 +#: src/view/com/util/moderation/PostHider.tsx:108 +#: src/view/screens/Settings.tsx:344 +msgid "Show" +msgstr "Göster" + +#: src/view/screens/PreferencesHomeFeed.tsx:68 +msgid "Show all replies" +msgstr "Tüm yanıtları göster" + +#: src/view/com/util/moderation/ScreenHider.tsx:132 +msgid "Show anyway" +msgstr "Yine de göster" + +#: src/view/com/modals/EmbedConsent.tsx:87 +msgid "Show embeds from {0}" +msgstr "{0} adresinden gömülü öğeleri göster" + +#: src/view/com/profile/ProfileHeader.tsx:498 +msgid "Show follows similar to {0}" +msgstr "{0} adresine benzer takipçileri göster" + +#: src/view/com/post-thread/PostThreadItem.tsx:571 +#: src/view/com/post/Post.tsx:197 src/view/com/posts/FeedItem.tsx:363 +msgid "Show More" +msgstr "Daha Fazla Göster" + +#: src/view/screens/PreferencesHomeFeed.tsx:258 +msgid "Show Posts from My Feeds" +msgstr "Beslemelerimden Gönderileri Göster" + +#: src/view/screens/PreferencesHomeFeed.tsx:222 +msgid "Show Quote Posts" +msgstr "Alıntı Gönderileri Göster" + +#: src/screens/Onboarding/StepFollowingFeed.tsx:118 +msgid "Show quote-posts in Following feed" +msgstr "Alıntı gönderileri takip etme beslemesinde göster" + +#: src/screens/Onboarding/StepFollowingFeed.tsx:134 +msgid "Show quotes in Following" +msgstr "Takip etme beslemesinde alıntıları göster" + +#: src/screens/Onboarding/StepFollowingFeed.tsx:94 +msgid "Show re-posts in Following feed" +msgstr "Yeniden göndermeleri takip etme beslemesinde göster" + +#: src/view/screens/PreferencesHomeFeed.tsx:119 +msgid "Show Replies" +msgstr "Yanıtları Göster" + +#: src/view/screens/PreferencesThreads.tsx:100 +msgid "Show replies by people you follow before all other replies." +msgstr "Takip ettiğiniz kişilerin yanıtlarını diğer tüm yanıtlardan önce göster." + +#: src/screens/Onboarding/StepFollowingFeed.tsx:86 +msgid "Show replies in Following" +msgstr "Takip etme beslemesinde yanıtları göster" + +#: src/screens/Onboarding/StepFollowingFeed.tsx:70 +msgid "Show replies in Following feed" +msgstr "Takip etme beslemesinde yanıtları göster" + +#: src/view/screens/PreferencesHomeFeed.tsx:70 +msgid "Show replies with at least {value} {0}" +msgstr "En az {value} {0} olan yanıtları göster" + +#: src/view/screens/PreferencesHomeFeed.tsx:188 +msgid "Show Reposts" +msgstr "Yeniden Göndermeleri Göster" + +#: src/screens/Onboarding/StepFollowingFeed.tsx:110 +msgid "Show reposts in Following" +msgstr "Takip etme beslemesinde yeniden göndermeleri göster" + +#: src/view/com/util/moderation/ContentHider.tsx:67 +#: src/view/com/util/moderation/PostHider.tsx:61 +msgid "Show the content" +msgstr "İçeriği göster" + +#: src/view/com/notifications/FeedItem.tsx:346 +msgid "Show users" +msgstr "Kullanıcıları göster" + +#: src/view/com/profile/ProfileHeader.tsx:501 +msgid "Shows a list of users similar to this user." +msgstr "Bu kullanıcıya benzer kullanıcıların listesini gösterir." + +#: src/view/com/profile/ProfileHeader.tsx:545 +msgid "Shows posts from {0} in your feed" +msgstr "Beslemenizde {0} adresinden gönderileri gösterir" + +#: src/view/com/auth/HomeLoggedOutCTA.tsx:70 +#: src/view/com/auth/login/Login.tsx:98 src/view/com/auth/SplashScreen.tsx:54 +#: src/view/shell/bottom-bar/BottomBar.tsx:285 +#: src/view/shell/bottom-bar/BottomBar.tsx:286 +#: src/view/shell/bottom-bar/BottomBar.tsx:288 +#: src/view/shell/bottom-bar/BottomBarWeb.tsx:178 +#: src/view/shell/bottom-bar/BottomBarWeb.tsx:179 +#: src/view/shell/bottom-bar/BottomBarWeb.tsx:181 +#: src/view/shell/NavSignupCard.tsx:58 src/view/shell/NavSignupCard.tsx:59 +msgid "Sign in" +msgstr "Giriş yap" + +#: src/view/com/auth/HomeLoggedOutCTA.tsx:78 +#: src/view/com/auth/SplashScreen.tsx:57 +#: src/view/com/auth/SplashScreen.web.tsx:87 +msgid "Sign In" +msgstr "Giriş Yap" + +#: src/view/com/auth/login/ChooseAccountForm.tsx:44 +msgid "Sign in as {0}" +msgstr "{0} olarak giriş yap" + +#: src/view/com/auth/login/ChooseAccountForm.tsx:118 +#: src/view/com/auth/login/Login.tsx:116 +msgid "Sign in as..." +msgstr "Olarak giriş yap..." + +#: src/view/com/auth/login/LoginForm.tsx:134 +msgid "Sign into" +msgstr "Olarak giriş yap" + +#: src/view/com/modals/SwitchAccount.tsx:64 +#: src/view/com/modals/SwitchAccount.tsx:69 src/view/screens/Settings.tsx:107 +#: src/view/screens/Settings.tsx:110 +msgid "Sign out" +msgstr "Çıkış yap" + +#: src/view/shell/bottom-bar/BottomBar.tsx:275 +#: src/view/shell/bottom-bar/BottomBar.tsx:276 +#: src/view/shell/bottom-bar/BottomBar.tsx:278 +#: src/view/shell/bottom-bar/BottomBarWeb.tsx:168 +#: src/view/shell/bottom-bar/BottomBarWeb.tsx:169 +#: src/view/shell/bottom-bar/BottomBarWeb.tsx:171 +#: src/view/shell/NavSignupCard.tsx:49 src/view/shell/NavSignupCard.tsx:50 +#: src/view/shell/NavSignupCard.tsx:52 +msgid "Sign up" +msgstr "Kaydol" + +#: src/view/shell/NavSignupCard.tsx:42 +msgid "Sign up or sign in to join the conversation" +msgstr "Konuşmaya katılmak için kaydolun veya giriş yapın" + +#: src/view/com/util/moderation/ScreenHider.tsx:76 +msgid "Sign-in Required" +msgstr "Giriş Yapılması Gerekiyor" + +#: src/view/screens/Settings.tsx:355 +msgid "Signed in as" +msgstr "Olarak giriş yapıldı" + +#: src/view/com/auth/login/ChooseAccountForm.tsx:103 +msgid "Signed in as @{0}" +msgstr "@{0} olarak giriş yapıldı" + +#: src/view/com/modals/SwitchAccount.tsx:66 +msgid "Signs {0} out of Bluesky" +msgstr "{0} adresini Bluesky'den çıkarır" + +#: src/screens/Onboarding/StepInterests/index.tsx:235 +#: src/screens/Onboarding/StepSuggestedAccounts/index.tsx:191 +#: src/view/com/auth/onboarding/WelcomeMobile.tsx:33 +msgid "Skip" +msgstr "Atla" + +#: src/screens/Onboarding/StepInterests/index.tsx:232 +msgid "Skip this flow" +msgstr "Bu akışı atla" + +#: src/view/com/auth/create/Step2.tsx:82 +msgid "SMS verification" +msgstr "SMS doğrulama" + +#: src/screens/Onboarding/index.tsx:40 +msgid "Software Dev" +msgstr "Yazılım Geliştirme" + +#: src/view/com/modals/ProfilePreview.tsx:62 +msgid "Something went wrong and we're not sure what." +msgstr "Bir şeyler yanlış gitti ve ne olduğundan emin değiliz." + +#: src/view/com/modals/Waitlist.tsx:51 +msgid "Something went wrong. Check your email and try again." +msgstr "Bir şeyler yanlış gitti. E-postanızı kontrol edin ve tekrar deneyin." + +#: src/App.native.tsx:60 +msgid "Sorry! Your session expired. Please log in again." +msgstr "Üzgünüz! Oturumunuzun süresi doldu. Lütfen tekrar giriş yapın." + +#: src/view/screens/PreferencesThreads.tsx:69 +msgid "Sort Replies" +msgstr "Yanıtları Sırala" + +#: src/view/screens/PreferencesThreads.tsx:72 +msgid "Sort replies to the same post by:" +msgstr "Aynı gönderiye verilen yanıtları şuna göre sırala:" + +#: src/screens/Onboarding/index.tsx:30 +msgid "Sports" +msgstr "Spor" + +#: src/view/com/modals/crop-image/CropImage.web.tsx:122 +msgid "Square" +msgstr "Kare" + +#: src/view/com/modals/ServerInput.tsx:62 +msgid "Staging" +msgstr "Staging" + +#: src/view/screens/Settings.tsx:853 +msgid "Status page" +msgstr "Durum sayfası" + +#: src/view/com/auth/create/StepHeader.tsx:22 +msgid "Step {0} of {numSteps}" +msgstr "{numSteps} adımdan {0}. adım" + +#: src/view/screens/Settings.tsx:276 +msgid "Storage cleared, you need to restart the app now." +msgstr "Depolama temizlendi, şimdi uygulamayı yeniden başlatmanız gerekiyor." + +#: src/Navigation.tsx:203 src/view/screens/Settings.tsx:789 +msgid "Storybook" +msgstr "Storybook" + +#: src/view/com/modals/AppealLabel.tsx:101 +msgid "Submit" +msgstr "Submit" + +#: src/view/screens/ProfileList.tsx:607 +msgid "Subscribe" +msgstr "Abone ol" + +#: src/screens/Onboarding/StepAlgoFeeds/FeedCard.tsx:173 +#: src/screens/Onboarding/StepAlgoFeeds/FeedCard.tsx:307 +msgid "Subscribe to the {0} feed" +msgstr "{0} beslemesine abone ol" + +#: src/view/screens/ProfileList.tsx:603 +msgid "Subscribe to this list" +msgstr "Bu listeye abone ol" + +#: src/view/screens/Search/Search.tsx:373 +msgid "Suggested Follows" +msgstr "Önerilen Takipçiler" + +#: src/view/com/profile/ProfileHeaderSuggestedFollows.tsx:64 +msgid "Suggested for you" +msgstr "Sana önerilenler" + +#: src/view/com/modals/SelfLabel.tsx:95 +msgid "Suggestive" +msgstr "Tehlikeli" + +#: src/Navigation.tsx:213 src/view/screens/Support.tsx:30 +#: src/view/screens/Support.tsx:33 +msgid "Support" +msgstr "Destek" + +#: src/view/com/modals/ProfilePreview.tsx:110 +msgid "Swipe up to see more" +msgstr "Daha fazlasını görmek için yukarı kaydır" + +#: src/view/com/modals/SwitchAccount.tsx:117 +msgid "Switch Account" +msgstr "Hesap Değiştir" + +#: src/view/com/modals/SwitchAccount.tsx:97 src/view/screens/Settings.tsx:137 +msgid "Switch to {0}" +msgstr "{0} adresine geç" + +#: src/view/com/modals/SwitchAccount.tsx:98 src/view/screens/Settings.tsx:138 +msgid "Switches the account you are logged in to" +msgstr "Giriş yaptığınız hesabı değiştirir" + +#: src/view/screens/Settings.tsx:466 +msgid "System" +msgstr "Sistem" + +#: src/view/screens/Settings.tsx:769 +msgid "System log" +msgstr "Sistem günlüğü" + +#: src/view/com/modals/crop-image/CropImage.web.tsx:112 +msgid "Tall" +msgstr "Uzun" + +#: src/view/com/util/images/AutoSizedImage.tsx:70 +msgid "Tap to view fully" +msgstr "Tamamen görüntülemek için dokunun" + +#: src/screens/Onboarding/index.tsx:39 +msgid "Tech" +msgstr "Teknoloji" + +#: src/view/shell/desktop/RightNav.tsx:93 +msgid "Terms" +msgstr "Şartlar" + +#: src/Navigation.tsx:223 src/view/screens/Settings.tsx:867 +#: src/view/screens/TermsOfService.tsx:29 src/view/shell/Drawer.tsx:259 +msgid "Terms of Service" +msgstr "Hizmet Şartları" + +#: src/view/com/modals/AppealLabel.tsx:70 +#: src/view/com/modals/report/InputIssueDetails.tsx:51 +msgid "Text input field" +msgstr "Metin giriş alanı" + +#: src/view/com/profile/ProfileHeader.tsx:310 +msgid "The account will be able to interact with you after unblocking." +msgstr "Hesap, engeli kaldırdıktan sonra sizinle etkileşime geçebilecek." + +#: src/view/screens/CommunityGuidelines.tsx:36 +msgid "The Community Guidelines have been moved to <0/>" +msgstr "Topluluk Kuralları <0/> konumuna taşındı" + +#: src/view/screens/CopyrightPolicy.tsx:33 +msgid "The Copyright Policy has been moved to <0/>" +msgstr "Telif Hakkı Politikası <0/> konumuna taşındı" + +#: src/screens/Onboarding/Layout.tsx:60 +msgid "The following steps will help customize your Bluesky experience." +msgstr "Aşağıdaki adımlar, Bluesky deneyiminizi özelleştirmenize yardımcı olacaktır." + +#: src/view/com/post-thread/PostThread.tsx:458 +msgid "The post may have been deleted." +msgstr "Gönderi silinmiş olabilir." + +#: src/view/screens/PrivacyPolicy.tsx:33 +msgid "The Privacy Policy has been moved to <0/>" +msgstr "Gizlilik Politikası <0/> konumuna taşındı" + +#: src/view/screens/Support.tsx:36 +msgid "The support form has been moved. If you need help, please <0/> or visit {HELP_DESK_URL} to get in touch with us." +msgstr "Destek formu taşındı. Yardıma ihtiyacınız varsa, lütfen <0/> veya bize ulaşmak için {HELP_DESK_URL} adresini ziyaret edin." + +#: src/view/screens/TermsOfService.tsx:33 +msgid "The Terms of Service have been moved to" +msgstr "Hizmet Şartları taşındı" + +#: src/screens/Onboarding/StepAlgoFeeds/index.tsx:135 +msgid "There are many feeds to try:" +msgstr "Denemek için birçok besleme var:" + +#: src/view/screens/ProfileFeed.tsx:549 +msgid "There was an an issue contacting the server, please check your internet connection and try again." +msgstr "Sunucuya ulaşma konusunda bir sorun oluştu, lütfen internet bağlantınızı kontrol edin ve tekrar deneyin." + +#: src/view/com/posts/FeedErrorMessage.tsx:139 +msgid "There was an an issue removing this feed. Please check your internet connection and try again." +msgstr "Bu beslemeyi kaldırma konusunda bir sorun oluştu. Lütfen internet bağlantınızı kontrol edin ve tekrar deneyin." + +#: src/view/screens/ProfileFeed.tsx:209 +msgid "There was an an issue updating your feeds, please check your internet connection and try again." +msgstr "Beslemelerinizi güncelleme konusunda bir sorun oluştu, lütfen internet bağlantınızı kontrol edin ve tekrar deneyin." + +#: src/view/screens/ProfileFeed.tsx:236 src/view/screens/ProfileList.tsx:266 +#: src/view/screens/SavedFeeds.tsx:209 src/view/screens/SavedFeeds.tsx:231 +#: src/view/screens/SavedFeeds.tsx:252 +msgid "There was an issue contacting the server" +msgstr "Sunucuya ulaşma konusunda bir sorun oluştu" + +#: src/view/com/auth/onboarding/RecommendedFeedsItem.tsx:57 +#: src/view/com/auth/onboarding/RecommendedFeedsItem.tsx:66 +#: src/view/com/feeds/FeedSourceCard.tsx:113 +#: src/view/com/feeds/FeedSourceCard.tsx:127 +#: src/view/com/feeds/FeedSourceCard.tsx:181 +msgid "There was an issue contacting your server" +msgstr "Sunucunuza ulaşma konusunda bir sorun oluştu" + +#: src/view/com/notifications/Feed.tsx:117 +msgid "There was an issue fetching notifications. Tap here to try again." +msgstr "Bildirimleri almakta bir sorun oluştu. Tekrar denemek için buraya dokunun." + +#: src/view/com/posts/Feed.tsx:263 +msgid "There was an issue fetching posts. Tap here to try again." +msgstr "Gönderileri almakta bir sorun oluştu. Tekrar denemek için buraya dokunun." + +#: src/view/com/lists/ListMembers.tsx:172 +msgid "There was an issue fetching the list. Tap here to try again." +msgstr "Listeyi almakta bir sorun oluştu. Tekrar denemek için buraya dokunun." + +#: src/view/com/feeds/ProfileFeedgens.tsx:148 +#: src/view/com/lists/ProfileLists.tsx:155 +msgid "There was an issue fetching your lists. Tap here to try again." +msgstr "Listelerinizi almakta bir sorun oluştu. Tekrar denemek için buraya dokunun." + +#: src/screens/Onboarding/StepModeration/AdultContentEnabledPref.tsx:63 +#: src/view/com/modals/ContentFilteringSettings.tsx:126 +msgid "There was an issue syncing your preferences with the server" +msgstr "Tercihlerinizi sunucuyla senkronize etme konusunda bir sorun oluştu" + +#: src/view/screens/AppPasswords.tsx:66 +msgid "There was an issue with fetching your app passwords" +msgstr "Uygulama şifrelerinizi almakta bir sorun oluştu" + +#: src/view/com/profile/ProfileHeader.tsx:204 +#: src/view/com/profile/ProfileHeader.tsx:225 +#: src/view/com/profile/ProfileHeader.tsx:264 +#: src/view/com/profile/ProfileHeader.tsx:277 +#: src/view/com/profile/ProfileHeader.tsx:297 +#: src/view/com/profile/ProfileHeader.tsx:319 +msgid "There was an issue! {0}" +msgstr "Bir sorun oluştu! {0}" + +#: src/view/screens/ProfileList.tsx:287 src/view/screens/ProfileList.tsx:306 +#: src/view/screens/ProfileList.tsx:328 src/view/screens/ProfileList.tsx:347 +msgid "There was an issue. Please check your internet connection and try again." +msgstr "Bir sorun oluştu. Lütfen internet bağlantınızı kontrol edin ve tekrar deneyin." + +#: src/view/com/util/ErrorBoundary.tsx:36 +msgid "There was an unexpected issue in the application. Please let us know if this happened to you!" +msgstr "Uygulamada beklenmeyen bir sorun oluştu. Bu size de olduysa lütfen bize bildirin!" + +#: src/screens/Deactivated.tsx:107 +msgid "There's been a rush of new users to Bluesky! We'll activate your account as soon as we can." +msgstr "Bluesky'e bir dizi yeni kullanıcı geldi! Hesabınızı en kısa sürede etkinleştireceğiz." + +#: src/view/com/auth/create/Step2.tsx:55 +msgid "There's something wrong with this number. Please choose your country and enter your full phone number!" +msgstr "Bu numarada bir sorun var. Lütfen ülkenizi seçin ve tam telefon numaranızı girin!" + +#: src/screens/Onboarding/StepSuggestedAccounts/index.tsx:138 +msgid "These are popular accounts you might like:" +msgstr "Bunlar, beğenebileceğiniz popüler hesaplar:" + +#: src/view/com/util/moderation/ScreenHider.tsx:88 +msgid "This {screenDescription} has been flagged:" +msgstr "Bu {screenDescription} işaretlendi:" + +#: src/view/com/util/moderation/ScreenHider.tsx:83 +msgid "This account has requested that users sign in to view their profile." +msgstr "Bu hesap, kullanıcıların profilini görüntülemek için giriş yapmalarını istedi." + +#: src/view/com/modals/EmbedConsent.tsx:68 +msgid "This content is hosted by {0}. Do you want to enable external media?" +msgstr "Bu içerik {0} tarafından barındırılıyor. Harici medyayı etkinleştirmek ister misiniz?" + +#: src/view/com/modals/ModerationDetails.tsx:67 +msgid "This content is not available because one of the users involved has blocked the other." +msgstr "Bu içerik, içerikte yer alan kullanıcılardan biri diğerini engellediği için mevcut değil." + +#: src/view/com/posts/FeedErrorMessage.tsx:108 +msgid "This content is not viewable without a Bluesky account." +msgstr "Bu içerik, bir Bluesky hesabı olmadan görüntülenemez." + +#: src/view/com/posts/FeedErrorMessage.tsx:114 +msgid "This feed is currently receiving high traffic and is temporarily unavailable. Please try again later." +msgstr "Bu besleme şu anda yüksek trafik alıyor ve geçici olarak kullanılamıyor. Lütfen daha sonra tekrar deneyin." + +#: src/view/screens/Profile.tsx:402 src/view/screens/ProfileFeed.tsx:475 +#: src/view/screens/ProfileList.tsx:660 +msgid "This feed is empty!" +msgstr "Bu besleme boş!" + +#: src/view/com/posts/CustomFeedEmptyState.tsx:37 +msgid "This feed is empty! You may need to follow more users or tune your language settings." +msgstr "Bu besleme boş! Daha fazla kullanıcı takip etmeniz veya dil ayarlarınızı ayarlamanız gerekebilir." + +#: src/view/com/modals/BirthDateSettings.tsx:61 +msgid "This information is not shared with other users." +msgstr "Bu bilgi diğer kullanıcılarla paylaşılmaz." + +#: src/view/com/modals/VerifyEmail.tsx:119 +msgid "This is important in case you ever need to change your email or reset your password." +msgstr "Bu, e-postanızı değiştirmeniz veya şifrenizi sıfırlamanız gerektiğinde önemlidir." + +#: src/view/com/modals/LinkWarning.tsx:58 +msgid "This link is taking you to the following website:" +msgstr "Bu bağlantı sizi aşağıdaki web sitesine götürüyor:" + +#: src/view/screens/ProfileList.tsx:834 +msgid "This list is empty!" +msgstr "Bu liste boş!" + +#: src/view/com/modals/AddAppPasswords.tsx:106 +msgid "This name is already in use" +msgstr "Bu isim zaten kullanılıyor" + +#: src/view/com/post-thread/PostThreadItem.tsx:124 +msgid "This post has been deleted." +msgstr "Bu gönderi silindi." + +#: src/view/com/modals/ModerationDetails.tsx:62 +msgid "This user has blocked you. You cannot view their content." +msgstr "Bu kullanıcı sizi engelledi. İçeriklerini göremezsiniz." + +#: src/view/com/modals/ModerationDetails.tsx:42 +msgid "This user is included in the <0/> list which you have blocked." +msgstr "Bu kullanıcı, engellediğiniz <0/> listesinde bulunuyor." + +#: src/view/com/modals/ModerationDetails.tsx:74 +msgid "This user is included in the <0/> list which you have muted." +msgstr "Bu kullanıcı, sessize aldığınız <0/> listesinde bulunuyor." + +#: src/view/com/modals/SelfLabel.tsx:137 +msgid "This warning is only available for posts with media attached." +msgstr "Bu uyarı yalnızca medya ekli gönderiler için mevcuttur." + +#: src/view/com/util/forms/PostDropdownBtn.tsx:192 +msgid "This will hide this post from your feeds." +msgstr "Bu, bu gönderiyi beslemelerinizden gizleyecektir." + +#: src/view/screens/PreferencesThreads.tsx:53 src/view/screens/Settings.tsx:559 +msgid "Thread Preferences" +msgstr "Konu Tercihleri" + +#: src/view/screens/PreferencesThreads.tsx:119 +msgid "Threaded Mode" +msgstr "Konu Tabanlı Mod" + +#: src/Navigation.tsx:253 +msgid "Threads Preferences" +msgstr "Konu Tercihleri" + +#: src/view/com/util/forms/DropdownButton.tsx:246 +msgid "Toggle dropdown" +msgstr "Açılır menüyü aç/kapat" + +#: src/view/com/modals/EditImage.tsx:271 +msgid "Transformations" +msgstr "Dönüşümler" + +#: src/view/com/post-thread/PostThreadItem.tsx:719 +#: src/view/com/post-thread/PostThreadItem.tsx:721 +#: src/view/com/util/forms/PostDropdownBtn.tsx:125 +msgid "Translate" +msgstr "Çevir" + +#: src/view/com/util/error/ErrorScreen.tsx:75 +msgctxt "action" +msgid "Try again" +msgstr "Tekrar dene" + +#: src/view/screens/ProfileList.tsx:505 +msgid "Un-block list" +msgstr "Listeyi engeli kaldır" + +#: src/view/screens/ProfileList.tsx:490 +msgid "Un-mute list" +msgstr "Listeyi sessizden çıkar" + +#: src/view/com/auth/create/CreateAccount.tsx:66 +#: src/view/com/auth/login/ForgotPasswordForm.tsx:87 +#: src/view/com/auth/login/Login.tsx:76 +#: src/view/com/auth/login/LoginForm.tsx:120 +#: src/view/com/modals/ChangePassword.tsx:70 +msgid "Unable to contact your service. Please check your Internet connection." +msgstr "Hizmetinize ulaşılamıyor. Lütfen internet bağlantınızı kontrol edin." + +#: src/view/com/profile/ProfileHeader.tsx:472 +#: src/view/screens/ProfileList.tsx:589 +msgid "Unblock" +msgstr "Engeli kaldır" + +#: src/view/com/profile/ProfileHeader.tsx:475 +msgctxt "action" +msgid "Unblock" +msgstr "Engeli kaldır" + +#: src/view/com/profile/ProfileHeader.tsx:308 +#: src/view/com/profile/ProfileHeader.tsx:392 +msgid "Unblock Account" +msgstr "Hesabın engelini kaldır" + +#: src/view/com/modals/Repost.tsx:42 src/view/com/modals/Repost.tsx:55 +#: src/view/com/util/post-ctrls/RepostButton.tsx:60 +#: src/view/com/util/post-ctrls/RepostButton.web.tsx:48 +msgid "Undo repost" +msgstr "Yeniden göndermeyi geri al" + +#: src/view/com/profile/FollowButton.tsx:55 +msgctxt "action" +msgid "Unfollow" +msgstr "Takibi bırak" + +#: src/view/com/profile/ProfileHeader.tsx:524 +msgid "Unfollow {0}" +msgstr "{0} adresini takibi bırak" + +#: src/view/com/auth/create/state.ts:300 +msgid "Unfortunately, you do not meet the requirements to create an account." +msgstr "Üzgünüz, bir hesap oluşturmak için gerekleri karşılamıyorsunuz." + +#: src/view/com/util/post-ctrls/PostCtrls.tsx:170 +msgid "Unlike" +msgstr "Beğenmeyi geri al" + +#: src/view/screens/ProfileList.tsx:596 +msgid "Unmute" +msgstr "Sessizden çıkar" + +#: src/view/com/profile/ProfileHeader.tsx:373 +msgid "Unmute Account" +msgstr "Hesabın sessizliğini kaldır" + +#: src/view/com/util/forms/PostDropdownBtn.tsx:171 +msgid "Unmute thread" +msgstr "Konunun sessizliğini kaldır" + +#: src/view/screens/ProfileFeed.tsx:353 src/view/screens/ProfileList.tsx:580 +msgid "Unpin" +msgstr "Sabitlemeyi kaldır" + +#: src/view/screens/ProfileList.tsx:473 +msgid "Unpin moderation list" +msgstr "Moderasyon listesini sabitlemeyi kaldır" + +#: src/view/screens/ProfileFeed.tsx:345 +msgid "Unsave" +msgstr "Kaydedilenlerden kaldır" + +#: src/view/com/modals/UserAddRemoveLists.tsx:70 +msgid "Update {displayName} in Lists" +msgstr "Listelerde {displayName} güncelle" + +#: src/lib/hooks/useOTAUpdate.ts:15 +msgid "Update Available" +msgstr "Güncelleme Mevcut" + +#: src/view/com/auth/login/SetNewPasswordForm.tsx:204 +msgid "Updating..." +msgstr "Güncelleniyor..." + +#: src/view/com/modals/ChangeHandle.tsx:455 +msgid "Upload a text file to:" +msgstr "Bir metin dosyası yükleyin:" + +#: src/view/screens/AppPasswords.tsx:195 +msgid "Use app passwords to login to other Bluesky clients without giving full access to your account or password." +msgstr "Uygulama şifrelerini kullanarak hesabınızın veya şifrenizin tam erişimini vermeden diğer Bluesky istemcilerine giriş yapın." + +#: src/view/com/modals/ChangeHandle.tsx:515 +msgid "Use default provider" +msgstr "Varsayılan sağlayıcıyı kullan" + +#: src/view/com/modals/InAppBrowserConsent.tsx:56 +#: src/view/com/modals/InAppBrowserConsent.tsx:58 +msgid "Use in-app browser" +msgstr "Uygulama içi tarayıcıyı kullan" + +#: src/view/com/modals/InAppBrowserConsent.tsx:66 +#: src/view/com/modals/InAppBrowserConsent.tsx:68 +msgid "Use my default browser" +msgstr "Varsayılan tarayıcımı kullan" + +#: src/view/com/modals/AddAppPasswords.tsx:155 +msgid "Use this to sign into the other app along with your handle." +msgstr "Bunu, kullanıcı adınızla birlikte diğer uygulamaya giriş yapmak için kullanın." + +#: src/view/com/modals/ServerInput.tsx:105 +msgid "Use your domain as your Bluesky client service provider" +msgstr "Alan adınızı Bluesky istemci sağlayıcınız olarak kullanın" + +#: src/view/com/modals/InviteCodes.tsx:200 +msgid "Used by:" +msgstr "Kullanıcı:" + +#: src/view/com/modals/ModerationDetails.tsx:54 +msgid "User Blocked" +msgstr "Kullanıcı Engellendi" + +#: src/view/com/modals/ModerationDetails.tsx:40 +msgid "User Blocked by List" +msgstr "Liste Tarafından Engellenen Kullanıcı" + +#: src/view/com/modals/ModerationDetails.tsx:60 +msgid "User Blocks You" +msgstr "Kullanıcı Sizi Engelledi" + +#: src/view/com/auth/create/Step3.tsx:41 +msgid "User handle" +msgstr "Kullanıcı adı" + +#: src/view/com/lists/ListCard.tsx:84 +#: src/view/com/modals/UserAddRemoveLists.tsx:198 +msgid "User list by {0}" +msgstr "{0} tarafından oluşturulan kullanıcı listesi" + +#: src/view/screens/ProfileList.tsx:762 +msgid "User list by <0/>" +msgstr "<0/> tarafından oluşturulan kullanıcı listesi" + +#: src/view/com/lists/ListCard.tsx:82 +#: src/view/com/modals/UserAddRemoveLists.tsx:196 +#: src/view/screens/ProfileList.tsx:760 +msgid "User list by you" +msgstr "Sizin tarafınızdan oluşturulan kullanıcı listesi" + +#: src/view/com/modals/CreateOrEditList.tsx:196 +msgid "User list created" +msgstr "Kullanıcı listesi oluşturuldu" + +#: src/view/com/modals/CreateOrEditList.tsx:182 +msgid "User list updated" +msgstr "Kullanıcı listesi güncellendi" + +#: src/view/screens/Lists.tsx:58 +msgid "User Lists" +msgstr "Kullanıcı Listeleri" + +#: src/view/com/auth/login/LoginForm.tsx:174 +#: src/view/com/auth/login/LoginForm.tsx:192 +msgid "Username or email address" +msgstr "Kullanıcı adı veya e-posta adresi" + +#: src/view/screens/ProfileList.tsx:796 +msgid "Users" +msgstr "Kullanıcılar" + +#: src/view/com/threadgate/WhoCanReply.tsx:143 +msgid "users followed by <0/>" +msgstr "<0/> tarafından takip edilen kullanıcılar" + +#: src/view/com/modals/Threadgate.tsx:106 +msgid "Users in \"{0}\"" +msgstr "\"{0}\" içindeki kullanıcılar" + +#: src/view/com/auth/create/Step2.tsx:243 +msgid "Verification code" +msgstr "Doğrulama kodu" + +#: src/view/screens/Settings.tsx:892 +msgid "Verify email" +msgstr "E-postayı doğrula" + +#: src/view/screens/Settings.tsx:917 +msgid "Verify my email" +msgstr "E-postamı doğrula" + +#: src/view/screens/Settings.tsx:926 +msgid "Verify My Email" +msgstr "E-postamı Doğrula" + +#: src/view/com/modals/ChangeEmail.tsx:205 +#: src/view/com/modals/ChangeEmail.tsx:207 +msgid "Verify New Email" +msgstr "Yeni E-postayı Doğrula" + +#: src/view/com/modals/VerifyEmail.tsx:103 +msgid "Verify Your Email" +msgstr "E-postanızı Doğrulayın" + +#: src/screens/Onboarding/index.tsx:42 +msgid "Video Games" +msgstr "Video Oyunları" + +#: src/view/com/profile/ProfileHeader.tsx:701 +msgid "View {0}'s avatar" +msgstr "{0}'ın avatarını görüntüle" + +#: src/view/screens/Log.tsx:52 +msgid "View debug entry" +msgstr "Hata ayıklama girişini görüntüle" + +#: src/view/com/posts/FeedSlice.tsx:103 +msgid "View full thread" +msgstr "Tam konuyu görüntüle" + +#: src/view/com/posts/FeedErrorMessage.tsx:172 +msgid "View profile" +msgstr "Profili görüntüle" + +#: src/view/com/profile/ProfileSubpageHeader.tsx:128 +msgid "View the avatar" +msgstr "Avatarı görüntüle" + +#: src/view/com/modals/LinkWarning.tsx:75 +msgid "Visit Site" +msgstr "Siteyi Ziyaret Et" + +#: src/screens/Onboarding/StepModeration/ModerationOption.tsx:42 +#: src/view/com/modals/ContentFilteringSettings.tsx:254 +msgid "Warn" +msgstr "Uyar" + +#: src/screens/Onboarding/StepAlgoFeeds/index.tsx:124 +msgid "We also think you'll like \"For You\" by Skygaze:" +msgstr "Ayrıca Skygaze tarafından \"Sana Özel\" beslemesini de beğeneceğinizi düşünüyoruz:" + +#: src/screens/Deactivated.tsx:134 +msgid "We estimate {estimatedTime} until your account is ready." +msgstr "Hesabınızın hazır olmasına {estimatedTime} tahmin ediyoruz." + +#: src/screens/Onboarding/StepFinished.tsx:93 +msgid "We hope you have a wonderful time. Remember, Bluesky is:" +msgstr "Harika vakit geçirmenizi umuyoruz. Unutmayın, Bluesky:" + +#: src/view/com/posts/DiscoverFallbackHeader.tsx:29 +msgid "We ran out of posts from your follows. Here's the latest from <0/>." +msgstr "Takipçilerinizden gönderi kalmadı. İşte <0/>'den en son gönderiler." + +#: src/screens/Onboarding/StepAlgoFeeds/index.tsx:119 +msgid "We recommend our \"Discover\" feed:" +msgstr "\"Keşfet\" beslememizi öneririz:" + +#: src/screens/Onboarding/StepInterests/index.tsx:133 +msgid "We weren't able to connect. Please try again to continue setting up your account. If it continues to fail, you can skip this flow." +msgstr "Bağlantı kuramadık. Hesabınızı kurmaya devam etmek için tekrar deneyin. Başarısız olmaya devam ederse bu akışı atlayabilirsiniz." + +#: src/screens/Deactivated.tsx:138 +msgid "We will let you know when your account is ready." +msgstr "Hesabınız hazır olduğunda size bildireceğiz." + +#: src/view/com/modals/AppealLabel.tsx:48 +msgid "We'll look into your appeal promptly." +msgstr "İtirazınıza hızlı bir şekilde bakacağız." + +#: src/screens/Onboarding/StepInterests/index.tsx:138 +msgid "We'll use this to help customize your experience." +msgstr "Bu, deneyiminizi özelleştirmenize yardımcı olmak için kullanılacak." + +#: src/view/com/auth/create/CreateAccount.tsx:123 +msgid "We're so excited to have you join us!" +msgstr "Sizi aramızda görmekten çok mutluyuz!" + +#: src/view/screens/ProfileList.tsx:85 +msgid "We're sorry, but we were unable to resolve this list. If this persists, please contact the list creator, @{handleOrDid}." +msgstr "Üzgünüz, ancak bu listeyi çözemedik. Bu durum devam ederse, lütfen liste oluşturucu, @{handleOrDid} ile iletişime geçin." + +#: src/view/screens/Search/Search.tsx:253 +msgid "We're sorry, but your search could not be completed. Please try again in a few minutes." +msgstr "Üzgünüz, ancak aramanız tamamlanamadı. Lütfen birkaç dakika içinde tekrar deneyin." + +#: src/view/screens/NotFound.tsx:48 +msgid "We're sorry! We can't find the page you were looking for." +msgstr "Üzgünüz! Aradığınız sayfayı bulamıyoruz." + +#: src/view/com/auth/onboarding/WelcomeMobile.tsx:46 +msgid "Welcome to <0>Bluesky" +msgstr "<0>Bluesky'e hoş geldiniz" + +#: src/screens/Onboarding/StepInterests/index.tsx:130 +msgid "What are your interests?" +msgstr "İlgi alanlarınız nelerdir?" + +#: src/view/com/modals/report/Modal.tsx:169 +msgid "What is the issue with this {collectionName}?" +msgstr "Bu {collectionName} ile ilgili sorun nedir?" + +#: src/view/com/auth/SplashScreen.tsx:34 src/view/com/composer/Composer.tsx:279 +msgid "What's up?" +msgstr "Nasılsınız?" + +#: src/view/com/modals/lang-settings/PostLanguagesSettings.tsx:78 +msgid "Which languages are used in this post?" +msgstr "Bu gönderide hangi diller kullanılıyor?" + +#: src/view/com/modals/lang-settings/ContentLanguagesSettings.tsx:77 +msgid "Which languages would you like to see in your algorithmic feeds?" +msgstr "Algoritmik beslemelerinizde hangi dilleri görmek istersiniz?" + +#: src/view/com/composer/threadgate/ThreadgateBtn.tsx:47 +#: src/view/com/modals/Threadgate.tsx:66 +msgid "Who can reply" +msgstr "Kimler yanıtlayabilir" + +#: src/view/com/modals/crop-image/CropImage.web.tsx:102 +msgid "Wide" +msgstr "Geniş" + +#: src/view/com/composer/Composer.tsx:415 +msgid "Write post" +msgstr "Gönderi yaz" + +#: src/view/com/composer/Composer.tsx:278 src/view/com/composer/Prompt.tsx:33 +msgid "Write your reply" +msgstr "Yanıtınızı yazın" + +#: src/screens/Onboarding/index.tsx:28 +msgid "Writers" +msgstr "Yazarlar" + +#: src/view/com/auth/create/Step2.tsx:263 +msgid "XXXXXX" +msgstr "XXXXXX" + +#: src/view/com/composer/select-language/SuggestedLanguage.tsx:77 +#: src/view/screens/PreferencesHomeFeed.tsx:129 +#: src/view/screens/PreferencesHomeFeed.tsx:201 +#: src/view/screens/PreferencesHomeFeed.tsx:236 +#: src/view/screens/PreferencesHomeFeed.tsx:271 +#: src/view/screens/PreferencesThreads.tsx:106 +#: src/view/screens/PreferencesThreads.tsx:129 +msgid "Yes" +msgstr "Evet" + +#: src/screens/Deactivated.tsx:131 +msgid "You are in line." +msgstr "Sıradasınız." + +#: src/view/com/posts/FollowingEmptyState.tsx:67 +#: src/view/com/posts/FollowingEndOfFeed.tsx:68 +msgid "You can also discover new Custom Feeds to follow." +msgstr "Ayrıca takip edebileceğiniz yeni Özel Beslemeler keşfedebilirsiniz." + +#: src/screens/Onboarding/StepFollowingFeed.tsx:142 +msgid "You can change these settings later." +msgstr "Bu ayarları daha sonra değiştirebilirsiniz." + +#: src/view/com/auth/login/Login.tsx:158 +#: src/view/com/auth/login/PasswordUpdatedForm.tsx:31 +msgid "You can now sign in with your new password." +msgstr "Artık yeni şifrenizle giriş yapabilirsiniz." + +#: src/view/com/modals/InviteCodes.tsx:66 +msgid "You don't have any invite codes yet! We'll send you some when you've been on Bluesky for a little longer." +msgstr "Henüz hiç davet kodunuz yok! Bluesky'de biraz daha uzun süre kaldıktan sonra size bazı kodlar göndereceğiz." + +#: src/view/screens/SavedFeeds.tsx:102 +msgid "You don't have any pinned feeds." +msgstr "Sabitlemiş beslemeniz yok." + +#: src/view/screens/Feeds.tsx:419 +msgid "You don't have any saved feeds!" +msgstr "Kaydedilmiş beslemeniz yok!" + +#: src/view/screens/SavedFeeds.tsx:135 +msgid "You don't have any saved feeds." +msgstr "Kaydedilmiş beslemeniz yok." + +#: src/view/com/post-thread/PostThread.tsx:406 +msgid "You have blocked the author or you have been blocked by the author." +msgstr "Yazarı engellediniz veya yazar tarafından engellendiniz." + +#: src/view/com/modals/ModerationDetails.tsx:56 +msgid "You have blocked this user. You cannot view their content." +msgstr "Bu kullanıcıyı engellediniz. İçeriklerini göremezsiniz." + +#: src/view/com/auth/login/SetNewPasswordForm.tsx:57 +#: src/view/com/auth/login/SetNewPasswordForm.tsx:92 +#: src/view/com/modals/ChangePassword.tsx:87 +#: src/view/com/modals/ChangePassword.tsx:121 +msgid "You have entered an invalid code. It should look like XXXXX-XXXXX." +msgstr "Geçersiz bir kod girdiniz. XXXXX-XXXXX gibi görünmelidir." + +#: src/view/com/modals/ModerationDetails.tsx:87 +msgid "You have muted this user." +msgstr "Bu kullanıcıyı sessize aldınız." + +#: src/view/com/feeds/ProfileFeedgens.tsx:136 +msgid "You have no feeds." +msgstr "Beslemeniz yok." + +#: src/view/com/lists/MyLists.tsx:89 src/view/com/lists/ProfileLists.tsx:140 +msgid "You have no lists." +msgstr "Listeniz yok." + +#: src/view/screens/ModerationBlockedAccounts.tsx:132 +msgid "You have not blocked any accounts yet. To block an account, go to their profile and selected \"Block account\" from the menu on their account." +msgstr "Henüz hiçbir hesabı engellemediniz. Bir hesabı engellemek için, profilinize gidin ve hesaplarının menüsünden \"Hesabı engelle\" seçeneğini seçin." + +#: src/view/screens/AppPasswords.tsx:87 +msgid "You have not created any app passwords yet. You can create one by pressing the button below." +msgstr "Henüz hiçbir uygulama şifresi oluşturmadınız. Aşağıdaki düğmeye basarak bir tane oluşturabilirsiniz." + +#: src/view/screens/ModerationMutedAccounts.tsx:131 +msgid "You have not muted any accounts yet. To mute an account, go to their profile and selected \"Mute account\" from the menu on their account." +msgstr "Henüz hiçbir hesabı sessize almadınız. Bir hesabı sessize almak için, profilinize gidin ve hesaplarının menüsünden \"Hesabı sessize al\" seçeneğini seçin." + +#: src/view/com/modals/ContentFilteringSettings.tsx:170 +msgid "You must be 18 or older to enable adult content." +msgstr "Yetişkin içeriği etkinleştirmek için 18 yaşında veya daha büyük olmalısınız." + +#: src/screens/Onboarding/StepModeration/AdultContentEnabledPref.tsx:103 +msgid "You must be 18 years or older to enable adult content" +msgstr "Yetişkin içeriğini etkinleştirmek için 18 yaşında veya daha büyük olmalısınız" + +#: src/view/com/util/forms/PostDropdownBtn.tsx:98 +msgid "You will no longer receive notifications for this thread" +msgstr "Artık bu konu için bildirim almayacaksınız" + +#: src/view/com/util/forms/PostDropdownBtn.tsx:101 +msgid "You will now receive notifications for this thread" +msgstr "Artık bu konu için bildirim alacaksınız" + +#: src/view/com/auth/login/SetNewPasswordForm.tsx:107 +msgid "You will receive an email with a \"reset code.\" Enter that code here, then enter your new password." +msgstr "Bir \"sıfırlama kodu\" içeren bir e-posta alacaksınız. Bu kodu buraya girin, ardından yeni şifrenizi girin." + +#: src/screens/Onboarding/StepModeration/index.tsx:72 +msgid "You're in control" +msgstr "Siz kontrol ediyorsunuz" + +#: src/screens/Deactivated.tsx:88 src/screens/Deactivated.tsx:89 +#: src/screens/Deactivated.tsx:104 +msgid "You're in line" +msgstr "Sıradasınız" + +#: src/screens/Onboarding/StepFinished.tsx:90 +msgid "You're ready to go!" +msgstr "Hazırsınız!" + +#: src/view/com/posts/FollowingEndOfFeed.tsx:48 +msgid "You've reached the end of your feed! Find some more accounts to follow." +msgstr "Beslemenizin sonuna ulaştınız! Takip edebileceğiniz daha fazla hesap bulun." + +#: src/view/com/auth/create/Step1.tsx:67 +msgid "Your account" +msgstr "Hesabınız" + +#: src/view/com/modals/DeleteAccount.tsx:67 +msgid "Your account has been deleted" +msgstr "Hesabınız silindi" + +#: src/view/com/auth/create/Step1.tsx:182 +msgid "Your birth date" +msgstr "Doğum tarihiniz" + +#: src/view/com/modals/InAppBrowserConsent.tsx:47 +msgid "Your choice will be saved, but can be changed later in settings." +msgstr "Seçiminiz kaydedilecek, ancak daha sonra ayarlarda değiştirilebilir." + +#: src/screens/Onboarding/StepFollowingFeed.tsx:61 +msgid "Your default feed is \"Following\"" +msgstr "Varsayılan beslemeniz \"Takip Edilenler\"" + +#: src/view/com/auth/create/state.ts:153 +#: src/view/com/auth/login/ForgotPasswordForm.tsx:70 +#: src/view/com/modals/ChangePassword.tsx:54 +msgid "Your email appears to be invalid." +msgstr "E-postanız geçersiz gibi görünüyor." + +#: src/view/com/modals/Waitlist.tsx:109 +msgid "Your email has been saved! We'll be in touch soon." +msgstr "E-postanız kaydedildi! Yakında sizinle iletişime geçeceğiz." + +#: src/view/com/modals/ChangeEmail.tsx:125 +msgid "Your email has been updated but not verified. As a next step, please verify your new email." +msgstr "E-postanız güncellendi ancak doğrulanmadı. Bir sonraki adım olarak, lütfen yeni e-postanızı doğrulayın." + +#: src/view/com/modals/VerifyEmail.tsx:114 +msgid "Your email has not yet been verified. This is an important security step which we recommend." +msgstr "E-postanız henüz doğrulanmadı. Bu, önerdiğimiz önemli bir güvenlik adımıdır." + +#: src/view/com/posts/FollowingEmptyState.tsx:47 +msgid "Your following feed is empty! Follow more users to see what's happening." +msgstr "Takip ettiğiniz besleme boş! Neler olduğunu görmek için daha fazla kullanıcı takip edin." + +#: src/view/com/auth/create/Step3.tsx:45 +msgid "Your full handle will be" +msgstr "Tam kullanıcı adınız" + +#: src/view/com/modals/ChangeHandle.tsx:270 +msgid "Your full handle will be <0>@{0}" +msgstr "Tam kullanıcı adınız <0>@{0} olacak" + +#: src/view/screens/Settings.tsx:430 src/view/shell/desktop/RightNav.tsx:137 +#: src/view/shell/Drawer.tsx:660 +msgid "Your invite codes are hidden when logged in using an App Password" +msgstr "Uygulama Şifresi kullanarak giriş yaptığınızda davet kodlarınız gizlenir" + +#: src/view/com/modals/ChangePassword.tsx:155 +msgid "Your password has been changed successfully!" +msgstr "Şifreniz başarıyla değiştirildi!" + +#: src/view/com/composer/Composer.tsx:267 +msgid "Your post has been published" +msgstr "Gönderiniz yayınlandı" + +#: src/screens/Onboarding/StepFinished.tsx:105 +#: src/view/com/auth/onboarding/WelcomeDesktop.tsx:59 +#: src/view/com/auth/onboarding/WelcomeMobile.tsx:59 +msgid "Your posts, likes, and blocks are public. Mutes are private." +msgstr "Gönderileriniz, beğenileriniz ve engellemeleriniz herkese açıktır. Sessizlikleriniz özeldir." + +#: src/view/com/modals/SwitchAccount.tsx:84 src/view/screens/Settings.tsx:125 +msgid "Your profile" +msgstr "Profiliniz" + +#: src/view/com/composer/Composer.tsx:266 +msgid "Your reply has been published" +msgstr "Yanıtınız yayınlandı" + +#: src/view/com/auth/create/Step3.tsx:28 +msgid "Your user handle" +msgstr "Kullanıcı adınız" diff --git a/src/locale/locales/zh-TW/messages.po b/src/locale/locales/zh-TW/messages.po new file mode 100644 index 0000000000..f337ca203c --- /dev/null +++ b/src/locale/locales/zh-TW/messages.po @@ -0,0 +1,5965 @@ +msgid "" +msgstr "" +"POT-Creation-Date: 2024-03-20 15:50+0800\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: zh_TW\n" +"Project-Id-Version: \n" +"Report-Msgid-Bugs-To: \n" +"PO-Revision-Date: \n" +"Last-Translator: Frudrax Cheng \n" +"Language-Team: Frudrax Cheng, Kuwa Lee, noeFly, snowleo208, Kisaragi Hiu, Yi-Jyun Pan, toto6038, cirx1e\n" +"Plural-Forms: \n" + +#: src/view/com/modals/VerifyEmail.tsx:142 +msgid "(no email)" +msgstr "(沒有郵件)" + +#: src/view/shell/desktop/RightNav.tsx:168 +#~ msgid "{0, plural, one {# invite code available} other {# invite codes available}}" +#~ msgstr "{0} 個可用的邀請碼" + +#: src/screens/Profile/Header/Metrics.tsx:45 +msgid "{following} following" +msgstr "{following} 個跟隨中" + +#: src/view/shell/desktop/RightNav.tsx:151 +#~ msgid "{invitesAvailable, plural, one {Invite codes: # available} other {Invite codes: # available}}" +#~ msgstr "可用的邀請碼:{invitesAvailable} 個" + +#: src/view/screens/Settings.tsx:435 +#: src/view/shell/Drawer.tsx:664 +#~ msgid "{invitesAvailable} invite code available" +#~ msgstr "{invitesAvailable} 個可用的邀請碼" + +#: src/view/screens/Settings.tsx:437 +#: src/view/shell/Drawer.tsx:666 +#~ msgid "{invitesAvailable} invite codes available" +#~ msgstr "{invitesAvailable} 個可用的邀請碼" + +#: src/view/shell/Drawer.tsx:443 +msgid "{numUnreadNotifications} unread" +msgstr "{numUnreadNotifications} 個未讀" + +#: src/view/com/threadgate/WhoCanReply.tsx:158 +msgid "<0/> members" +msgstr "<0/> 個成員" + +#: src/view/shell/Drawer.tsx:97 +msgid "<0>{0} following" +msgstr "" + +#: src/screens/Profile/Header/Metrics.tsx:46 +msgid "<0>{following} <1>following" +msgstr "<0>{following} <1>個跟隨中" + +#: src/view/com/auth/onboarding/RecommendedFeeds.tsx:30 +msgid "<0>Choose your<1>Recommended<2>Feeds" +msgstr "<0>選擇你的<1>推薦<2>訊息流" + +#: src/view/com/auth/onboarding/RecommendedFollows.tsx:37 +msgid "<0>Follow some<1>Recommended<2>Users" +msgstr "<0>跟隨一些<1>推薦的<2>使用者" + +#: src/view/com/auth/onboarding/WelcomeDesktop.tsx:21 +msgid "<0>Welcome to<1>Bluesky" +msgstr "<0>歡迎來到<1>Bluesky" + +#: src/screens/Profile/Header/Handle.tsx:42 +msgid "⚠Invalid Handle" +msgstr "⚠無效的帳號代碼" + +#: src/view/com/util/moderation/LabelInfo.tsx:45 +#~ msgid "A content warning has been applied to this {0}." +#~ msgstr "內容警告已套用到這個{0}。" + +#: src/lib/hooks/useOTAUpdate.ts:16 +#~ msgid "A new version of the app is available. Please update to continue using the app." +#~ msgstr "新版本應用程式已發佈,請更新以繼續使用。" + +#: src/view/com/util/ViewHeader.tsx:89 +#: src/view/screens/Search/Search.tsx:648 +msgid "Access navigation links and settings" +msgstr "存取導覽連結和設定" + +#: src/view/com/home/HomeHeaderLayoutMobile.tsx:52 +msgid "Access profile and other navigation links" +msgstr "存取個人資料和其他導覽連結" + +#: src/view/com/modals/EditImage.tsx:299 +#: src/view/screens/Settings/index.tsx:470 +msgid "Accessibility" +msgstr "協助工具" + +#: src/components/moderation/LabelsOnMe.tsx:42 +msgid "account" +msgstr "帳號" + +#: src/view/com/auth/login/LoginForm.tsx:169 +#: src/view/screens/Settings/index.tsx:327 +#: src/view/screens/Settings/index.tsx:743 +msgid "Account" +msgstr "帳號" + +#: src/view/com/profile/ProfileMenu.tsx:139 +msgid "Account blocked" +msgstr "已封鎖帳號" + +#: src/view/com/profile/ProfileMenu.tsx:153 +msgid "Account followed" +msgstr "已跟隨帳號" + +#: src/view/com/profile/ProfileMenu.tsx:113 +msgid "Account muted" +msgstr "已靜音帳號" + +#: src/components/moderation/ModerationDetailsDialog.tsx:94 +#: src/lib/moderation/useModerationCauseDescription.ts:91 +msgid "Account Muted" +msgstr "已靜音帳號" + +#: src/components/moderation/ModerationDetailsDialog.tsx:83 +msgid "Account Muted by List" +msgstr "帳號已被列表靜音" + +#: src/view/com/util/AccountDropdownBtn.tsx:41 +msgid "Account options" +msgstr "帳號選項" + +#: src/view/com/util/AccountDropdownBtn.tsx:25 +msgid "Account removed from quick access" +msgstr "已從快速存取中移除帳號" + +#: src/screens/Profile/Header/ProfileHeaderStandard.tsx:130 +#: src/view/com/profile/ProfileMenu.tsx:128 +msgid "Account unblocked" +msgstr "已取消封鎖帳號" + +#: src/view/com/profile/ProfileMenu.tsx:166 +msgid "Account unfollowed" +msgstr "已取消跟隨帳號" + +#: src/view/com/profile/ProfileMenu.tsx:102 +msgid "Account unmuted" +msgstr "已取消靜音帳號" + +#: src/components/dialogs/MutedWords.tsx:165 +#: src/view/com/auth/onboarding/RecommendedFeedsItem.tsx:150 +#: src/view/com/modals/ListAddRemoveUsers.tsx:268 +#: src/view/com/modals/UserAddRemoveLists.tsx:219 +#: src/view/screens/ProfileList.tsx:827 +msgid "Add" +msgstr "新增" + +#: src/view/com/modals/SelfLabel.tsx:56 +msgid "Add a content warning" +msgstr "新增內容警告" + +#: src/view/screens/ProfileList.tsx:817 +msgid "Add a user to this list" +msgstr "將使用者新增至此列表" + +#: src/view/screens/Settings/index.tsx:402 +#: src/view/screens/Settings/index.tsx:411 +msgid "Add account" +msgstr "新增帳號" + +#: src/view/com/composer/photos/Gallery.tsx:119 +#: src/view/com/composer/photos/Gallery.tsx:180 +#: src/view/com/modals/AltImage.tsx:116 +msgid "Add alt text" +msgstr "新增替代文字" + +#: src/view/screens/AppPasswords.tsx:104 +#: src/view/screens/AppPasswords.tsx:145 +#: src/view/screens/AppPasswords.tsx:158 +msgid "Add App Password" +msgstr "新增應用程式專用密碼" + +#: src/view/com/modals/report/InputIssueDetails.tsx:41 +#: src/view/com/modals/report/Modal.tsx:191 +#~ msgid "Add details" +#~ msgstr "新增細節" + +#: src/view/com/modals/report/Modal.tsx:194 +#~ msgid "Add details to report" +#~ msgstr "補充回報詳細內容" + +#: src/view/com/composer/Composer.tsx:466 +msgid "Add link card" +msgstr "新增連結卡片" + +#: src/view/com/composer/Composer.tsx:471 +msgid "Add link card:" +msgstr "新增連結卡片:" + +#: src/components/dialogs/MutedWords.tsx:158 +msgid "Add mute word for configured settings" +msgstr "" + +#: src/components/dialogs/MutedWords.tsx:87 +msgid "Add muted words and tags" +msgstr "" + +#: src/view/com/modals/ChangeHandle.tsx:417 +msgid "Add the following DNS record to your domain:" +msgstr "將以下 DNS 記錄新增到你的網域:" + +#: src/view/com/profile/ProfileMenu.tsx:263 +#: src/view/com/profile/ProfileMenu.tsx:266 +msgid "Add to Lists" +msgstr "新增至列表" + +#: src/view/com/feeds/FeedSourceCard.tsx:234 +msgid "Add to my feeds" +msgstr "新增至自訂訊息流" + +#: src/view/com/auth/onboarding/RecommendedFeedsItem.tsx:139 +msgid "Added" +msgstr "已新增" + +#: src/view/com/modals/ListAddRemoveUsers.tsx:191 +#: src/view/com/modals/UserAddRemoveLists.tsx:144 +msgid "Added to list" +msgstr "新增至列表" + +#: src/view/com/feeds/FeedSourceCard.tsx:108 +msgid "Added to my feeds" +msgstr "新增至自訂訊息流" + +#: src/view/screens/PreferencesFollowingFeed.tsx:173 +msgid "Adjust the number of likes a reply must have to be shown in your feed." +msgstr "調整回覆要在你的訊息流顯示所需的最低喜歡數。" + +#: src/screens/Onboarding/StepModeration/AdultContentEnabledPref.tsx:117 +#: src/view/com/modals/SelfLabel.tsx:75 +msgid "Adult Content" +msgstr "成人內容" + +#: src/view/com/modals/ContentFilteringSettings.tsx:141 +#~ msgid "Adult content can only be enabled via the Web at <0/>." +#~ msgstr "成人內容只能在網頁上<0/>啟用。" + +#: src/components/moderation/ModerationLabelPref.tsx:114 +msgid "Adult content is disabled." +msgstr "" + +#: src/screens/Moderation/index.tsx:377 +#: src/view/screens/Settings/index.tsx:684 +msgid "Advanced" +msgstr "詳細設定" + +#: src/view/screens/Feeds.tsx:666 +msgid "All the feeds you've saved, right in one place." +msgstr "你已儲存的所有訊息流都集中在一處。" + +#: src/view/com/auth/login/ForgotPasswordForm.tsx:221 +#: src/view/com/modals/ChangePassword.tsx:170 +msgid "Already have a code?" +msgstr "已經有重設碼了?" + +#: src/view/com/auth/login/ChooseAccountForm.tsx:103 +msgid "Already signed in as @{0}" +msgstr "已以@{0}身份登入" + +#: src/view/com/composer/photos/Gallery.tsx:130 +msgid "ALT" +msgstr "ALT" + +#: src/view/com/modals/EditImage.tsx:315 +msgid "Alt text" +msgstr "替代文字" + +#: src/view/com/composer/photos/Gallery.tsx:209 +msgid "Alt text describes images for blind and low-vision users, and helps give context to everyone." +msgstr "替代文字為盲人和視覺受損的使用者描述圖片,並幫助所有人提供上下文。" + +#: src/view/com/modals/VerifyEmail.tsx:124 +msgid "An email has been sent to {0}. It includes a confirmation code which you can enter below." +msgstr "一封電子郵件已發送至 {0}。請查閱郵件並在下方輸入驗證碼。" + +#: src/view/com/modals/ChangeEmail.tsx:119 +msgid "An email has been sent to your previous address, {0}. It includes a confirmation code which you can enter below." +msgstr "一封電子郵件已發送至先前填寫的電子郵件地址 {0}。請查閱郵件並在下方輸入驗證碼。" + +#: src/lib/moderation/useReportOptions.ts:26 +msgid "An issue not included in these options" +msgstr "" + +#: src/view/com/profile/FollowButton.tsx:35 +#: src/view/com/profile/FollowButton.tsx:45 +#: src/view/com/profile/ProfileHeaderSuggestedFollows.tsx:188 +#: src/view/com/profile/ProfileHeaderSuggestedFollows.tsx:198 +msgid "An issue occurred, please try again." +msgstr "出現問題,請重試。" + +#: src/view/com/notifications/FeedItem.tsx:240 +#: src/view/com/threadgate/WhoCanReply.tsx:178 +msgid "and" +msgstr "和" + +#: src/screens/Onboarding/index.tsx:32 +msgid "Animals" +msgstr "動物" + +#: src/lib/moderation/useReportOptions.ts:31 +msgid "Anti-Social Behavior" +msgstr "" + +#: src/view/screens/LanguageSettings.tsx:95 +msgid "App Language" +msgstr "應用程式語言" + +#: src/view/screens/AppPasswords.tsx:223 +msgid "App password deleted" +msgstr "應用程式專用密碼已刪除" + +#: src/view/com/modals/AddAppPasswords.tsx:134 +msgid "App Password names can only contain letters, numbers, spaces, dashes, and underscores." +msgstr "應用程式專用密碼只能包含字母、數字、空格、破折號及底線。" + +#: src/view/com/modals/AddAppPasswords.tsx:99 +msgid "App Password names must be at least 4 characters long." +msgstr "應用程式專用密碼名稱必須至少為 4 個字元。" + +#: src/view/screens/Settings/index.tsx:695 +msgid "App password settings" +msgstr "應用程式專用密碼設定" + +#: src/view/screens/Settings.tsx:650 +#~ msgid "App passwords" +#~ msgstr "應用程式專用密碼" + +#: src/Navigation.tsx:251 +#: src/view/screens/AppPasswords.tsx:189 +#: src/view/screens/Settings/index.tsx:704 +msgid "App Passwords" +msgstr "應用程式專用密碼" + +#: src/components/moderation/LabelsOnMeDialog.tsx:134 +#: src/components/moderation/LabelsOnMeDialog.tsx:137 +msgid "Appeal" +msgstr "" + +#: src/components/moderation/LabelsOnMeDialog.tsx:202 +msgid "Appeal \"{0}\" label" +msgstr "" + +#: src/view/com/util/forms/PostDropdownBtn.tsx:337 +#: src/view/com/util/forms/PostDropdownBtn.tsx:346 +#~ msgid "Appeal content warning" +#~ msgstr "申訴內容警告" + +#: src/view/com/modals/AppealLabel.tsx:65 +#~ msgid "Appeal Content Warning" +#~ msgstr "申訴內容警告" + +#: src/components/moderation/LabelsOnMeDialog.tsx:193 +msgid "Appeal submitted." +msgstr "" + +#: src/view/com/util/moderation/LabelInfo.tsx:52 +#~ msgid "Appeal this decision" +#~ msgstr "對此決定提出申訴" + +#: src/view/com/util/moderation/LabelInfo.tsx:56 +#~ msgid "Appeal this decision." +#~ msgstr "對此決定提出申訴。" + +#: src/view/screens/Settings/index.tsx:485 +msgid "Appearance" +msgstr "外觀" + +#: src/view/screens/AppPasswords.tsx:265 +msgid "Are you sure you want to delete the app password \"{name}\"?" +msgstr "你確定要刪除這個應用程式專用密碼「{name}」嗎?" + +#: src/view/com/feeds/FeedSourceCard.tsx:280 +msgid "Are you sure you want to remove {0} from your feeds?" +msgstr "" + +#: src/view/com/composer/Composer.tsx:508 +msgid "Are you sure you'd like to discard this draft?" +msgstr "你確定要捨棄此草稿嗎?" + +#: src/components/dialogs/MutedWords.tsx:282 +msgid "Are you sure?" +msgstr "你確定嗎?" + +#: src/view/com/util/forms/PostDropdownBtn.tsx:322 +#~ msgid "Are you sure? This cannot be undone." +#~ msgstr "你確定嗎?此操作無法撤銷。" + +#: src/view/com/composer/select-language/SuggestedLanguage.tsx:60 +msgid "Are you writing in <0>{0}?" +msgstr "你正在使用 <0>{0} 書寫嗎?" + +#: src/screens/Onboarding/index.tsx:26 +msgid "Art" +msgstr "藝術" + +#: src/view/com/modals/SelfLabel.tsx:123 +msgid "Artistic or non-erotic nudity." +msgstr "藝術作品或非情色的裸露。" + +#: src/components/moderation/LabelsOnMeDialog.tsx:247 +#: src/components/moderation/LabelsOnMeDialog.tsx:248 +#: src/screens/Profile/Header/Shell.tsx:97 +#: src/view/com/auth/create/CreateAccount.tsx:158 +#: src/view/com/auth/login/ChooseAccountForm.tsx:160 +#: src/view/com/auth/login/ForgotPasswordForm.tsx:174 +#: src/view/com/auth/login/LoginForm.tsx:262 +#: src/view/com/auth/login/SetNewPasswordForm.tsx:179 +#: src/view/com/util/ViewHeader.tsx:87 +msgid "Back" +msgstr "返回" + +#: src/view/com/post-thread/PostThread.tsx:480 +#~ msgctxt "action" +#~ msgid "Back" +#~ msgstr "返回" + +#: src/screens/Onboarding/StepSuggestedAccounts/index.tsx:136 +msgid "Based on your interest in {interestsText}" +msgstr "因為你對 {interestsText} 感興趣" + +#: src/view/screens/Settings/index.tsx:542 +msgid "Basics" +msgstr "基礎資訊" + +#: src/components/dialogs/BirthDateSettings.tsx:107 +#: src/view/com/auth/create/Step1.tsx:227 +msgid "Birthday" +msgstr "生日" + +#: src/view/screens/Settings/index.tsx:359 +msgid "Birthday:" +msgstr "生日:" + +#: src/screens/Profile/Header/ProfileHeaderStandard.tsx:278 +#: src/view/com/profile/ProfileMenu.tsx:361 +msgid "Block" +msgstr "封鎖" + +#: src/view/com/profile/ProfileMenu.tsx:300 +#: src/view/com/profile/ProfileMenu.tsx:307 +msgid "Block Account" +msgstr "封鎖帳號" + +#: src/view/com/profile/ProfileMenu.tsx:344 +msgid "Block Account?" +msgstr "封鎖帳號?" + +#: src/view/screens/ProfileList.tsx:530 +msgid "Block accounts" +msgstr "封鎖帳號" + +#: src/view/screens/ProfileList.tsx:478 +#: src/view/screens/ProfileList.tsx:634 +msgid "Block list" +msgstr "封鎖列表" + +#: src/view/screens/ProfileList.tsx:629 +msgid "Block these accounts?" +msgstr "封鎖這些帳號?" + +#: src/view/screens/ProfileList.tsx:320 +#~ msgid "Block this List" +#~ msgstr "封鎖此列表" + +#: src/view/com/lists/ListCard.tsx:110 +#: src/view/com/util/post-embeds/QuoteEmbed.tsx:55 +msgid "Blocked" +msgstr "已封鎖" + +#: src/screens/Moderation/index.tsx:269 +msgid "Blocked accounts" +msgstr "已封鎖帳號" + +#: src/Navigation.tsx:134 +#: src/view/screens/ModerationBlockedAccounts.tsx:107 +msgid "Blocked Accounts" +msgstr "已封鎖帳號" + +#: src/view/com/profile/ProfileMenu.tsx:356 +msgid "Blocked accounts cannot reply in your threads, mention you, or otherwise interact with you." +msgstr "被封鎖的帳號無法在你的貼文中回覆、提及你,或以其他方式與你互動。" + +#: src/view/screens/ModerationBlockedAccounts.tsx:115 +msgid "Blocked accounts cannot reply in your threads, mention you, or otherwise interact with you. You will not see their content and they will be prevented from seeing yours." +msgstr "被封鎖的帳號無法在你的貼文中回覆、提及你,或以其他方式與你互動。你將不會看到他們所發佈的內容,同樣他們也無法查看你的內容。" + +#: src/view/com/post-thread/PostThread.tsx:313 +msgid "Blocked post." +msgstr "已封鎖貼文。" + +#: src/screens/Profile/Sections/Labels.tsx:153 +msgid "Blocking does not prevent this labeler from placing labels on your account." +msgstr "" + +#: src/view/screens/ProfileList.tsx:631 +msgid "Blocking is public. Blocked accounts cannot reply in your threads, mention you, or otherwise interact with you." +msgstr "封鎖是公開的。被封鎖的帳號無法在你的貼文中回覆、提及你,或以其他方式與你互動。" + +#: src/view/com/profile/ProfileMenu.tsx:353 +msgid "Blocking will not prevent labels from being applied on your account, but it will stop this account from replying in your threads or interacting with you." +msgstr "" + +#: src/view/com/auth/HomeLoggedOutCTA.tsx:97 +#: src/view/com/auth/SplashScreen.web.tsx:133 +msgid "Blog" +msgstr "部落格" + +#: src/view/com/auth/HomeLoggedOutCTA.tsx:31 +#: src/view/com/auth/server-input/index.tsx:89 +#: src/view/com/auth/server-input/index.tsx:90 +msgid "Bluesky" +msgstr "Bluesky" + +#: src/view/com/auth/server-input/index.tsx:150 +msgid "Bluesky is an open network where you can choose your hosting provider. Custom hosting is now available in beta for developers." +msgstr "Bluesky 是一個開放的網路,你可以自行挑選託管服務提供商。現在,開發者也可以參與自訂託管服務的測試版本。" + +#: src/view/com/auth/onboarding/WelcomeDesktop.tsx:80 +#: src/view/com/auth/onboarding/WelcomeMobile.tsx:82 +msgid "Bluesky is flexible." +msgstr "Bluesky 非常靈活。" + +#: src/view/com/auth/onboarding/WelcomeDesktop.tsx:69 +#: src/view/com/auth/onboarding/WelcomeMobile.tsx:71 +msgid "Bluesky is open." +msgstr "Bluesky 保持開放。" + +#: src/view/com/auth/onboarding/WelcomeDesktop.tsx:56 +#: src/view/com/auth/onboarding/WelcomeMobile.tsx:58 +msgid "Bluesky is public." +msgstr "Bluesky 為公眾而生。" + +#: src/view/com/modals/Waitlist.tsx:70 +#~ msgid "Bluesky uses invites to build a healthier community. If you don't know anybody with an invite, you can sign up for the waitlist and we'll send one soon." +#~ msgstr "Bluesky 使用邀請制來打造更健康的社群環境。如果你不認識擁有邀請碼的人,你可以先填寫並加入候補清單,我們會儘快審核並發送邀請碼。" + +#: src/screens/Moderation/index.tsx:535 +msgid "Bluesky will not show your profile and posts to logged-out users. Other apps may not honor this request. This does not make your account private." +msgstr "Bluesky 不會向未登入的使用者顯示你的個人資料和貼文。但其他應用可能不會遵照此請求,這無法確保你的帳號隱私。" + +#: src/view/com/modals/ServerInput.tsx:78 +#~ msgid "Bluesky.Social" +#~ msgstr "Bluesky.Social" + +#: src/lib/moderation/useLabelBehaviorDescription.ts:53 +msgid "Blur images" +msgstr "" + +#: src/lib/moderation/useLabelBehaviorDescription.ts:51 +msgid "Blur images and filter from feeds" +msgstr "" + +#: src/screens/Onboarding/index.tsx:33 +msgid "Books" +msgstr "書籍" + +#: src/view/screens/Settings/index.tsx:893 +msgid "Build version {0} {1}" +msgstr "建構版本號 {0} {1}" + +#: src/view/com/auth/HomeLoggedOutCTA.tsx:91 +#: src/view/com/auth/SplashScreen.web.tsx:128 +msgid "Business" +msgstr "商務" + +#: src/view/com/modals/ServerInput.tsx:115 +#~ msgid "Button disabled. Input custom domain to proceed." +#~ msgstr "按鈕已停用。請輸入自訂網域以繼續。" + +#: src/view/com/profile/ProfileSubpageHeader.tsx:157 +msgid "by —" +msgstr "來自 —" + +#: src/view/com/auth/onboarding/RecommendedFeedsItem.tsx:100 +msgid "by {0}" +msgstr "來自 {0}" + +#: src/components/LabelingServiceCard/index.tsx:57 +msgid "By {0}" +msgstr "來自 {0}" + +#: src/view/com/profile/ProfileSubpageHeader.tsx:161 +msgid "by <0/>" +msgstr "來自 <0/>" + +#: src/view/com/auth/create/Policies.tsx:87 +msgid "By creating an account you agree to the {els}." +msgstr "" + +#: src/view/com/profile/ProfileSubpageHeader.tsx:159 +msgid "by you" +msgstr "來自你" + +#: src/view/com/composer/photos/OpenCameraBtn.tsx:77 +msgid "Camera" +msgstr "相機" + +#: src/view/com/modals/AddAppPasswords.tsx:216 +msgid "Can only contain letters, numbers, spaces, dashes, and underscores. Must be at least 4 characters long, but no more than 32 characters long." +msgstr "只能包含字母、數字、空格、破折號及底線。長度必須至少 4 個字元,但不超過 32 個字元。" + +#: src/components/Menu/index.tsx:213 +#: src/components/Prompt.tsx:116 +#: src/components/Prompt.tsx:118 +#: src/components/TagMenu/index.tsx:268 +#: src/view/com/composer/Composer.tsx:316 +#: src/view/com/composer/Composer.tsx:321 +#: src/view/com/modals/ChangeEmail.tsx:218 +#: src/view/com/modals/ChangeEmail.tsx:220 +#: src/view/com/modals/ChangeHandle.tsx:153 +#: src/view/com/modals/ChangePassword.tsx:267 +#: src/view/com/modals/ChangePassword.tsx:270 +#: src/view/com/modals/CreateOrEditList.tsx:355 +#: src/view/com/modals/crop-image/CropImage.web.tsx:137 +#: src/view/com/modals/EditImage.tsx:323 +#: src/view/com/modals/EditProfile.tsx:249 +#: src/view/com/modals/InAppBrowserConsent.tsx:78 +#: src/view/com/modals/InAppBrowserConsent.tsx:80 +#: src/view/com/modals/LinkWarning.tsx:87 +#: src/view/com/modals/LinkWarning.tsx:89 +#: src/view/com/modals/Repost.tsx:87 +#: src/view/com/modals/VerifyEmail.tsx:247 +#: src/view/com/modals/VerifyEmail.tsx:253 +#: src/view/screens/Search/Search.tsx:717 +#: src/view/shell/desktop/Search.tsx:239 +msgid "Cancel" +msgstr "取消" + +#: src/view/com/modals/CreateOrEditList.tsx:360 +#: src/view/com/modals/DeleteAccount.tsx:156 +#: src/view/com/modals/DeleteAccount.tsx:234 +msgctxt "action" +msgid "Cancel" +msgstr "取消" + +#: src/view/com/modals/DeleteAccount.tsx:152 +#: src/view/com/modals/DeleteAccount.tsx:230 +msgid "Cancel account deletion" +msgstr "取消刪除帳號" + +#: src/view/com/modals/ChangeHandle.tsx:149 +msgid "Cancel change handle" +msgstr "取消修改帳號代碼" + +#: src/view/com/modals/crop-image/CropImage.web.tsx:134 +msgid "Cancel image crop" +msgstr "取消裁剪圖片" + +#: src/view/com/modals/EditProfile.tsx:244 +msgid "Cancel profile editing" +msgstr "取消編輯個人資料" + +#: src/view/com/modals/Repost.tsx:78 +msgid "Cancel quote post" +msgstr "取消引用貼文" + +#: src/view/com/modals/ListAddRemoveUsers.tsx:87 +#: src/view/shell/desktop/Search.tsx:235 +msgid "Cancel search" +msgstr "取消搜尋" + +#: src/view/com/modals/Waitlist.tsx:136 +#~ msgid "Cancel waitlist signup" +#~ msgstr "取消候補清單註冊" + +#: src/view/com/modals/LinkWarning.tsx:88 +msgid "Cancels opening the linked website" +msgstr "" + +#: src/view/com/modals/VerifyEmail.tsx:152 +msgid "Change" +msgstr "變更" + +#: src/view/screens/Settings/index.tsx:353 +msgctxt "action" +msgid "Change" +msgstr "變更" + +#: src/view/screens/Settings/index.tsx:716 +msgid "Change handle" +msgstr "變更帳號代碼" + +#: src/view/com/modals/ChangeHandle.tsx:161 +#: src/view/screens/Settings/index.tsx:727 +msgid "Change Handle" +msgstr "變更帳號代碼" + +#: src/view/com/modals/VerifyEmail.tsx:147 +msgid "Change my email" +msgstr "變更我的電子郵件地址" + +#: src/view/screens/Settings/index.tsx:754 +msgid "Change password" +msgstr "變更密碼" + +#: src/view/com/modals/ChangePassword.tsx:141 +#: src/view/screens/Settings/index.tsx:765 +msgid "Change Password" +msgstr "變更密碼" + +#: src/view/com/composer/select-language/SuggestedLanguage.tsx:73 +msgid "Change post language to {0}" +msgstr "變更貼文的發佈語言至 {0}" + +#: src/view/screens/Settings/index.tsx:733 +#~ msgid "Change your Bluesky password" +#~ msgstr "變更你的 Bluesky 密碼" + +#: src/view/com/modals/ChangeEmail.tsx:109 +msgid "Change Your Email" +msgstr "變更你的電子郵件地址" + +#: src/screens/Deactivated.tsx:72 +#: src/screens/Deactivated.tsx:76 +msgid "Check my status" +msgstr "檢查我的狀態" + +#: src/view/com/auth/onboarding/RecommendedFeeds.tsx:121 +msgid "Check out some recommended feeds. Tap + to add them to your list of pinned feeds." +msgstr "來看看一些推薦的訊息流吧。點擊 + 將它們新增到你的釘選訊息流清單中。" + +#: src/view/com/auth/onboarding/RecommendedFollows.tsx:185 +msgid "Check out some recommended users. Follow them to see similar users." +msgstr "來看看一些推薦的使用者吧。跟隨人來查看類似的使用者。" + +#: src/view/com/modals/DeleteAccount.tsx:169 +msgid "Check your inbox for an email with the confirmation code to enter below:" +msgstr "查看寄送至你電子郵件地址的確認郵件,然後在下方輸入收到的驗證碼:" + +#: src/view/com/modals/Threadgate.tsx:72 +msgid "Choose \"Everybody\" or \"Nobody\"" +msgstr "選擇「所有人」或「沒有人」" + +#: src/view/screens/Settings/index.tsx:697 +#~ msgid "Choose a new Bluesky username or create" +#~ msgstr "選擇一個新的 Bluesky 使用者名稱或重新建立" + +#: src/view/com/auth/server-input/index.tsx:79 +msgid "Choose Service" +msgstr "選擇服務" + +#: src/screens/Onboarding/StepFinished.tsx:135 +msgid "Choose the algorithms that power your custom feeds." +msgstr "選擇你的自訂訊息流所使用的演算法。" + +#: src/view/com/auth/onboarding/WelcomeDesktop.tsx:83 +#: src/view/com/auth/onboarding/WelcomeMobile.tsx:85 +msgid "Choose the algorithms that power your experience with custom feeds." +msgstr "選擇你的自訂訊息流體驗所使用的演算法。" + +#: src/screens/Onboarding/StepAlgoFeeds/index.tsx:103 +msgid "Choose your main feeds" +msgstr "選擇你的主要訊息流" + +#: src/view/com/auth/create/Step1.tsx:196 +msgid "Choose your password" +msgstr "選擇你的密碼" + +#: src/view/screens/Settings/index.tsx:868 +msgid "Clear all legacy storage data" +msgstr "清除所有舊儲存資料" + +#: src/view/screens/Settings/index.tsx:871 +msgid "Clear all legacy storage data (restart after this)" +msgstr "清除所有舊儲存資料(並重啟)" + +#: src/view/screens/Settings/index.tsx:880 +msgid "Clear all storage data" +msgstr "清除所有資料" + +#: src/view/screens/Settings/index.tsx:883 +msgid "Clear all storage data (restart after this)" +msgstr "清除所有資料(並重啟)" + +#: src/view/com/util/forms/SearchInput.tsx:88 +#: src/view/screens/Search/Search.tsx:698 +msgid "Clear search query" +msgstr "清除搜尋記錄" + +#: src/view/screens/Settings/index.tsx:869 +msgid "Clears all legacy storage data" +msgstr "" + +#: src/view/screens/Settings/index.tsx:881 +msgid "Clears all storage data" +msgstr "" + +#: src/view/screens/Support.tsx:40 +msgid "click here" +msgstr "點擊這裡" + +#: src/components/TagMenu/index.web.tsx:138 +msgid "Click here to open tag menu for {tag}" +msgstr "" + +#: src/components/RichText.tsx:191 +msgid "Click here to open tag menu for #{tag}" +msgstr "" + +#: src/screens/Onboarding/index.tsx:35 +msgid "Climate" +msgstr "氣象" + +#: src/view/com/modals/ChangePassword.tsx:267 +#: src/view/com/modals/ChangePassword.tsx:270 +msgid "Close" +msgstr "關閉" + +#: src/components/Dialog/index.web.tsx:84 +#: src/components/Dialog/index.web.tsx:198 +msgid "Close active dialog" +msgstr "關閉打開的對話框" + +#: src/view/com/auth/login/PasswordUpdatedForm.tsx:38 +msgid "Close alert" +msgstr "關閉警告" + +#: src/view/com/util/BottomSheetCustomBackdrop.tsx:36 +msgid "Close bottom drawer" +msgstr "關閉底部抽屜" + +#: src/view/com/lightbox/ImageViewing/components/ImageDefaultHeader.tsx:36 +msgid "Close image" +msgstr "關閉圖片" + +#: src/view/com/lightbox/Lightbox.web.tsx:129 +msgid "Close image viewer" +msgstr "關閉圖片檢視器" + +#: src/view/shell/index.web.tsx:55 +msgid "Close navigation footer" +msgstr "關閉導覽頁腳" + +#: src/components/Menu/index.tsx:207 +#: src/components/TagMenu/index.tsx:262 +msgid "Close this dialog" +msgstr "" + +#: src/view/shell/index.web.tsx:56 +msgid "Closes bottom navigation bar" +msgstr "關閉底部導覽列" + +#: src/view/com/auth/login/PasswordUpdatedForm.tsx:39 +msgid "Closes password update alert" +msgstr "關閉密碼更新警告" + +#: src/view/com/composer/Composer.tsx:318 +msgid "Closes post composer and discards post draft" +msgstr "關閉貼文編輯頁並捨棄草稿" + +#: src/view/com/lightbox/ImageViewing/components/ImageDefaultHeader.tsx:37 +msgid "Closes viewer for header image" +msgstr "關閉標題圖片檢視器" + +#: src/view/com/notifications/FeedItem.tsx:321 +msgid "Collapses list of users for a given notification" +msgstr "折疊指定通知的使用者清單" + +#: src/screens/Onboarding/index.tsx:41 +msgid "Comedy" +msgstr "喜劇" + +#: src/screens/Onboarding/index.tsx:27 +msgid "Comics" +msgstr "漫畫" + +#: src/Navigation.tsx:241 +#: src/view/screens/CommunityGuidelines.tsx:32 +msgid "Community Guidelines" +msgstr "社群準則" + +#: src/screens/Onboarding/StepFinished.tsx:148 +msgid "Complete onboarding and start using your account" +msgstr "完成初始設定並開始使用你的帳號" + +#: src/view/com/auth/create/Step3.tsx:73 +msgid "Complete the challenge" +msgstr "完成驗證" + +#: src/view/com/composer/Composer.tsx:437 +msgid "Compose posts up to {MAX_GRAPHEME_LENGTH} characters in length" +msgstr "撰寫貼文的長度最多為 {MAX_GRAPHEME_LENGTH} 個字元" + +#: src/view/com/composer/Prompt.tsx:24 +msgid "Compose reply" +msgstr "撰寫回覆" + +#: src/components/moderation/GlobalModerationLabelPref.tsx:69 +#: src/components/moderation/ModerationLabelPref.tsx:149 +#: src/screens/Onboarding/StepModeration/ModerationOption.tsx:81 +msgid "Configure content filtering setting for category: {0}" +msgstr "調整類別的內容過濾設定:{0}" + +#: src/components/moderation/ModerationLabelPref.tsx:116 +msgid "Configured in <0>moderation settings." +msgstr "" + +#: src/components/Prompt.tsx:152 +#: src/components/Prompt.tsx:155 +#: src/view/com/modals/SelfLabel.tsx:154 +#: src/view/com/modals/VerifyEmail.tsx:231 +#: src/view/com/modals/VerifyEmail.tsx:233 +#: src/view/screens/PreferencesFollowingFeed.tsx:308 +#: src/view/screens/PreferencesThreads.tsx:159 +msgid "Confirm" +msgstr "確認" + +#: src/view/com/modals/Confirm.tsx:75 +#: src/view/com/modals/Confirm.tsx:78 +#~ msgctxt "action" +#~ msgid "Confirm" +#~ msgstr "確認" + +#: src/view/com/modals/ChangeEmail.tsx:193 +#: src/view/com/modals/ChangeEmail.tsx:195 +msgid "Confirm Change" +msgstr "確認更改" + +#: src/view/com/modals/lang-settings/ConfirmLanguagesButton.tsx:34 +msgid "Confirm content language settings" +msgstr "確認內容語言設定" + +#: src/view/com/modals/DeleteAccount.tsx:220 +msgid "Confirm delete account" +msgstr "確認刪除帳號" + +#: src/view/com/modals/ContentFilteringSettings.tsx:156 +#~ msgid "Confirm your age to enable adult content." +#~ msgstr "確認你的年齡以顯示成人內容。" + +#: src/screens/Moderation/index.tsx:303 +msgid "Confirm your age:" +msgstr "" + +#: src/screens/Moderation/index.tsx:294 +msgid "Confirm your birthdate" +msgstr "" + +#: src/view/com/modals/ChangeEmail.tsx:157 +#: src/view/com/modals/DeleteAccount.tsx:176 +#: src/view/com/modals/DeleteAccount.tsx:182 +#: src/view/com/modals/VerifyEmail.tsx:165 +msgid "Confirmation code" +msgstr "驗證碼" + +#: src/view/com/modals/Waitlist.tsx:120 +#~ msgid "Confirms signing up {email} to the waitlist" +#~ msgstr "確認將 {email} 註冊到候補列表" + +#: src/view/com/auth/create/CreateAccount.tsx:193 +#: src/view/com/auth/login/LoginForm.tsx:281 +msgid "Connecting..." +msgstr "連線中…" + +#: src/view/com/auth/create/CreateAccount.tsx:213 +msgid "Contact support" +msgstr "聯絡支援" + +#: src/components/moderation/LabelsOnMe.tsx:42 +msgid "content" +msgstr "" + +#: src/lib/moderation/useGlobalLabelStrings.ts:18 +msgid "Content Blocked" +msgstr "" + +#: src/view/screens/Moderation.tsx:83 +#~ msgid "Content filtering" +#~ msgstr "內容過濾" + +#: src/view/com/modals/ContentFilteringSettings.tsx:44 +#~ msgid "Content Filtering" +#~ msgstr "內容過濾" + +#: src/screens/Moderation/index.tsx:287 +msgid "Content filters" +msgstr "內容過濾" + +#: src/view/com/modals/lang-settings/ContentLanguagesSettings.tsx:74 +#: src/view/screens/LanguageSettings.tsx:278 +msgid "Content Languages" +msgstr "內容語言" + +#: src/components/moderation/ModerationDetailsDialog.tsx:76 +#: src/lib/moderation/useModerationCauseDescription.ts:75 +msgid "Content Not Available" +msgstr "內容不可用" + +#: src/components/moderation/ModerationDetailsDialog.tsx:47 +#: src/components/moderation/ScreenHider.tsx:100 +#: src/lib/moderation/useGlobalLabelStrings.ts:22 +#: src/lib/moderation/useModerationCauseDescription.ts:38 +msgid "Content Warning" +msgstr "內容警告" + +#: src/view/com/composer/labels/LabelsBtn.tsx:31 +msgid "Content warnings" +msgstr "內容警告" + +#: src/components/Menu/index.web.tsx:84 +msgid "Context menu backdrop, click to close the menu." +msgstr "" + +#: src/screens/Onboarding/StepAlgoFeeds/index.tsx:170 +#: src/screens/Onboarding/StepFollowingFeed.tsx:153 +#: src/screens/Onboarding/StepInterests/index.tsx:248 +#: src/screens/Onboarding/StepModeration/index.tsx:102 +#: src/screens/Onboarding/StepTopicalFeeds.tsx:114 +#: src/view/com/auth/onboarding/RecommendedFeeds.tsx:148 +#: src/view/com/auth/onboarding/RecommendedFollows.tsx:209 +#: src/view/com/auth/onboarding/WelcomeMobile.tsx:96 +msgid "Continue" +msgstr "繼續" + +#: src/screens/Onboarding/StepFollowingFeed.tsx:150 +#: src/screens/Onboarding/StepInterests/index.tsx:245 +#: src/screens/Onboarding/StepModeration/index.tsx:99 +#: src/screens/Onboarding/StepTopicalFeeds.tsx:111 +msgid "Continue to next step" +msgstr "繼續下一步" + +#: src/screens/Onboarding/StepAlgoFeeds/index.tsx:167 +msgid "Continue to the next step" +msgstr "繼續下一步" + +#: src/screens/Onboarding/StepSuggestedAccounts/index.tsx:191 +msgid "Continue to the next step without following any accounts" +msgstr "繼續下一步,不跟隨任何帳號" + +#: src/screens/Onboarding/index.tsx:44 +msgid "Cooking" +msgstr "烹飪" + +#: src/view/com/modals/AddAppPasswords.tsx:195 +#: src/view/com/modals/InviteCodes.tsx:182 +msgid "Copied" +msgstr "已複製" + +#: src/view/screens/Settings/index.tsx:251 +msgid "Copied build version to clipboard" +msgstr "已複製建構版本號至剪貼簿" + +#: src/view/com/modals/AddAppPasswords.tsx:76 +#: src/view/com/modals/ChangeHandle.tsx:327 +#: src/view/com/modals/InviteCodes.tsx:152 +#: src/view/com/util/forms/PostDropdownBtn.tsx:158 +msgid "Copied to clipboard" +msgstr "已複製至剪貼簿" + +#: src/view/com/modals/AddAppPasswords.tsx:189 +msgid "Copies app password" +msgstr "複製應用程式專用密碼" + +#: src/view/com/modals/AddAppPasswords.tsx:188 +msgid "Copy" +msgstr "複製" + +#: src/view/com/modals/ChangeHandle.tsx:481 +msgid "Copy {0}" +msgstr "複製 {0}" + +#: src/view/screens/ProfileList.tsx:388 +msgid "Copy link to list" +msgstr "複製列表連結" + +#: src/view/com/util/forms/PostDropdownBtn.tsx:228 +#: src/view/com/util/forms/PostDropdownBtn.tsx:237 +msgid "Copy link to post" +msgstr "複製貼文連結" + +#: src/view/com/profile/ProfileHeader.tsx:295 +#~ msgid "Copy link to profile" +#~ msgstr "複製個人資料連結" + +#: src/view/com/util/forms/PostDropdownBtn.tsx:220 +#: src/view/com/util/forms/PostDropdownBtn.tsx:222 +msgid "Copy post text" +msgstr "複製貼文文字" + +#: src/Navigation.tsx:246 +#: src/view/screens/CopyrightPolicy.tsx:29 +msgid "Copyright Policy" +msgstr "著作權政策" + +#: src/view/screens/ProfileFeed.tsx:102 +msgid "Could not load feed" +msgstr "無法載入訊息流" + +#: src/view/screens/ProfileList.tsx:907 +msgid "Could not load list" +msgstr "無法載入列表" + +#: src/view/com/auth/create/Step2.tsx:91 +#~ msgid "Country" +#~ msgstr "國家" + +#: src/view/com/auth/HomeLoggedOutCTA.tsx:64 +#: src/view/com/auth/SplashScreen.tsx:73 +#: src/view/com/auth/SplashScreen.web.tsx:81 +msgid "Create a new account" +msgstr "建立新帳號" + +#: src/view/screens/Settings/index.tsx:403 +msgid "Create a new Bluesky account" +msgstr "建立新的 Bluesky 帳號" + +#: src/view/com/auth/create/CreateAccount.tsx:133 +msgid "Create Account" +msgstr "建立帳號" + +#: src/view/com/modals/AddAppPasswords.tsx:226 +msgid "Create App Password" +msgstr "建立應用程式專用密碼" + +#: src/view/com/auth/HomeLoggedOutCTA.tsx:54 +#: src/view/com/auth/SplashScreen.tsx:68 +msgid "Create new account" +msgstr "建立新帳號" + +#: src/components/ReportDialog/SelectReportOptionView.tsx:94 +msgid "Create report for {0}" +msgstr "" + +#: src/view/screens/AppPasswords.tsx:246 +msgid "Created {0}" +msgstr "{0} 已建立" + +#: src/view/screens/ProfileFeed.tsx:616 +#~ msgid "Created by <0/>" +#~ msgstr "由 <0/> 建立" + +#: src/view/screens/ProfileFeed.tsx:614 +#~ msgid "Created by you" +#~ msgstr "由你建立" + +#: src/view/com/composer/Composer.tsx:468 +msgid "Creates a card with a thumbnail. The card links to {url}" +msgstr "建立帶有縮圖的卡片。該卡片連結到 {url}" + +#: src/screens/Onboarding/index.tsx:29 +msgid "Culture" +msgstr "文化" + +#: src/view/com/auth/server-input/index.tsx:95 +#: src/view/com/auth/server-input/index.tsx:96 +msgid "Custom" +msgstr "自訂" + +#: src/view/com/modals/ChangeHandle.tsx:389 +msgid "Custom domain" +msgstr "自訂網域" + +#: src/screens/Onboarding/StepAlgoFeeds/index.tsx:106 +#: src/view/screens/Feeds.tsx:692 +msgid "Custom feeds built by the community bring you new experiences and help you find the content you love." +msgstr "由社群打造的自訂訊息流帶來新鮮體驗,協助你找到所愛內容。" + +#: src/view/screens/PreferencesExternalEmbeds.tsx:55 +msgid "Customize media from external sites." +msgstr "自訂外部網站的媒體。" + +#: src/view/screens/Settings.tsx:687 +#~ msgid "Danger Zone" +#~ msgstr "危險區域" + +#: src/view/screens/Settings/index.tsx:504 +#: src/view/screens/Settings/index.tsx:530 +msgid "Dark" +msgstr "深黑" + +#: src/view/screens/Debug.tsx:63 +msgid "Dark mode" +msgstr "深色模式" + +#: src/view/screens/Settings/index.tsx:517 +msgid "Dark Theme" +msgstr "深色主題" + +#: src/view/screens/Settings/index.tsx:841 +msgid "Debug Moderation" +msgstr "" + +#: src/view/screens/Debug.tsx:83 +msgid "Debug panel" +msgstr "除錯面板" + +#: src/view/com/util/forms/PostDropdownBtn.tsx:319 +#: src/view/screens/AppPasswords.tsx:268 +#: src/view/screens/ProfileList.tsx:613 +msgid "Delete" +msgstr "刪除" + +#: src/view/screens/Settings/index.tsx:796 +msgid "Delete account" +msgstr "刪除帳號" + +#: src/view/com/modals/DeleteAccount.tsx:87 +msgid "Delete Account" +msgstr "刪除帳號" + +#: src/view/screens/AppPasswords.tsx:239 +msgid "Delete app password" +msgstr "刪除應用程式專用密碼" + +#: src/view/screens/AppPasswords.tsx:263 +msgid "Delete app password?" +msgstr "刪除應用程式專用密碼?" + +#: src/view/screens/ProfileList.tsx:415 +msgid "Delete List" +msgstr "刪除列表" + +#: src/view/com/modals/DeleteAccount.tsx:223 +msgid "Delete my account" +msgstr "刪除我的帳號" + +#: src/view/screens/Settings.tsx:706 +#~ msgid "Delete my account…" +#~ msgstr "刪除我的帳號…" + +#: src/view/screens/Settings/index.tsx:808 +msgid "Delete My Account…" +msgstr "刪除我的帳號…" + +#: src/view/com/util/forms/PostDropdownBtn.tsx:302 +#: src/view/com/util/forms/PostDropdownBtn.tsx:304 +msgid "Delete post" +msgstr "刪除貼文" + +#: src/view/screens/ProfileList.tsx:608 +msgid "Delete this list?" +msgstr "" + +#: src/view/com/util/forms/PostDropdownBtn.tsx:314 +msgid "Delete this post?" +msgstr "刪除這條貼文?" + +#: src/view/com/util/post-embeds/QuoteEmbed.tsx:64 +msgid "Deleted" +msgstr "已刪除" + +#: src/view/com/post-thread/PostThread.tsx:305 +msgid "Deleted post." +msgstr "已刪除貼文。" + +#: src/view/com/modals/CreateOrEditList.tsx:300 +#: src/view/com/modals/CreateOrEditList.tsx:321 +#: src/view/com/modals/EditProfile.tsx:198 +#: src/view/com/modals/EditProfile.tsx:210 +msgid "Description" +msgstr "描述" + +#: src/view/screens/Settings.tsx:760 +#~ msgid "Developer Tools" +#~ msgstr "開發者工具" + +#: src/view/com/composer/Composer.tsx:217 +msgid "Did you want to say anything?" +msgstr "有什麼想說的嗎?" + +#: src/view/screens/Settings/index.tsx:523 +msgid "Dim" +msgstr "暗淡" + +#: src/lib/moderation/useLabelBehaviorDescription.ts:32 +#: src/lib/moderation/useLabelBehaviorDescription.ts:42 +#: src/lib/moderation/useLabelBehaviorDescription.ts:68 +#: src/screens/Moderation/index.tsx:343 +msgid "Disabled" +msgstr "" + +#: src/view/com/composer/Composer.tsx:510 +msgid "Discard" +msgstr "捨棄" + +#: src/view/com/composer/Composer.tsx:145 +#~ msgid "Discard draft" +#~ msgstr "捨棄草稿" + +#: src/view/com/composer/Composer.tsx:507 +msgid "Discard draft?" +msgstr "捨棄草稿?" + +#: src/screens/Moderation/index.tsx:520 +#: src/screens/Moderation/index.tsx:524 +msgid "Discourage apps from showing my account to logged-out users" +msgstr "鼓勵應用程式不要向未登入使用者顯示我的帳號" + +#: src/view/com/posts/FollowingEmptyState.tsx:74 +#: src/view/com/posts/FollowingEndOfFeed.tsx:75 +msgid "Discover new custom feeds" +msgstr "探索新的自訂訊息流" + +#: src/view/screens/Feeds.tsx:473 +#~ msgid "Discover new feeds" +#~ msgstr "探索新的訊息流" + +#: src/view/screens/Feeds.tsx:689 +msgid "Discover New Feeds" +msgstr "探索新的訊息流" + +#: src/view/com/modals/EditProfile.tsx:192 +msgid "Display name" +msgstr "顯示名稱" + +#: src/view/com/modals/EditProfile.tsx:180 +msgid "Display Name" +msgstr "顯示名稱" + +#: src/view/com/modals/ChangeHandle.tsx:398 +msgid "DNS Panel" +msgstr "" + +#: src/lib/moderation/useGlobalLabelStrings.ts:39 +msgid "Does not include nudity." +msgstr "" + +#: src/view/com/modals/ChangeHandle.tsx:482 +msgid "Domain Value" +msgstr "" + +#: src/view/com/modals/ChangeHandle.tsx:489 +msgid "Domain verified!" +msgstr "網域已驗證!" + +#: src/view/com/auth/create/Step1.tsx:170 +#~ msgid "Don't have an invite code?" +#~ msgstr "沒有邀請碼?" + +#: src/components/dialogs/BirthDateSettings.tsx:119 +#: src/components/dialogs/BirthDateSettings.tsx:125 +#: src/view/com/auth/server-input/index.tsx:165 +#: src/view/com/auth/server-input/index.tsx:166 +#: src/view/com/modals/AddAppPasswords.tsx:226 +#: src/view/com/modals/AltImage.tsx:139 +#: src/view/com/modals/crop-image/CropImage.web.tsx:152 +#: src/view/com/modals/InviteCodes.tsx:80 +#: src/view/com/modals/InviteCodes.tsx:123 +#: src/view/com/modals/ListAddRemoveUsers.tsx:142 +#: src/view/screens/PreferencesFollowingFeed.tsx:311 +#: src/view/screens/Settings/ExportCarDialog.tsx:94 +#: src/view/screens/Settings/ExportCarDialog.tsx:95 +msgid "Done" +msgstr "完成" + +#: src/view/com/auth/onboarding/RecommendedFollows.tsx:86 +#: src/view/com/modals/EditImage.tsx:333 +#: src/view/com/modals/ListAddRemoveUsers.tsx:144 +#: src/view/com/modals/SelfLabel.tsx:157 +#: src/view/com/modals/Threadgate.tsx:129 +#: src/view/com/modals/Threadgate.tsx:132 +#: src/view/com/modals/UserAddRemoveLists.tsx:95 +#: src/view/com/modals/UserAddRemoveLists.tsx:98 +#: src/view/screens/PreferencesThreads.tsx:162 +msgctxt "action" +msgid "Done" +msgstr "完成" + +#: src/view/com/modals/lang-settings/ConfirmLanguagesButton.tsx:42 +msgid "Done{extraText}" +msgstr "完成{extraText}" + +#: src/view/com/auth/login/ChooseAccountForm.tsx:46 +msgid "Double tap to sign in" +msgstr "雙擊以登入" + +#: src/view/screens/Settings/index.tsx:755 +#~ msgid "Download Bluesky account data (repository)" +#~ msgstr "下載 Bluesky 帳號資料(存放庫)" + +#: src/view/screens/Settings/ExportCarDialog.tsx:59 +#: src/view/screens/Settings/ExportCarDialog.tsx:63 +msgid "Download CAR file" +msgstr "下載 CAR 檔案" + +#: src/view/com/composer/text-input/TextInput.web.tsx:249 +msgid "Drop to add images" +msgstr "拖放即可新增圖片" + +#: src/screens/Onboarding/StepModeration/AdultContentEnabledPref.tsx:120 +msgid "Due to Apple policies, adult content can only be enabled on the web after completing sign up." +msgstr "受 Apple 政策限制,成人內容只能在完成註冊後在網頁端啟用顯示。" + +#: src/view/com/modals/ChangeHandle.tsx:257 +msgid "e.g. alice" +msgstr "" + +#: src/view/com/modals/EditProfile.tsx:185 +msgid "e.g. Alice Roberts" +msgstr "例如:張藍天" + +#: src/view/com/modals/ChangeHandle.tsx:381 +msgid "e.g. alice.com" +msgstr "" + +#: src/view/com/modals/EditProfile.tsx:203 +msgid "e.g. Artist, dog-lover, and avid reader." +msgstr "例如:藝術家、愛狗人士和狂熱讀者。" + +#: src/lib/moderation/useGlobalLabelStrings.ts:43 +msgid "E.g. artistic nudes." +msgstr "" + +#: src/view/com/modals/CreateOrEditList.tsx:283 +msgid "e.g. Great Posters" +msgstr "例如:優秀的發文者" + +#: src/view/com/modals/CreateOrEditList.tsx:284 +msgid "e.g. Spammers" +msgstr "例如:垃圾內容製造者" + +#: src/view/com/modals/CreateOrEditList.tsx:312 +msgid "e.g. The posters who never miss." +msgstr "例如:絕對不容錯過的發文者。" + +#: src/view/com/modals/CreateOrEditList.tsx:313 +msgid "e.g. Users that repeatedly reply with ads." +msgstr "例如:張貼廣告回覆的使用者。" + +#: src/view/com/modals/InviteCodes.tsx:96 +msgid "Each code works once. You'll receive more invite codes periodically." +msgstr "每個邀請碼僅能使用一次。你將定期收到更多的邀請碼。" + +#: src/view/com/lists/ListMembers.tsx:149 +msgctxt "action" +msgid "Edit" +msgstr "編輯" + +#: src/view/com/util/UserAvatar.tsx:299 +#: src/view/com/util/UserBanner.tsx:85 +msgid "Edit avatar" +msgstr "" + +#: src/view/com/composer/photos/Gallery.tsx:144 +#: src/view/com/modals/EditImage.tsx:207 +msgid "Edit image" +msgstr "編輯圖片" + +#: src/view/screens/ProfileList.tsx:403 +msgid "Edit list details" +msgstr "編輯列表詳情" + +#: src/view/com/modals/CreateOrEditList.tsx:250 +msgid "Edit Moderation List" +msgstr "編輯管理列表" + +#: src/Navigation.tsx:256 +#: src/view/screens/Feeds.tsx:434 +#: src/view/screens/SavedFeeds.tsx:84 +msgid "Edit My Feeds" +msgstr "編輯自訂訊息流" + +#: src/view/com/modals/EditProfile.tsx:152 +msgid "Edit my profile" +msgstr "編輯我的個人資料" + +#: src/screens/Profile/Header/ProfileHeaderLabeler.tsx:172 +#: src/screens/Profile/Header/ProfileHeaderStandard.tsx:161 +msgid "Edit profile" +msgstr "編輯個人資料" + +#: src/screens/Profile/Header/ProfileHeaderLabeler.tsx:175 +#: src/screens/Profile/Header/ProfileHeaderStandard.tsx:164 +msgid "Edit Profile" +msgstr "編輯個人資料" + +#: src/view/com/home/HomeHeaderLayout.web.tsx:62 +#: src/view/screens/Feeds.tsx:355 +msgid "Edit Saved Feeds" +msgstr "編輯已儲存的訊息流" + +#: src/view/com/modals/CreateOrEditList.tsx:245 +msgid "Edit User List" +msgstr "編輯使用者列表" + +#: src/view/com/modals/EditProfile.tsx:193 +msgid "Edit your display name" +msgstr "編輯你的顯示名稱" + +#: src/view/com/modals/EditProfile.tsx:211 +msgid "Edit your profile description" +msgstr "編輯你的帳號描述" + +#: src/screens/Onboarding/index.tsx:34 +msgid "Education" +msgstr "教育" + +#: src/view/com/auth/create/Step1.tsx:176 +#: src/view/com/auth/login/ForgotPasswordForm.tsx:156 +#: src/view/com/modals/ChangeEmail.tsx:141 +msgid "Email" +msgstr "電子郵件" + +#: src/view/com/auth/create/Step1.tsx:167 +#: src/view/com/auth/login/ForgotPasswordForm.tsx:147 +msgid "Email address" +msgstr "電子郵件地址" + +#: src/view/com/modals/ChangeEmail.tsx:56 +#: src/view/com/modals/ChangeEmail.tsx:88 +msgid "Email updated" +msgstr "電子郵件已更新" + +#: src/view/com/modals/ChangeEmail.tsx:111 +msgid "Email Updated" +msgstr "電子郵件已更新" + +#: src/view/com/modals/VerifyEmail.tsx:78 +msgid "Email verified" +msgstr "電子郵件已驗證" + +#: src/view/screens/Settings/index.tsx:331 +msgid "Email:" +msgstr "電子郵件:" + +#: src/view/com/modals/EmbedConsent.tsx:113 +msgid "Enable {0} only" +msgstr "僅啟用 {0}" + +#: src/screens/Moderation/index.tsx:331 +msgid "Enable adult content" +msgstr "顯示成人內容" + +#: src/screens/Onboarding/StepModeration/AdultContentEnabledPref.tsx:94 +msgid "Enable Adult Content" +msgstr "顯示成人內容" + +#: src/screens/Onboarding/StepModeration/AdultContentEnabledPref.tsx:78 +#: src/screens/Onboarding/StepModeration/AdultContentEnabledPref.tsx:79 +msgid "Enable adult content in your feeds" +msgstr "允許在你的訊息流中出現成人內容" + +#: src/view/com/modals/EmbedConsent.tsx:97 +msgid "Enable External Media" +msgstr "啟用外部媒體" + +#: src/view/screens/PreferencesExternalEmbeds.tsx:75 +msgid "Enable media players for" +msgstr "啟用媒體播放器" + +#: src/view/screens/PreferencesFollowingFeed.tsx:147 +msgid "Enable this setting to only see replies between people you follow." +msgstr "啟用此設定來只顯示你跟隨的人之間的回覆。" + +#: src/screens/Moderation/index.tsx:341 +msgid "Enabled" +msgstr "啟用" + +#: src/screens/Profile/Sections/Feed.tsx:84 +msgid "End of feed" +msgstr "訊息流的結尾" + +#: src/view/com/modals/AddAppPasswords.tsx:166 +msgid "Enter a name for this App Password" +msgstr "輸入此應用程式專用密碼的名稱" + +#: src/components/dialogs/MutedWords.tsx:100 +#: src/components/dialogs/MutedWords.tsx:101 +msgid "Enter a word or tag" +msgstr "" + +#: src/view/com/modals/VerifyEmail.tsx:105 +msgid "Enter Confirmation Code" +msgstr "輸入驗證碼" + +#: src/view/com/modals/ChangePassword.tsx:153 +msgid "Enter the code you received to change your password." +msgstr "輸入你收到的驗證碼以更改密碼。" + +#: src/view/com/modals/ChangeHandle.tsx:371 +msgid "Enter the domain you want to use" +msgstr "輸入你想使用的網域" + +#: src/view/com/auth/login/ForgotPasswordForm.tsx:107 +msgid "Enter the email you used to create your account. We'll send you a \"reset code\" so you can set a new password." +msgstr "輸入你用於建立帳號的電子郵件。我們將向你發送重設碼,以便你設定新密碼。" + +#: src/components/dialogs/BirthDateSettings.tsx:108 +#: src/view/com/auth/create/Step1.tsx:228 +msgid "Enter your birth date" +msgstr "輸入你的出生日期" + +#: src/view/com/modals/Waitlist.tsx:78 +#~ msgid "Enter your email" +#~ msgstr "輸入你的電子郵件地址" + +#: src/view/com/auth/create/Step1.tsx:172 +msgid "Enter your email address" +msgstr "輸入你的電子郵件地址" + +#: src/view/com/modals/ChangeEmail.tsx:41 +msgid "Enter your new email above" +msgstr "請在上方輸入你的新電子郵件地址" + +#: src/view/com/modals/ChangeEmail.tsx:117 +msgid "Enter your new email address below." +msgstr "請在下方輸入你的新電子郵件地址。" + +#: src/view/com/auth/create/Step2.tsx:188 +#~ msgid "Enter your phone number" +#~ msgstr "輸入你的手機號碼" + +#: src/view/com/auth/login/Login.tsx:99 +msgid "Enter your username and password" +msgstr "輸入你的使用者名稱和密碼" + +#: src/view/com/auth/create/Step3.tsx:67 +msgid "Error receiving captcha response." +msgstr "Captcha 給出了錯誤的回應。" + +#: src/view/screens/Search/Search.tsx:110 +msgid "Error:" +msgstr "錯誤:" + +#: src/view/com/modals/Threadgate.tsx:76 +msgid "Everybody" +msgstr "所有人" + +#: src/lib/moderation/useReportOptions.ts:66 +msgid "Excessive mentions or replies" +msgstr "" + +#: src/view/com/modals/DeleteAccount.tsx:231 +msgid "Exits account deletion process" +msgstr "" + +#: src/view/com/modals/ChangeHandle.tsx:150 +msgid "Exits handle change process" +msgstr "離開修改帳號代碼流程" + +#: src/view/com/modals/crop-image/CropImage.web.tsx:135 +msgid "Exits image cropping process" +msgstr "" + +#: src/view/com/lightbox/Lightbox.web.tsx:130 +msgid "Exits image view" +msgstr "離開圖片檢視器" + +#: src/view/com/modals/ListAddRemoveUsers.tsx:88 +#: src/view/shell/desktop/Search.tsx:236 +msgid "Exits inputting search query" +msgstr "離開搜尋字詞輸入" + +#: src/view/com/modals/Waitlist.tsx:138 +#~ msgid "Exits signing up for waitlist with {email}" +#~ msgstr "將 {email} 從候補列表中移除" + +#: src/view/com/lightbox/Lightbox.web.tsx:183 +msgid "Expand alt text" +msgstr "展開替代文字" + +#: src/view/com/composer/ComposerReplyTo.tsx:81 +#: src/view/com/composer/ComposerReplyTo.tsx:84 +msgid "Expand or collapse the full post you are replying to" +msgstr "展開或摺疊你要回覆的完整貼文" + +#: src/lib/moderation/useGlobalLabelStrings.ts:47 +msgid "Explicit or potentially disturbing media." +msgstr "" + +#: src/lib/moderation/useGlobalLabelStrings.ts:35 +msgid "Explicit sexual images." +msgstr "" + +#: src/view/screens/Settings/index.tsx:777 +msgid "Export my data" +msgstr "匯出我的資料" + +#: src/view/screens/Settings/ExportCarDialog.tsx:44 +#: src/view/screens/Settings/index.tsx:788 +msgid "Export My Data" +msgstr "匯出我的資料" + +#: src/view/com/modals/EmbedConsent.tsx:64 +msgid "External Media" +msgstr "外部媒體" + +#: src/view/com/modals/EmbedConsent.tsx:75 +#: src/view/screens/PreferencesExternalEmbeds.tsx:66 +msgid "External media may allow websites to collect information about you and your device. No information is sent or requested until you press the \"play\" button." +msgstr "外部媒體可能允許網站收集有關你和你裝置的信息。在你按下「播放」按鈕之前,將不會發送或請求任何外部信息。" + +#: src/Navigation.tsx:275 +#: src/view/screens/PreferencesExternalEmbeds.tsx:52 +#: src/view/screens/Settings/index.tsx:677 +msgid "External Media Preferences" +msgstr "外部媒體偏好設定" + +#: src/view/screens/Settings/index.tsx:668 +msgid "External media settings" +msgstr "外部媒體設定" + +#: src/view/com/modals/AddAppPasswords.tsx:115 +#: src/view/com/modals/AddAppPasswords.tsx:119 +msgid "Failed to create app password." +msgstr "建立應用程式專用密碼失敗。" + +#: src/view/com/modals/CreateOrEditList.tsx:206 +msgid "Failed to create the list. Check your internet connection and try again." +msgstr "無法建立列表。請檢查你的網路連線並重試。" + +#: src/view/com/util/forms/PostDropdownBtn.tsx:125 +msgid "Failed to delete post, please try again" +msgstr "無法刪除貼文,請重試" + +#: src/view/com/auth/onboarding/RecommendedFeeds.tsx:109 +#: src/view/com/auth/onboarding/RecommendedFeeds.tsx:141 +msgid "Failed to load recommended feeds" +msgstr "無法載入推薦訊息流" + +#: src/view/com/lightbox/Lightbox.tsx:83 +msgid "Failed to save image: {0}" +msgstr "" + +#: src/Navigation.tsx:196 +msgid "Feed" +msgstr "訊息流" + +#: src/view/com/feeds/FeedSourceCard.tsx:218 +msgid "Feed by {0}" +msgstr "{0} 建立的訊息流" + +#: src/view/screens/Feeds.tsx:605 +msgid "Feed offline" +msgstr "訊息流已離線" + +#: src/view/com/feeds/FeedPage.tsx:143 +#~ msgid "Feed Preferences" +#~ msgstr "訊息流偏好設定" + +#: src/view/shell/desktop/RightNav.tsx:61 +#: src/view/shell/Drawer.tsx:314 +msgid "Feedback" +msgstr "意見回饋" + +#: src/Navigation.tsx:464 +#: src/view/screens/Feeds.tsx:419 +#: src/view/screens/Feeds.tsx:524 +#: src/view/screens/Profile.tsx:192 +#: src/view/shell/bottom-bar/BottomBar.tsx:183 +#: src/view/shell/desktop/LeftNav.tsx:346 +#: src/view/shell/Drawer.tsx:479 +#: src/view/shell/Drawer.tsx:480 +msgid "Feeds" +msgstr "訊息流" + +#: src/screens/Onboarding/StepAlgoFeeds/index.tsx:106 +#~ msgid "Feeds are created by users and organizations. They offer you varied experiences and suggest content you may like using algorithms." +#~ msgstr "訊息流由使用者和組織建立,結合演算法為你推薦可能喜歡的內容,可為你帶來不一樣的體驗。" + +#: src/view/com/auth/onboarding/RecommendedFeeds.tsx:57 +msgid "Feeds are created by users to curate content. Choose some feeds that you find interesting." +msgstr "訊息流由使用者建立並管理。選擇一些你覺得有趣的訊息流。" + +#: src/view/screens/SavedFeeds.tsx:156 +msgid "Feeds are custom algorithms that users build with a little coding expertise. <0/> for more information." +msgstr "訊息流是使用者用一點程式技能建立的自訂演算法。更多資訊請見 <0/>。" + +#: src/screens/Onboarding/StepTopicalFeeds.tsx:76 +msgid "Feeds can be topical as well!" +msgstr "訊息流也可以圍繞某些話題!" + +#: src/view/com/modals/ChangeHandle.tsx:482 +msgid "File Contents" +msgstr "" + +#: src/lib/moderation/useLabelBehaviorDescription.ts:66 +msgid "Filter from feeds" +msgstr "" + +#: src/screens/Onboarding/StepFinished.tsx:151 +msgid "Finalizing" +msgstr "最終確定" + +#: src/view/com/posts/CustomFeedEmptyState.tsx:47 +#: src/view/com/posts/FollowingEmptyState.tsx:57 +#: src/view/com/posts/FollowingEndOfFeed.tsx:58 +msgid "Find accounts to follow" +msgstr "尋找一些要跟隨的帳號" + +#: src/view/screens/Search/Search.tsx:441 +msgid "Find users on Bluesky" +msgstr "在 Bluesky 上尋找使用者" + +#: src/view/screens/Search/Search.tsx:439 +msgid "Find users with the search tool on the right" +msgstr "使用右側的搜尋工具尋找使用者" + +#: src/view/com/auth/onboarding/RecommendedFollowsItem.tsx:155 +msgid "Finding similar accounts..." +msgstr "正在尋找相似的帳號…" + +#: src/view/screens/PreferencesFollowingFeed.tsx:111 +msgid "Fine-tune the content you see on your Following feed." +msgstr "" + +#: src/view/screens/PreferencesHomeFeed.tsx:111 +#~ msgid "Fine-tune the content you see on your home screen." +#~ msgstr "調整你在首頁上所看到的內容。" + +#: src/view/screens/PreferencesThreads.tsx:60 +msgid "Fine-tune the discussion threads." +msgstr "調整討論主題。" + +#: src/screens/Onboarding/index.tsx:38 +msgid "Fitness" +msgstr "健康" + +#: src/screens/Onboarding/StepFinished.tsx:131 +msgid "Flexible" +msgstr "靈活" + +#: src/view/com/modals/EditImage.tsx:115 +msgid "Flip horizontal" +msgstr "水平翻轉" + +#: src/view/com/modals/EditImage.tsx:120 +#: src/view/com/modals/EditImage.tsx:287 +msgid "Flip vertically" +msgstr "垂直翻轉" + +#: src/screens/Onboarding/StepSuggestedAccounts/index.tsx:181 +#: src/screens/Profile/Header/ProfileHeaderStandard.tsx:229 +#: src/view/com/auth/onboarding/RecommendedFollowsItem.tsx:141 +#: src/view/com/post-thread/PostThreadFollowBtn.tsx:139 +#: src/view/com/profile/ProfileHeaderSuggestedFollows.tsx:246 +msgid "Follow" +msgstr "跟隨" + +#: src/view/com/profile/FollowButton.tsx:69 +msgctxt "action" +msgid "Follow" +msgstr "跟隨" + +#: src/screens/Onboarding/StepSuggestedAccounts/index.tsx:58 +#: src/screens/Profile/Header/ProfileHeaderStandard.tsx:214 +#: src/view/com/post-thread/PostThreadFollowBtn.tsx:125 +msgid "Follow {0}" +msgstr "跟隨 {0}" + +#: src/view/com/profile/ProfileMenu.tsx:242 +#: src/view/com/profile/ProfileMenu.tsx:253 +msgid "Follow Account" +msgstr "跟隨帳號" + +#: src/screens/Onboarding/StepSuggestedAccounts/index.tsx:179 +msgid "Follow All" +msgstr "跟隨所有" + +#: src/screens/Onboarding/StepSuggestedAccounts/index.tsx:174 +msgid "Follow selected accounts and continue to the next step" +msgstr "跟隨選擇的使用者並繼續下一步" + +#: src/view/com/auth/onboarding/RecommendedFollows.tsx:64 +msgid "Follow some users to get started. We can recommend you more users based on who you find interesting." +msgstr "跟隨一些使用者以開始,我們可以根據你感興趣的使用者向你推薦更多相似使用者。" + +#: src/view/com/profile/ProfileCard.tsx:216 +msgid "Followed by {0}" +msgstr "由 {0} 跟隨" + +#: src/view/com/modals/Threadgate.tsx:98 +msgid "Followed users" +msgstr "已跟隨的使用者" + +#: src/view/screens/PreferencesFollowingFeed.tsx:154 +msgid "Followed users only" +msgstr "僅限已跟隨的使用者" + +#: src/view/com/notifications/FeedItem.tsx:170 +msgid "followed you" +msgstr "已跟隨" + +#: src/view/com/profile/ProfileFollowers.tsx:109 +#: src/view/screens/ProfileFollowers.tsx:25 +msgid "Followers" +msgstr "跟隨者" + +#: src/screens/Profile/Header/ProfileHeaderStandard.tsx:227 +#: src/view/com/post-thread/PostThreadFollowBtn.tsx:139 +#: src/view/com/profile/ProfileFollows.tsx:108 +#: src/view/screens/ProfileFollows.tsx:25 +msgid "Following" +msgstr "跟隨中" + +#: src/screens/Profile/Header/ProfileHeaderStandard.tsx:89 +msgid "Following {0}" +msgstr "跟隨中:{0}" + +#: src/view/screens/Settings/index.tsx:553 +msgid "Following feed preferences" +msgstr "" + +#: src/Navigation.tsx:262 +#: src/view/com/home/HomeHeaderLayout.web.tsx:50 +#: src/view/com/home/HomeHeaderLayoutMobile.tsx:84 +#: src/view/screens/PreferencesFollowingFeed.tsx:104 +#: src/view/screens/Settings/index.tsx:562 +msgid "Following Feed Preferences" +msgstr "" + +#: src/screens/Profile/Header/Handle.tsx:24 +msgid "Follows you" +msgstr "跟隨你" + +#: src/view/com/profile/ProfileCard.tsx:141 +msgid "Follows You" +msgstr "跟隨你" + +#: src/screens/Onboarding/index.tsx:43 +msgid "Food" +msgstr "食物" + +#: src/view/com/modals/DeleteAccount.tsx:111 +msgid "For security reasons, we'll need to send a confirmation code to your email address." +msgstr "為了保護你的帳號安全,我們需要將驗證碼發送到你的電子郵件地址。" + +#: src/view/com/modals/AddAppPasswords.tsx:209 +msgid "For security reasons, you won't be able to view this again. If you lose this password, you'll need to generate a new one." +msgstr "為了保護你的帳號安全,你將無法再次查看此內容。如果你丟失了此密碼,你將需要產生一個新密碼。" + +#: src/view/com/auth/login/LoginForm.tsx:244 +msgid "Forgot" +msgstr "忘記" + +#: src/view/com/auth/login/LoginForm.tsx:241 +msgid "Forgot password" +msgstr "忘記密碼" + +#: src/view/com/auth/login/Login.tsx:127 +#: src/view/com/auth/login/Login.tsx:143 +msgid "Forgot Password" +msgstr "忘記密碼" + +#: src/lib/moderation/useReportOptions.ts:52 +msgid "Frequently Posts Unwanted Content" +msgstr "" + +#: src/screens/Hashtag.tsx:108 +#: src/screens/Hashtag.tsx:148 +msgid "From @{sanitizedAuthor}" +msgstr "" + +#: src/view/com/posts/FeedItem.tsx:179 +msgctxt "from-feed" +msgid "From <0/>" +msgstr "來自 <0/>" + +#: src/view/com/composer/photos/SelectPhotoBtn.tsx:43 +msgid "Gallery" +msgstr "相簿" + +#: src/view/com/modals/VerifyEmail.tsx:189 +#: src/view/com/modals/VerifyEmail.tsx:191 +msgid "Get Started" +msgstr "開始" + +#: src/lib/moderation/useReportOptions.ts:37 +msgid "Glaring violations of law or terms of service" +msgstr "" + +#: src/components/moderation/ScreenHider.tsx:144 +#: src/components/moderation/ScreenHider.tsx:153 +#: src/view/com/auth/LoggedOut.tsx:81 +#: src/view/com/auth/LoggedOut.tsx:82 +#: src/view/screens/NotFound.tsx:55 +#: src/view/screens/ProfileFeed.tsx:111 +#: src/view/screens/ProfileList.tsx:916 +#: src/view/shell/desktop/LeftNav.tsx:108 +msgid "Go back" +msgstr "返回" + +#: src/screens/Profile/ErrorState.tsx:62 +#: src/screens/Profile/ErrorState.tsx:66 +#: src/view/screens/NotFound.tsx:54 +#: src/view/screens/ProfileFeed.tsx:116 +#: src/view/screens/ProfileList.tsx:921 +msgid "Go Back" +msgstr "返回" + +#: src/components/ReportDialog/SelectReportOptionView.tsx:74 +#: src/components/ReportDialog/SubmitView.tsx:104 +#: src/screens/Onboarding/Layout.tsx:104 +#: src/screens/Onboarding/Layout.tsx:193 +msgid "Go back to previous step" +msgstr "返回上一步" + +#: src/view/screens/NotFound.tsx:55 +msgid "Go home" +msgstr "" + +#: src/view/screens/NotFound.tsx:54 +msgid "Go Home" +msgstr "" + +#: src/view/screens/Search/Search.tsx:748 +#: src/view/shell/desktop/Search.tsx:263 +msgid "Go to @{queryMaybeHandle}" +msgstr "前往 @{queryMaybeHandle}" + +#: src/view/com/auth/login/ForgotPasswordForm.tsx:189 +#: src/view/com/auth/login/ForgotPasswordForm.tsx:218 +#: src/view/com/auth/login/LoginForm.tsx:291 +#: src/view/com/auth/login/SetNewPasswordForm.tsx:195 +#: src/view/com/modals/ChangePassword.tsx:167 +msgid "Go to next" +msgstr "前往下一步" + +#: src/lib/moderation/useGlobalLabelStrings.ts:46 +msgid "Graphic Media" +msgstr "" + +#: src/view/com/modals/ChangeHandle.tsx:265 +msgid "Handle" +msgstr "帳號代碼" + +#: src/lib/moderation/useReportOptions.ts:32 +msgid "Harassment, trolling, or intolerance" +msgstr "" + +#: src/Navigation.tsx:282 +msgid "Hashtag" +msgstr "" + +#: src/components/RichText.tsx:188 +#~ msgid "Hashtag: {tag}" +#~ msgstr "" + +#: src/components/RichText.tsx:190 +msgid "Hashtag: #{tag}" +msgstr "" + +#: src/view/com/auth/create/CreateAccount.tsx:208 +msgid "Having trouble?" +msgstr "遇到問題?" + +#: src/view/shell/desktop/RightNav.tsx:90 +#: src/view/shell/Drawer.tsx:324 +msgid "Help" +msgstr "幫助" + +#: src/screens/Onboarding/StepSuggestedAccounts/index.tsx:132 +msgid "Here are some accounts for you to follow" +msgstr "這裡有一些你可以跟隨的帳號" + +#: src/screens/Onboarding/StepTopicalFeeds.tsx:85 +msgid "Here are some popular topical feeds. You can choose to follow as many as you like." +msgstr "這裡有一些熱門的話題訊息流。跟隨的訊息流數量沒有限制。" + +#: src/screens/Onboarding/StepTopicalFeeds.tsx:80 +msgid "Here are some topical feeds based on your interests: {interestsText}. You can choose to follow as many as you like." +msgstr "這裡有一些根據您的興趣({interestsText})所推薦的熱門的話題訊息流。跟隨的訊息流數量沒有限制。" + +#: src/view/com/modals/AddAppPasswords.tsx:153 +msgid "Here is your app password." +msgstr "這是你的應用程式專用密碼。" + +#: src/components/moderation/ContentHider.tsx:115 +#: src/components/moderation/GlobalModerationLabelPref.tsx:43 +#: src/components/moderation/PostHider.tsx:107 +#: src/lib/moderation/useLabelBehaviorDescription.ts:15 +#: src/lib/moderation/useLabelBehaviorDescription.ts:20 +#: src/lib/moderation/useLabelBehaviorDescription.ts:25 +#: src/lib/moderation/useLabelBehaviorDescription.ts:30 +#: src/screens/Onboarding/StepModeration/ModerationOption.tsx:52 +#: src/screens/Onboarding/StepModeration/ModerationOption.tsx:76 +#: src/view/com/util/forms/PostDropdownBtn.tsx:328 +msgid "Hide" +msgstr "隱藏" + +#: src/view/com/notifications/FeedItem.tsx:329 +msgctxt "action" +msgid "Hide" +msgstr "隱藏" + +#: src/view/com/util/forms/PostDropdownBtn.tsx:276 +#: src/view/com/util/forms/PostDropdownBtn.tsx:278 +msgid "Hide post" +msgstr "隱藏貼文" + +#: src/components/moderation/ContentHider.tsx:67 +#: src/components/moderation/PostHider.tsx:64 +msgid "Hide the content" +msgstr "隱藏內容" + +#: src/view/com/util/forms/PostDropdownBtn.tsx:325 +msgid "Hide this post?" +msgstr "隱藏這則貼文?" + +#: src/view/com/notifications/FeedItem.tsx:319 +msgid "Hide user list" +msgstr "隱藏使用者列表" + +#: src/view/com/profile/ProfileHeader.tsx:487 +#~ msgid "Hides posts from {0} in your feed" +#~ msgstr "在你的訂閱中隱藏來自 {0} 的貼文" + +#: src/view/com/posts/FeedErrorMessage.tsx:111 +msgid "Hmm, some kind of issue occurred when contacting the feed server. Please let the feed owner know about this issue." +msgstr "唔,與訊息流伺服器連線時發生了某種問題。請告訴該訊息流的擁有者這個問題。" + +#: src/view/com/posts/FeedErrorMessage.tsx:99 +msgid "Hmm, the feed server appears to be misconfigured. Please let the feed owner know about this issue." +msgstr "唔,訊息流伺服器似乎設置錯誤。請告訴該訊息流的擁有者這個問題。" + +#: src/view/com/posts/FeedErrorMessage.tsx:105 +msgid "Hmm, the feed server appears to be offline. Please let the feed owner know about this issue." +msgstr "唔,訊息流伺服器似乎已離線。請告訴該訊息流的擁有者這個問題。" + +#: src/view/com/posts/FeedErrorMessage.tsx:102 +msgid "Hmm, the feed server gave a bad response. Please let the feed owner know about this issue." +msgstr "唔,訊息流伺服器給出了錯誤的回應。請告訴該訊息流的擁有者這個問題。" + +#: src/view/com/posts/FeedErrorMessage.tsx:96 +msgid "Hmm, we're having trouble finding this feed. It may have been deleted." +msgstr "唔,我們無法找到這個訊息流,它可能已被刪除。" + +#: src/screens/Moderation/index.tsx:61 +msgid "Hmmmm, it seems we're having trouble loading this data. See below for more details. If this issue persists, please contact us." +msgstr "" + +#: src/screens/Profile/ErrorState.tsx:31 +msgid "Hmmmm, we couldn't load that moderation service." +msgstr "" + +#: src/Navigation.tsx:454 +#: src/view/shell/bottom-bar/BottomBar.tsx:139 +#: src/view/shell/desktop/LeftNav.tsx:310 +#: src/view/shell/Drawer.tsx:401 +#: src/view/shell/Drawer.tsx:402 +msgid "Home" +msgstr "首頁" + +#: src/Navigation.tsx:247 +#: src/view/com/pager/FeedsTabBarMobile.tsx:123 +#: src/view/screens/PreferencesHomeFeed.tsx:104 +#: src/view/screens/Settings/index.tsx:543 +#~ msgid "Home Feed Preferences" +#~ msgstr "首頁訊息流偏好" + +#: src/view/com/modals/ChangeHandle.tsx:421 +msgid "Host:" +msgstr "" + +#: src/view/com/auth/create/Step1.tsx:75 +#: src/view/com/auth/login/ForgotPasswordForm.tsx:120 +#: src/view/com/modals/ChangeHandle.tsx:280 +msgid "Hosting provider" +msgstr "托管服務提供商" + +#: src/view/com/modals/InAppBrowserConsent.tsx:44 +msgid "How should we open this link?" +msgstr "我們該如何開啟此連結?" + +#: src/view/com/modals/VerifyEmail.tsx:214 +msgid "I have a code" +msgstr "我有驗證碼" + +#: src/view/com/modals/VerifyEmail.tsx:216 +msgid "I have a confirmation code" +msgstr "我有驗證碼" + +#: src/view/com/modals/ChangeHandle.tsx:283 +msgid "I have my own domain" +msgstr "我擁有自己的網域" + +#: src/view/com/lightbox/Lightbox.web.tsx:185 +msgid "If alt text is long, toggles alt text expanded state" +msgstr "替代文字過長時,切換替代文字的展開狀態" + +#: src/view/com/modals/SelfLabel.tsx:127 +msgid "If none are selected, suitable for all ages." +msgstr "若不勾選,則預設為全年齡向。" + +#: src/view/com/auth/create/Policies.tsx:91 +msgid "If you are not yet an adult according to the laws of your country, your parent or legal guardian must read these Terms on your behalf." +msgstr "" + +#: src/view/screens/ProfileList.tsx:610 +msgid "If you delete this list, you won't be able to recover it." +msgstr "" + +#: src/view/com/util/forms/PostDropdownBtn.tsx:316 +msgid "If you remove this post, you won't be able to recover it." +msgstr "" + +#: src/view/com/modals/ChangePassword.tsx:148 +msgid "If you want to change your password, we will send you a code to verify that this is your account." +msgstr "如果你想更改密碼,我們將向你發送一個驗證碼以確認這是你的帳號。" + +#: src/lib/moderation/useReportOptions.ts:36 +msgid "Illegal and Urgent" +msgstr "" + +#: src/view/com/util/images/Gallery.tsx:38 +msgid "Image" +msgstr "圖片" + +#: src/view/com/modals/AltImage.tsx:120 +msgid "Image alt text" +msgstr "圖片替代文字" + +#: src/view/com/util/UserAvatar.tsx:311 +#: src/view/com/util/UserBanner.tsx:118 +#~ msgid "Image options" +#~ msgstr "圖片選項" + +#: src/lib/moderation/useReportOptions.ts:47 +msgid "Impersonation or false claims about identity or affiliation" +msgstr "" + +#: src/view/com/auth/login/SetNewPasswordForm.tsx:138 +msgid "Input code sent to your email for password reset" +msgstr "輸入發送到你電子郵件地址的重設碼以重設密碼" + +#: src/view/com/modals/DeleteAccount.tsx:184 +msgid "Input confirmation code for account deletion" +msgstr "輸入刪除帳號的驗證碼" + +#: src/view/com/auth/create/Step1.tsx:177 +msgid "Input email for Bluesky account" +msgstr "輸入 Bluesky 帳號的電子郵件地址" + +#: src/view/com/auth/create/Step1.tsx:151 +msgid "Input invite code to proceed" +msgstr "輸入邀請碼以繼續" + +#: src/view/com/modals/AddAppPasswords.tsx:180 +msgid "Input name for app password" +msgstr "輸入應用程式專用密碼名稱" + +#: src/view/com/auth/login/SetNewPasswordForm.tsx:162 +msgid "Input new password" +msgstr "輸入新密碼" + +#: src/view/com/modals/DeleteAccount.tsx:203 +msgid "Input password for account deletion" +msgstr "輸入密碼以刪除帳號" + +#: src/view/com/auth/create/Step2.tsx:196 +#~ msgid "Input phone number for SMS verification" +#~ msgstr "輸入手機號碼進行簡訊驗證" + +#: src/view/com/auth/login/LoginForm.tsx:233 +msgid "Input the password tied to {identifier}" +msgstr "輸入與 {identifier} 關聯的密碼" + +#: src/view/com/auth/login/LoginForm.tsx:200 +msgid "Input the username or email address you used at signup" +msgstr "輸入註冊時使用的使用者名稱或電子郵件地址" + +#: src/view/com/auth/create/Step2.tsx:271 +#~ msgid "Input the verification code we have texted to you" +#~ msgstr "輸入我們發送到你手機的驗證碼" + +#: src/view/com/modals/Waitlist.tsx:90 +#~ msgid "Input your email to get on the Bluesky waitlist" +#~ msgstr "輸入你的電子郵件地址以加入 Bluesky 候補列表" + +#: src/view/com/auth/login/LoginForm.tsx:232 +msgid "Input your password" +msgstr "輸入你的密碼" + +#: src/view/com/modals/ChangeHandle.tsx:390 +msgid "Input your preferred hosting provider" +msgstr "" + +#: src/view/com/auth/create/Step2.tsx:80 +msgid "Input your user handle" +msgstr "輸入你的帳號代碼" + +#: src/view/com/post-thread/PostThreadItem.tsx:221 +msgid "Invalid or unsupported post record" +msgstr "無效或不支援的貼文紀錄" + +#: src/view/com/auth/login/LoginForm.tsx:116 +msgid "Invalid username or password" +msgstr "使用者名稱或密碼無效" + +#: src/view/screens/Settings.tsx:411 +#~ msgid "Invite" +#~ msgstr "邀請" + +#: src/view/com/modals/InviteCodes.tsx:93 +msgid "Invite a Friend" +msgstr "邀請朋友" + +#: src/view/com/auth/create/Step1.tsx:141 +#: src/view/com/auth/create/Step1.tsx:150 +msgid "Invite code" +msgstr "邀請碼" + +#: src/view/com/auth/create/state.ts:158 +msgid "Invite code not accepted. Check that you input it correctly and try again." +msgstr "邀請碼無效。請檢查你輸入的內容是否正確,然後重試。" + +#: src/view/com/modals/InviteCodes.tsx:170 +msgid "Invite codes: {0} available" +msgstr "邀請碼:{0} 個可用" + +#: src/view/shell/Drawer.tsx:645 +#~ msgid "Invite codes: {invitesAvailable} available" +#~ msgstr "邀請碼:{invitesAvailable} 個可用" + +#: src/view/com/modals/InviteCodes.tsx:169 +msgid "Invite codes: 1 available" +msgstr "邀請碼:1 個可用" + +#: src/screens/Onboarding/StepFollowingFeed.tsx:64 +msgid "It shows posts from the people you follow as they happen." +msgstr "它會即時顯示你所跟隨的人發佈的貼文。" + +#: src/view/com/auth/HomeLoggedOutCTA.tsx:103 +#: src/view/com/auth/SplashScreen.web.tsx:138 +msgid "Jobs" +msgstr "工作" + +#: src/view/com/modals/Waitlist.tsx:67 +#~ msgid "Join the waitlist" +#~ msgstr "加入候補列表" + +#: src/view/com/auth/create/Step1.tsx:174 +#: src/view/com/auth/create/Step1.tsx:178 +#~ msgid "Join the waitlist." +#~ msgstr "加入候補列表。" + +#: src/view/com/modals/Waitlist.tsx:128 +#~ msgid "Join Waitlist" +#~ msgstr "加入候補列表" + +#: src/screens/Onboarding/index.tsx:24 +msgid "Journalism" +msgstr "新聞學" + +#: src/components/moderation/LabelsOnMe.tsx:59 +msgid "label has been placed on this {labelTarget}" +msgstr "" + +#: src/components/moderation/ContentHider.tsx:144 +msgid "Labeled by {0}." +msgstr "" + +#: src/components/moderation/ContentHider.tsx:142 +msgid "Labeled by the author." +msgstr "" + +#: src/view/screens/Profile.tsx:186 +msgid "Labels" +msgstr "" + +#: src/screens/Profile/Sections/Labels.tsx:143 +msgid "Labels are annotations on users and content. They can be used to hide, warn, and categorize the network." +msgstr "" + +#: src/components/moderation/LabelsOnMe.tsx:61 +msgid "labels have been placed on this {labelTarget}" +msgstr "" + +#: src/components/moderation/LabelsOnMeDialog.tsx:63 +msgid "Labels on your account" +msgstr "" + +#: src/components/moderation/LabelsOnMeDialog.tsx:65 +msgid "Labels on your content" +msgstr "" + +#: src/view/com/composer/select-language/SelectLangBtn.tsx:104 +msgid "Language selection" +msgstr "語言選擇" + +#: src/view/screens/Settings/index.tsx:614 +msgid "Language settings" +msgstr "語言設定" + +#: src/Navigation.tsx:144 +#: src/view/screens/LanguageSettings.tsx:89 +msgid "Language Settings" +msgstr "語言設定" + +#: src/view/screens/Settings/index.tsx:623 +msgid "Languages" +msgstr "語言" + +#: src/view/com/auth/create/StepHeader.tsx:20 +msgid "Last step!" +msgstr "最後一步!" + +#: src/view/com/util/moderation/ContentHider.tsx:103 +#~ msgid "Learn more" +#~ msgstr "瞭解詳情" + +#: src/components/moderation/ScreenHider.tsx:129 +msgid "Learn More" +msgstr "瞭解詳情" + +#: src/components/moderation/ContentHider.tsx:65 +#: src/components/moderation/ContentHider.tsx:128 +msgid "Learn more about the moderation applied to this content." +msgstr "" + +#: src/components/moderation/PostHider.tsx:85 +#: src/components/moderation/ScreenHider.tsx:126 +msgid "Learn more about this warning" +msgstr "瞭解有關此警告的更多資訊" + +#: src/screens/Moderation/index.tsx:551 +msgid "Learn more about what is public on Bluesky." +msgstr "瞭解有關 Bluesky 上公開內容的更多資訊。" + +#: src/components/moderation/ContentHider.tsx:152 +msgid "Learn more." +msgstr "瞭解詳情" + +#: src/view/com/modals/lang-settings/ContentLanguagesSettings.tsx:82 +msgid "Leave them all unchecked to see any language." +msgstr "全部留空以查看所有語言。" + +#: src/view/com/modals/LinkWarning.tsx:51 +msgid "Leaving Bluesky" +msgstr "離開 Bluesky" + +#: src/screens/Deactivated.tsx:128 +msgid "left to go." +msgstr "尚未完成。" + +#: src/view/screens/Settings/index.tsx:296 +msgid "Legacy storage cleared, you need to restart the app now." +msgstr "舊儲存資料已清除,你需要立即重新啟動應用程式。" + +#: src/view/com/auth/login/Login.tsx:128 +#: src/view/com/auth/login/Login.tsx:144 +msgid "Let's get your password reset!" +msgstr "讓我們來重設你的密碼吧!" + +#: src/screens/Onboarding/StepFinished.tsx:151 +msgid "Let's go!" +msgstr "讓我們開始吧!" + +#: src/view/com/util/UserAvatar.tsx:248 +#: src/view/com/util/UserBanner.tsx:62 +#~ msgid "Library" +#~ msgstr "圖片庫" + +#: src/view/screens/Settings/index.tsx:498 +msgid "Light" +msgstr "亮色" + +#: src/view/com/util/post-ctrls/PostCtrls.tsx:185 +msgid "Like" +msgstr "喜歡" + +#: src/screens/Profile/Header/ProfileHeaderLabeler.tsx:257 +#: src/view/screens/ProfileFeed.tsx:572 +msgid "Like this feed" +msgstr "喜歡這個訊息流" + +#: src/components/LikesDialog.tsx:87 +#: src/Navigation.tsx:201 +#: src/Navigation.tsx:206 +msgid "Liked by" +msgstr "喜歡" + +#: src/screens/Profile/ProfileLabelerLikedBy.tsx:42 +#: src/view/screens/PostLikedBy.tsx:27 +#: src/view/screens/ProfileFeedLikedBy.tsx:27 +msgid "Liked By" +msgstr "喜歡" + +#: src/view/com/feeds/FeedSourceCard.tsx:268 +msgid "Liked by {0} {1}" +msgstr "{0} 個 {1} 喜歡" + +#: src/components/LabelingServiceCard/index.tsx:72 +msgid "Liked by {count} {0}" +msgstr "" + +#: src/screens/Profile/Header/ProfileHeaderLabeler.tsx:277 +#: src/screens/Profile/Header/ProfileHeaderLabeler.tsx:291 +#: src/view/screens/ProfileFeed.tsx:587 +msgid "Liked by {likeCount} {0}" +msgstr "{likeCount} 個 {0} 喜歡" + +#: src/view/com/notifications/FeedItem.tsx:174 +msgid "liked your custom feed" +msgstr "喜歡你的自訂訊息流" + +#: src/view/com/notifications/FeedItem.tsx:159 +msgid "liked your post" +msgstr "喜歡你的貼文" + +#: src/view/screens/Profile.tsx:191 +msgid "Likes" +msgstr "喜歡" + +#: src/view/com/post-thread/PostThreadItem.tsx:182 +msgid "Likes on this post" +msgstr "這條貼文的喜歡數" + +#: src/Navigation.tsx:170 +msgid "List" +msgstr "列表" + +#: src/view/com/modals/CreateOrEditList.tsx:261 +msgid "List Avatar" +msgstr "列表頭像" + +#: src/view/screens/ProfileList.tsx:311 +msgid "List blocked" +msgstr "列表已封鎖" + +#: src/view/com/feeds/FeedSourceCard.tsx:220 +msgid "List by {0}" +msgstr "列表由 {0} 建立" + +#: src/view/screens/ProfileList.tsx:355 +msgid "List deleted" +msgstr "列表已刪除" + +#: src/view/screens/ProfileList.tsx:283 +msgid "List muted" +msgstr "列表已靜音" + +#: src/view/com/modals/CreateOrEditList.tsx:275 +msgid "List Name" +msgstr "列表名稱" + +#: src/view/screens/ProfileList.tsx:325 +msgid "List unblocked" +msgstr "解除封鎖列表" + +#: src/view/screens/ProfileList.tsx:297 +msgid "List unmuted" +msgstr "解除靜音列表" + +#: src/Navigation.tsx:114 +#: src/view/screens/Profile.tsx:187 +#: src/view/screens/Profile.tsx:193 +#: src/view/shell/desktop/LeftNav.tsx:383 +#: src/view/shell/Drawer.tsx:495 +#: src/view/shell/Drawer.tsx:496 +msgid "Lists" +msgstr "列表" + +#: src/view/com/post-thread/PostThread.tsx:333 +#: src/view/com/post-thread/PostThread.tsx:341 +#~ msgid "Load more posts" +#~ msgstr "載入更多貼文" + +#: src/view/screens/Notifications.tsx:159 +msgid "Load new notifications" +msgstr "載入新的通知" + +#: src/screens/Profile/Sections/Feed.tsx:70 +#: src/view/com/feeds/FeedPage.tsx:124 +#: src/view/screens/ProfileFeed.tsx:495 +#: src/view/screens/ProfileList.tsx:695 +msgid "Load new posts" +msgstr "載入新的貼文" + +#: src/view/com/composer/text-input/mobile/Autocomplete.tsx:99 +msgid "Loading..." +msgstr "載入中…" + +#: src/view/com/modals/ServerInput.tsx:50 +#~ msgid "Local dev server" +#~ msgstr "本地開發伺服器" + +#: src/Navigation.tsx:221 +msgid "Log" +msgstr "日誌" + +#: src/screens/Deactivated.tsx:149 +#: src/screens/Deactivated.tsx:152 +#: src/screens/Deactivated.tsx:178 +#: src/screens/Deactivated.tsx:181 +msgid "Log out" +msgstr "登出" + +#: src/screens/Moderation/index.tsx:444 +msgid "Logged-out visibility" +msgstr "登出可見性" + +#: src/view/com/auth/login/ChooseAccountForm.tsx:142 +msgid "Login to account that is not listed" +msgstr "登入未列出的帳號" + +#: src/view/com/modals/LinkWarning.tsx:65 +msgid "Make sure this is where you intend to go!" +msgstr "請確認這是你想要去的的地方!" + +#: src/components/dialogs/MutedWords.tsx:83 +msgid "Manage your muted words and tags" +msgstr "" + +#: src/view/com/auth/create/Step2.tsx:118 +msgid "May not be longer than 253 characters" +msgstr "" + +#: src/view/com/auth/create/Step2.tsx:109 +msgid "May only contain letters and numbers" +msgstr "" + +#: src/view/screens/Profile.tsx:190 +msgid "Media" +msgstr "媒體" + +#: src/view/com/threadgate/WhoCanReply.tsx:139 +msgid "mentioned users" +msgstr "提及的使用者" + +#: src/view/com/modals/Threadgate.tsx:93 +msgid "Mentioned users" +msgstr "提及的使用者" + +#: src/view/com/util/ViewHeader.tsx:87 +#: src/view/screens/Search/Search.tsx:647 +msgid "Menu" +msgstr "選單" + +#: src/view/com/posts/FeedErrorMessage.tsx:192 +msgid "Message from server: {0}" +msgstr "來自伺服器的訊息:{0}" + +#: src/lib/moderation/useReportOptions.ts:45 +msgid "Misleading Account" +msgstr "" + +#: src/Navigation.tsx:119 +#: src/screens/Moderation/index.tsx:106 +#: src/view/screens/Settings/index.tsx:645 +#: src/view/shell/desktop/LeftNav.tsx:401 +#: src/view/shell/Drawer.tsx:514 +#: src/view/shell/Drawer.tsx:515 +msgid "Moderation" +msgstr "限制" + +#: src/components/moderation/ModerationDetailsDialog.tsx:113 +msgid "Moderation details" +msgstr "" + +#: src/view/com/lists/ListCard.tsx:93 +#: src/view/com/modals/UserAddRemoveLists.tsx:206 +msgid "Moderation list by {0}" +msgstr "{0} 建立的限制列表" + +#: src/view/screens/ProfileList.tsx:789 +msgid "Moderation list by <0/>" +msgstr " 建立的限制列表" + +#: src/view/com/lists/ListCard.tsx:91 +#: src/view/com/modals/UserAddRemoveLists.tsx:204 +#: src/view/screens/ProfileList.tsx:787 +msgid "Moderation list by you" +msgstr "你建立的限制列表" + +#: src/view/com/modals/CreateOrEditList.tsx:197 +msgid "Moderation list created" +msgstr "已建立限制列表" + +#: src/view/com/modals/CreateOrEditList.tsx:183 +msgid "Moderation list updated" +msgstr "限制列表已更新" + +#: src/screens/Moderation/index.tsx:245 +msgid "Moderation lists" +msgstr "限制列表" + +#: src/Navigation.tsx:124 +#: src/view/screens/ModerationModlists.tsx:58 +msgid "Moderation Lists" +msgstr "限制列表" + +#: src/view/screens/Settings/index.tsx:639 +msgid "Moderation settings" +msgstr "限制設定" + +#: src/Navigation.tsx:216 +msgid "Moderation states" +msgstr "" + +#: src/screens/Moderation/index.tsx:217 +msgid "Moderation tools" +msgstr "" + +#: src/components/moderation/ModerationDetailsDialog.tsx:49 +#: src/lib/moderation/useModerationCauseDescription.ts:40 +msgid "Moderator has chosen to set a general warning on the content." +msgstr "限制選擇對內容設定一般警告。" + +#: src/view/com/post-thread/PostThreadItem.tsx:541 +msgid "More" +msgstr "" + +#: src/view/shell/desktop/Feeds.tsx:65 +msgid "More feeds" +msgstr "更多訊息流" + +#: src/view/screens/ProfileList.tsx:599 +msgid "More options" +msgstr "更多選項" + +#: src/view/com/util/forms/PostDropdownBtn.tsx:315 +#~ msgid "More post options" +#~ msgstr "更多貼文選項" + +#: src/view/screens/PreferencesThreads.tsx:82 +msgid "Most-liked replies first" +msgstr "最多按喜歡數優先" + +#: src/view/com/auth/create/Step2.tsx:122 +msgid "Must be at least 3 characters" +msgstr "" + +#: src/components/TagMenu/index.tsx:249 +msgid "Mute" +msgstr "" + +#: src/components/TagMenu/index.web.tsx:105 +msgid "Mute {truncatedTag}" +msgstr "" + +#: src/view/com/profile/ProfileMenu.tsx:279 +#: src/view/com/profile/ProfileMenu.tsx:286 +msgid "Mute Account" +msgstr "靜音帳號" + +#: src/view/screens/ProfileList.tsx:518 +msgid "Mute accounts" +msgstr "靜音帳號" + +#: src/components/TagMenu/index.tsx:209 +msgid "Mute all {displayTag} posts" +msgstr "" + +#: src/components/TagMenu/index.tsx:211 +#~ msgid "Mute all {tag} posts" +#~ msgstr "" + +#: src/components/dialogs/MutedWords.tsx:149 +msgid "Mute in tags only" +msgstr "" + +#: src/components/dialogs/MutedWords.tsx:134 +msgid "Mute in text & tags" +msgstr "" + +#: src/view/screens/ProfileList.tsx:461 +#: src/view/screens/ProfileList.tsx:624 +msgid "Mute list" +msgstr "靜音列表" + +#: src/view/screens/ProfileList.tsx:619 +msgid "Mute these accounts?" +msgstr "靜音這些帳號?" + +#: src/view/screens/ProfileList.tsx:279 +#~ msgid "Mute this List" +#~ msgstr "靜音這個列表" + +#: src/components/dialogs/MutedWords.tsx:127 +msgid "Mute this word in post text and tags" +msgstr "在帖子文本和话题标签中隐藏该词" + +#: src/components/dialogs/MutedWords.tsx:142 +msgid "Mute this word in tags only" +msgstr "仅在话题标签中隐藏该词" + +#: src/view/com/util/forms/PostDropdownBtn.tsx:251 +#: src/view/com/util/forms/PostDropdownBtn.tsx:257 +msgid "Mute thread" +msgstr "靜音對話串" + +#: src/view/com/util/forms/PostDropdownBtn.tsx:267 +#: src/view/com/util/forms/PostDropdownBtn.tsx:269 +msgid "Mute words & tags" +msgstr "" + +#: src/view/com/lists/ListCard.tsx:102 +msgid "Muted" +msgstr "已靜音" + +#: src/screens/Moderation/index.tsx:257 +msgid "Muted accounts" +msgstr "已靜音帳號" + +#: src/Navigation.tsx:129 +#: src/view/screens/ModerationMutedAccounts.tsx:107 +msgid "Muted Accounts" +msgstr "已靜音帳號" + +#: src/view/screens/ModerationMutedAccounts.tsx:115 +msgid "Muted accounts have their posts removed from your feed and from your notifications. Mutes are completely private." +msgstr "已靜音的帳號將不會在你的通知或時間線中顯示,被靜音的帳號將不會收到通知。" + +#: src/lib/moderation/useModerationCauseDescription.ts:85 +msgid "Muted by \"{0}\"" +msgstr "" + +#: src/screens/Moderation/index.tsx:233 +msgid "Muted words & tags" +msgstr "" + +#: src/view/screens/ProfileList.tsx:621 +msgid "Muting is private. Muted accounts can interact with you, but you will not see their posts or receive notifications from them." +msgstr "封鎖是私人的。被封鎖的帳號可以與你互動,但你將無法看到他們的貼文或收到來自他們的通知。" + +#: src/components/dialogs/BirthDateSettings.tsx:35 +#: src/components/dialogs/BirthDateSettings.tsx:38 +msgid "My Birthday" +msgstr "我的生日" + +#: src/view/screens/Feeds.tsx:663 +msgid "My Feeds" +msgstr "自定訊息流" + +#: src/view/shell/desktop/LeftNav.tsx:65 +msgid "My Profile" +msgstr "我的個人資料" + +#: src/view/screens/Settings/index.tsx:596 +msgid "My saved feeds" +msgstr "我儲存的訊息流" + +#: src/view/screens/Settings/index.tsx:602 +msgid "My Saved Feeds" +msgstr "我儲存的訊息流" + +#: src/view/com/auth/server-input/index.tsx:118 +#~ msgid "my-server.com" +#~ msgstr "my-server.com" + +#: src/view/com/modals/AddAppPasswords.tsx:179 +#: src/view/com/modals/CreateOrEditList.tsx:290 +msgid "Name" +msgstr "名稱" + +#: src/view/com/modals/CreateOrEditList.tsx:145 +msgid "Name is required" +msgstr "名稱是必填項" + +#: src/lib/moderation/useReportOptions.ts:57 +#: src/lib/moderation/useReportOptions.ts:78 +#: src/lib/moderation/useReportOptions.ts:86 +msgid "Name or Description Violates Community Standards" +msgstr "" + +#: src/screens/Onboarding/index.tsx:25 +msgid "Nature" +msgstr "自然" + +#: src/view/com/auth/login/ForgotPasswordForm.tsx:190 +#: src/view/com/auth/login/ForgotPasswordForm.tsx:219 +#: src/view/com/auth/login/LoginForm.tsx:292 +#: src/view/com/auth/login/SetNewPasswordForm.tsx:196 +#: src/view/com/modals/ChangePassword.tsx:168 +msgid "Navigates to the next screen" +msgstr "切換到下一畫面" + +#: src/view/shell/Drawer.tsx:71 +msgid "Navigates to your profile" +msgstr "切換到你的個人檔案" + +#: src/components/ReportDialog/SelectReportOptionView.tsx:124 +msgid "Need to report a copyright violation?" +msgstr "" + +#: src/view/com/modals/EmbedConsent.tsx:107 +#: src/view/com/modals/EmbedConsent.tsx:123 +msgid "Never load embeds from {0}" +msgstr "永不載入來自 {0} 的嵌入內容" + +#: src/view/com/auth/onboarding/WelcomeDesktop.tsx:72 +#: src/view/com/auth/onboarding/WelcomeMobile.tsx:74 +msgid "Never lose access to your followers and data." +msgstr "永遠不會失去對你的跟隨者和資料的存取權。" + +#: src/screens/Onboarding/StepFinished.tsx:119 +msgid "Never lose access to your followers or data." +msgstr "永遠不會失去對你的跟隨者或資料的存取權。" + +#: src/components/dialogs/MutedWords.tsx:293 +#~ msgid "Nevermind" +#~ msgstr "" + +#: src/view/com/modals/ChangeHandle.tsx:520 +msgid "Nevermind, create a handle for me" +msgstr "" + +#: src/view/screens/Lists.tsx:76 +msgctxt "action" +msgid "New" +msgstr "新增" + +#: src/view/screens/ModerationModlists.tsx:78 +msgid "New" +msgstr "新增" + +#: src/view/com/modals/CreateOrEditList.tsx:252 +msgid "New Moderation List" +msgstr "新的限制列表" + +#: src/view/com/auth/login/SetNewPasswordForm.tsx:150 +#: src/view/com/modals/ChangePassword.tsx:212 +msgid "New password" +msgstr "新密碼" + +#: src/view/com/modals/ChangePassword.tsx:217 +msgid "New Password" +msgstr "新密碼" + +#: src/view/com/feeds/FeedPage.tsx:135 +msgctxt "action" +msgid "New post" +msgstr "新貼文" + +#: src/view/screens/Feeds.tsx:555 +#: src/view/screens/Notifications.tsx:168 +#: src/view/screens/Profile.tsx:450 +#: src/view/screens/ProfileFeed.tsx:433 +#: src/view/screens/ProfileList.tsx:199 +#: src/view/screens/ProfileList.tsx:227 +#: src/view/shell/desktop/LeftNav.tsx:252 +msgid "New post" +msgstr "新貼文" + +#: src/view/shell/desktop/LeftNav.tsx:262 +msgctxt "action" +msgid "New Post" +msgstr "新貼文" + +#: src/view/com/modals/CreateOrEditList.tsx:247 +msgid "New User List" +msgstr "新的使用者列表" + +#: src/view/screens/PreferencesThreads.tsx:79 +msgid "Newest replies first" +msgstr "最新回覆優先" + +#: src/screens/Onboarding/index.tsx:23 +msgid "News" +msgstr "新聞" + +#: src/view/com/auth/create/CreateAccount.tsx:172 +#: src/view/com/auth/login/ForgotPasswordForm.tsx:182 +#: src/view/com/auth/login/ForgotPasswordForm.tsx:192 +#: src/view/com/auth/login/LoginForm.tsx:294 +#: src/view/com/auth/login/SetNewPasswordForm.tsx:187 +#: src/view/com/auth/login/SetNewPasswordForm.tsx:198 +#: src/view/com/auth/onboarding/RecommendedFeeds.tsx:79 +#: src/view/com/modals/ChangePassword.tsx:253 +#: src/view/com/modals/ChangePassword.tsx:255 +msgid "Next" +msgstr "下一個" + +#: src/view/com/auth/onboarding/WelcomeDesktop.tsx:103 +msgctxt "action" +msgid "Next" +msgstr "下一個" + +#: src/view/com/lightbox/Lightbox.web.tsx:169 +msgid "Next image" +msgstr "下一張圖片" + +#: src/view/screens/PreferencesFollowingFeed.tsx:129 +#: src/view/screens/PreferencesFollowingFeed.tsx:200 +#: src/view/screens/PreferencesFollowingFeed.tsx:235 +#: src/view/screens/PreferencesFollowingFeed.tsx:272 +#: src/view/screens/PreferencesThreads.tsx:106 +#: src/view/screens/PreferencesThreads.tsx:129 +msgid "No" +msgstr "關" + +#: src/view/screens/ProfileFeed.tsx:561 +#: src/view/screens/ProfileList.tsx:769 +msgid "No description" +msgstr "沒有描述" + +#: src/view/com/modals/ChangeHandle.tsx:406 +msgid "No DNS Panel" +msgstr "" + +#: src/screens/Profile/Header/ProfileHeaderStandard.tsx:111 +msgid "No longer following {0}" +msgstr "不再跟隨 {0}" + +#: src/view/com/notifications/Feed.tsx:109 +msgid "No notifications yet!" +msgstr "還沒有通知!" + +#: src/view/com/composer/text-input/mobile/Autocomplete.tsx:101 +#: src/view/com/composer/text-input/web/Autocomplete.tsx:195 +msgid "No result" +msgstr "沒有結果" + +#: src/components/Lists.tsx:189 +msgid "No results found" +msgstr "未找到結果" + +#: src/view/screens/Feeds.tsx:495 +msgid "No results found for \"{query}\"" +msgstr "未找到「{query}」的結果" + +#: src/view/com/modals/ListAddRemoveUsers.tsx:127 +#: src/view/screens/Search/Search.tsx:282 +#: src/view/screens/Search/Search.tsx:310 +msgid "No results found for {query}" +msgstr "未找到 {query} 的結果" + +#: src/view/com/modals/EmbedConsent.tsx:129 +msgid "No thanks" +msgstr "不,謝謝" + +#: src/view/com/modals/Threadgate.tsx:82 +msgid "Nobody" +msgstr "沒有人" + +#: src/components/LikedByList.tsx:102 +#: src/components/LikesDialog.tsx:99 +msgid "Nobody has liked this yet. Maybe you should be the first!" +msgstr "" + +#: src/lib/moderation/useGlobalLabelStrings.ts:42 +msgid "Non-sexual Nudity" +msgstr "" + +#: src/view/com/modals/SelfLabel.tsx:135 +msgid "Not Applicable." +msgstr "不適用。" + +#: src/Navigation.tsx:109 +#: src/view/screens/Profile.tsx:97 +msgid "Not Found" +msgstr "未找到" + +#: src/view/com/modals/VerifyEmail.tsx:246 +#: src/view/com/modals/VerifyEmail.tsx:252 +msgid "Not right now" +msgstr "暫時不需要" + +#: src/view/com/profile/ProfileMenu.tsx:368 +#: src/view/com/util/forms/PostDropdownBtn.tsx:342 +msgid "Note about sharing" +msgstr "" + +#: src/screens/Moderation/index.tsx:542 +msgid "Note: Bluesky is an open and public network. This setting only limits the visibility of your content on the Bluesky app and website, and other apps may not respect this setting. Your content may still be shown to logged-out users by other apps and websites." +msgstr "注意:Bluesky 是一個開放且公開的網路。此設定僅限制你在 Bluesky 應用程式和網站上的內容可見性,其他應用程式可能不尊重此設定。你的內容仍可能由其他應用程式和網站顯示給未登入的使用者。" + +#: src/Navigation.tsx:469 +#: src/view/screens/Notifications.tsx:124 +#: src/view/screens/Notifications.tsx:148 +#: src/view/shell/bottom-bar/BottomBar.tsx:207 +#: src/view/shell/desktop/LeftNav.tsx:365 +#: src/view/shell/Drawer.tsx:438 +#: src/view/shell/Drawer.tsx:439 +msgid "Notifications" +msgstr "通知" + +#: src/view/com/modals/SelfLabel.tsx:103 +msgid "Nudity" +msgstr "裸露" + +#: src/lib/moderation/useReportOptions.ts:71 +msgid "Nudity or pornography not labeled as such" +msgstr "" + +#: src/lib/moderation/useLabelBehaviorDescription.ts:11 +msgid "Off" +msgstr "" + +#: src/view/com/util/ErrorBoundary.tsx:49 +msgid "Oh no!" +msgstr "糟糕!" + +#: src/screens/Onboarding/StepInterests/index.tsx:128 +msgid "Oh no! Something went wrong." +msgstr "糟糕!發生了一些錯誤。" + +#: src/screens/Onboarding/StepModeration/AdultContentEnabledPref.tsx:127 +msgid "OK" +msgstr "" + +#: src/view/com/auth/login/PasswordUpdatedForm.tsx:41 +msgid "Okay" +msgstr "好的" + +#: src/view/screens/PreferencesThreads.tsx:78 +msgid "Oldest replies first" +msgstr "最舊的回覆優先" + +#: src/view/screens/Settings/index.tsx:244 +msgid "Onboarding reset" +msgstr "重新開始引導流程" + +#: src/view/com/composer/Composer.tsx:391 +msgid "One or more images is missing alt text." +msgstr "至少有一張圖片缺失了替代文字。" + +#: src/view/com/threadgate/WhoCanReply.tsx:100 +msgid "Only {0} can reply." +msgstr "只有 {0} 可以回覆。" + +#: src/components/Lists.tsx:83 +msgid "Oops, something went wrong!" +msgstr "" + +#: src/components/Lists.tsx:157 +#: src/view/screens/AppPasswords.tsx:67 +#: src/view/screens/Profile.tsx:97 +msgid "Oops!" +msgstr "糟糕!" + +#: src/screens/Onboarding/StepFinished.tsx:115 +msgid "Open" +msgstr "開啟" + +#: src/view/screens/Moderation.tsx:75 +#~ msgid "Open content filtering settings" +#~ msgstr "" + +#: src/view/com/composer/Composer.tsx:490 +#: src/view/com/composer/Composer.tsx:491 +msgid "Open emoji picker" +msgstr "開啟表情符號選擇器" + +#: src/view/screens/ProfileFeed.tsx:299 +msgid "Open feed options menu" +msgstr "" + +#: src/view/screens/Settings/index.tsx:734 +msgid "Open links with in-app browser" +msgstr "在內建瀏覽器中開啟連結" + +#: src/screens/Moderation/index.tsx:229 +msgid "Open muted words and tags settings" +msgstr "" + +#: src/view/screens/Moderation.tsx:92 +#~ msgid "Open muted words settings" +#~ msgstr "打开隐藏词设置" + +#: src/view/com/home/HomeHeaderLayoutMobile.tsx:50 +msgid "Open navigation" +msgstr "開啟導覽" + +#: src/view/com/util/forms/PostDropdownBtn.tsx:183 +msgid "Open post options menu" +msgstr "" + +#: src/view/screens/Settings/index.tsx:828 +#: src/view/screens/Settings/index.tsx:838 +msgid "Open storybook page" +msgstr "開啟故事書頁面" + +#: src/view/screens/Settings/index.tsx:816 +msgid "Open system log" +msgstr "" + +#: src/view/com/util/forms/DropdownButton.tsx:154 +msgid "Opens {numItems} options" +msgstr "開啟 {numItems} 個選項" + +#: src/view/screens/Log.tsx:54 +msgid "Opens additional details for a debug entry" +msgstr "開啟除錯項目的額外詳細資訊" + +#: src/view/com/notifications/FeedItem.tsx:353 +msgid "Opens an expanded list of users in this notification" +msgstr "展開此通知的使用者列表" + +#: src/view/com/composer/photos/OpenCameraBtn.tsx:78 +msgid "Opens camera on device" +msgstr "開啟裝置相機" + +#: src/view/com/composer/Prompt.tsx:25 +msgid "Opens composer" +msgstr "開啟編輯器" + +#: src/view/screens/Settings/index.tsx:615 +msgid "Opens configurable language settings" +msgstr "開啟可以更改的語言設定" + +#: src/view/com/composer/photos/SelectPhotoBtn.tsx:44 +msgid "Opens device photo gallery" +msgstr "開啟裝置相簿" + +#: src/view/com/profile/ProfileHeader.tsx:420 +#~ msgid "Opens editor for profile display name, avatar, background image, and description" +#~ msgstr "開啟個人資料(如名稱、頭貼、背景圖片、描述等)編輯器" + +#: src/view/screens/Settings/index.tsx:669 +msgid "Opens external embeds settings" +msgstr "開啟外部嵌入設定" + +#: src/view/com/auth/HomeLoggedOutCTA.tsx:56 +#: src/view/com/auth/SplashScreen.tsx:70 +msgid "Opens flow to create a new Bluesky account" +msgstr "" + +#: src/view/com/auth/HomeLoggedOutCTA.tsx:74 +#: src/view/com/auth/SplashScreen.tsx:83 +msgid "Opens flow to sign into your existing Bluesky account" +msgstr "" + +#: src/view/com/profile/ProfileHeader.tsx:575 +#~ msgid "Opens followers list" +#~ msgstr "開啟跟隨者列表" + +#: src/view/com/profile/ProfileHeader.tsx:594 +#~ msgid "Opens following list" +#~ msgstr "開啟正在跟隨列表" + +#: src/view/screens/Settings.tsx:412 +#~ msgid "Opens invite code list" +#~ msgstr "開啟邀請碼列表" + +#: src/view/com/modals/InviteCodes.tsx:172 +msgid "Opens list of invite codes" +msgstr "開啟邀請碼列表" + +#: src/view/screens/Settings/index.tsx:798 +msgid "Opens modal for account deletion confirmation. Requires email code" +msgstr "" + +#: src/view/screens/Settings/index.tsx:774 +#~ msgid "Opens modal for account deletion confirmation. Requires email code." +#~ msgstr "開啟用於帳號刪除確認的彈窗。需要電子郵件驗證碼。" + +#: src/view/screens/Settings/index.tsx:756 +msgid "Opens modal for changing your Bluesky password" +msgstr "" + +#: src/view/screens/Settings/index.tsx:718 +msgid "Opens modal for choosing a new Bluesky handle" +msgstr "" + +#: src/view/screens/Settings/index.tsx:779 +msgid "Opens modal for downloading your Bluesky account data (repository)" +msgstr "" + +#: src/view/screens/Settings/index.tsx:970 +msgid "Opens modal for email verification" +msgstr "" + +#: src/view/com/modals/ChangeHandle.tsx:281 +msgid "Opens modal for using custom domain" +msgstr "開啟使用自訂網域的彈窗" + +#: src/view/screens/Settings/index.tsx:640 +msgid "Opens moderation settings" +msgstr "開啟限制設定" + +#: src/view/com/auth/login/LoginForm.tsx:242 +msgid "Opens password reset form" +msgstr "開啟密碼重設表單" + +#: src/view/com/home/HomeHeaderLayout.web.tsx:63 +#: src/view/screens/Feeds.tsx:356 +msgid "Opens screen to edit Saved Feeds" +msgstr "開啟編輯已儲存訊息流的畫面" + +#: src/view/screens/Settings/index.tsx:597 +msgid "Opens screen with all saved feeds" +msgstr "開啟包含所有已儲存訊息流的畫面" + +#: src/view/screens/Settings/index.tsx:696 +msgid "Opens the app password settings" +msgstr "" + +#: src/view/screens/Settings/index.tsx:676 +#~ msgid "Opens the app password settings page" +#~ msgstr "開啟應用程式專用密碼設定頁面" + +#: src/view/screens/Settings/index.tsx:554 +msgid "Opens the Following feed preferences" +msgstr "" + +#: src/view/screens/Settings/index.tsx:535 +#~ msgid "Opens the home feed preferences" +#~ msgstr "開啟首頁訊息流設定偏好" + +#: src/view/com/modals/LinkWarning.tsx:76 +msgid "Opens the linked website" +msgstr "" + +#: src/view/screens/Settings/index.tsx:829 +#: src/view/screens/Settings/index.tsx:839 +msgid "Opens the storybook page" +msgstr "開啟故事書頁面" + +#: src/view/screens/Settings/index.tsx:817 +msgid "Opens the system log page" +msgstr "開啟系統日誌頁面" + +#: src/view/screens/Settings/index.tsx:575 +msgid "Opens the threads preferences" +msgstr "開啟對話串設定偏好" + +#: src/view/com/util/forms/DropdownButton.tsx:280 +msgid "Option {0} of {numItems}" +msgstr "{0} 選項,共 {numItems} 個" + +#: src/components/ReportDialog/SubmitView.tsx:162 +msgid "Optionally provide additional information below:" +msgstr "" + +#: src/view/com/modals/Threadgate.tsx:89 +msgid "Or combine these options:" +msgstr "或者選擇組合這些選項:" + +#: src/lib/moderation/useReportOptions.ts:25 +msgid "Other" +msgstr "" + +#: src/view/com/auth/login/ChooseAccountForm.tsx:147 +msgid "Other account" +msgstr "其他帳號" + +#: src/view/com/modals/ServerInput.tsx:88 +#~ msgid "Other service" +#~ msgstr "其他服務" + +#: src/view/com/composer/select-language/SelectLangBtn.tsx:91 +msgid "Other..." +msgstr "其他…" + +#: src/components/Lists.tsx:190 +#: src/view/screens/NotFound.tsx:45 +msgid "Page not found" +msgstr "頁面不存在" + +#: src/view/screens/NotFound.tsx:42 +msgid "Page Not Found" +msgstr "頁面不存在" + +#: src/view/com/auth/create/Step1.tsx:191 +#: src/view/com/auth/create/Step1.tsx:201 +#: src/view/com/auth/login/LoginForm.tsx:213 +#: src/view/com/auth/login/LoginForm.tsx:229 +#: src/view/com/auth/login/SetNewPasswordForm.tsx:161 +#: src/view/com/modals/DeleteAccount.tsx:195 +#: src/view/com/modals/DeleteAccount.tsx:202 +msgid "Password" +msgstr "密碼" + +#: src/view/com/modals/ChangePassword.tsx:142 +msgid "Password Changed" +msgstr "" + +#: src/view/com/auth/login/Login.tsx:157 +msgid "Password updated" +msgstr "密碼已更新" + +#: src/view/com/auth/login/PasswordUpdatedForm.tsx:28 +msgid "Password updated!" +msgstr "密碼已更新!" + +#: src/Navigation.tsx:164 +msgid "People followed by @{0}" +msgstr "被 @{0} 跟隨的人" + +#: src/Navigation.tsx:157 +msgid "People following @{0}" +msgstr "跟隨 @{0} 的人" + +#: src/view/com/lightbox/Lightbox.tsx:66 +msgid "Permission to access camera roll is required." +msgstr "需要相機的存取權限。" + +#: src/view/com/lightbox/Lightbox.tsx:72 +msgid "Permission to access camera roll was denied. Please enable it in your system settings." +msgstr "相機的存取權限已被拒絕,請在系統設定中啟用。" + +#: src/screens/Onboarding/index.tsx:31 +msgid "Pets" +msgstr "寵物" + +#: src/view/com/auth/create/Step2.tsx:183 +#~ msgid "Phone number" +#~ msgstr "手機號碼" + +#: src/view/com/modals/SelfLabel.tsx:121 +msgid "Pictures meant for adults." +msgstr "適合成年人的圖像。" + +#: src/view/screens/ProfileFeed.tsx:291 +#: src/view/screens/ProfileList.tsx:563 +msgid "Pin to home" +msgstr "固定到首頁" + +#: src/view/screens/ProfileFeed.tsx:294 +msgid "Pin to Home" +msgstr "" + +#: src/view/screens/SavedFeeds.tsx:88 +msgid "Pinned Feeds" +msgstr "固定訊息流列表" + +#: src/view/com/util/post-embeds/ExternalGifEmbed.tsx:111 +msgid "Play {0}" +msgstr "播放 {0}" + +#: src/view/com/util/post-embeds/ExternalPlayerEmbed.tsx:54 +#: src/view/com/util/post-embeds/ExternalPlayerEmbed.tsx:55 +msgid "Play Video" +msgstr "播放影片" + +#: src/view/com/util/post-embeds/ExternalGifEmbed.tsx:110 +msgid "Plays the GIF" +msgstr "播放 GIF" + +#: src/view/com/auth/create/state.ts:124 +msgid "Please choose your handle." +msgstr "請選擇你的帳號代碼。" + +#: src/view/com/auth/create/state.ts:117 +msgid "Please choose your password." +msgstr "請選擇你的密碼。" + +#: src/view/com/auth/create/state.ts:131 +msgid "Please complete the verification captcha." +msgstr "請完成 Captcha 驗證。" + +#: src/view/com/modals/ChangeEmail.tsx:67 +msgid "Please confirm your email before changing it. This is a temporary requirement while email-updating tools are added, and it will soon be removed." +msgstr "更改前請先確認你的電子郵件地址。這是電子郵件更新工具的臨時要求,此限制將很快被移除。" + +#: src/view/com/modals/AddAppPasswords.tsx:90 +msgid "Please enter a name for your app password. All spaces is not allowed." +msgstr "請輸入應用程式專用密碼的名稱。所有空格均不允許使用。" + +#: src/view/com/auth/create/Step2.tsx:206 +#~ msgid "Please enter a phone number that can receive SMS text messages." +#~ msgstr "請輸入可以接收簡訊的手機號碼。" + +#: src/view/com/modals/AddAppPasswords.tsx:145 +msgid "Please enter a unique name for this App Password or use our randomly generated one." +msgstr "請輸入此應用程式專用密碼的唯一名稱,或使用我們提供的隨機生成名稱。" + +#: src/components/dialogs/MutedWords.tsx:68 +msgid "Please enter a valid word, tag, or phrase to mute" +msgstr "" + +#: src/view/com/auth/create/state.ts:170 +#~ msgid "Please enter the code you received by SMS." +#~ msgstr "請輸入你收到的簡訊驗證碼。" + +#: src/view/com/auth/create/Step2.tsx:282 +#~ msgid "Please enter the verification code sent to {phoneNumberFormatted}." +#~ msgstr "請輸入發送到 {phoneNumberFormatted} 的驗證碼。" + +#: src/view/com/auth/create/state.ts:103 +msgid "Please enter your email." +msgstr "請輸入你的電子郵件。" + +#: src/view/com/modals/DeleteAccount.tsx:191 +msgid "Please enter your password as well:" +msgstr "請輸入你的密碼:" + +#: src/components/moderation/LabelsOnMeDialog.tsx:222 +msgid "Please explain why you think this label was incorrectly applied by {0}" +msgstr "" + +#: src/view/com/modals/AppealLabel.tsx:72 +#: src/view/com/modals/AppealLabel.tsx:75 +#~ msgid "Please tell us why you think this content warning was incorrectly applied!" +#~ msgstr "請告訴我們你認為這個內容警告標示有誤的原因!" + +#: src/view/com/modals/VerifyEmail.tsx:101 +msgid "Please Verify Your Email" +msgstr "請驗證你的電子郵件地址" + +#: src/view/com/composer/Composer.tsx:221 +msgid "Please wait for your link card to finish loading" +msgstr "請等待你的連結卡載入完畢" + +#: src/screens/Onboarding/index.tsx:37 +msgid "Politics" +msgstr "政治" + +#: src/view/com/modals/SelfLabel.tsx:111 +msgid "Porn" +msgstr "情色內容" + +#: src/lib/moderation/useGlobalLabelStrings.ts:34 +msgid "Pornography" +msgstr "" + +#: src/view/com/composer/Composer.tsx:366 +#: src/view/com/composer/Composer.tsx:374 +msgctxt "action" +msgid "Post" +msgstr "發佈" + +#: src/view/com/post-thread/PostThread.tsx:292 +msgctxt "description" +msgid "Post" +msgstr "發佈" + +#: src/view/com/post-thread/PostThreadItem.tsx:175 +msgid "Post by {0}" +msgstr "{0} 的貼文" + +#: src/Navigation.tsx:176 +#: src/Navigation.tsx:183 +#: src/Navigation.tsx:190 +msgid "Post by @{0}" +msgstr "@{0} 的貼文" + +#: src/view/com/util/forms/PostDropdownBtn.tsx:105 +msgid "Post deleted" +msgstr "貼文已刪除" + +#: src/view/com/post-thread/PostThread.tsx:157 +msgid "Post hidden" +msgstr "貼文已隱藏" + +#: src/components/moderation/ModerationDetailsDialog.tsx:98 +#: src/lib/moderation/useModerationCauseDescription.ts:99 +msgid "Post Hidden by Muted Word" +msgstr "" + +#: src/components/moderation/ModerationDetailsDialog.tsx:101 +#: src/lib/moderation/useModerationCauseDescription.ts:108 +msgid "Post Hidden by You" +msgstr "" + +#: src/view/com/composer/select-language/SelectLangBtn.tsx:87 +msgid "Post language" +msgstr "貼文語言" + +#: src/view/com/modals/lang-settings/PostLanguagesSettings.tsx:75 +msgid "Post Languages" +msgstr "貼文語言" + +#: src/view/com/post-thread/PostThread.tsx:152 +#: src/view/com/post-thread/PostThread.tsx:164 +msgid "Post not found" +msgstr "找不到貼文" + +#: src/components/TagMenu/index.tsx:253 +msgid "posts" +msgstr "貼文" + +#: src/view/screens/Profile.tsx:188 +msgid "Posts" +msgstr "貼文" + +#: src/components/dialogs/MutedWords.tsx:90 +msgid "Posts can be muted based on their text, their tags, or both." +msgstr "" + +#: src/view/com/posts/FeedErrorMessage.tsx:64 +msgid "Posts hidden" +msgstr "貼文已隱藏" + +#: src/view/com/modals/LinkWarning.tsx:46 +msgid "Potentially Misleading Link" +msgstr "潛在誤導性連結" + +#: src/components/Lists.tsx:88 +msgid "Press to retry" +msgstr "" + +#: src/view/com/lightbox/Lightbox.web.tsx:150 +msgid "Previous image" +msgstr "上一張圖片" + +#: src/view/screens/LanguageSettings.tsx:187 +msgid "Primary Language" +msgstr "主要語言" + +#: src/view/screens/PreferencesThreads.tsx:97 +msgid "Prioritize Your Follows" +msgstr "優先顯示跟隨者" + +#: src/view/screens/Settings/index.tsx:652 +#: src/view/shell/desktop/RightNav.tsx:72 +msgid "Privacy" +msgstr "隱私" + +#: src/Navigation.tsx:231 +#: src/view/com/auth/create/Policies.tsx:69 +#: src/view/screens/PrivacyPolicy.tsx:29 +#: src/view/screens/Settings/index.tsx:925 +#: src/view/shell/Drawer.tsx:265 +msgid "Privacy Policy" +msgstr "隱私政策" + +#: src/view/com/auth/login/ForgotPasswordForm.tsx:198 +msgid "Processing..." +msgstr "處理中…" + +#: src/view/screens/DebugMod.tsx:888 +#: src/view/screens/Profile.tsx:340 +msgid "profile" +msgstr "個人檔案" + +#: src/view/shell/bottom-bar/BottomBar.tsx:251 +#: src/view/shell/desktop/LeftNav.tsx:419 +#: src/view/shell/Drawer.tsx:70 +#: src/view/shell/Drawer.tsx:549 +#: src/view/shell/Drawer.tsx:550 +msgid "Profile" +msgstr "個人檔案" + +#: src/view/com/modals/EditProfile.tsx:128 +msgid "Profile updated" +msgstr "個人檔案已更新" + +#: src/view/screens/Settings/index.tsx:983 +msgid "Protect your account by verifying your email." +msgstr "通過驗證電子郵件地址來保護你的帳號。" + +#: src/screens/Onboarding/StepFinished.tsx:101 +msgid "Public" +msgstr "公開內容" + +#: src/view/screens/ModerationModlists.tsx:61 +msgid "Public, shareable lists of users to mute or block in bulk." +msgstr "公開且可共享的批量靜音或封鎖列表。" + +#: src/view/screens/Lists.tsx:61 +msgid "Public, shareable lists which can drive feeds." +msgstr "公開且可共享的列表,可作為訊息流使用。" + +#: src/view/com/composer/Composer.tsx:351 +msgid "Publish post" +msgstr "發佈貼文" + +#: src/view/com/composer/Composer.tsx:351 +msgid "Publish reply" +msgstr "發佈回覆" + +#: src/view/com/modals/Repost.tsx:65 +msgctxt "action" +msgid "Quote post" +msgstr "引用貼文" + +#: src/view/com/util/post-ctrls/RepostButton.web.tsx:58 +msgid "Quote post" +msgstr "引用貼文" + +#: src/view/com/modals/Repost.tsx:70 +msgctxt "action" +msgid "Quote Post" +msgstr "引用貼文" + +#: src/view/screens/PreferencesThreads.tsx:86 +msgid "Random (aka \"Poster's Roulette\")" +msgstr "隨機顯示 (又名試試手氣)" + +#: src/view/com/modals/EditImage.tsx:236 +msgid "Ratios" +msgstr "比率" + +#: src/view/screens/Search/Search.tsx:776 +msgid "Recent Searches" +msgstr "" + +#: src/view/com/auth/onboarding/RecommendedFeeds.tsx:116 +msgid "Recommended Feeds" +msgstr "推薦訊息流" + +#: src/view/com/auth/onboarding/RecommendedFollows.tsx:180 +msgid "Recommended Users" +msgstr "推薦的使用者" + +#: src/components/dialogs/MutedWords.tsx:287 +#: src/view/com/feeds/FeedSourceCard.tsx:283 +#: src/view/com/modals/ListAddRemoveUsers.tsx:268 +#: src/view/com/modals/SelfLabel.tsx:83 +#: src/view/com/modals/UserAddRemoveLists.tsx:219 +#: src/view/com/posts/FeedErrorMessage.tsx:204 +msgid "Remove" +msgstr "移除" + +#: src/view/com/feeds/FeedSourceCard.tsx:108 +#~ msgid "Remove {0} from my feeds?" +#~ msgstr "將 {0} 從我的訊息流移除?" + +#: src/view/com/util/AccountDropdownBtn.tsx:22 +msgid "Remove account" +msgstr "刪除帳號" + +#: src/view/com/util/UserAvatar.tsx:358 +msgid "Remove Avatar" +msgstr "" + +#: src/view/com/util/UserBanner.tsx:148 +msgid "Remove Banner" +msgstr "" + +#: src/view/com/posts/FeedErrorMessage.tsx:160 +msgid "Remove feed" +msgstr "刪除訊息流" + +#: src/view/com/posts/FeedErrorMessage.tsx:201 +msgid "Remove feed?" +msgstr "" + +#: src/view/com/feeds/FeedSourceCard.tsx:173 +#: src/view/com/feeds/FeedSourceCard.tsx:233 +#: src/view/screens/ProfileFeed.tsx:334 +#: src/view/screens/ProfileFeed.tsx:340 +msgid "Remove from my feeds" +msgstr "從我的訊息流中刪除" + +#: src/view/com/feeds/FeedSourceCard.tsx:278 +msgid "Remove from my feeds?" +msgstr "" + +#: src/view/com/composer/photos/Gallery.tsx:167 +msgid "Remove image" +msgstr "刪除圖片" + +#: src/view/com/composer/ExternalEmbed.tsx:70 +msgid "Remove image preview" +msgstr "刪除圖片預覽" + +#: src/components/dialogs/MutedWords.tsx:330 +msgid "Remove mute word from your list" +msgstr "" + +#: src/view/com/modals/Repost.tsx:47 +msgid "Remove repost" +msgstr "刪除轉發" + +#: src/view/com/feeds/FeedSourceCard.tsx:175 +#~ msgid "Remove this feed from my feeds?" +#~ msgstr "將這個訊息流從我的訊息流列表中刪除?" + +#: src/view/com/posts/FeedErrorMessage.tsx:202 +msgid "Remove this feed from your saved feeds" +msgstr "" + +#: src/view/com/posts/FeedErrorMessage.tsx:132 +#~ msgid "Remove this feed from your saved feeds?" +#~ msgstr "將這個訊息流從儲存的訊息流列表中刪除?" + +#: src/view/com/modals/ListAddRemoveUsers.tsx:199 +#: src/view/com/modals/UserAddRemoveLists.tsx:152 +msgid "Removed from list" +msgstr "從列表中刪除" + +#: src/view/com/feeds/FeedSourceCard.tsx:121 +msgid "Removed from my feeds" +msgstr "從我的訊息流中刪除" + +#: src/view/screens/ProfileFeed.tsx:208 +msgid "Removed from your feeds" +msgstr "" + +#: src/view/com/composer/ExternalEmbed.tsx:71 +msgid "Removes default thumbnail from {0}" +msgstr "從 {0} 中刪除預設縮略圖" + +#: src/view/screens/Profile.tsx:189 +msgid "Replies" +msgstr "回覆" + +#: src/view/com/threadgate/WhoCanReply.tsx:98 +msgid "Replies to this thread are disabled" +msgstr "對此對話串的回覆已被停用" + +#: src/view/com/composer/Composer.tsx:364 +msgctxt "action" +msgid "Reply" +msgstr "回覆" + +#: src/view/screens/PreferencesFollowingFeed.tsx:144 +msgid "Reply Filters" +msgstr "回覆過濾器" + +#: src/view/com/post/Post.tsx:166 +#: src/view/com/posts/FeedItem.tsx:280 +msgctxt "description" +msgid "Reply to <0/>" +msgstr "回覆 <0/>" + +#: src/view/com/modals/report/Modal.tsx:166 +#~ msgid "Report {collectionName}" +#~ msgstr "檢舉 {collectionName}" + +#: src/view/com/profile/ProfileMenu.tsx:319 +#: src/view/com/profile/ProfileMenu.tsx:322 +msgid "Report Account" +msgstr "檢舉帳號" + +#: src/view/screens/ProfileFeed.tsx:351 +#: src/view/screens/ProfileFeed.tsx:353 +msgid "Report feed" +msgstr "檢舉訊息流" + +#: src/view/screens/ProfileList.tsx:429 +msgid "Report List" +msgstr "檢舉列表" + +#: src/view/com/util/forms/PostDropdownBtn.tsx:292 +#: src/view/com/util/forms/PostDropdownBtn.tsx:294 +msgid "Report post" +msgstr "檢舉貼文" + +#: src/components/ReportDialog/SelectReportOptionView.tsx:43 +msgid "Report this content" +msgstr "" + +#: src/components/ReportDialog/SelectReportOptionView.tsx:56 +msgid "Report this feed" +msgstr "" + +#: src/components/ReportDialog/SelectReportOptionView.tsx:53 +msgid "Report this list" +msgstr "" + +#: src/components/ReportDialog/SelectReportOptionView.tsx:50 +msgid "Report this post" +msgstr "" + +#: src/components/ReportDialog/SelectReportOptionView.tsx:47 +msgid "Report this user" +msgstr "" + +#: src/view/com/modals/Repost.tsx:43 +#: src/view/com/modals/Repost.tsx:48 +#: src/view/com/modals/Repost.tsx:53 +#: src/view/com/util/post-ctrls/RepostButton.tsx:61 +msgctxt "action" +msgid "Repost" +msgstr "轉發" + +#: src/view/com/util/post-ctrls/RepostButton.web.tsx:48 +msgid "Repost" +msgstr "轉發" + +#: src/view/com/util/post-ctrls/RepostButton.web.tsx:94 +#: src/view/com/util/post-ctrls/RepostButton.web.tsx:105 +msgid "Repost or quote post" +msgstr "轉發或引用貼文" + +#: src/view/screens/PostRepostedBy.tsx:27 +msgid "Reposted By" +msgstr "轉發" + +#: src/view/com/posts/FeedItem.tsx:197 +msgid "Reposted by {0}" +msgstr "由 {0} 轉發" + +#: src/view/com/posts/FeedItem.tsx:214 +msgid "Reposted by <0/>" +msgstr "由 <0/> 轉發" + +#: src/view/com/notifications/FeedItem.tsx:166 +msgid "reposted your post" +msgstr "轉發你的貼文" + +#: src/view/com/post-thread/PostThreadItem.tsx:187 +msgid "Reposts of this post" +msgstr "轉發這條貼文" + +#: src/view/com/modals/ChangeEmail.tsx:181 +#: src/view/com/modals/ChangeEmail.tsx:183 +msgid "Request Change" +msgstr "請求變更" + +#: src/view/com/auth/create/Step2.tsx:219 +#~ msgid "Request code" +#~ msgstr "請求碼" + +#: src/view/com/modals/ChangePassword.tsx:241 +#: src/view/com/modals/ChangePassword.tsx:243 +msgid "Request Code" +msgstr "請求代碼" + +#: src/view/screens/Settings/index.tsx:475 +msgid "Require alt text before posting" +msgstr "要求發佈前提供替代文字" + +#: src/view/com/auth/create/Step1.tsx:146 +msgid "Required for this provider" +msgstr "提供商要求必填" + +#: src/view/com/auth/login/SetNewPasswordForm.tsx:124 +#: src/view/com/auth/login/SetNewPasswordForm.tsx:136 +#: src/view/com/modals/ChangePassword.tsx:185 +msgid "Reset code" +msgstr "重設碼" + +#: src/view/com/modals/ChangePassword.tsx:192 +msgid "Reset Code" +msgstr "重設碼" + +#: src/view/screens/Settings/index.tsx:824 +#~ msgid "Reset onboarding" +#~ msgstr "重設初始設定進行狀態" + +#: src/view/screens/Settings/index.tsx:858 +#: src/view/screens/Settings/index.tsx:861 +msgid "Reset onboarding state" +msgstr "重設初始設定進行狀態" + +#: src/view/com/auth/login/ForgotPasswordForm.tsx:104 +msgid "Reset password" +msgstr "重設密碼" + +#: src/view/screens/Settings/index.tsx:814 +#~ msgid "Reset preferences" +#~ msgstr "重設偏好設定" + +#: src/view/screens/Settings/index.tsx:848 +#: src/view/screens/Settings/index.tsx:851 +msgid "Reset preferences state" +msgstr "重設偏好設定狀態" + +#: src/view/screens/Settings/index.tsx:859 +msgid "Resets the onboarding state" +msgstr "重設初始設定狀態" + +#: src/view/screens/Settings/index.tsx:849 +msgid "Resets the preferences state" +msgstr "重設偏好設定狀態" + +#: src/view/com/auth/login/LoginForm.tsx:272 +msgid "Retries login" +msgstr "重試登入" + +#: src/view/com/util/error/ErrorMessage.tsx:57 +#: src/view/com/util/error/ErrorScreen.tsx:74 +msgid "Retries the last action, which errored out" +msgstr "重試上次出錯的操作" + +#: src/components/Lists.tsx:98 +#: src/screens/Onboarding/StepInterests/index.tsx:221 +#: src/screens/Onboarding/StepInterests/index.tsx:224 +#: src/view/com/auth/create/CreateAccount.tsx:181 +#: src/view/com/auth/create/CreateAccount.tsx:186 +#: src/view/com/auth/login/LoginForm.tsx:271 +#: src/view/com/auth/login/LoginForm.tsx:274 +#: src/view/com/util/error/ErrorMessage.tsx:55 +#: src/view/com/util/error/ErrorScreen.tsx:72 +msgid "Retry" +msgstr "重試" + +#: src/view/com/auth/create/Step2.tsx:247 +#~ msgid "Retry." +#~ msgstr "重試。" + +#: src/view/screens/ProfileList.tsx:917 +msgid "Return to previous page" +msgstr "返回上一頁" + +#: src/view/screens/NotFound.tsx:59 +msgid "Returns to home page" +msgstr "" + +#: src/view/screens/NotFound.tsx:58 +#: src/view/screens/ProfileFeed.tsx:112 +msgid "Returns to previous page" +msgstr "" + +#: src/view/shell/desktop/RightNav.tsx:55 +#~ msgid "SANDBOX. Posts and accounts are not permanent." +#~ msgstr "沙盒模式。貼文和帳號不會永久儲存。" + +#: src/components/dialogs/BirthDateSettings.tsx:125 +#: src/view/com/modals/ChangeHandle.tsx:173 +#: src/view/com/modals/CreateOrEditList.tsx:337 +#: src/view/com/modals/EditProfile.tsx:224 +msgid "Save" +msgstr "儲存" + +#: src/view/com/lightbox/Lightbox.tsx:132 +#: src/view/com/modals/CreateOrEditList.tsx:345 +msgctxt "action" +msgid "Save" +msgstr "儲存" + +#: src/view/com/modals/AltImage.tsx:130 +msgid "Save alt text" +msgstr "儲存替代文字" + +#: src/components/dialogs/BirthDateSettings.tsx:119 +msgid "Save birthday" +msgstr "" + +#: src/view/com/modals/EditProfile.tsx:232 +msgid "Save Changes" +msgstr "儲存更改" + +#: src/view/com/modals/ChangeHandle.tsx:170 +msgid "Save handle change" +msgstr "儲存帳號代碼更改" + +#: src/view/com/modals/crop-image/CropImage.web.tsx:144 +msgid "Save image crop" +msgstr "儲存圖片裁剪" + +#: src/view/screens/ProfileFeed.tsx:335 +#: src/view/screens/ProfileFeed.tsx:341 +msgid "Save to my feeds" +msgstr "" + +#: src/view/screens/SavedFeeds.tsx:122 +msgid "Saved Feeds" +msgstr "已儲存訊息流" + +#: src/view/com/lightbox/Lightbox.tsx:81 +msgid "Saved to your camera roll." +msgstr "" + +#: src/view/screens/ProfileFeed.tsx:212 +msgid "Saved to your feeds" +msgstr "" + +#: src/view/com/modals/EditProfile.tsx:225 +msgid "Saves any changes to your profile" +msgstr "儲存個人資料中所做的變更" + +#: src/view/com/modals/ChangeHandle.tsx:171 +msgid "Saves handle change to {handle}" +msgstr "儲存帳號代碼更改至 {handle}" + +#: src/view/com/modals/crop-image/CropImage.web.tsx:145 +msgid "Saves image crop settings" +msgstr "" + +#: src/screens/Onboarding/index.tsx:36 +msgid "Science" +msgstr "科學" + +#: src/view/screens/ProfileList.tsx:873 +msgid "Scroll to top" +msgstr "滾動到頂部" + +#: src/Navigation.tsx:459 +#: src/view/com/auth/LoggedOut.tsx:122 +#: src/view/com/modals/ListAddRemoveUsers.tsx:75 +#: src/view/com/util/forms/SearchInput.tsx:67 +#: src/view/com/util/forms/SearchInput.tsx:79 +#: src/view/screens/Search/Search.tsx:420 +#: src/view/screens/Search/Search.tsx:669 +#: src/view/screens/Search/Search.tsx:687 +#: src/view/shell/bottom-bar/BottomBar.tsx:161 +#: src/view/shell/desktop/LeftNav.tsx:328 +#: src/view/shell/desktop/Search.tsx:215 +#: src/view/shell/desktop/Search.tsx:224 +#: src/view/shell/Drawer.tsx:365 +#: src/view/shell/Drawer.tsx:366 +msgid "Search" +msgstr "搜尋" + +#: src/view/screens/Search/Search.tsx:736 +#: src/view/shell/desktop/Search.tsx:256 +msgid "Search for \"{query}\"" +msgstr "搜尋「{query}」" + +#: src/components/TagMenu/index.tsx:145 +msgid "Search for all posts by @{authorHandle} with tag {displayTag}" +msgstr "" + +#: src/components/TagMenu/index.tsx:145 +#~ msgid "Search for all posts by @{authorHandle} with tag {tag}" +#~ msgstr "" + +#: src/components/TagMenu/index.tsx:94 +msgid "Search for all posts with tag {displayTag}" +msgstr "" + +#: src/components/TagMenu/index.tsx:90 +#~ msgid "Search for all posts with tag {tag}" +#~ msgstr "" + +#: src/view/com/auth/LoggedOut.tsx:104 +#: src/view/com/auth/LoggedOut.tsx:105 +#: src/view/com/modals/ListAddRemoveUsers.tsx:70 +msgid "Search for users" +msgstr "搜尋使用者" + +#: src/view/com/modals/ChangeEmail.tsx:110 +msgid "Security Step Required" +msgstr "所需的安全步驟" + +#: src/components/TagMenu/index.web.tsx:66 +msgid "See {truncatedTag} posts" +msgstr "" + +#: src/components/TagMenu/index.web.tsx:83 +msgid "See {truncatedTag} posts by user" +msgstr "" + +#: src/components/TagMenu/index.tsx:128 +msgid "See <0>{displayTag} posts" +msgstr "" + +#: src/components/TagMenu/index.tsx:187 +msgid "See <0>{displayTag} posts by this user" +msgstr "" + +#: src/components/TagMenu/index.tsx:128 +#~ msgid "See <0>{tag} posts" +#~ msgstr "" + +#: src/components/TagMenu/index.tsx:189 +#~ msgid "See <0>{tag} posts by this user" +#~ msgstr "" + +#: src/view/screens/SavedFeeds.tsx:163 +msgid "See this guide" +msgstr "查看指南" + +#: src/view/com/auth/HomeLoggedOutCTA.tsx:39 +msgid "See what's next" +msgstr "查看下一步" + +#: src/view/com/util/Selector.tsx:106 +msgid "Select {item}" +msgstr "選擇 {item}" + +#: src/view/com/modals/ServerInput.tsx:75 +#~ msgid "Select Bluesky Social" +#~ msgstr "選擇 Bluesky Social" + +#: src/view/com/auth/login/Login.tsx:117 +msgid "Select from an existing account" +msgstr "從現有帳號中選擇" + +#: src/view/screens/LanguageSettings.tsx:299 +msgid "Select languages" +msgstr "" + +#: src/components/ReportDialog/SelectLabelerView.tsx:32 +msgid "Select moderator" +msgstr "" + +#: src/view/com/util/Selector.tsx:107 +msgid "Select option {i} of {numItems}" +msgstr "選擇 {numItems} 個項目中的第 {i} 項" + +#: src/view/com/auth/create/Step1.tsx:96 +#: src/view/com/auth/login/LoginForm.tsx:153 +msgid "Select service" +msgstr "選擇服務" + +#: src/screens/Onboarding/StepSuggestedAccounts/index.tsx:52 +msgid "Select some accounts below to follow" +msgstr "在下面選擇一些要跟隨的帳號" + +#: src/components/ReportDialog/SubmitView.tsx:135 +msgid "Select the moderation service(s) to report to" +msgstr "" + +#: src/view/com/auth/server-input/index.tsx:82 +msgid "Select the service that hosts your data." +msgstr "選擇用來託管你的資料的服務商。" + +#: src/screens/Onboarding/StepTopicalFeeds.tsx:96 +msgid "Select topical feeds to follow from the list below" +msgstr "從下面的列表中選擇要跟隨的主題訊息流" + +#: src/screens/Onboarding/StepModeration/index.tsx:62 +msgid "Select what you want to see (or not see), and we’ll handle the rest." +msgstr "選擇你想看到(或不想看到)的內容,剩下的由我們來處理。" + +#: src/view/screens/LanguageSettings.tsx:281 +msgid "Select which languages you want your subscribed feeds to include. If none are selected, all languages will be shown." +msgstr "選擇你希望訂閱訊息流中所包含的語言。未選擇任何語言時會預設顯示所有語言。" + +#: src/view/screens/LanguageSettings.tsx:98 +#~ msgid "Select your app language for the default text to display in the app" +#~ msgstr "選擇應用程式中顯示預設文字的語言" + +#: src/view/screens/LanguageSettings.tsx:98 +msgid "Select your app language for the default text to display in the app." +msgstr "" + +#: src/screens/Onboarding/StepInterests/index.tsx:196 +msgid "Select your interests from the options below" +msgstr "下面選擇你感興趣的選項" + +#: src/view/com/auth/create/Step2.tsx:155 +#~ msgid "Select your phone's country" +#~ msgstr "選擇你的電話區號" + +#: src/view/screens/LanguageSettings.tsx:190 +msgid "Select your preferred language for translations in your feed." +msgstr "選擇你在訂閱訊息流中希望進行翻譯的目標語言偏好。" + +#: src/screens/Onboarding/StepAlgoFeeds/index.tsx:116 +msgid "Select your primary algorithmic feeds" +msgstr "選擇你的訊息流主要算法" + +#: src/screens/Onboarding/StepAlgoFeeds/index.tsx:142 +msgid "Select your secondary algorithmic feeds" +msgstr "選擇你的訊息流次要算法" + +#: src/view/com/modals/VerifyEmail.tsx:202 +#: src/view/com/modals/VerifyEmail.tsx:204 +msgid "Send Confirmation Email" +msgstr "發送確認電子郵件" + +#: src/view/com/modals/DeleteAccount.tsx:131 +msgid "Send email" +msgstr "發送電子郵件" + +#: src/view/com/modals/DeleteAccount.tsx:144 +msgctxt "action" +msgid "Send Email" +msgstr "發送電子郵件" + +#: src/view/shell/Drawer.tsx:298 +#: src/view/shell/Drawer.tsx:319 +msgid "Send feedback" +msgstr "提交意見" + +#: src/components/ReportDialog/SubmitView.tsx:214 +#: src/components/ReportDialog/SubmitView.tsx:218 +msgid "Send report" +msgstr "提交舉報" + +#: src/view/com/modals/report/SendReportButton.tsx:45 +#~ msgid "Send Report" +#~ msgstr "提交舉報" + +#: src/components/ReportDialog/SelectLabelerView.tsx:46 +msgid "Send report to {0}" +msgstr "" + +#: src/view/com/modals/DeleteAccount.tsx:133 +msgid "Sends email with confirmation code for account deletion" +msgstr "發送包含帳號刪除確認碼的電子郵件" + +#: src/view/com/auth/server-input/index.tsx:110 +msgid "Server address" +msgstr "伺服器地址" + +#: src/view/com/modals/ContentFilteringSettings.tsx:311 +#~ msgid "Set {value} for {labelGroup} content moderation policy" +#~ msgstr "將 {labelGroup} 內容審核政策設為 {value}" + +#: src/view/com/modals/ContentFilteringSettings.tsx:160 +#: src/view/com/modals/ContentFilteringSettings.tsx:179 +#~ msgctxt "action" +#~ msgid "Set Age" +#~ msgstr "設定年齡" + +#: src/screens/Moderation/index.tsx:306 +msgid "Set birthdate" +msgstr "" + +#: src/view/screens/Settings/index.tsx:488 +#~ msgid "Set color theme to dark" +#~ msgstr "設定主題為深色模式" + +#: src/view/screens/Settings/index.tsx:481 +#~ msgid "Set color theme to light" +#~ msgstr "設定主題為亮色模式" + +#: src/view/screens/Settings/index.tsx:475 +#~ msgid "Set color theme to system setting" +#~ msgstr "設定主題跟隨系統設定" + +#: src/view/screens/Settings/index.tsx:514 +#~ msgid "Set dark theme to the dark theme" +#~ msgstr "設定深色模式至深黑" + +#: src/view/screens/Settings/index.tsx:507 +#~ msgid "Set dark theme to the dim theme" +#~ msgstr "設定深色模式至暗淡" + +#: src/view/com/auth/login/SetNewPasswordForm.tsx:104 +msgid "Set new password" +msgstr "設定新密碼" + +#: src/view/com/auth/create/Step1.tsx:202 +msgid "Set password" +msgstr "設定密碼" + +#: src/view/screens/PreferencesFollowingFeed.tsx:225 +msgid "Set this setting to \"No\" to hide all quote posts from your feed. Reposts will still be visible." +msgstr "將此設定項設為「關」會隱藏來自訂閱訊息流的所有引用貼文。轉發仍將可見。" + +#: src/view/screens/PreferencesFollowingFeed.tsx:122 +msgid "Set this setting to \"No\" to hide all replies from your feed." +msgstr "將此設定項設為「關」以隱藏來自訂閱訊息流的所有回覆。" + +#: src/view/screens/PreferencesFollowingFeed.tsx:191 +msgid "Set this setting to \"No\" to hide all reposts from your feed." +msgstr "將此設定項設為「關」以隱藏來自訂閱訊息流的所有轉發。" + +#: src/view/screens/PreferencesThreads.tsx:122 +msgid "Set this setting to \"Yes\" to show replies in a threaded view. This is an experimental feature." +msgstr "將此設定項設為「開」以在分層視圖中顯示回覆。這是一個實驗性功能。" + +#: src/view/screens/PreferencesHomeFeed.tsx:261 +#~ msgid "Set this setting to \"Yes\" to show samples of your saved feeds in your following feed. This is an experimental feature." +#~ msgstr "將此設定項設為「開」以在跟隨訊息流中顯示已儲存訊息流的樣本。這是一個實驗性功能。" + +#: src/view/screens/PreferencesFollowingFeed.tsx:261 +msgid "Set this setting to \"Yes\" to show samples of your saved feeds in your Following feed. This is an experimental feature." +msgstr "" + +#: src/screens/Onboarding/Layout.tsx:50 +msgid "Set up your account" +msgstr "設定你的帳號" + +#: src/view/com/modals/ChangeHandle.tsx:266 +msgid "Sets Bluesky username" +msgstr "設定 Bluesky 使用者名稱" + +#: src/view/screens/Settings/index.tsx:507 +msgid "Sets color theme to dark" +msgstr "" + +#: src/view/screens/Settings/index.tsx:500 +msgid "Sets color theme to light" +msgstr "" + +#: src/view/screens/Settings/index.tsx:494 +msgid "Sets color theme to system setting" +msgstr "" + +#: src/view/screens/Settings/index.tsx:533 +msgid "Sets dark theme to the dark theme" +msgstr "" + +#: src/view/screens/Settings/index.tsx:526 +msgid "Sets dark theme to the dim theme" +msgstr "" + +#: src/view/com/auth/login/ForgotPasswordForm.tsx:157 +msgid "Sets email for password reset" +msgstr "設定用於重設密碼的電子郵件" + +#: src/view/com/auth/login/ForgotPasswordForm.tsx:122 +msgid "Sets hosting provider for password reset" +msgstr "設定用於密碼重設的主機提供商資訊" + +#: src/view/com/modals/crop-image/CropImage.web.tsx:123 +msgid "Sets image aspect ratio to square" +msgstr "" + +#: src/view/com/modals/crop-image/CropImage.web.tsx:113 +msgid "Sets image aspect ratio to tall" +msgstr "" + +#: src/view/com/modals/crop-image/CropImage.web.tsx:103 +msgid "Sets image aspect ratio to wide" +msgstr "" + +#: src/view/com/auth/create/Step1.tsx:97 +#: src/view/com/auth/login/LoginForm.tsx:154 +msgid "Sets server for the Bluesky client" +msgstr "設定 Bluesky 用戶端的伺服器" + +#: src/Navigation.tsx:139 +#: src/view/screens/Settings/index.tsx:313 +#: src/view/shell/desktop/LeftNav.tsx:437 +#: src/view/shell/Drawer.tsx:570 +#: src/view/shell/Drawer.tsx:571 +msgid "Settings" +msgstr "設定" + +#: src/view/com/modals/SelfLabel.tsx:125 +msgid "Sexual activity or erotic nudity." +msgstr "性行為或性暗示裸露。" + +#: src/lib/moderation/useGlobalLabelStrings.ts:38 +msgid "Sexually Suggestive" +msgstr "" + +#: src/view/com/lightbox/Lightbox.tsx:141 +msgctxt "action" +msgid "Share" +msgstr "分享" + +#: src/view/com/profile/ProfileMenu.tsx:215 +#: src/view/com/profile/ProfileMenu.tsx:224 +#: src/view/com/util/forms/PostDropdownBtn.tsx:228 +#: src/view/com/util/forms/PostDropdownBtn.tsx:237 +#: src/view/com/util/post-ctrls/PostCtrls.tsx:218 +#: src/view/screens/ProfileList.tsx:388 +msgid "Share" +msgstr "分享" + +#: src/view/com/profile/ProfileMenu.tsx:373 +#: src/view/com/util/forms/PostDropdownBtn.tsx:347 +msgid "Share anyway" +msgstr "" + +#: src/view/screens/ProfileFeed.tsx:361 +#: src/view/screens/ProfileFeed.tsx:363 +msgid "Share feed" +msgstr "分享訊息流" + +#: src/components/moderation/ContentHider.tsx:115 +#: src/components/moderation/GlobalModerationLabelPref.tsx:45 +#: src/components/moderation/PostHider.tsx:107 +#: src/screens/Onboarding/StepModeration/ModerationOption.tsx:54 +#: src/view/screens/Settings/index.tsx:363 +msgid "Show" +msgstr "顯示" + +#: src/view/screens/PreferencesFollowingFeed.tsx:68 +msgid "Show all replies" +msgstr "顯示所有回覆" + +#: src/components/moderation/ScreenHider.tsx:162 +#: src/components/moderation/ScreenHider.tsx:165 +msgid "Show anyway" +msgstr "仍然顯示" + +#: src/lib/moderation/useLabelBehaviorDescription.ts:27 +#: src/lib/moderation/useLabelBehaviorDescription.ts:63 +msgid "Show badge" +msgstr "" + +#: src/lib/moderation/useLabelBehaviorDescription.ts:61 +msgid "Show badge and filter from feeds" +msgstr "" + +#: src/view/com/modals/EmbedConsent.tsx:87 +msgid "Show embeds from {0}" +msgstr "顯示來自 {0} 的嵌入內容" + +#: src/screens/Profile/Header/ProfileHeaderStandard.tsx:193 +msgid "Show follows similar to {0}" +msgstr "顯示類似於 {0} 的跟隨者" + +#: src/view/com/post-thread/PostThreadItem.tsx:507 +#: src/view/com/post/Post.tsx:201 +#: src/view/com/posts/FeedItem.tsx:355 +msgid "Show More" +msgstr "顯示更多" + +#: src/view/screens/PreferencesFollowingFeed.tsx:258 +msgid "Show Posts from My Feeds" +msgstr "在自訂訊息流中顯示貼文" + +#: src/view/screens/PreferencesFollowingFeed.tsx:222 +msgid "Show Quote Posts" +msgstr "顯示引用貼文" + +#: src/screens/Onboarding/StepFollowingFeed.tsx:118 +msgid "Show quote-posts in Following feed" +msgstr "在跟隨訊息流中顯示引用" + +#: src/screens/Onboarding/StepFollowingFeed.tsx:134 +msgid "Show quotes in Following" +msgstr "在跟隨中顯示引用" + +#: src/screens/Onboarding/StepFollowingFeed.tsx:94 +msgid "Show re-posts in Following feed" +msgstr "在跟隨訊息流中顯示轉發" + +#: src/view/screens/PreferencesFollowingFeed.tsx:119 +msgid "Show Replies" +msgstr "顯示回覆" + +#: src/view/screens/PreferencesThreads.tsx:100 +msgid "Show replies by people you follow before all other replies." +msgstr "在所有其他回覆之前顯示你跟隨的人的回覆。" + +#: src/screens/Onboarding/StepFollowingFeed.tsx:86 +msgid "Show replies in Following" +msgstr "在跟隨中顯示回覆" + +#: src/screens/Onboarding/StepFollowingFeed.tsx:70 +msgid "Show replies in Following feed" +msgstr "在跟隨訊息流中顯示回覆" + +#: src/view/screens/PreferencesFollowingFeed.tsx:70 +msgid "Show replies with at least {value} {0}" +msgstr "顯示至少包含 {value} 個{0}的回覆" + +#: src/view/screens/PreferencesFollowingFeed.tsx:188 +msgid "Show Reposts" +msgstr "顯示轉發" + +#: src/screens/Onboarding/StepFollowingFeed.tsx:110 +msgid "Show reposts in Following" +msgstr "在跟隨中顯示轉發" + +#: src/components/moderation/ContentHider.tsx:68 +#: src/components/moderation/PostHider.tsx:64 +msgid "Show the content" +msgstr "顯示內容" + +#: src/view/com/notifications/FeedItem.tsx:351 +msgid "Show users" +msgstr "顯示使用者" + +#: src/lib/moderation/useLabelBehaviorDescription.ts:58 +msgid "Show warning" +msgstr "" + +#: src/lib/moderation/useLabelBehaviorDescription.ts:56 +msgid "Show warning and filter from feeds" +msgstr "" + +#: src/view/com/profile/ProfileHeader.tsx:462 +#~ msgid "Shows a list of users similar to this user." +#~ msgstr "顯示與該使用者相似的使用者列表。" + +#: src/view/com/post-thread/PostThreadFollowBtn.tsx:127 +msgid "Shows posts from {0} in your feed" +msgstr "在你的訊息流中顯示來自 {0} 的貼文" + +#: src/view/com/auth/HomeLoggedOutCTA.tsx:72 +#: src/view/com/auth/login/Login.tsx:98 +#: src/view/com/auth/SplashScreen.tsx:81 +#: src/view/shell/bottom-bar/BottomBar.tsx:289 +#: src/view/shell/bottom-bar/BottomBar.tsx:290 +#: src/view/shell/bottom-bar/BottomBar.tsx:292 +#: src/view/shell/bottom-bar/BottomBarWeb.tsx:178 +#: src/view/shell/bottom-bar/BottomBarWeb.tsx:179 +#: src/view/shell/bottom-bar/BottomBarWeb.tsx:181 +#: src/view/shell/NavSignupCard.tsx:58 +#: src/view/shell/NavSignupCard.tsx:59 +#: src/view/shell/NavSignupCard.tsx:61 +msgid "Sign in" +msgstr "登入" + +#: src/view/com/auth/HomeLoggedOutCTA.tsx:82 +#: src/view/com/auth/SplashScreen.tsx:86 +#: src/view/com/auth/SplashScreen.web.tsx:91 +msgid "Sign In" +msgstr "登入" + +#: src/view/com/auth/login/ChooseAccountForm.tsx:45 +msgid "Sign in as {0}" +msgstr "以 {0} 登入" + +#: src/view/com/auth/login/ChooseAccountForm.tsx:127 +#: src/view/com/auth/login/Login.tsx:116 +msgid "Sign in as..." +msgstr "登入為…" + +#: src/view/com/auth/login/LoginForm.tsx:140 +msgid "Sign into" +msgstr "登入到" + +#: src/view/com/modals/SwitchAccount.tsx:68 +#: src/view/com/modals/SwitchAccount.tsx:73 +#: src/view/screens/Settings/index.tsx:107 +#: src/view/screens/Settings/index.tsx:110 +msgid "Sign out" +msgstr "登出" + +#: src/view/shell/bottom-bar/BottomBar.tsx:279 +#: src/view/shell/bottom-bar/BottomBar.tsx:280 +#: src/view/shell/bottom-bar/BottomBar.tsx:282 +#: src/view/shell/bottom-bar/BottomBarWeb.tsx:168 +#: src/view/shell/bottom-bar/BottomBarWeb.tsx:169 +#: src/view/shell/bottom-bar/BottomBarWeb.tsx:171 +#: src/view/shell/NavSignupCard.tsx:49 +#: src/view/shell/NavSignupCard.tsx:50 +#: src/view/shell/NavSignupCard.tsx:52 +msgid "Sign up" +msgstr "註冊" + +#: src/view/shell/NavSignupCard.tsx:42 +msgid "Sign up or sign in to join the conversation" +msgstr "註冊或登入以參與對話" + +#: src/components/moderation/ScreenHider.tsx:98 +#: src/lib/moderation/useGlobalLabelStrings.ts:28 +msgid "Sign-in Required" +msgstr "需要登入" + +#: src/view/screens/Settings/index.tsx:374 +msgid "Signed in as" +msgstr "登入身分" + +#: src/view/com/auth/login/ChooseAccountForm.tsx:112 +msgid "Signed in as @{0}" +msgstr "以 @{0} 身分登入" + +#: src/view/com/modals/SwitchAccount.tsx:70 +msgid "Signs {0} out of Bluesky" +msgstr "從 {0} 登出 Bluesky" + +#: src/screens/Onboarding/StepInterests/index.tsx:235 +#: src/screens/Onboarding/StepSuggestedAccounts/index.tsx:195 +#: src/view/com/auth/onboarding/WelcomeMobile.tsx:35 +msgid "Skip" +msgstr "跳過" + +#: src/screens/Onboarding/StepInterests/index.tsx:232 +msgid "Skip this flow" +msgstr "跳過此流程" + +#: src/view/com/auth/create/Step2.tsx:82 +#~ msgid "SMS verification" +#~ msgstr "簡訊驗證" + +#: src/screens/Onboarding/index.tsx:40 +msgid "Software Dev" +msgstr "軟體開發" + +#: src/view/com/modals/ProfilePreview.tsx:62 +#~ msgid "Something went wrong and we're not sure what." +#~ msgstr "發生了一些問題,我們不確定是什麼原因。" + +#: src/components/ReportDialog/index.tsx:52 +#: src/screens/Moderation/index.tsx:116 +#: src/screens/Profile/Sections/Labels.tsx:77 +msgid "Something went wrong, please try again." +msgstr "" + +#: src/components/Lists.tsx:203 +#~ msgid "Something went wrong!" +#~ msgstr "發生了一些問題!" + +#: src/view/com/modals/Waitlist.tsx:51 +#~ msgid "Something went wrong. Check your email and try again." +#~ msgstr "發生了一些問題。請檢查你的電子郵件,然後重試。" + +#: src/App.native.tsx:71 +msgid "Sorry! Your session expired. Please log in again." +msgstr "抱歉!你的登入已過期。請重新登入。" + +#: src/view/screens/PreferencesThreads.tsx:69 +msgid "Sort Replies" +msgstr "排序回覆" + +#: src/view/screens/PreferencesThreads.tsx:72 +msgid "Sort replies to the same post by:" +msgstr "對同一貼文的回覆進行排序:" + +#: src/components/moderation/LabelsOnMeDialog.tsx:147 +msgid "Source:" +msgstr "" + +#: src/lib/moderation/useReportOptions.ts:65 +msgid "Spam" +msgstr "" + +#: src/lib/moderation/useReportOptions.ts:53 +msgid "Spam; excessive mentions or replies" +msgstr "" + +#: src/screens/Onboarding/index.tsx:30 +msgid "Sports" +msgstr "運動" + +#: src/view/com/modals/crop-image/CropImage.web.tsx:122 +msgid "Square" +msgstr "方塊" + +#: src/view/com/modals/ServerInput.tsx:62 +#~ msgid "Staging" +#~ msgstr "臨時" + +#: src/view/screens/Settings/index.tsx:905 +msgid "Status page" +msgstr "狀態頁" + +#: src/view/com/auth/create/StepHeader.tsx:22 +msgid "Step {0} of {numSteps}" +msgstr "第 {0} 步,共 {numSteps} 步" + +#: src/view/screens/Settings/index.tsx:292 +msgid "Storage cleared, you need to restart the app now." +msgstr "已清除儲存資料,你需要立即重啟應用程式。" + +#: src/Navigation.tsx:211 +#: src/view/screens/Settings/index.tsx:831 +msgid "Storybook" +msgstr "故事書" + +#: src/components/moderation/LabelsOnMeDialog.tsx:256 +#: src/components/moderation/LabelsOnMeDialog.tsx:257 +msgid "Submit" +msgstr "提交" + +#: src/view/screens/ProfileList.tsx:590 +msgid "Subscribe" +msgstr "訂閱" + +#: src/screens/Profile/Sections/Labels.tsx:181 +msgid "Subscribe to @{0} to use these labels:" +msgstr "" + +#: src/screens/Profile/Header/ProfileHeaderLabeler.tsx:222 +msgid "Subscribe to Labeler" +msgstr "" + +#: src/screens/Onboarding/StepAlgoFeeds/FeedCard.tsx:173 +#: src/screens/Onboarding/StepAlgoFeeds/FeedCard.tsx:308 +msgid "Subscribe to the {0} feed" +msgstr "訂閱 {0} 訊息流" + +#: src/screens/Profile/Header/ProfileHeaderLabeler.tsx:185 +msgid "Subscribe to this labeler" +msgstr "" + +#: src/view/screens/ProfileList.tsx:586 +msgid "Subscribe to this list" +msgstr "訂閱這個列表" + +#: src/view/screens/Search/Search.tsx:375 +msgid "Suggested Follows" +msgstr "推薦的跟隨者" + +#: src/view/com/profile/ProfileHeaderSuggestedFollows.tsx:65 +msgid "Suggested for you" +msgstr "為你推薦" + +#: src/view/com/modals/SelfLabel.tsx:95 +msgid "Suggestive" +msgstr "建議" + +#: src/Navigation.tsx:226 +#: src/view/screens/Support.tsx:30 +#: src/view/screens/Support.tsx:33 +msgid "Support" +msgstr "支援" + +#: src/view/com/modals/ProfilePreview.tsx:110 +#~ msgid "Swipe up to see more" +#~ msgstr "向上滑動查看更多" + +#: src/view/com/modals/SwitchAccount.tsx:123 +msgid "Switch Account" +msgstr "切換帳號" + +#: src/view/com/modals/SwitchAccount.tsx:103 +#: src/view/screens/Settings/index.tsx:139 +msgid "Switch to {0}" +msgstr "切換到 {0}" + +#: src/view/com/modals/SwitchAccount.tsx:104 +#: src/view/screens/Settings/index.tsx:140 +msgid "Switches the account you are logged in to" +msgstr "切換你登入的帳號" + +#: src/view/screens/Settings/index.tsx:491 +msgid "System" +msgstr "系統" + +#: src/view/screens/Settings/index.tsx:819 +msgid "System log" +msgstr "系統日誌" + +#: src/components/dialogs/MutedWords.tsx:324 +msgid "tag" +msgstr "" + +#: src/components/TagMenu/index.tsx:78 +msgid "Tag menu: {displayTag}" +msgstr "" + +#: src/components/TagMenu/index.tsx:74 +#~ msgid "Tag menu: {tag}" +#~ msgstr "" + +#: src/view/com/modals/crop-image/CropImage.web.tsx:112 +msgid "Tall" +msgstr "高" + +#: src/view/com/util/images/AutoSizedImage.tsx:70 +msgid "Tap to view fully" +msgstr "點擊查看完整內容" + +#: src/screens/Onboarding/index.tsx:39 +msgid "Tech" +msgstr "科技" + +#: src/view/shell/desktop/RightNav.tsx:81 +msgid "Terms" +msgstr "條款" + +#: src/Navigation.tsx:236 +#: src/view/com/auth/create/Policies.tsx:59 +#: src/view/screens/Settings/index.tsx:919 +#: src/view/screens/TermsOfService.tsx:29 +#: src/view/shell/Drawer.tsx:259 +msgid "Terms of Service" +msgstr "服務條款" + +#: src/lib/moderation/useReportOptions.ts:58 +#: src/lib/moderation/useReportOptions.ts:79 +#: src/lib/moderation/useReportOptions.ts:87 +msgid "Terms used violate community standards" +msgstr "" + +#: src/components/dialogs/MutedWords.tsx:324 +msgid "text" +msgstr "文字" + +#: src/components/moderation/LabelsOnMeDialog.tsx:220 +msgid "Text input field" +msgstr "文字輸入框" + +#: src/components/ReportDialog/SubmitView.tsx:78 +msgid "Thank you. Your report has been sent." +msgstr "" + +#: src/view/com/modals/ChangeHandle.tsx:466 +msgid "That contains the following:" +msgstr "" + +#: src/view/com/auth/create/CreateAccount.tsx:94 +msgid "That handle is already taken." +msgstr "這個帳號代碼已被使用。" + +#: src/screens/Profile/Header/ProfileHeaderStandard.tsx:274 +#: src/view/com/profile/ProfileMenu.tsx:349 +msgid "The account will be able to interact with you after unblocking." +msgstr "解除封鎖後,該帳號將能夠與你互動。" + +#: src/components/moderation/ModerationDetailsDialog.tsx:128 +msgid "the author" +msgstr "" + +#: src/view/screens/CommunityGuidelines.tsx:36 +msgid "The Community Guidelines have been moved to <0/>" +msgstr "社群準則已移動到 <0/>" + +#: src/view/screens/CopyrightPolicy.tsx:33 +msgid "The Copyright Policy has been moved to <0/>" +msgstr "版權政策已移動到 <0/>" + +#: src/components/moderation/LabelsOnMeDialog.tsx:49 +msgid "The following labels were applied to your account." +msgstr "" + +#: src/components/moderation/LabelsOnMeDialog.tsx:50 +msgid "The following labels were applied to your content." +msgstr "" + +#: src/screens/Onboarding/Layout.tsx:60 +msgid "The following steps will help customize your Bluesky experience." +msgstr "以下步驟將幫助自訂你的 Bluesky 體驗。" + +#: src/view/com/post-thread/PostThread.tsx:153 +#: src/view/com/post-thread/PostThread.tsx:165 +msgid "The post may have been deleted." +msgstr "此貼文可能已被刪除。" + +#: src/view/screens/PrivacyPolicy.tsx:33 +msgid "The Privacy Policy has been moved to <0/>" +msgstr "隱私政策已移動到 <0/>" + +#: src/view/screens/Support.tsx:36 +msgid "The support form has been moved. If you need help, please <0/> or visit {HELP_DESK_URL} to get in touch with us." +msgstr "支援表單已移至別處。如果需協助,請點擊<0/>或前往 {HELP_DESK_URL} 與我們聯繫。" + +#: src/view/screens/TermsOfService.tsx:33 +msgid "The Terms of Service have been moved to" +msgstr "服務條款已遷移到" + +#: src/screens/Onboarding/StepAlgoFeeds/index.tsx:150 +msgid "There are many feeds to try:" +msgstr "這裡有些訊息流你可以嘗試:" + +#: src/screens/Profile/Header/ProfileHeaderLabeler.tsx:113 +#: src/view/screens/ProfileFeed.tsx:543 +msgid "There was an an issue contacting the server, please check your internet connection and try again." +msgstr "連線至伺服器時出現問題,請檢查你的網路連線並重試。" + +#: src/view/com/posts/FeedErrorMessage.tsx:138 +msgid "There was an an issue removing this feed. Please check your internet connection and try again." +msgstr "刪除訊息流時出現問題,請檢查你的網路連線並重試。" + +#: src/view/screens/ProfileFeed.tsx:217 +msgid "There was an an issue updating your feeds, please check your internet connection and try again." +msgstr "更新訊息流時出現問題,請檢查你的網路連線並重試。" + +#: src/view/screens/ProfileFeed.tsx:244 +#: src/view/screens/ProfileList.tsx:275 +#: src/view/screens/SavedFeeds.tsx:209 +#: src/view/screens/SavedFeeds.tsx:231 +#: src/view/screens/SavedFeeds.tsx:252 +msgid "There was an issue contacting the server" +msgstr "連線伺服器時出現問題" + +#: src/view/com/auth/onboarding/RecommendedFeedsItem.tsx:57 +#: src/view/com/auth/onboarding/RecommendedFeedsItem.tsx:66 +#: src/view/com/feeds/FeedSourceCard.tsx:110 +#: src/view/com/feeds/FeedSourceCard.tsx:123 +msgid "There was an issue contacting your server" +msgstr "連線伺服器時出現問題" + +#: src/view/com/notifications/Feed.tsx:117 +msgid "There was an issue fetching notifications. Tap here to try again." +msgstr "取得通知時發生問題,點擊這裡重試。" + +#: src/view/com/posts/Feed.tsx:283 +msgid "There was an issue fetching posts. Tap here to try again." +msgstr "取得貼文時發生問題,點擊這裡重試。" + +#: src/view/com/lists/ListMembers.tsx:172 +msgid "There was an issue fetching the list. Tap here to try again." +msgstr "取得列表時發生問題,點擊這裡重試。" + +#: src/view/com/feeds/ProfileFeedgens.tsx:148 +#: src/view/com/lists/ProfileLists.tsx:155 +msgid "There was an issue fetching your lists. Tap here to try again." +msgstr "取得列表時發生問題,點擊這裡重試。" + +#: src/components/ReportDialog/SubmitView.tsx:83 +msgid "There was an issue sending your report. Please check your internet connection." +msgstr "" + +#: src/screens/Onboarding/StepModeration/AdultContentEnabledPref.tsx:65 +msgid "There was an issue syncing your preferences with the server" +msgstr "與伺服器同步偏好設定時發生問題" + +#: src/view/screens/AppPasswords.tsx:68 +msgid "There was an issue with fetching your app passwords" +msgstr "取得應用程式專用密碼時發生問題" + +#: src/screens/Profile/Header/ProfileHeaderStandard.tsx:98 +#: src/screens/Profile/Header/ProfileHeaderStandard.tsx:120 +#: src/screens/Profile/Header/ProfileHeaderStandard.tsx:134 +#: src/view/com/post-thread/PostThreadFollowBtn.tsx:96 +#: src/view/com/post-thread/PostThreadFollowBtn.tsx:108 +#: src/view/com/profile/ProfileMenu.tsx:106 +#: src/view/com/profile/ProfileMenu.tsx:117 +#: src/view/com/profile/ProfileMenu.tsx:132 +#: src/view/com/profile/ProfileMenu.tsx:143 +#: src/view/com/profile/ProfileMenu.tsx:157 +#: src/view/com/profile/ProfileMenu.tsx:170 +msgid "There was an issue! {0}" +msgstr "發生問題了!{0}" + +#: src/view/screens/ProfileList.tsx:288 +#: src/view/screens/ProfileList.tsx:302 +#: src/view/screens/ProfileList.tsx:316 +#: src/view/screens/ProfileList.tsx:330 +msgid "There was an issue. Please check your internet connection and try again." +msgstr "發生問題了。請檢查你的網路連線並重試。" + +#: src/view/com/util/ErrorBoundary.tsx:51 +msgid "There was an unexpected issue in the application. Please let us know if this happened to you!" +msgstr "應用程式中發生了意外問題。請告訴我們是否發生在你身上!" + +#: src/screens/Deactivated.tsx:106 +msgid "There's been a rush of new users to Bluesky! We'll activate your account as soon as we can." +msgstr "Bluesky 迎來了大量新使用者!我們將儘快啟用你的帳號。" + +#: src/view/com/auth/create/Step2.tsx:55 +#~ msgid "There's something wrong with this number. Please choose your country and enter your full phone number!" +#~ msgstr "電話號碼有誤,請選擇區號並輸入完整的電話號碼!" + +#: src/screens/Onboarding/StepSuggestedAccounts/index.tsx:138 +msgid "These are popular accounts you might like:" +msgstr "這裡是一些受歡迎的帳號,你可能會喜歡:" + +#: src/components/moderation/ScreenHider.tsx:117 +msgid "This {screenDescription} has been flagged:" +msgstr "{screenDescription} 已被標記:" + +#: src/components/moderation/ScreenHider.tsx:112 +msgid "This account has requested that users sign in to view their profile." +msgstr "此帳號要求使用者登入後才能查看其個人資料。" + +#: src/components/moderation/LabelsOnMeDialog.tsx:205 +msgid "This appeal will be sent to <0>{0}." +msgstr "" + +#: src/lib/moderation/useGlobalLabelStrings.ts:19 +msgid "This content has been hidden by the moderators." +msgstr "" + +#: src/lib/moderation/useGlobalLabelStrings.ts:24 +msgid "This content has received a general warning from moderators." +msgstr "" + +#: src/view/com/modals/EmbedConsent.tsx:68 +msgid "This content is hosted by {0}. Do you want to enable external media?" +msgstr "此內容由 {0} 托管。是否要啟用外部媒體?" + +#: src/components/moderation/ModerationDetailsDialog.tsx:78 +#: src/lib/moderation/useModerationCauseDescription.ts:77 +msgid "This content is not available because one of the users involved has blocked the other." +msgstr "由於其中一個使用者封鎖了另一個使用者,無法查看此內容。" + +#: src/view/com/posts/FeedErrorMessage.tsx:108 +msgid "This content is not viewable without a Bluesky account." +msgstr "沒有 Bluesky 帳號,無法查看此內容。" + +#: src/view/screens/Settings/ExportCarDialog.tsx:75 +#~ msgid "This feature is in beta. You can read more about repository exports in <0>this blogpost." +#~ msgstr "此功能目前為測試版本。您可以在<0>這篇部落格文章中了解更多有關匯出存放庫的資訊" + +#: src/view/screens/Settings/ExportCarDialog.tsx:75 +msgid "This feature is in beta. You can read more about repository exports in <0>this blogpost." +msgstr "" + +#: src/view/com/posts/FeedErrorMessage.tsx:114 +msgid "This feed is currently receiving high traffic and is temporarily unavailable. Please try again later." +msgstr "此訊息流由於目前使用人數眾多而暫時無法使用。請稍後再試。" + +#: src/screens/Profile/Sections/Feed.tsx:50 +#: src/view/screens/ProfileFeed.tsx:476 +#: src/view/screens/ProfileList.tsx:675 +msgid "This feed is empty!" +msgstr "這個訊息流是空的!" + +#: src/view/com/posts/CustomFeedEmptyState.tsx:37 +msgid "This feed is empty! You may need to follow more users or tune your language settings." +msgstr "這個訊息流是空的!你或許需要先跟隨更多的人或檢查你的語言設定。" + +#: src/components/dialogs/BirthDateSettings.tsx:41 +msgid "This information is not shared with other users." +msgstr "此資訊不會分享給其他使用者。" + +#: src/view/com/modals/VerifyEmail.tsx:119 +msgid "This is important in case you ever need to change your email or reset your password." +msgstr "這很重要,以防你將來需要更改電子郵件地址或重設密碼。" + +#: src/components/moderation/ModerationDetailsDialog.tsx:125 +msgid "This label was applied by {0}." +msgstr "" + +#: src/screens/Profile/Sections/Labels.tsx:168 +msgid "This labeler hasn't declared what labels it publishes, and may not be active." +msgstr "" + +#: src/view/com/modals/LinkWarning.tsx:58 +msgid "This link is taking you to the following website:" +msgstr "此連結將帶你到以下網站:" + +#: src/view/screens/ProfileList.tsx:853 +msgid "This list is empty!" +msgstr "此列表為空!" + +#: src/screens/Profile/ErrorState.tsx:40 +msgid "This moderation service is unavailable. See below for more details. If this issue persists, contact us." +msgstr "" + +#: src/view/com/modals/AddAppPasswords.tsx:106 +msgid "This name is already in use" +msgstr "此名稱已被使用" + +#: src/view/com/post-thread/PostThreadItem.tsx:125 +msgid "This post has been deleted." +msgstr "此貼文已被刪除。" + +#: src/view/com/util/forms/PostDropdownBtn.tsx:344 +msgid "This post is only visible to logged-in users. It won't be visible to people who aren't logged in." +msgstr "" + +#: src/view/com/util/forms/PostDropdownBtn.tsx:326 +msgid "This post will be hidden from feeds." +msgstr "" + +#: src/view/com/profile/ProfileMenu.tsx:370 +msgid "This profile is only visible to logged-in users. It won't be visible to people who aren't logged in." +msgstr "" + +#: src/view/com/auth/create/Policies.tsx:46 +msgid "This service has not provided terms of service or a privacy policy." +msgstr "" + +#: src/view/com/modals/ChangeHandle.tsx:446 +msgid "This should create a domain record at:" +msgstr "" + +#: src/view/com/profile/ProfileFollowers.tsx:95 +msgid "This user doesn't have any followers." +msgstr "" + +#: src/components/moderation/ModerationDetailsDialog.tsx:73 +#: src/lib/moderation/useModerationCauseDescription.ts:68 +msgid "This user has blocked you. You cannot view their content." +msgstr "此使用者已封鎖你,你無法查看他們的內容。" + +#: src/lib/moderation/useGlobalLabelStrings.ts:30 +msgid "This user has requested that their content only be shown to signed-in users." +msgstr "" + +#: src/view/com/modals/ModerationDetails.tsx:42 +#~ msgid "This user is included in the <0/> list which you have blocked." +#~ msgstr "此使用者包含在你已封鎖的 <0/> 列表中。" + +#: src/view/com/modals/ModerationDetails.tsx:74 +#~ msgid "This user is included in the <0/> list which you have muted." +#~ msgstr "此使用者包含在你已靜音的 <0/> 列表中。" + +#: src/components/moderation/ModerationDetailsDialog.tsx:56 +msgid "This user is included in the <0>{0} list which you have blocked." +msgstr "" + +#: src/components/moderation/ModerationDetailsDialog.tsx:85 +msgid "This user is included in the <0>{0} list which you have muted." +msgstr "" + +#: src/view/com/modals/ModerationDetails.tsx:74 +#~ msgid "This user is included the <0/> list which you have muted." +#~ msgstr "此使用者包含在你已靜音的 <0/> 列表中。" + +#: src/view/com/profile/ProfileFollows.tsx:94 +msgid "This user isn't following anyone." +msgstr "" + +#: src/view/com/modals/SelfLabel.tsx:137 +msgid "This warning is only available for posts with media attached." +msgstr "此警告僅適用於附帶媒體的貼文。" + +#: src/components/dialogs/MutedWords.tsx:284 +msgid "This will delete {0} from your muted words. You can always add it back later." +msgstr "" + +#: src/view/com/util/forms/PostDropdownBtn.tsx:282 +#~ msgid "This will hide this post from your feeds." +#~ msgstr "這將在你的訊息流中隱藏此貼文。" + +#: src/view/screens/Settings/index.tsx:574 +msgid "Thread preferences" +msgstr "" + +#: src/view/screens/PreferencesThreads.tsx:53 +#: src/view/screens/Settings/index.tsx:584 +msgid "Thread Preferences" +msgstr "對話串偏好" + +#: src/view/screens/PreferencesThreads.tsx:119 +msgid "Threaded Mode" +msgstr "對話串模式" + +#: src/Navigation.tsx:269 +msgid "Threads Preferences" +msgstr "對話串偏好" + +#: src/components/ReportDialog/SelectLabelerView.tsx:35 +msgid "To whom would you like to send this report?" +msgstr "" + +#: src/components/dialogs/MutedWords.tsx:113 +msgid "Toggle between muted word options." +msgstr "" + +#: src/view/com/util/forms/DropdownButton.tsx:246 +msgid "Toggle dropdown" +msgstr "切換下拉式選單" + +#: src/screens/Moderation/index.tsx:334 +msgid "Toggle to enable or disable adult content" +msgstr "" + +#: src/view/com/modals/EditImage.tsx:271 +msgid "Transformations" +msgstr "轉換" + +#: src/view/com/post-thread/PostThreadItem.tsx:644 +#: src/view/com/post-thread/PostThreadItem.tsx:646 +#: src/view/com/util/forms/PostDropdownBtn.tsx:212 +#: src/view/com/util/forms/PostDropdownBtn.tsx:214 +msgid "Translate" +msgstr "翻譯" + +#: src/view/com/util/error/ErrorScreen.tsx:82 +msgctxt "action" +msgid "Try again" +msgstr "重試" + +#: src/view/com/modals/ChangeHandle.tsx:429 +msgid "Type:" +msgstr "" + +#: src/view/screens/ProfileList.tsx:478 +msgid "Un-block list" +msgstr "取消封鎖列表" + +#: src/view/screens/ProfileList.tsx:461 +msgid "Un-mute list" +msgstr "取消靜音列表" + +#: src/view/com/auth/create/CreateAccount.tsx:58 +#: src/view/com/auth/login/ForgotPasswordForm.tsx:87 +#: src/view/com/auth/login/Login.tsx:76 +#: src/view/com/auth/login/LoginForm.tsx:121 +#: src/view/com/modals/ChangePassword.tsx:70 +msgid "Unable to contact your service. Please check your Internet connection." +msgstr "無法連線到服務,請檢查你的網路連線。" + +#: src/screens/Profile/Header/ProfileHeaderStandard.tsx:174 +#: src/screens/Profile/Header/ProfileHeaderStandard.tsx:278 +#: src/view/com/profile/ProfileMenu.tsx:361 +#: src/view/screens/ProfileList.tsx:572 +msgid "Unblock" +msgstr "取消封鎖" + +#: src/screens/Profile/Header/ProfileHeaderStandard.tsx:179 +msgctxt "action" +msgid "Unblock" +msgstr "取消封鎖" + +#: src/view/com/profile/ProfileMenu.tsx:299 +#: src/view/com/profile/ProfileMenu.tsx:305 +msgid "Unblock Account" +msgstr "取消封鎖" + +#: src/screens/Profile/Header/ProfileHeaderStandard.tsx:272 +#: src/view/com/profile/ProfileMenu.tsx:343 +msgid "Unblock Account?" +msgstr "" + +#: src/view/com/modals/Repost.tsx:42 +#: src/view/com/modals/Repost.tsx:55 +#: src/view/com/util/post-ctrls/RepostButton.tsx:60 +#: src/view/com/util/post-ctrls/RepostButton.web.tsx:48 +msgid "Undo repost" +msgstr "取消轉發" + +#: src/view/com/auth/onboarding/RecommendedFollowsItem.tsx:141 +#: src/view/com/profile/ProfileHeaderSuggestedFollows.tsx:246 +msgid "Unfollow" +msgstr "" + +#: src/view/com/profile/FollowButton.tsx:60 +msgctxt "action" +msgid "Unfollow" +msgstr "取消跟隨" + +#: src/screens/Profile/Header/ProfileHeaderStandard.tsx:213 +msgid "Unfollow {0}" +msgstr "取消跟隨 {0}" + +#: src/view/com/profile/ProfileMenu.tsx:241 +#: src/view/com/profile/ProfileMenu.tsx:251 +msgid "Unfollow Account" +msgstr "" + +#: src/view/com/auth/create/state.ts:262 +msgid "Unfortunately, you do not meet the requirements to create an account." +msgstr "很遺憾,你不符合建立帳號的要求。" + +#: src/view/com/util/post-ctrls/PostCtrls.tsx:185 +msgid "Unlike" +msgstr "取消喜歡" + +#: src/view/screens/ProfileFeed.tsx:572 +msgid "Unlike this feed" +msgstr "" + +#: src/components/TagMenu/index.tsx:249 +#: src/view/screens/ProfileList.tsx:579 +msgid "Unmute" +msgstr "取消靜音" + +#: src/components/TagMenu/index.web.tsx:104 +msgid "Unmute {truncatedTag}" +msgstr "" + +#: src/view/com/profile/ProfileMenu.tsx:278 +#: src/view/com/profile/ProfileMenu.tsx:284 +msgid "Unmute Account" +msgstr "取消靜音帳號" + +#: src/components/TagMenu/index.tsx:208 +msgid "Unmute all {displayTag} posts" +msgstr "" + +#: src/components/TagMenu/index.tsx:210 +#~ msgid "Unmute all {tag} posts" +#~ msgstr "" + +#: src/view/com/util/forms/PostDropdownBtn.tsx:251 +#: src/view/com/util/forms/PostDropdownBtn.tsx:256 +msgid "Unmute thread" +msgstr "取消靜音對話串" + +#: src/view/screens/ProfileFeed.tsx:294 +#: src/view/screens/ProfileList.tsx:563 +msgid "Unpin" +msgstr "取消固定" + +#: src/view/screens/ProfileFeed.tsx:291 +msgid "Unpin from home" +msgstr "" + +#: src/view/screens/ProfileList.tsx:444 +msgid "Unpin moderation list" +msgstr "取消固定限制列表" + +#: src/view/screens/ProfileFeed.tsx:346 +#~ msgid "Unsave" +#~ msgstr "取消儲存" + +#: src/screens/Profile/Header/ProfileHeaderLabeler.tsx:220 +msgid "Unsubscribe" +msgstr "" + +#: src/screens/Profile/Header/ProfileHeaderLabeler.tsx:184 +msgid "Unsubscribe from this labeler" +msgstr "" + +#: src/lib/moderation/useReportOptions.ts:70 +msgid "Unwanted Sexual Content" +msgstr "" + +#: src/view/com/modals/UserAddRemoveLists.tsx:70 +msgid "Update {displayName} in Lists" +msgstr "更新列表中的 {displayName}" + +#: src/lib/hooks/useOTAUpdate.ts:15 +#~ msgid "Update Available" +#~ msgstr "更新可用" + +#: src/view/com/modals/ChangeHandle.tsx:509 +msgid "Update to {handle}" +msgstr "" + +#: src/view/com/auth/login/SetNewPasswordForm.tsx:204 +msgid "Updating..." +msgstr "更新中…" + +#: src/view/com/modals/ChangeHandle.tsx:455 +msgid "Upload a text file to:" +msgstr "上傳文字檔案至:" + +#: src/view/com/util/UserAvatar.tsx:326 +#: src/view/com/util/UserAvatar.tsx:329 +#: src/view/com/util/UserBanner.tsx:116 +#: src/view/com/util/UserBanner.tsx:119 +msgid "Upload from Camera" +msgstr "" + +#: src/view/com/util/UserAvatar.tsx:343 +#: src/view/com/util/UserBanner.tsx:133 +msgid "Upload from Files" +msgstr "" + +#: src/view/com/util/UserAvatar.tsx:337 +#: src/view/com/util/UserAvatar.tsx:341 +#: src/view/com/util/UserBanner.tsx:127 +#: src/view/com/util/UserBanner.tsx:131 +msgid "Upload from Library" +msgstr "" + +#: src/view/com/modals/ChangeHandle.tsx:409 +msgid "Use a file on your server" +msgstr "" + +#: src/view/screens/AppPasswords.tsx:197 +msgid "Use app passwords to login to other Bluesky clients without giving full access to your account or password." +msgstr "使用應用程式專用密碼登入到其他 Bluesky 用戶端,而無需提供你的帳號或密碼。" + +#: src/view/com/modals/ChangeHandle.tsx:518 +msgid "Use bsky.social as hosting provider" +msgstr "" + +#: src/view/com/modals/ChangeHandle.tsx:517 +msgid "Use default provider" +msgstr "使用預設提供商" + +#: src/view/com/modals/InAppBrowserConsent.tsx:56 +#: src/view/com/modals/InAppBrowserConsent.tsx:58 +msgid "Use in-app browser" +msgstr "使用內建瀏覽器" + +#: src/view/com/modals/InAppBrowserConsent.tsx:66 +#: src/view/com/modals/InAppBrowserConsent.tsx:68 +msgid "Use my default browser" +msgstr "使用我的預設瀏覽器" + +#: src/view/com/modals/ChangeHandle.tsx:401 +msgid "Use the DNS panel" +msgstr "" + +#: src/view/com/modals/AddAppPasswords.tsx:155 +msgid "Use this to sign into the other app along with your handle." +msgstr "使用這個和你的帳號代碼一起登入其他應用程式。" + +#: src/view/com/modals/ServerInput.tsx:105 +#~ msgid "Use your domain as your Bluesky client service provider" +#~ msgstr "將你的網域用作 Bluesky 用戶端服務提供商" + +#: src/view/com/modals/InviteCodes.tsx:200 +msgid "Used by:" +msgstr "使用者:" + +#: src/components/moderation/ModerationDetailsDialog.tsx:65 +#: src/lib/moderation/useModerationCauseDescription.ts:56 +msgid "User Blocked" +msgstr "使用者被封鎖" + +#: src/lib/moderation/useModerationCauseDescription.ts:48 +msgid "User Blocked by \"{0}\"" +msgstr "" + +#: src/components/moderation/ModerationDetailsDialog.tsx:54 +msgid "User Blocked by List" +msgstr "使用者被列表封鎖" + +#: src/lib/moderation/useModerationCauseDescription.ts:66 +msgid "User Blocking You" +msgstr "" + +#: src/components/moderation/ModerationDetailsDialog.tsx:71 +msgid "User Blocks You" +msgstr "使用者封鎖了你" + +#: src/view/com/auth/create/Step2.tsx:79 +msgid "User handle" +msgstr "帳號代碼" + +#: src/view/com/lists/ListCard.tsx:85 +#: src/view/com/modals/UserAddRemoveLists.tsx:198 +msgid "User list by {0}" +msgstr "{0} 的使用者列表" + +#: src/view/screens/ProfileList.tsx:777 +msgid "User list by <0/>" +msgstr "<0/> 的使用者列表" + +#: src/view/com/lists/ListCard.tsx:83 +#: src/view/com/modals/UserAddRemoveLists.tsx:196 +#: src/view/screens/ProfileList.tsx:775 +msgid "User list by you" +msgstr "你的使用者列表" + +#: src/view/com/modals/CreateOrEditList.tsx:196 +msgid "User list created" +msgstr "使用者列表已建立" + +#: src/view/com/modals/CreateOrEditList.tsx:182 +msgid "User list updated" +msgstr "使用者列表已更新" + +#: src/view/screens/Lists.tsx:58 +msgid "User Lists" +msgstr "使用者列表" + +#: src/view/com/auth/login/LoginForm.tsx:180 +#: src/view/com/auth/login/LoginForm.tsx:198 +msgid "Username or email address" +msgstr "使用者名稱或電子郵件地址" + +#: src/view/screens/ProfileList.tsx:811 +msgid "Users" +msgstr "使用者" + +#: src/view/com/threadgate/WhoCanReply.tsx:143 +msgid "users followed by <0/>" +msgstr "跟隨 <0/> 的使用者" + +#: src/view/com/modals/Threadgate.tsx:106 +msgid "Users in \"{0}\"" +msgstr "「{0}」中的使用者" + +#: src/components/LikesDialog.tsx:85 +msgid "Users that have liked this content or profile" +msgstr "" + +#: src/view/com/modals/ChangeHandle.tsx:437 +msgid "Value:" +msgstr "" + +#: src/view/com/auth/create/Step2.tsx:243 +#~ msgid "Verification code" +#~ msgstr "驗證碼" + +#: src/view/com/modals/ChangeHandle.tsx:510 +msgid "Verify {0}" +msgstr "" + +#: src/view/screens/Settings/index.tsx:944 +msgid "Verify email" +msgstr "驗證電子郵件" + +#: src/view/screens/Settings/index.tsx:969 +msgid "Verify my email" +msgstr "驗證我的電子郵件" + +#: src/view/screens/Settings/index.tsx:978 +msgid "Verify My Email" +msgstr "驗證我的電子郵件" + +#: src/view/com/modals/ChangeEmail.tsx:205 +#: src/view/com/modals/ChangeEmail.tsx:207 +msgid "Verify New Email" +msgstr "驗證新的電子郵件" + +#: src/view/com/modals/VerifyEmail.tsx:103 +msgid "Verify Your Email" +msgstr "驗證你的電子郵件" + +#: src/screens/Onboarding/index.tsx:42 +msgid "Video Games" +msgstr "電子遊戲" + +#: src/screens/Profile/Header/Shell.tsx:110 +msgid "View {0}'s avatar" +msgstr "查看{0}的頭貼" + +#: src/view/screens/Log.tsx:52 +msgid "View debug entry" +msgstr "查看除錯項目" + +#: src/components/ReportDialog/SelectReportOptionView.tsx:133 +msgid "View details" +msgstr "" + +#: src/components/ReportDialog/SelectReportOptionView.tsx:128 +msgid "View details for reporting a copyright violation" +msgstr "" + +#: src/view/com/posts/FeedSlice.tsx:99 +msgid "View full thread" +msgstr "查看整個對話串" + +#: src/components/moderation/LabelsOnMe.tsx:51 +msgid "View information about these labels" +msgstr "" + +#: src/view/com/posts/FeedErrorMessage.tsx:166 +msgid "View profile" +msgstr "查看資料" + +#: src/view/com/profile/ProfileSubpageHeader.tsx:128 +msgid "View the avatar" +msgstr "查看頭像" + +#: src/components/LabelingServiceCard/index.tsx:140 +msgid "View the labeling service provided by @{0}" +msgstr "" + +#: src/view/screens/ProfileFeed.tsx:584 +msgid "View users who like this feed" +msgstr "" + +#: src/view/com/modals/LinkWarning.tsx:75 +#: src/view/com/modals/LinkWarning.tsx:77 +msgid "Visit Site" +msgstr "造訪網站" + +#: src/components/moderation/GlobalModerationLabelPref.tsx:44 +#: src/lib/moderation/useLabelBehaviorDescription.ts:17 +#: src/lib/moderation/useLabelBehaviorDescription.ts:22 +#: src/screens/Onboarding/StepModeration/ModerationOption.tsx:53 +msgid "Warn" +msgstr "警告" + +#: src/lib/moderation/useLabelBehaviorDescription.ts:48 +msgid "Warn content" +msgstr "" + +#: src/lib/moderation/useLabelBehaviorDescription.ts:46 +msgid "Warn content and filter from feeds" +msgstr "" + +#: src/screens/Onboarding/StepAlgoFeeds/index.tsx:134 +msgid "We also think you'll like \"For You\" by Skygaze:" +msgstr "我們認為你還會喜歡 Skygaze 維護的「For You」:" + +#: src/screens/Hashtag.tsx:132 +msgid "We couldn't find any results for that hashtag." +msgstr "" + +#: src/screens/Deactivated.tsx:133 +msgid "We estimate {estimatedTime} until your account is ready." +msgstr "我們估計還需要 {estimatedTime} 才能準備好你的帳號。" + +#: src/screens/Onboarding/StepFinished.tsx:93 +msgid "We hope you have a wonderful time. Remember, Bluesky is:" +msgstr "我們希望你在此度過愉快的時光。請記住,Bluesky 是:" + +#: src/view/com/posts/DiscoverFallbackHeader.tsx:29 +msgid "We ran out of posts from your follows. Here's the latest from <0/>." +msgstr "你已看完了你跟隨的貼文。這是 <0/> 的最新貼文。" + +#: src/components/dialogs/MutedWords.tsx:204 +msgid "We recommend avoiding common words that appear in many posts, since it can result in no posts being shown." +msgstr "" + +#: src/screens/Onboarding/StepAlgoFeeds/index.tsx:124 +msgid "We recommend our \"Discover\" feed:" +msgstr "我們推薦我們的「Discover」訊息流:" + +#: src/components/dialogs/BirthDateSettings.tsx:52 +msgid "We were unable to load your birth date preferences. Please try again." +msgstr "" + +#: src/screens/Moderation/index.tsx:387 +msgid "We were unable to load your configured labelers at this time." +msgstr "" + +#: src/screens/Onboarding/StepInterests/index.tsx:133 +msgid "We weren't able to connect. Please try again to continue setting up your account. If it continues to fail, you can skip this flow." +msgstr "我們無法連線到網際網路,請重試以繼續設定你的帳號。如果仍繼續失敗,你可以選擇跳過此流程。" + +#: src/screens/Deactivated.tsx:137 +msgid "We will let you know when your account is ready." +msgstr "我們會在你的帳號準備好時通知你。" + +#: src/view/com/modals/AppealLabel.tsx:48 +#~ msgid "We'll look into your appeal promptly." +#~ msgstr "我們將迅速審查你的申訴。" + +#: src/screens/Onboarding/StepInterests/index.tsx:138 +msgid "We'll use this to help customize your experience." +msgstr "我們將使用這些資訊來幫助定制你的體驗。" + +#: src/view/com/auth/create/CreateAccount.tsx:134 +msgid "We're so excited to have you join us!" +msgstr "我們非常高興你加入我們!" + +#: src/view/screens/ProfileList.tsx:89 +msgid "We're sorry, but we were unable to resolve this list. If this persists, please contact the list creator, @{handleOrDid}." +msgstr "很抱歉,我們無法解析此列表。如果問題持續發生,請聯繫列表建立者 @{handleOrDid}。" + +#: src/components/dialogs/MutedWords.tsx:230 +msgid "We're sorry, but we weren't able to load your muted words at this time. Please try again." +msgstr "" + +#: src/view/screens/Search/Search.tsx:255 +msgid "We're sorry, but your search could not be completed. Please try again in a few minutes." +msgstr "很抱歉,無法完成你的搜尋請求。請稍後再試。" + +#: src/components/Lists.tsx:194 +#: src/view/screens/NotFound.tsx:48 +msgid "We're sorry! We can't find the page you were looking for." +msgstr "很抱歉!我們找不到你正在尋找的頁面。" + +#: src/screens/Profile/Header/ProfileHeaderLabeler.tsx:319 +msgid "We're sorry! You can only subscribe to ten labelers, and you've reached your limit of ten." +msgstr "" + +#: src/view/com/auth/onboarding/WelcomeMobile.tsx:48 +msgid "Welcome to <0>Bluesky" +msgstr "歡迎來到 <0>Bluesky" + +#: src/screens/Onboarding/StepInterests/index.tsx:130 +msgid "What are your interests?" +msgstr "你感興趣的是什麼?" + +#: src/view/com/modals/report/Modal.tsx:169 +#~ msgid "What is the issue with this {collectionName}?" +#~ msgstr "這個 {collectionName} 有什麼問題?" + +#: src/view/com/auth/SplashScreen.tsx:59 +#: src/view/com/composer/Composer.tsx:295 +msgid "What's up?" +msgstr "發生了什麼新鮮事?" + +#: src/view/com/modals/lang-settings/PostLanguagesSettings.tsx:78 +msgid "Which languages are used in this post?" +msgstr "這個貼文使用了哪些語言?" + +#: src/view/com/modals/lang-settings/ContentLanguagesSettings.tsx:77 +msgid "Which languages would you like to see in your algorithmic feeds?" +msgstr "你想在演算法訊息流中看到哪些語言?" + +#: src/view/com/composer/threadgate/ThreadgateBtn.tsx:47 +#: src/view/com/modals/Threadgate.tsx:66 +msgid "Who can reply" +msgstr "誰可以回覆" + +#: src/components/ReportDialog/SelectReportOptionView.tsx:44 +msgid "Why should this content be reviewed?" +msgstr "" + +#: src/components/ReportDialog/SelectReportOptionView.tsx:57 +msgid "Why should this feed be reviewed?" +msgstr "" + +#: src/components/ReportDialog/SelectReportOptionView.tsx:54 +msgid "Why should this list be reviewed?" +msgstr "" + +#: src/components/ReportDialog/SelectReportOptionView.tsx:51 +msgid "Why should this post be reviewed?" +msgstr "" + +#: src/components/ReportDialog/SelectReportOptionView.tsx:48 +msgid "Why should this user be reviewed?" +msgstr "" + +#: src/view/com/modals/crop-image/CropImage.web.tsx:102 +msgid "Wide" +msgstr "寬" + +#: src/view/com/composer/Composer.tsx:435 +msgid "Write post" +msgstr "撰寫貼文" + +#: src/view/com/composer/Composer.tsx:294 +#: src/view/com/composer/Prompt.tsx:37 +msgid "Write your reply" +msgstr "撰寫你的回覆" + +#: src/screens/Onboarding/index.tsx:28 +msgid "Writers" +msgstr "作家" + +#: src/view/com/auth/create/Step2.tsx:263 +#~ msgid "XXXXXX" +#~ msgstr "XXXXXX" + +#: src/view/com/composer/select-language/SuggestedLanguage.tsx:77 +#: src/view/screens/PreferencesFollowingFeed.tsx:129 +#: src/view/screens/PreferencesFollowingFeed.tsx:201 +#: src/view/screens/PreferencesFollowingFeed.tsx:236 +#: src/view/screens/PreferencesFollowingFeed.tsx:271 +#: src/view/screens/PreferencesThreads.tsx:106 +#: src/view/screens/PreferencesThreads.tsx:129 +msgid "Yes" +msgstr "開" + +#: src/screens/Deactivated.tsx:130 +msgid "You are in line." +msgstr "輪到你了。" + +#: src/view/com/profile/ProfileFollows.tsx:93 +msgid "You are not following anyone." +msgstr "" + +#: src/view/com/posts/FollowingEmptyState.tsx:67 +#: src/view/com/posts/FollowingEndOfFeed.tsx:68 +msgid "You can also discover new Custom Feeds to follow." +msgstr "你也可以探索並跟隨新的自訂訊息流。" + +#: src/screens/Onboarding/StepFollowingFeed.tsx:142 +msgid "You can change these settings later." +msgstr "你可以稍後在設定中更改。" + +#: src/view/com/auth/login/Login.tsx:158 +#: src/view/com/auth/login/PasswordUpdatedForm.tsx:31 +msgid "You can now sign in with your new password." +msgstr "你現在可以使用新密碼登入。" + +#: src/view/com/profile/ProfileFollowers.tsx:94 +msgid "You do not have any followers." +msgstr "" + +#: src/view/com/modals/InviteCodes.tsx:66 +msgid "You don't have any invite codes yet! We'll send you some when you've been on Bluesky for a little longer." +msgstr "你目前還沒有邀請碼!當你持續使用 Bluesky 一段時間後,我們將提供一些新的邀請碼給你。" + +#: src/view/screens/SavedFeeds.tsx:102 +msgid "You don't have any pinned feeds." +msgstr "你目前還沒有任何固定的訊息流。" + +#: src/view/screens/Feeds.tsx:452 +msgid "You don't have any saved feeds!" +msgstr "你目前還沒有任何儲存的訊息流!" + +#: src/view/screens/SavedFeeds.tsx:135 +msgid "You don't have any saved feeds." +msgstr "你目前還沒有任何儲存的訊息流。" + +#: src/view/com/post-thread/PostThread.tsx:159 +msgid "You have blocked the author or you have been blocked by the author." +msgstr "你已封鎖該作者,或你已被該作者封鎖。" + +#: src/components/moderation/ModerationDetailsDialog.tsx:67 +#: src/lib/moderation/useModerationCauseDescription.ts:50 +#: src/lib/moderation/useModerationCauseDescription.ts:58 +msgid "You have blocked this user. You cannot view their content." +msgstr "你已封鎖了此使用者,你將無法查看他們發佈的內容。" + +#: src/view/com/auth/login/SetNewPasswordForm.tsx:57 +#: src/view/com/auth/login/SetNewPasswordForm.tsx:92 +#: src/view/com/modals/ChangePassword.tsx:87 +#: src/view/com/modals/ChangePassword.tsx:121 +msgid "You have entered an invalid code. It should look like XXXXX-XXXXX." +msgstr "你輸入的邀請碼無效。它應該長得像這樣 XXXXX-XXXXX。" + +#: src/lib/moderation/useModerationCauseDescription.ts:109 +msgid "You have hidden this post" +msgstr "" + +#: src/components/moderation/ModerationDetailsDialog.tsx:102 +msgid "You have hidden this post." +msgstr "" + +#: src/components/moderation/ModerationDetailsDialog.tsx:95 +#: src/lib/moderation/useModerationCauseDescription.ts:92 +msgid "You have muted this account." +msgstr "" + +#: src/lib/moderation/useModerationCauseDescription.ts:86 +msgid "You have muted this user" +msgstr "" + +#: src/view/com/modals/ModerationDetails.tsx:87 +#~ msgid "You have muted this user." +#~ msgstr "你已將這個使用者靜音。" + +#: src/view/com/feeds/ProfileFeedgens.tsx:136 +msgid "You have no feeds." +msgstr "你沒有訂閱訊息流。" + +#: src/view/com/lists/MyLists.tsx:89 +#: src/view/com/lists/ProfileLists.tsx:140 +msgid "You have no lists." +msgstr "你沒有列表。" + +#: src/view/screens/ModerationBlockedAccounts.tsx:132 +msgid "You have not blocked any accounts yet. To block an account, go to their profile and select \"Block account\" from the menu on their account." +msgstr "" + +#: src/view/screens/ModerationBlockedAccounts.tsx:132 +#~ msgid "You have not blocked any accounts yet. To block an account, go to their profile and selected \"Block account\" from the menu on their account." +#~ msgstr "你還沒有封鎖任何帳號。要封鎖帳號,請轉到其個人資料並在其帳號上的選單中選擇「封鎖帳號」。" + +#: src/view/screens/AppPasswords.tsx:89 +msgid "You have not created any app passwords yet. You can create one by pressing the button below." +msgstr "你還沒有建立任何應用程式專用密碼,如你想建立一個,按下面的按鈕。" + +#: src/view/screens/ModerationMutedAccounts.tsx:131 +msgid "You have not muted any accounts yet. To mute an account, go to their profile and select \"Mute account\" from the menu on their account." +msgstr "" + +#: src/view/screens/ModerationMutedAccounts.tsx:131 +#~ msgid "You have not muted any accounts yet. To mute an account, go to their profile and selected \"Mute account\" from the menu on their account." +#~ msgstr "你還沒有靜音任何帳號。要靜音帳號,請轉到其個人資料並在其帳號上的選單中選擇「靜音帳號」。" + +#: src/components/dialogs/MutedWords.tsx:250 +msgid "You haven't muted any words or tags yet" +msgstr "你还没有隐藏任何词或话题标签" + +#: src/components/moderation/LabelsOnMeDialog.tsx:69 +msgid "You may appeal these labels if you feel they were placed in error." +msgstr "" + +#: src/view/com/modals/ContentFilteringSettings.tsx:175 +#~ msgid "You must be 18 or older to enable adult content." +#~ msgstr "你必須年滿 18 歲才能啟用成人內容。" + +#: src/screens/Onboarding/StepModeration/AdultContentEnabledPref.tsx:110 +msgid "You must be 18 years or older to enable adult content" +msgstr "你必須年滿 18 歲才能啟用成人內容" + +#: src/components/ReportDialog/SubmitView.tsx:205 +msgid "You must select at least one labeler for a report" +msgstr "" + +#: src/view/com/util/forms/PostDropdownBtn.tsx:144 +msgid "You will no longer receive notifications for this thread" +msgstr "你將不再收到這條對話串的通知" + +#: src/view/com/util/forms/PostDropdownBtn.tsx:147 +msgid "You will now receive notifications for this thread" +msgstr "你將收到這條對話串的通知" + +#: src/view/com/auth/login/SetNewPasswordForm.tsx:107 +msgid "You will receive an email with a \"reset code.\" Enter that code here, then enter your new password." +msgstr "你將收到一封包含重設碼的電子郵件。請在此輸入該重設代碼,然後輸入你的新密碼。" + +#: src/screens/Onboarding/StepModeration/index.tsx:59 +msgid "You're in control" +msgstr "你盡在掌控" + +#: src/screens/Deactivated.tsx:87 +#: src/screens/Deactivated.tsx:88 +#: src/screens/Deactivated.tsx:103 +msgid "You're in line" +msgstr "輪到你了" + +#: src/screens/Onboarding/StepFinished.tsx:90 +msgid "You're ready to go!" +msgstr "你已設定完成!" + +#: src/components/moderation/ModerationDetailsDialog.tsx:99 +#: src/lib/moderation/useModerationCauseDescription.ts:101 +msgid "You've chosen to hide a word or tag within this post." +msgstr "" + +#: src/view/com/posts/FollowingEndOfFeed.tsx:48 +msgid "You've reached the end of your feed! Find some more accounts to follow." +msgstr "你已經瀏覽完你的訂閱訊息流啦!跟隨其他帳號吧。" + +#: src/view/com/auth/create/Step1.tsx:67 +msgid "Your account" +msgstr "你的帳號" + +#: src/view/com/modals/DeleteAccount.tsx:67 +msgid "Your account has been deleted" +msgstr "你的帳號已刪除" + +#: src/view/screens/Settings/ExportCarDialog.tsx:47 +msgid "Your account repository, containing all public data records, can be downloaded as a \"CAR\" file. This file does not include media embeds, such as images, or your private data, which must be fetched separately." +msgstr "你可以將你的帳號存放庫下載為一個「CAR」檔案。該檔案包含了所有公開的資料紀錄,但不包括嵌入媒體,例如圖片或你的私人資料,目前這些資料必須另外擷取。" + +#: src/view/com/auth/create/Step1.tsx:215 +msgid "Your birth date" +msgstr "你的生日" + +#: src/view/com/modals/InAppBrowserConsent.tsx:47 +msgid "Your choice will be saved, but can be changed later in settings." +msgstr "你的選擇將被儲存,但可以稍後在設定中更改。" + +#: src/screens/Onboarding/StepFollowingFeed.tsx:61 +msgid "Your default feed is \"Following\"" +msgstr "你的預設訊息流為「跟隨」" + +#: src/view/com/auth/create/state.ts:110 +#: src/view/com/auth/login/ForgotPasswordForm.tsx:70 +#: src/view/com/modals/ChangePassword.tsx:54 +msgid "Your email appears to be invalid." +msgstr "你的電子郵件地址似乎無效。" + +#: src/view/com/modals/Waitlist.tsx:109 +#~ msgid "Your email has been saved! We'll be in touch soon." +#~ msgstr "你的電子郵件地址已儲存!我們將很快聯繫你。" + +#: src/view/com/modals/ChangeEmail.tsx:125 +msgid "Your email has been updated but not verified. As a next step, please verify your new email." +msgstr "你的電子郵件地址已更新但尚未驗證。作為下一步,請驗證你的新電子郵件地址。" + +#: src/view/com/modals/VerifyEmail.tsx:114 +msgid "Your email has not yet been verified. This is an important security step which we recommend." +msgstr "你的電子郵件地址尚未驗證。這是一個我們建議的重要安全步驟。" + +#: src/view/com/posts/FollowingEmptyState.tsx:47 +msgid "Your following feed is empty! Follow more users to see what's happening." +msgstr "你的跟隨訊息流是空的!跟隨更多使用者看看發生了什麼事情。" + +#: src/view/com/auth/create/Step2.tsx:83 +msgid "Your full handle will be" +msgstr "你的完整帳號代碼將修改為" + +#: src/view/com/modals/ChangeHandle.tsx:270 +msgid "Your full handle will be <0>@{0}" +msgstr "你的完整帳號代碼將修改為 <0>@{0}" + +#: src/view/screens/Settings.tsx:430 +#: src/view/shell/desktop/RightNav.tsx:137 +#: src/view/shell/Drawer.tsx:660 +#~ msgid "Your invite codes are hidden when logged in using an App Password" +#~ msgstr "在使用應用程式專用密碼登入時,你的邀請碼將被隱藏" + +#: src/components/dialogs/MutedWords.tsx:221 +msgid "Your muted words" +msgstr "" + +#: src/view/com/modals/ChangePassword.tsx:157 +msgid "Your password has been changed successfully!" +msgstr "你的密碼已成功更改!" + +#: src/view/com/composer/Composer.tsx:283 +msgid "Your post has been published" +msgstr "你的貼文已發佈" + +#: src/screens/Onboarding/StepFinished.tsx:105 +#: src/view/com/auth/onboarding/WelcomeDesktop.tsx:59 +#: src/view/com/auth/onboarding/WelcomeMobile.tsx:61 +msgid "Your posts, likes, and blocks are public. Mutes are private." +msgstr "你的貼文、按喜歡和封鎖是公開可見的,而靜音是私人的。" + +#: src/view/com/modals/SwitchAccount.tsx:88 +#: src/view/screens/Settings/index.tsx:125 +msgid "Your profile" +msgstr "你的個人資料" + +#: src/view/com/composer/Composer.tsx:282 +msgid "Your reply has been published" +msgstr "你的回覆已發佈" + +#: src/view/com/auth/create/Step2.tsx:65 +msgid "Your user handle" +msgstr "你的帳號代碼" diff --git a/src/platform/polyfills.web.ts b/src/platform/polyfills.web.ts index 0b4a282835..462f65a260 100644 --- a/src/platform/polyfills.web.ts +++ b/src/platform/polyfills.web.ts @@ -6,3 +6,32 @@ findLast.shim() // @ts-ignore whatever typescript wants to complain about here, I dont care about -prf window.setImmediate = (cb: () => void) => setTimeout(cb, 0) + +if (process.env.NODE_ENV !== 'production') { + // In development, react-native-web's tries to validate that + // text is wrapped into . It doesn't catch all cases but is useful. + // Unfortunately, it only does that via console.error so it's easy to miss. + // This is a hack to get it showing as a redbox on the web so we catch it early. + const realConsoleError = console.error + const thrownErrors = new WeakSet() + console.error = function consoleErrorWrapper(msgOrError) { + if ( + typeof msgOrError === 'string' && + msgOrError.startsWith('Unexpected text node') + ) { + if ( + msgOrError === + 'Unexpected text node: . A text node cannot be a child of a .' + ) { + // This is due to a stray empty string. + // React already handles this fine, so RNW warning is a false positive. Ignore. + return + } + const err = new Error(msgOrError) + thrownErrors.add(err) + throw err + } else if (!thrownErrors.has(msgOrError)) { + return realConsoleError.apply(this, arguments as any) + } + } +} diff --git a/src/screens/Hashtag.tsx b/src/screens/Hashtag.tsx index 46452f087e..5388593f14 100644 --- a/src/screens/Hashtag.tsx +++ b/src/screens/Hashtag.tsx @@ -1,28 +1,30 @@ import React from 'react' import {ListRenderItemInfo, Pressable} from 'react-native' +import {PostView} from '@atproto/api/dist/client/types/app/bsky/feed/defs' +import {msg} from '@lingui/macro' +import {useLingui} from '@lingui/react' import {useFocusEffect} from '@react-navigation/native' -import {useSetMinimalShellMode} from 'state/shell' -import {ViewHeader} from 'view/com/util/ViewHeader' import {NativeStackScreenProps} from '@react-navigation/native-stack' + +import {HITSLOP_10} from 'lib/constants' +import {useInitialNumToRender} from 'lib/hooks/useInitialNumToRender' import {CommonNavigatorParams} from 'lib/routes/types' +import {shareUrl} from 'lib/sharing' +import {cleanError} from 'lib/strings/errors' +import {sanitizeHandle} from 'lib/strings/handles' +import {enforceLen} from 'lib/strings/helpers' +import {isNative} from 'platform/detection' import {useSearchPostsQuery} from 'state/queries/search-posts' +import {useSetMinimalShellMode} from 'state/shell' import {Post} from 'view/com/post/Post' -import {PostView} from '@atproto/api/dist/client/types/app/bsky/feed/defs' -import {enforceLen} from 'lib/strings/helpers' +import {List} from 'view/com/util/List' +import {ViewHeader} from 'view/com/util/ViewHeader' +import {ArrowOutOfBox_Stroke2_Corner0_Rounded} from '#/components/icons/ArrowOutOfBox' import { ListFooter, ListHeaderDesktop, ListMaybePlaceholder, } from '#/components/Lists' -import {List} from 'view/com/util/List' -import {msg} from '@lingui/macro' -import {useLingui} from '@lingui/react' -import {sanitizeHandle} from 'lib/strings/handles' -import {ArrowOutOfBox_Stroke2_Corner0_Rounded} from '#/components/icons/ArrowOutOfBox' -import {shareUrl} from 'lib/sharing' -import {HITSLOP_10} from 'lib/constants' -import {isNative} from 'platform/detection' -import {useInitialNumToRender} from 'lib/hooks/useInitialNumToRender' const renderItem = ({item}: ListRenderItemInfo) => { return @@ -61,9 +63,8 @@ export default function HashtagScreen({ const { data, - isFetching, + isFetchingNextPage, isLoading, - isRefetching, isError, error, refetch, @@ -97,9 +98,9 @@ export default function HashtagScreen({ }, [refetch]) const onEndReached = React.useCallback(() => { - if (isFetching || !hasNextPage || error) return + if (isFetchingNextPage || !hasNextPage || error) return fetchNextPage() - }, [isFetching, hasNextPage, error, fetchNextPage]) + }, [isFetchingNextPage, hasNextPage, error, fetchNextPage]) return ( <> @@ -123,16 +124,16 @@ export default function HashtagScreen({ : undefined } /> - - {!isLoading && posts.length > 0 && ( - + {posts.length < 1 ? ( + + ) : ( + } diff --git a/src/screens/Login/ChooseAccountForm.tsx b/src/screens/Login/ChooseAccountForm.tsx index d0d4c784d0..01eca18760 100644 --- a/src/screens/Login/ChooseAccountForm.tsx +++ b/src/screens/Login/ChooseAccountForm.tsx @@ -5,76 +5,15 @@ import {useLingui} from '@lingui/react' import {useAnalytics} from '#/lib/analytics/analytics' import {logEvent} from '#/lib/statsig/statsig' -import {colors} from '#/lib/styles' -import {useProfileQuery} from '#/state/queries/profile' import {SessionAccount, useSession, useSessionApi} from '#/state/session' import {useLoggedOutViewControls} from '#/state/shell/logged-out' import * as Toast from '#/view/com/util/Toast' -import {UserAvatar} from '#/view/com/util/UserAvatar' -import {atoms as a, useTheme} from '#/alf' +import {atoms as a} from '#/alf' +import {AccountList} from '#/components/AccountList' import {Button} from '#/components/Button' import * as TextField from '#/components/forms/TextField' -import {Check_Stroke2_Corner0_Rounded as Check} from '#/components/icons/Check' -import {ChevronRight_Stroke2_Corner0_Rounded as Chevron} from '#/components/icons/Chevron' -import {Text} from '#/components/Typography' import {FormContainer} from './FormContainer' -function AccountItem({ - account, - onSelect, - isCurrentAccount, -}: { - account: SessionAccount - onSelect: (account: SessionAccount) => void - isCurrentAccount: boolean -}) { - const t = useTheme() - const {_} = useLingui() - const {data: profile} = useProfileQuery({did: account.did}) - - const onPress = React.useCallback(() => { - onSelect(account) - }, [account, onSelect]) - - return ( - - ) -} export const ChooseAccountForm = ({ onSelectAccount, onPressBack, @@ -84,8 +23,7 @@ export const ChooseAccountForm = ({ }) => { const {track, screen} = useAnalytics() const {_} = useLingui() - const t = useTheme() - const {accounts, currentAccount} = useSession() + const {currentAccount} = useSession() const {initSession} = useSessionApi() const {setShowLoggedOut} = useLoggedOutViewControls() @@ -120,57 +58,15 @@ export const ChooseAccountForm = ({ return ( Select account}> + titleText={Select account}> - + Sign in as... - - - {accounts.map(account => ( - - - - - ))} - - + + onSelectAccount()} + /> - This is a prompt - + This is a prompt + This is a generic prompt component. It accepts a title and a description, as well as two actions. - + Cancel {}}>Confirm diff --git a/src/view/screens/Storybook/Forms.tsx b/src/view/screens/Storybook/Forms.tsx index 2d5495d706..182eacfde8 100644 --- a/src/view/screens/Storybook/Forms.tsx +++ b/src/view/screens/Storybook/Forms.tsx @@ -2,13 +2,13 @@ import React from 'react' import {View} from 'react-native' import {atoms as a} from '#/alf' -import {H1, H3} from '#/components/Typography' +import {Button} from '#/components/Button' +import {DateField, LabelText} from '#/components/forms/DateField' import * as TextField from '#/components/forms/TextField' -import {DateField, Label} from '#/components/forms/DateField' import * as Toggle from '#/components/forms/Toggle' import * as ToggleButton from '#/components/forms/ToggleButton' -import {Button} from '#/components/Button' import {Globe_Stroke2_Corner0_Rounded as Globe} from '#/components/icons/Globe' +import {H1, H3} from '#/components/Typography' export function Forms() { const [toggleGroupAValues, setToggleGroupAValues] = React.useState(['a']) @@ -42,7 +42,7 @@ export function Forms() { - Text field + Text field - @gmail.com + + @gmail.com + - Textarea + Textarea DateField - + Date - Uncontrolled toggle + Uncontrolled toggle - Click me + Click me - Click me + Click me - Click me + Click me - Click me + Click me - Click me + Click me @@ -128,23 +130,23 @@ export function Forms() { - Click me + Click me - Click me + Click me - Click me + Click me - Click me + Click me - Click me + Click me @@ -157,23 +159,23 @@ export function Forms() { - Click me + Click me - Click me + Click me - Click me + Click me - Click me + Click me - Click me + Click me diff --git a/src/view/screens/Storybook/Links.tsx b/src/view/screens/Storybook/Links.tsx index f9ecfba554..d35db79bc4 100644 --- a/src/view/screens/Storybook/Links.tsx +++ b/src/view/screens/Storybook/Links.tsx @@ -1,9 +1,9 @@ import React from 'react' import {View} from 'react-native' -import {useTheme, atoms as a} from '#/alf' +import {atoms as a, useTheme} from '#/alf' import {ButtonText} from '#/components/Button' -import {InlineLink, Link} from '#/components/Link' +import {InlineLinkText, Link} from '#/components/Link' import {H1, Text} from '#/components/Typography' export function Links() { @@ -13,20 +13,22 @@ export function Links() {

Links

- + https://google.com - - + + External with custom children (google.com) - - + Internal (bsky.social) - - + + Internal (bsky.app) - + { closeAllActiveElements() @@ -99,204 +102,213 @@ export function BottomBar({navigation}: BottomTabBarProps) { const onPressProfile = React.useCallback(() => { onPressTab('MyProfile') }, [onPressTab]) + const onLongPressProfile = React.useCallback(() => { - openModal({name: 'switch-account'}) - }, [openModal]) + Haptics.default() + accountSwitchControl.open() + }, [accountSwitchControl]) return ( - { - footerHeight.value = e.nativeEvent.layout.height - }}> - {hasSession ? ( - <> - - ) : ( - - ) - } - onPress={onPressHome} - accessibilityRole="tab" - accessibilityLabel={_(msg`Home`)} - accessibilityHint="" - /> - - ) : ( - - ) - } - onPress={onPressSearch} - accessibilityRole="search" - accessibilityLabel={_(msg`Search`)} - accessibilityHint="" - /> - - ) : ( - - ) - } - onPress={onPressFeeds} - accessibilityRole="tab" - accessibilityLabel={_(msg`Feeds`)} - accessibilityHint="" - /> - - ) : ( - - ) - } - onPress={onPressNotifications} - notificationCount={numUnreadNotifications} - accessible={true} - accessibilityRole="tab" - accessibilityLabel={_(msg`Notifications`)} - accessibilityHint={ - numUnreadNotifications === '' - ? '' - : `${numUnreadNotifications} unread` - } - /> - - {isAtMyProfile ? ( - - - + <> + + + { + footerHeight.value = e.nativeEvent.layout.height + }}> + {hasSession ? ( + <> + ) : ( - - - - )} - - } - onPress={onPressProfile} - onLongPress={onLongPressProfile} - accessibilityRole="tab" - accessibilityLabel={_(msg`Profile`)} - accessibilityHint="" - /> - - ) : ( - <> - - - - - + + ) + } + onPress={onPressHome} + accessibilityRole="tab" + accessibilityLabel={_(msg`Home`)} + accessibilityHint="" + /> + + ) : ( + + ) + } + onPress={onPressSearch} + accessibilityRole="search" + accessibilityLabel={_(msg`Search`)} + accessibilityHint="" + /> + + ) : ( + + ) + } + onPress={onPressFeeds} + accessibilityRole="tab" + accessibilityLabel={_(msg`Feeds`)} + accessibilityHint="" + /> + + ) : ( + + ) + } + onPress={onPressNotifications} + notificationCount={numUnreadNotifications} + accessible={true} + accessibilityRole="tab" + accessibilityLabel={_(msg`Notifications`)} + accessibilityHint={ + numUnreadNotifications === '' + ? '' + : `${numUnreadNotifications} unread` + } + /> + + {isAtMyProfile ? ( + + + + ) : ( + + + + )} + + } + onPress={onPressProfile} + onLongPress={onLongPressProfile} + accessibilityRole="tab" + accessibilityLabel={_(msg`Profile`)} + accessibilityHint="" + /> + + ) : ( + <> + + + + + + - - - + + - + + - - - )} - + + )} + + ) } diff --git a/web/index.html b/web/index.html index 7df097f224..06d00dec97 100644 --- a/web/index.html +++ b/web/index.html @@ -224,6 +224,21 @@ .nativeDropdown-item:focus { outline: none; } + + /* Spinner component */ + @keyframes rotate { + 0% { + transform: rotate(0deg); + } + 100% { + transform: rotate(360deg); + } + } + .rotate-500ms { + position: absolute; + inset:0; + animation: rotate 500ms linear infinite; + } diff --git a/webpack.config.js b/webpack.config.js index 7515db8e94..f57ba2e36f 100644 --- a/webpack.config.js +++ b/webpack.config.js @@ -1,6 +1,10 @@ const createExpoWebpackConfigAsync = require('@expo/webpack-config') const {withAlias} = require('@expo/webpack-config/addons') const ReactRefreshWebpackPlugin = require('@pmmmwh/react-refresh-webpack-plugin') +const {BundleAnalyzerPlugin} = require('webpack-bundle-analyzer') + +const GENERATE_STATS = process.env.EXPO_PUBLIC_GENERATE_STATS === '1' +const OPEN_ANALYZER = process.env.EXPO_PUBLIC_OPEN_ANALYZER === '1' const reactNativeWebWebviewConfiguration = { test: /postMock.html$/, @@ -26,5 +30,17 @@ module.exports = async function (env, argv) { if (env.mode === 'development') { config.plugins.push(new ReactRefreshWebpackPlugin()) } + + if (GENERATE_STATS || OPEN_ANALYZER) { + config.plugins.push( + new BundleAnalyzerPlugin({ + openAnalyzer: OPEN_ANALYZER, + generateStatsFile: true, + statsFilename: '../stats.json', + analyzerMode: OPEN_ANALYZER ? 'server' : 'json', + defaultSizes: 'parsed', + }), + ) + } return config } diff --git a/yarn.lock b/yarn.lock index c81f120664..106599fc14 100644 --- a/yarn.lock +++ b/yarn.lock @@ -2821,7 +2821,7 @@ "@gorhom/portal" "1.0.14" invariant "^2.2.4" -"@discoveryjs/json-ext@^0.5.0": +"@discoveryjs/json-ext@0.5.7", "@discoveryjs/json-ext@^0.5.0": version "0.5.7" resolved "https://registry.yarnpkg.com/@discoveryjs/json-ext/-/json-ext-0.5.7.tgz#1d572bfbbe14b7704e0ba0f39b74815b84870d70" integrity sha512-dBVuXR082gk3jsFp7Rd/JI4kytwGHecnCoTtXFb7DB6CNHp4rg5k1bhg0nWdLGLnOV71lmDzGQaLMy8iPLY0pw== @@ -4456,6 +4456,11 @@ schema-utils "^3.0.0" source-map "^0.7.3" +"@polka/url@^1.0.0-next.24": + version "1.0.0-next.25" + resolved "https://registry.yarnpkg.com/@polka/url/-/url-1.0.0-next.25.tgz#f077fdc0b5d0078d30893396ff4827a13f99e817" + integrity sha512-j7P6Rgr3mmtdkeDGTe0E/aYyWEWVtc5yFXtHCRHs28/jptDEWfaVOc5T7cblqy1XKPPfCxJc/8DwQ5YgLOZOVQ== + "@popperjs/core@^2.9.0": version "2.11.8" resolved "https://registry.yarnpkg.com/@popperjs/core/-/core-2.11.8.tgz#6b79032e760a0899cd4204710beede972a3a185f" @@ -8297,6 +8302,11 @@ acorn-walk@^7.1.1: resolved "https://registry.yarnpkg.com/acorn-walk/-/acorn-walk-7.2.0.tgz#0de889a601203909b0fbe07b8938dc21d2e967bc" integrity sha512-OPdCF6GsMIP+Az+aWfAAOEt2/+iVDKE7oy6lJ098aoe59oAmK76qV6Gw60SbZ8jHuG2wH058GF4pLFbYamYrVA== +acorn-walk@^8.0.0: + version "8.3.2" + resolved "https://registry.yarnpkg.com/acorn-walk/-/acorn-walk-8.3.2.tgz#7703af9415f1b6db9315d6895503862e231d34aa" + integrity sha512-cjkyv4OtNCIeqhHrfS81QWXoCBPExR/J62oyEqepVw8WaQeSqpW2uhuLPh1m9eWhDuOo/jUXVTlifvesOWp/4A== + acorn-walk@^8.0.2, acorn-walk@^8.1.1: version "8.2.0" resolved "https://registry.yarnpkg.com/acorn-walk/-/acorn-walk-8.2.0.tgz#741210f2e2426454508853a2f44d0ab83b7f69c1" @@ -8307,6 +8317,11 @@ acorn@^7.1.1: resolved "https://registry.yarnpkg.com/acorn/-/acorn-7.4.1.tgz#feaed255973d2e77555b83dbc08851a6c63520fa" integrity sha512-nQyp0o1/mNdbTO1PO6kHkwSrmgZ0MT/jCCpNiwbUjGoRN4dlBhqJtoQuCnEOKzgTVwg0ZWiCoQy6SxMebQVh8A== +acorn@^8.0.4: + version "8.11.3" + resolved "https://registry.yarnpkg.com/acorn/-/acorn-8.11.3.tgz#71e0b14e13a4ec160724b38fb7b0f233b1b81d7a" + integrity sha512-Y9rRfJG5jcKOE0CLisYbojUjIrIEE7AGMzA/Sm4BslANhbS+cDMpgBdcPT91oJ7OuJ9hYJBx59RjbhxVnrF8Xg== + acorn@^8.1.0, acorn@^8.2.4, acorn@^8.4.1, acorn@^8.7.1, acorn@^8.8.1, acorn@^8.8.2, acorn@^8.9.0: version "8.10.0" resolved "https://registry.yarnpkg.com/acorn/-/acorn-8.10.0.tgz#8be5b3907a67221a81ab23c7889c4c5526b62ec5" @@ -10483,6 +10498,11 @@ dayjs@^1.8.15: resolved "https://registry.yarnpkg.com/dayjs/-/dayjs-1.11.9.tgz#9ca491933fadd0a60a2c19f6c237c03517d71d1a" integrity sha512-QvzAURSbQ0pKdIye2txOzNaHmxtUBXerpY0FJsFXUMKbIZeFm5ht1LS/jFsrncjnmtv8HsG0W2g6c0zUjZWmpA== +debounce@^1.2.1: + version "1.2.1" + resolved "https://registry.yarnpkg.com/debounce/-/debounce-1.2.1.tgz#38881d8f4166a5c5848020c11827b834bcb3e0a5" + integrity sha512-XRRe6Glud4rd/ZGQfiV1ruXSfbvfJedlV9Y6zOlP+2K04vBYiJEte6stfFkCP03aMnY5tsipamumUjL14fofug== + debug@2.6.9, debug@^2.2.0, debug@^2.6.0, debug@^2.6.9: version "2.6.9" resolved "https://registry.yarnpkg.com/debug/-/debug-2.6.9.tgz#5d128515df134ff327e90a4c93f4e077a536341f" @@ -11384,6 +11404,10 @@ eslint-module-utils@^2.8.0: dependencies: debug "^3.2.7" +"eslint-plugin-bsky-internal@link:./eslint": + version "0.0.0" + uid "" + eslint-plugin-detox@^1.0.0: version "1.0.0" resolved "https://registry.yarnpkg.com/eslint-plugin-detox/-/eslint-plugin-detox-1.0.0.tgz#2d9c0130e8ebc4ced56efb6eeaf0d0f5c163398d" @@ -13143,7 +13167,7 @@ html-entities@^2.1.0, html-entities@^2.3.2: resolved "https://registry.yarnpkg.com/html-entities/-/html-entities-2.4.0.tgz#edd0cee70402584c8c76cc2c0556db09d1f45061" integrity sha512-igBTJcNNNhvZFRtm8uA6xMY6xYleeDwn3PeBCkDz7tHttv4F2hsDI2aPgNERWzvRcNYHNT3ymRaQzllmXj4YsQ== -html-escaper@^2.0.0: +html-escaper@^2.0.0, html-escaper@^2.0.2: version "2.0.2" resolved "https://registry.yarnpkg.com/html-escaper/-/html-escaper-2.0.2.tgz#dfd60027da36a36dfcbe236262c00a5822681453" integrity sha512-H2iMtd0I4Mt5eYiapRdIDjp+XzelXQ0tFE4JS7YFwFevXXMmOp9myNrUvCg0D6ws8iqkRPBfKHgbwig1SmlLfg== @@ -13782,6 +13806,11 @@ is-plain-object@^2.0.4: dependencies: isobject "^3.0.1" +is-plain-object@^5.0.0: + version "5.0.0" + resolved "https://registry.yarnpkg.com/is-plain-object/-/is-plain-object-5.0.0.tgz#4427f50ab3429e9025ea7d52e9043a9ef4159344" + integrity sha512-VRSzKkbMm5jMDoKLbltAkFQ5Qr7VDiTFGXxYFXXowVj387GeGNOCsOH6Msy00SGZ3Fp84b1Naa1psqgcCIEP5Q== + is-potential-custom-element-name@^1.0.1: version "1.0.1" resolved "https://registry.yarnpkg.com/is-potential-custom-element-name/-/is-potential-custom-element-name-1.0.1.tgz#171ed6f19e3ac554394edf78caa05784a45bebb5" @@ -16329,6 +16358,11 @@ mrmime@^1.0.0: resolved "https://registry.yarnpkg.com/mrmime/-/mrmime-1.0.1.tgz#5f90c825fad4bdd41dc914eff5d1a8cfdaf24f27" integrity sha512-hzzEagAgDyoU1Q6yg5uI+AorQgdvMCur3FcKf7NhMKWsaYg+RnbTyHRa/9IlLF9rf455MOCtcqqrQQ83pPP7Uw== +mrmime@^2.0.0: + version "2.0.0" + resolved "https://registry.yarnpkg.com/mrmime/-/mrmime-2.0.0.tgz#151082a6e06e59a9a39b46b3e14d5cfe92b3abb4" + integrity sha512-eu38+hdgojoyq63s+yTpN4XMBdt5l8HhMhc4VKLO9KM5caLIBvUm4thi7fFaxyTmCKeNnXZ5pAlBwCUnhA09uw== + ms@2.0.0: version "2.0.0" resolved "https://registry.yarnpkg.com/ms/-/ms-2.0.0.tgz#5608aeadfc00be6c2901df5f9861788de0d597c8" @@ -16851,6 +16885,11 @@ open@^8.0.4, open@^8.0.9, open@^8.3.0, open@^8.4.0: is-docker "^2.1.1" is-wsl "^2.2.0" +opener@^1.5.2: + version "1.5.2" + resolved "https://registry.yarnpkg.com/opener/-/opener-1.5.2.tgz#5d37e1f35077b9dcac4301372271afdeb2a13598" + integrity sha512-ur5UIdyw5Y7yEj9wLzhqXiy6GZ3Mwx0yGI+5sMn2r0N0v3cKJvUmFH5yPP+WXh9e0xfyzyJX95D8l088DNFj7A== + optionator@^0.9.3: version "0.9.3" resolved "https://registry.yarnpkg.com/optionator/-/optionator-0.9.3.tgz#007397d44ed1872fdc6ed31360190f81814e2c64" @@ -18721,9 +18760,10 @@ react-native-svg@14.1.0: css-select "^5.1.0" css-tree "^1.1.3" -"react-native-ui-text-view@link:./modules/react-native-ui-text-view": - version "0.0.0" - uid "" +react-native-uitextview@^1.1.6: + version "1.1.6" + resolved "https://registry.yarnpkg.com/react-native-uitextview/-/react-native-uitextview-1.1.6.tgz#a70d039f415158445c90de8e8e546a7c3b251d6d" + integrity sha512-OTGTw4Y2DDn4dHTwN7aKOndXP6NoS/AS35Rj/Rsss+KRsGHToiv2g3ZdzQ0ZhZabhwl1u+Oht+wSU/FU+SoJ+Q== react-native-url-polyfill@^1.3.0: version "1.3.0" @@ -19863,6 +19903,15 @@ simple-swizzle@^0.2.2: dependencies: is-arrayish "^0.3.1" +sirv@^2.0.3: + version "2.0.4" + resolved "https://registry.yarnpkg.com/sirv/-/sirv-2.0.4.tgz#5dd9a725c578e34e449f332703eb2a74e46a29b0" + integrity sha512-94Bdh3cC2PKrbgSOUqTiGPWVZeSiXfKOVZNJniWoqrWrRkB1CJzBU3NEbiTsPcYy1lDsANA/THzS+9WBiy5nfQ== + dependencies: + "@polka/url" "^1.0.0-next.24" + mrmime "^2.0.0" + totalist "^3.0.0" + sisteransi@^1.0.5: version "1.0.5" resolved "https://registry.yarnpkg.com/sisteransi/-/sisteransi-1.0.5.tgz#134d681297756437cc05ca01370d3a7a571075ed" @@ -20841,6 +20890,11 @@ token-types@^4.1.1: "@tokenizer/token" "^0.3.0" ieee754 "^1.2.1" +totalist@^3.0.0: + version "3.0.1" + resolved "https://registry.yarnpkg.com/totalist/-/totalist-3.0.1.tgz#ba3a3d600c915b1a97872348f79c127475f6acf8" + integrity sha512-sf4i37nQ2LBx4m3wB74y+ubopq6W/dIzXg0FDGjsYnZHVa1Da8FH853wlL2gtUhg+xJXjfk3kUZS3BRoQeoQBQ== + tough-cookie@^4.0.0, tough-cookie@^4.1.2: version "4.1.3" resolved "https://registry.yarnpkg.com/tough-cookie/-/tough-cookie-4.1.3.tgz#97b9adb0728b42280aa3d814b6b999b2ff0318bf" @@ -21537,6 +21591,25 @@ webidl-conversions@^7.0.0: resolved "https://registry.yarnpkg.com/webidl-conversions/-/webidl-conversions-7.0.0.tgz#256b4e1882be7debbf01d05f0aa2039778ea080a" integrity sha512-VwddBukDzu71offAQR975unBIGqfKZpM+8ZX6ySk8nYhVoo5CYaZyzt3YBvYtRtO+aoGlqxPg/B87NGVZ/fu6g== +webpack-bundle-analyzer@^4.10.1: + version "4.10.1" + resolved "https://registry.yarnpkg.com/webpack-bundle-analyzer/-/webpack-bundle-analyzer-4.10.1.tgz#84b7473b630a7b8c21c741f81d8fe4593208b454" + integrity sha512-s3P7pgexgT/HTUSYgxJyn28A+99mmLq4HsJepMPzu0R8ImJc52QNqaFYW1Z2z2uIb1/J3eYgaAWVpaC+v/1aAQ== + dependencies: + "@discoveryjs/json-ext" "0.5.7" + acorn "^8.0.4" + acorn-walk "^8.0.0" + commander "^7.2.0" + debounce "^1.2.1" + escape-string-regexp "^4.0.0" + gzip-size "^6.0.0" + html-escaper "^2.0.2" + is-plain-object "^5.0.0" + opener "^1.5.2" + picocolors "^1.0.0" + sirv "^2.0.3" + ws "^7.3.1" + webpack-cli@^5.0.1: version "5.1.4" resolved "https://registry.yarnpkg.com/webpack-cli/-/webpack-cli-5.1.4.tgz#c8e046ba7eaae4911d7e71e2b25b776fcc35759b" @@ -22092,7 +22165,7 @@ ws@^6.2.2: dependencies: async-limiter "~1.0.0" -ws@^7, ws@^7.0.0, ws@^7.4.6, ws@^7.5.1: +ws@^7, ws@^7.0.0, ws@^7.3.1, ws@^7.4.6, ws@^7.5.1: version "7.5.9" resolved "https://registry.yarnpkg.com/ws/-/ws-7.5.9.tgz#54fa7db29f4c7cec68b1ddd3a89de099942bb591" integrity sha512-F+P9Jil7UiSKSkppIiD94dN07AwvFixvLIj1Og1Rl9GGMuNipJnV9JzjD6XuqmAeiswGvUmNLjr5cFuXwNS77Q== From c9c8e0065df3ab840a21c5d7c248ebbebf8f1aa2 Mon Sep 17 00:00:00 2001 From: Hailey Date: Thu, 4 Apr 2024 17:04:57 -0700 Subject: [PATCH 09/54] adjust `app.config.js` to prevent development manifest error --- app.config.js | 30 ++++++++++++++++-------------- 1 file changed, 16 insertions(+), 14 deletions(-) diff --git a/app.config.js b/app.config.js index 21b794917b..2095e55ab1 100644 --- a/app.config.js +++ b/app.config.js @@ -124,20 +124,22 @@ module.exports = function (config) { web: { favicon: './assets/favicon.png', }, - updates: { - url: 'https://updates.bsky.app/manifest', - // TODO Eventually we want to enable this for all environments, but for now it will only be used for - // TestFlight builds - enabled: IS_TESTFLIGHT, - fallbackToCacheTimeout: 30000, - codeSigningCertificate: './code-signing/certificate.pem', - codeSigningMetadata: { - keyid: 'main', - alg: 'rsa-v1_5-sha256', - }, - checkAutomatically: 'NEVER', - channel: UPDATES_CHANNEL, - }, + // TODO Eventually we want to enable this for all environments, but for now it will only be used for + // TestFlight builds + updates: IS_TESTFLIGHT + ? { + url: 'https://updates.bsky.app/manifest', + enabled: true, + fallbackToCacheTimeout: 30000, + codeSigningCertificate: './code-signing/certificate.pem', + codeSigningMetadata: { + keyid: 'main', + alg: 'rsa-v1_5-sha256', + }, + checkAutomatically: 'NEVER', + channel: UPDATES_CHANNEL, + } + : undefined, assetBundlePatterns: ['**/*'], plugins: [ 'expo-localization', From 02697bb7c8df48a752022ca23bd8fd3410d03d5e Mon Sep 17 00:00:00 2001 From: Hailey Date: Thu, 4 Apr 2024 17:21:51 -0700 Subject: [PATCH 10/54] rev change --- app.config.js | 30 ++++++++++++++++-------------- 1 file changed, 16 insertions(+), 14 deletions(-) diff --git a/app.config.js b/app.config.js index 2095e55ab1..9036d5e331 100644 --- a/app.config.js +++ b/app.config.js @@ -124,22 +124,24 @@ module.exports = function (config) { web: { favicon: './assets/favicon.png', }, - // TODO Eventually we want to enable this for all environments, but for now it will only be used for - // TestFlight builds - updates: IS_TESTFLIGHT - ? { - url: 'https://updates.bsky.app/manifest', - enabled: true, - fallbackToCacheTimeout: 30000, - codeSigningCertificate: './code-signing/certificate.pem', - codeSigningMetadata: { + updates: { + url: 'https://updates.bsky.app/manifest', + // TODO Eventually we want to enable this for all environments, but for now it will only be used for + // TestFlight builds + enabled: IS_TESTFLIGHT, + fallbackToCacheTimeout: 30000, + codeSigningCertificate: IS_TESTFLIGHT + ? './code-signing/certificate.pem' + : undefined, + codeSigningMetadata: IS_TESTFLIGHT + ? { keyid: 'main', alg: 'rsa-v1_5-sha256', - }, - checkAutomatically: 'NEVER', - channel: UPDATES_CHANNEL, - } - : undefined, + } + : undefined, + checkAutomatically: 'NEVER', + channel: UPDATES_CHANNEL, + }, assetBundlePatterns: ['**/*'], plugins: [ 'expo-localization', From b295e844e9c0ca2fc7dfb2449c01de765225e9b2 Mon Sep 17 00:00:00 2001 From: Hailey Date: Thu, 4 Apr 2024 21:18:23 -0700 Subject: [PATCH 11/54] add oauth client --- package.json | 1 + src/lib/oauth.ts | 9 +- src/screens/Login/hooks/useLogin.ts | 24 +++++- yarn.lock | 123 +++++++++++++++++++++++++++- 4 files changed, 151 insertions(+), 6 deletions(-) diff --git a/package.json b/package.json index 6165d953af..5c52a844d4 100644 --- a/package.json +++ b/package.json @@ -50,6 +50,7 @@ }, "dependencies": { "@atproto/api": "^0.12.2", + "@atproto/oauth-client": "https://gitpkg.now.sh/haileyok/atproto-oauth/packages/oauth-client?feat-oauth-server", "@bam.tech/react-native-image-resizer": "^3.0.4", "@braintree/sanitize-url": "^6.0.2", "@discord/bottom-sheet": "https://github.com/bluesky-social/react-native-bottom-sheet.git#discord-fork-4.6.1", diff --git a/src/lib/oauth.ts b/src/lib/oauth.ts index 3843dfbf6a..fc25980012 100644 --- a/src/lib/oauth.ts +++ b/src/lib/oauth.ts @@ -3,10 +3,13 @@ import {isWeb} from 'platform/detection' export const OAUTH_CLIENT_ID = 'https://bsky.app' export const OAUTH_REDIRECT_URI = 'https://bsky.app/auth/callback' export const OAUTH_SCOPE = 'openid profile email phone offline_access' -export const OAUTH_GRANT_TYPES = ['authorization_code', 'refresh_token'] -export const OAUTH_RESPONSE_TYPES = ['code', 'code id_token'] +export const OAUTH_GRANT_TYPES = [ + 'authorization_code', + 'refresh_token', +] as const +export const OAUTH_RESPONSE_TYPES = ['code', 'code id_token'] as const export const DPOP_BOUND_ACCESS_TOKENS = true -export const APPLICATION_TYPE = isWeb ? 'web' : 'native' // TODO what should we put here for native +export const OAUTH_APPLICATION_TYPE = isWeb ? 'web' : 'native' // TODO what should we put here for native export const buildOAuthUrl = (serviceUrl: string, state: string) => { const url = new URL(serviceUrl) diff --git a/src/screens/Login/hooks/useLogin.ts b/src/screens/Login/hooks/useLogin.ts index d798498ee3..b5c9dbc4b1 100644 --- a/src/screens/Login/hooks/useLogin.ts +++ b/src/screens/Login/hooks/useLogin.ts @@ -1,7 +1,17 @@ import React from 'react' import * as Browser from 'expo-web-browser' +import {OAuthClientFactory} from '@atproto/oauth-client' -import {buildOAuthUrl, OAUTH_REDIRECT_URI} from 'lib/oauth' +import { + buildOAuthUrl, + DPOP_BOUND_ACCESS_TOKENS, + OAUTH_APPLICATION_TYPE, + OAUTH_CLIENT_ID, + OAUTH_GRANT_TYPES, + OAUTH_REDIRECT_URI, + OAUTH_RESPONSE_TYPES, + OAUTH_SCOPE, +} from 'lib/oauth' // TODO remove hack const serviceUrl = 'http://localhost:2583/oauth/authorize' @@ -9,6 +19,18 @@ const serviceUrl = 'http://localhost:2583/oauth/authorize' // Service URL here is just a placeholder, this isn't how it will actually work export function useLogin(serviceUrl: string | undefined) { const openAuthSession = React.useCallback(async () => { + const oauthFactory = new OAuthClientFactory({ + clientMetadata: { + client_id: OAUTH_CLIENT_ID, + redirect_uris: [OAUTH_REDIRECT_URI], + grant_types: OAUTH_GRANT_TYPES, + response_types: OAUTH_RESPONSE_TYPES, + scope: OAUTH_SCOPE, + dpop_bound_access_tokens: DPOP_BOUND_ACCESS_TOKENS, + application_type: OAUTH_APPLICATION_TYPE, + }, + }) + if (!serviceUrl) return const url = buildOAuthUrl(serviceUrl, '123') // TODO replace '123' with the appropriate state diff --git a/yarn.lock b/yarn.lock index 106599fc14..bbf3d9abee 100644 --- a/yarn.lock +++ b/yarn.lock @@ -77,6 +77,12 @@ multiformats "^9.9.0" uint8arrays "3.0.0" +"@atproto/b64@https://gitpkg.now.sh/haileyok/atproto-oauth/packages/b64?feat-oauth-server": + version "0.0.1" + resolved "https://gitpkg.now.sh/haileyok/atproto-oauth/packages/b64?feat-oauth-server#00a0db82a412e99828a9bd241790bdf6e07b9b1590de8de8d31cd97e8efa74ed9ab6d779b0168c9c0e98c43a407570e1b1526efab5076db2cb22a6ca7655255c" + dependencies: + base64-js "^1.5.1" + "@atproto/bsky@^0.0.28": version "0.0.28" resolved "https://registry.yarnpkg.com/@atproto/bsky/-/bsky-0.0.28.tgz#d9516f682883ceba60f52e3944d93dbd81375a7e" @@ -132,6 +138,12 @@ pino-http "^8.2.1" typed-emitter "^2.1.0" +"@atproto/caching@https://gitpkg.now.sh/haileyok/atproto-oauth/packages/caching?feat-oauth-server": + version "0.0.1" + resolved "https://gitpkg.now.sh/haileyok/atproto-oauth/packages/caching?feat-oauth-server#55421450c3f7e2db1f873a4e3537338ee1afc4f3d35b5d2b56bc5cd72dd68c831f93be4affa1cab8219064e3e392e7bec15af6858f8c447e9f4172fbfa262d68" + dependencies: + lru-cache "^10.2.0" + "@atproto/common-web@^0.2.3": version "0.2.3" resolved "https://registry.yarnpkg.com/@atproto/common-web/-/common-web-0.2.3.tgz#c44c1e177ae8309d5116347d49850209e8e478cc" @@ -233,6 +245,50 @@ sharp "^0.32.6" uint8arrays "3.0.0" +"@atproto/did@https://gitpkg.now.sh/haileyok/atproto-oauth/packages/did?feat-oauth-server": + version "0.0.1" + uid d8e31a16bcecd25fc34b1aadb99b12b01a9cd280f432cfe8e0dd8cf78201749c551884f6783523f7704e6568a07d59a0e4a8e7d4d635c0c66c8d8f12767f0031 + resolved "https://gitpkg.now.sh/haileyok/atproto-oauth/packages/did?feat-oauth-server#d8e31a16bcecd25fc34b1aadb99b12b01a9cd280f432cfe8e0dd8cf78201749c551884f6783523f7704e6568a07d59a0e4a8e7d4d635c0c66c8d8f12767f0031" + dependencies: + "@atproto/caching" "https://gitpkg.now.sh/haileyok/atproto-oauth/packages/caching?feat-oauth-server" + "@atproto/fetch" "https://gitpkg.now.sh/haileyok/atproto-oauth/packages/fetch?feat-oauth-server" + "@atproto/jwk" "https://gitpkg.now.sh/haileyok/atproto-oauth/packages/jwk?feat-oauth-server" + "@atproto/transformer" "https://gitpkg.now.sh/haileyok/atproto-oauth/packages/transformer?feat-oauth-server" + zod "^3.22.4" + +"@atproto/fetch-dpop@https://gitpkg.now.sh/haileyok/atproto-oauth/packages/fetch-dpop?feat-oauth-server": + version "0.0.0" + resolved "https://gitpkg.now.sh/haileyok/atproto-oauth/packages/fetch-dpop?feat-oauth-server#cf83e1357eefb8bdf1542850d66d8007d620e4050b5715dc83f4a921d36ce9ce47d0d13c5d85f2b0ff8318d2877eec2f63b931bd47417a81a538327af927da3e" + +"@atproto/fetch@https://gitpkg.now.sh/haileyok/atproto-oauth/packages/fetch?feat-oauth-server": + version "0.0.1" + uid "7fe9c1ef0eaa7b976e9817a3caf432e667cc540498fc9a63f9d1262dd604d48cc53aa0d5c059e9912ff401b51259d36e954b37935cf8578e8f6a682f8573405a" + resolved "https://gitpkg.now.sh/haileyok/atproto-oauth/packages/fetch?feat-oauth-server#7fe9c1ef0eaa7b976e9817a3caf432e667cc540498fc9a63f9d1262dd604d48cc53aa0d5c059e9912ff401b51259d36e954b37935cf8578e8f6a682f8573405a" + dependencies: + "@atproto/transformer" "https://gitpkg.now.sh/haileyok/atproto-oauth/packages/transformer?feat-oauth-server" + tslib "^2.6.2" + zod "^3.22.4" + +"@atproto/handle-resolver@https://gitpkg.now.sh/haileyok/atproto-oauth/packages/handle-resolver?feat-oauth-server": + version "0.0.1" + uid fe063702cb0ba71eddf1072d8d3838c04be573f4300d5161fb8775b8d2f3ee2aa5b434bfa2799f3fb275bdbe100be0069ef0af3381cbc778fa1b365f1477d651 + resolved "https://gitpkg.now.sh/haileyok/atproto-oauth/packages/handle-resolver?feat-oauth-server#fe063702cb0ba71eddf1072d8d3838c04be573f4300d5161fb8775b8d2f3ee2aa5b434bfa2799f3fb275bdbe100be0069ef0af3381cbc778fa1b365f1477d651" + dependencies: + "@atproto/caching" "https://gitpkg.now.sh/haileyok/atproto-oauth/packages/caching?feat-oauth-server" + "@atproto/did" "https://gitpkg.now.sh/haileyok/atproto-oauth/packages/did?feat-oauth-server" + "@atproto/fetch" "https://gitpkg.now.sh/haileyok/atproto-oauth/packages/fetch?feat-oauth-server" + lru-cache "^10.2.0" + zod "^3.22.4" + +"@atproto/identity-resolver@https://gitpkg.now.sh/haileyok/atproto-oauth/packages/identity-resolver?feat-oauth-server": + version "0.0.1" + resolved "https://gitpkg.now.sh/haileyok/atproto-oauth/packages/identity-resolver?feat-oauth-server#d3be310a6b9bb004e1183f336cc9507b8fa1d451641d6ac8da6b37a0aa9bb8dd25faad661c2d93d3f7ca16fc4961af54f2ad1c65fd0f01549a2c8aef959ab0d4" + dependencies: + "@atproto/did" "https://gitpkg.now.sh/haileyok/atproto-oauth/packages/did?feat-oauth-server" + "@atproto/fetch" "https://gitpkg.now.sh/haileyok/atproto-oauth/packages/fetch?feat-oauth-server" + "@atproto/handle-resolver" "https://gitpkg.now.sh/haileyok/atproto-oauth/packages/handle-resolver?feat-oauth-server" + "@atproto/syntax" "workspace:*" + "@atproto/identity@^0.3.2": version "0.3.2" resolved "https://registry.yarnpkg.com/@atproto/identity/-/identity-0.3.2.tgz#8a0536bc19ccbc45a04df84c3f30d86f58f964ee" @@ -242,6 +298,14 @@ "@atproto/crypto" "^0.3.0" axios "^0.27.2" +"@atproto/jwk@https://gitpkg.now.sh/haileyok/atproto-oauth/packages/jwk?feat-oauth-server": + version "0.0.1" + resolved "https://gitpkg.now.sh/haileyok/atproto-oauth/packages/jwk?feat-oauth-server#4a9e5a00740b15287e73e34d76de2c56c5a131f680f66963c2b2e4e5b34a517d265629ad498ed51256b27e87ee120e5c82ef1564c5ad3ff8ef1ec7d6c57f8074" + dependencies: + "@atproto/b64" "https://gitpkg.now.sh/haileyok/atproto-oauth/packages/b64?feat-oauth-server" + tslib "^2.6.2" + zod "^3.22.4" + "@atproto/lexicon@^0.3.1": version "0.3.1" resolved "https://registry.yarnpkg.com/@atproto/lexicon/-/lexicon-0.3.1.tgz#5d7275d041883a1c930404e3274a6fe7affc151f" @@ -264,6 +328,44 @@ multiformats "^9.9.0" zod "^3.21.4" +"@atproto/oauth-client-metadata@https://gitpkg.now.sh/haileyok/atproto-oauth/packages/oauth-client-metadata?feat-oauth-server": + version "0.0.1" + resolved "https://gitpkg.now.sh/haileyok/atproto-oauth/packages/oauth-client-metadata?feat-oauth-server#a3ec43488818d43579d1884487983fb9525c99293efc46e4c2d366090848c9ddfdd40ec5b6a5f67ddd77018fcb332e3a60abbf2b9059f325b233bac9c7d14298" + dependencies: + "@atproto/jwk" "https://gitpkg.now.sh/haileyok/atproto-oauth/packages/jwk?feat-oauth-server" + zod "^3.22.4" + +"@atproto/oauth-client@https://gitpkg.now.sh/haileyok/atproto-oauth/packages/oauth-client?feat-oauth-server": + version "0.0.1" + resolved "https://gitpkg.now.sh/haileyok/atproto-oauth/packages/oauth-client?feat-oauth-server#108b0d02de722083b5c4a8ccfe1c120a1017b53f9b855df8a6552bcb87f07d94c94d63021716b7a04f10db47bb8d78980bffb0c0a7e928752638bb0bd13df888" + dependencies: + "@atproto/b64" "https://gitpkg.now.sh/haileyok/atproto-oauth/packages/b64?feat-oauth-server" + "@atproto/caching" "https://gitpkg.now.sh/haileyok/atproto-oauth/packages/caching?feat-oauth-server" + "@atproto/did" "https://gitpkg.now.sh/haileyok/atproto-oauth/packages/did?feat-oauth-server" + "@atproto/fetch" "https://gitpkg.now.sh/haileyok/atproto-oauth/packages/fetch?feat-oauth-server" + "@atproto/fetch-dpop" "https://gitpkg.now.sh/haileyok/atproto-oauth/packages/fetch-dpop?feat-oauth-server" + "@atproto/handle-resolver" "https://gitpkg.now.sh/haileyok/atproto-oauth/packages/handle-resolver?feat-oauth-server" + "@atproto/identity-resolver" "https://gitpkg.now.sh/haileyok/atproto-oauth/packages/identity-resolver?feat-oauth-server" + "@atproto/jwk" "https://gitpkg.now.sh/haileyok/atproto-oauth/packages/jwk?feat-oauth-server" + "@atproto/oauth-client-metadata" "https://gitpkg.now.sh/haileyok/atproto-oauth/packages/oauth-client-metadata?feat-oauth-server" + "@atproto/oauth-server-metadata" "https://gitpkg.now.sh/haileyok/atproto-oauth/packages/oauth-server-metadata?feat-oauth-server" + "@atproto/oauth-server-metadata-resolver" "https://gitpkg.now.sh/haileyok/atproto-oauth/packages/oauth-server-metadata-resolver?feat-oauth-server" + +"@atproto/oauth-server-metadata-resolver@https://gitpkg.now.sh/haileyok/atproto-oauth/packages/oauth-server-metadata-resolver?feat-oauth-server": + version "0.0.1" + resolved "https://gitpkg.now.sh/haileyok/atproto-oauth/packages/oauth-server-metadata-resolver?feat-oauth-server#013607541029653b2d4078d390d8c2db53da818ab5e16682d34b8661d5d662c3caa9751c89c20cff3974baa1596eac24baf47cf43938971c38f719c99cdffb78" + dependencies: + "@atproto/caching" "https://gitpkg.now.sh/haileyok/atproto-oauth/packages/caching?feat-oauth-server" + "@atproto/fetch" "https://gitpkg.now.sh/haileyok/atproto-oauth/packages/fetch?feat-oauth-server" + "@atproto/oauth-server-metadata" "https://gitpkg.now.sh/haileyok/atproto-oauth/packages/oauth-server-metadata?feat-oauth-server" + zod "^3.22.4" + +"@atproto/oauth-server-metadata@https://gitpkg.now.sh/haileyok/atproto-oauth/packages/oauth-server-metadata?feat-oauth-server": + version "0.0.1" + resolved "https://gitpkg.now.sh/haileyok/atproto-oauth/packages/oauth-server-metadata?feat-oauth-server#9674d5d704a1662d22e675aa0d5b44ca1f27fdce5a5d3360032567fe895fe66f21c6c5f2d98dd3cd3be3cc90c814300d6ade3be03811a58a19196e91216ca434" + dependencies: + zod "^3.22.4" + "@atproto/ozone@^0.0.7": version "0.0.7" resolved "https://registry.yarnpkg.com/@atproto/ozone/-/ozone-0.0.7.tgz#bfad82bc1d0900e79401a82f13581f707415505a" @@ -359,11 +461,18 @@ dependencies: "@atproto/common-web" "^0.2.3" -"@atproto/syntax@^0.3.0": +"@atproto/syntax@^0.3.0", "@atproto/syntax@workspace:*": version "0.3.0" resolved "https://registry.yarnpkg.com/@atproto/syntax/-/syntax-0.3.0.tgz#fafa2dbea9add37253005cb663e7373e05e618b3" integrity sha512-Weq0ZBxffGHDXHl9U7BQc2BFJi/e23AL+k+i5+D9hUq/bzT4yjGsrCejkjq0xt82xXDjmhhvQSZ0LqxyZ5woxA== +"@atproto/transformer@https://gitpkg.now.sh/haileyok/atproto-oauth/packages/transformer?feat-oauth-server": + version "0.0.1" + uid ebc22db7baa265745f1021cbd95280c3ef0150eed0f1286a3e761d5a288af207d6878e5da450023613fa2cf66523e4f4708dbd755f1442e3624bf10e9df03fe9 + resolved "https://gitpkg.now.sh/haileyok/atproto-oauth/packages/transformer?feat-oauth-server#ebc22db7baa265745f1021cbd95280c3ef0150eed0f1286a3e761d5a288af207d6878e5da450023613fa2cf66523e4f4708dbd755f1442e3624bf10e9df03fe9" + dependencies: + tslib "^2.6.2" + "@atproto/xrpc-server@^0.4.2": version "0.4.2" resolved "https://registry.yarnpkg.com/@atproto/xrpc-server/-/xrpc-server-0.4.2.tgz#23efd89086b85933f1b0cc00c86e895adcaac315" @@ -15753,6 +15862,11 @@ lower-case@^2.0.2: dependencies: tslib "^2.0.3" +lru-cache@^10.2.0: + version "10.2.0" + resolved "https://registry.yarnpkg.com/lru-cache/-/lru-cache-10.2.0.tgz#0bd445ca57363465900f4d1f9bd8db343a4d95c3" + integrity "sha1-C9RFylc2NGWQD00fm9jbNDpNlcM= sha512-2bIM8x+VAf6JT4bKAljS1qUWgMsqZRPGJS6FSahIMPVvctcNhyVp7AJu7quxOW9jwkryBReKZY5tY5JYv2n/7Q==" + lru-cache@^4.0.1: version "4.1.5" resolved "https://registry.yarnpkg.com/lru-cache/-/lru-cache-4.1.5.tgz#8bbe50ea85bed59bc9e33dcab8235ee9bcf443cd" @@ -20999,7 +21113,7 @@ tslib@^1.11.1, tslib@^1.8.1, tslib@^1.9.0, tslib@^1.9.3: resolved "https://registry.yarnpkg.com/tslib/-/tslib-1.14.1.tgz#cf2d38bdc34a134bcaf1091c41f6619e2f672d00" integrity sha512-Xni35NKzjgMrwevysHTCArtLDpPvye8zV/0E4EyYn43P7/7qvQwPh9BGkHewbMulVntbigmcT7rdX3BNo9wRJg== -tslib@^2.0.0, tslib@^2.0.1, tslib@^2.0.3, tslib@^2.1.0, tslib@^2.3.1, tslib@^2.4.0, tslib@^2.4.1, tslib@^2.5.0, tslib@^2.5.3: +tslib@^2.0.0, tslib@^2.0.1, tslib@^2.0.3, tslib@^2.1.0, tslib@^2.3.1, tslib@^2.4.0, tslib@^2.4.1, tslib@^2.5.0, tslib@^2.5.3, tslib@^2.6.2: version "2.6.2" resolved "https://registry.yarnpkg.com/tslib/-/tslib-2.6.2.tgz#703ac29425e7b37cd6fd456e92404d46d1f3e4ae" integrity sha512-AEYxH93jGFPn/a2iVAwW87VuUIkR1FVUKB77NwMF7nBTDkDrrT/Hpt/IrCJ0QXhW27jTBDcf5ZY7w6RiqTMw2Q== @@ -22390,3 +22504,8 @@ zod@^3.14.2, zod@^3.20.2, zod@^3.21.4: version "3.22.2" resolved "https://registry.yarnpkg.com/zod/-/zod-3.22.2.tgz#3add8c682b7077c05ac6f979fea6998b573e157b" integrity sha512-wvWkphh5WQsJbVk1tbx1l1Ly4yg+XecD+Mq280uBGt9wa5BKSWf4Mhp6GmrkPixhMxmabYY7RbzlwVP32pbGCg== + +zod@^3.22.4: + version "3.22.4" + resolved "https://registry.yarnpkg.com/zod/-/zod-3.22.4.tgz#f31c3a9386f61b1f228af56faa9255e845cf3fff" + integrity "sha1-8xw6k4b2Gx8iivVvqpJV6EXPP/8= sha512-iC+8Io04lddc+mVqQ9AZ7OQ2MrUKGN+oIQyq1vemgt46jwCwLfhq7/pwnBnNXXXZb8VTVLKwp9EDkx+ryxIWmg==" From 28acdf70b65d4503554fc7139e43ac1656a8f57d Mon Sep 17 00:00:00 2001 From: Hailey Date: Thu, 4 Apr 2024 22:33:51 -0700 Subject: [PATCH 12/54] remove --- package.json | 1 - yarn.lock | 123 +-------------------------------------------------- 2 files changed, 2 insertions(+), 122 deletions(-) diff --git a/package.json b/package.json index 5c52a844d4..6165d953af 100644 --- a/package.json +++ b/package.json @@ -50,7 +50,6 @@ }, "dependencies": { "@atproto/api": "^0.12.2", - "@atproto/oauth-client": "https://gitpkg.now.sh/haileyok/atproto-oauth/packages/oauth-client?feat-oauth-server", "@bam.tech/react-native-image-resizer": "^3.0.4", "@braintree/sanitize-url": "^6.0.2", "@discord/bottom-sheet": "https://github.com/bluesky-social/react-native-bottom-sheet.git#discord-fork-4.6.1", diff --git a/yarn.lock b/yarn.lock index bbf3d9abee..106599fc14 100644 --- a/yarn.lock +++ b/yarn.lock @@ -77,12 +77,6 @@ multiformats "^9.9.0" uint8arrays "3.0.0" -"@atproto/b64@https://gitpkg.now.sh/haileyok/atproto-oauth/packages/b64?feat-oauth-server": - version "0.0.1" - resolved "https://gitpkg.now.sh/haileyok/atproto-oauth/packages/b64?feat-oauth-server#00a0db82a412e99828a9bd241790bdf6e07b9b1590de8de8d31cd97e8efa74ed9ab6d779b0168c9c0e98c43a407570e1b1526efab5076db2cb22a6ca7655255c" - dependencies: - base64-js "^1.5.1" - "@atproto/bsky@^0.0.28": version "0.0.28" resolved "https://registry.yarnpkg.com/@atproto/bsky/-/bsky-0.0.28.tgz#d9516f682883ceba60f52e3944d93dbd81375a7e" @@ -138,12 +132,6 @@ pino-http "^8.2.1" typed-emitter "^2.1.0" -"@atproto/caching@https://gitpkg.now.sh/haileyok/atproto-oauth/packages/caching?feat-oauth-server": - version "0.0.1" - resolved "https://gitpkg.now.sh/haileyok/atproto-oauth/packages/caching?feat-oauth-server#55421450c3f7e2db1f873a4e3537338ee1afc4f3d35b5d2b56bc5cd72dd68c831f93be4affa1cab8219064e3e392e7bec15af6858f8c447e9f4172fbfa262d68" - dependencies: - lru-cache "^10.2.0" - "@atproto/common-web@^0.2.3": version "0.2.3" resolved "https://registry.yarnpkg.com/@atproto/common-web/-/common-web-0.2.3.tgz#c44c1e177ae8309d5116347d49850209e8e478cc" @@ -245,50 +233,6 @@ sharp "^0.32.6" uint8arrays "3.0.0" -"@atproto/did@https://gitpkg.now.sh/haileyok/atproto-oauth/packages/did?feat-oauth-server": - version "0.0.1" - uid d8e31a16bcecd25fc34b1aadb99b12b01a9cd280f432cfe8e0dd8cf78201749c551884f6783523f7704e6568a07d59a0e4a8e7d4d635c0c66c8d8f12767f0031 - resolved "https://gitpkg.now.sh/haileyok/atproto-oauth/packages/did?feat-oauth-server#d8e31a16bcecd25fc34b1aadb99b12b01a9cd280f432cfe8e0dd8cf78201749c551884f6783523f7704e6568a07d59a0e4a8e7d4d635c0c66c8d8f12767f0031" - dependencies: - "@atproto/caching" "https://gitpkg.now.sh/haileyok/atproto-oauth/packages/caching?feat-oauth-server" - "@atproto/fetch" "https://gitpkg.now.sh/haileyok/atproto-oauth/packages/fetch?feat-oauth-server" - "@atproto/jwk" "https://gitpkg.now.sh/haileyok/atproto-oauth/packages/jwk?feat-oauth-server" - "@atproto/transformer" "https://gitpkg.now.sh/haileyok/atproto-oauth/packages/transformer?feat-oauth-server" - zod "^3.22.4" - -"@atproto/fetch-dpop@https://gitpkg.now.sh/haileyok/atproto-oauth/packages/fetch-dpop?feat-oauth-server": - version "0.0.0" - resolved "https://gitpkg.now.sh/haileyok/atproto-oauth/packages/fetch-dpop?feat-oauth-server#cf83e1357eefb8bdf1542850d66d8007d620e4050b5715dc83f4a921d36ce9ce47d0d13c5d85f2b0ff8318d2877eec2f63b931bd47417a81a538327af927da3e" - -"@atproto/fetch@https://gitpkg.now.sh/haileyok/atproto-oauth/packages/fetch?feat-oauth-server": - version "0.0.1" - uid "7fe9c1ef0eaa7b976e9817a3caf432e667cc540498fc9a63f9d1262dd604d48cc53aa0d5c059e9912ff401b51259d36e954b37935cf8578e8f6a682f8573405a" - resolved "https://gitpkg.now.sh/haileyok/atproto-oauth/packages/fetch?feat-oauth-server#7fe9c1ef0eaa7b976e9817a3caf432e667cc540498fc9a63f9d1262dd604d48cc53aa0d5c059e9912ff401b51259d36e954b37935cf8578e8f6a682f8573405a" - dependencies: - "@atproto/transformer" "https://gitpkg.now.sh/haileyok/atproto-oauth/packages/transformer?feat-oauth-server" - tslib "^2.6.2" - zod "^3.22.4" - -"@atproto/handle-resolver@https://gitpkg.now.sh/haileyok/atproto-oauth/packages/handle-resolver?feat-oauth-server": - version "0.0.1" - uid fe063702cb0ba71eddf1072d8d3838c04be573f4300d5161fb8775b8d2f3ee2aa5b434bfa2799f3fb275bdbe100be0069ef0af3381cbc778fa1b365f1477d651 - resolved "https://gitpkg.now.sh/haileyok/atproto-oauth/packages/handle-resolver?feat-oauth-server#fe063702cb0ba71eddf1072d8d3838c04be573f4300d5161fb8775b8d2f3ee2aa5b434bfa2799f3fb275bdbe100be0069ef0af3381cbc778fa1b365f1477d651" - dependencies: - "@atproto/caching" "https://gitpkg.now.sh/haileyok/atproto-oauth/packages/caching?feat-oauth-server" - "@atproto/did" "https://gitpkg.now.sh/haileyok/atproto-oauth/packages/did?feat-oauth-server" - "@atproto/fetch" "https://gitpkg.now.sh/haileyok/atproto-oauth/packages/fetch?feat-oauth-server" - lru-cache "^10.2.0" - zod "^3.22.4" - -"@atproto/identity-resolver@https://gitpkg.now.sh/haileyok/atproto-oauth/packages/identity-resolver?feat-oauth-server": - version "0.0.1" - resolved "https://gitpkg.now.sh/haileyok/atproto-oauth/packages/identity-resolver?feat-oauth-server#d3be310a6b9bb004e1183f336cc9507b8fa1d451641d6ac8da6b37a0aa9bb8dd25faad661c2d93d3f7ca16fc4961af54f2ad1c65fd0f01549a2c8aef959ab0d4" - dependencies: - "@atproto/did" "https://gitpkg.now.sh/haileyok/atproto-oauth/packages/did?feat-oauth-server" - "@atproto/fetch" "https://gitpkg.now.sh/haileyok/atproto-oauth/packages/fetch?feat-oauth-server" - "@atproto/handle-resolver" "https://gitpkg.now.sh/haileyok/atproto-oauth/packages/handle-resolver?feat-oauth-server" - "@atproto/syntax" "workspace:*" - "@atproto/identity@^0.3.2": version "0.3.2" resolved "https://registry.yarnpkg.com/@atproto/identity/-/identity-0.3.2.tgz#8a0536bc19ccbc45a04df84c3f30d86f58f964ee" @@ -298,14 +242,6 @@ "@atproto/crypto" "^0.3.0" axios "^0.27.2" -"@atproto/jwk@https://gitpkg.now.sh/haileyok/atproto-oauth/packages/jwk?feat-oauth-server": - version "0.0.1" - resolved "https://gitpkg.now.sh/haileyok/atproto-oauth/packages/jwk?feat-oauth-server#4a9e5a00740b15287e73e34d76de2c56c5a131f680f66963c2b2e4e5b34a517d265629ad498ed51256b27e87ee120e5c82ef1564c5ad3ff8ef1ec7d6c57f8074" - dependencies: - "@atproto/b64" "https://gitpkg.now.sh/haileyok/atproto-oauth/packages/b64?feat-oauth-server" - tslib "^2.6.2" - zod "^3.22.4" - "@atproto/lexicon@^0.3.1": version "0.3.1" resolved "https://registry.yarnpkg.com/@atproto/lexicon/-/lexicon-0.3.1.tgz#5d7275d041883a1c930404e3274a6fe7affc151f" @@ -328,44 +264,6 @@ multiformats "^9.9.0" zod "^3.21.4" -"@atproto/oauth-client-metadata@https://gitpkg.now.sh/haileyok/atproto-oauth/packages/oauth-client-metadata?feat-oauth-server": - version "0.0.1" - resolved "https://gitpkg.now.sh/haileyok/atproto-oauth/packages/oauth-client-metadata?feat-oauth-server#a3ec43488818d43579d1884487983fb9525c99293efc46e4c2d366090848c9ddfdd40ec5b6a5f67ddd77018fcb332e3a60abbf2b9059f325b233bac9c7d14298" - dependencies: - "@atproto/jwk" "https://gitpkg.now.sh/haileyok/atproto-oauth/packages/jwk?feat-oauth-server" - zod "^3.22.4" - -"@atproto/oauth-client@https://gitpkg.now.sh/haileyok/atproto-oauth/packages/oauth-client?feat-oauth-server": - version "0.0.1" - resolved "https://gitpkg.now.sh/haileyok/atproto-oauth/packages/oauth-client?feat-oauth-server#108b0d02de722083b5c4a8ccfe1c120a1017b53f9b855df8a6552bcb87f07d94c94d63021716b7a04f10db47bb8d78980bffb0c0a7e928752638bb0bd13df888" - dependencies: - "@atproto/b64" "https://gitpkg.now.sh/haileyok/atproto-oauth/packages/b64?feat-oauth-server" - "@atproto/caching" "https://gitpkg.now.sh/haileyok/atproto-oauth/packages/caching?feat-oauth-server" - "@atproto/did" "https://gitpkg.now.sh/haileyok/atproto-oauth/packages/did?feat-oauth-server" - "@atproto/fetch" "https://gitpkg.now.sh/haileyok/atproto-oauth/packages/fetch?feat-oauth-server" - "@atproto/fetch-dpop" "https://gitpkg.now.sh/haileyok/atproto-oauth/packages/fetch-dpop?feat-oauth-server" - "@atproto/handle-resolver" "https://gitpkg.now.sh/haileyok/atproto-oauth/packages/handle-resolver?feat-oauth-server" - "@atproto/identity-resolver" "https://gitpkg.now.sh/haileyok/atproto-oauth/packages/identity-resolver?feat-oauth-server" - "@atproto/jwk" "https://gitpkg.now.sh/haileyok/atproto-oauth/packages/jwk?feat-oauth-server" - "@atproto/oauth-client-metadata" "https://gitpkg.now.sh/haileyok/atproto-oauth/packages/oauth-client-metadata?feat-oauth-server" - "@atproto/oauth-server-metadata" "https://gitpkg.now.sh/haileyok/atproto-oauth/packages/oauth-server-metadata?feat-oauth-server" - "@atproto/oauth-server-metadata-resolver" "https://gitpkg.now.sh/haileyok/atproto-oauth/packages/oauth-server-metadata-resolver?feat-oauth-server" - -"@atproto/oauth-server-metadata-resolver@https://gitpkg.now.sh/haileyok/atproto-oauth/packages/oauth-server-metadata-resolver?feat-oauth-server": - version "0.0.1" - resolved "https://gitpkg.now.sh/haileyok/atproto-oauth/packages/oauth-server-metadata-resolver?feat-oauth-server#013607541029653b2d4078d390d8c2db53da818ab5e16682d34b8661d5d662c3caa9751c89c20cff3974baa1596eac24baf47cf43938971c38f719c99cdffb78" - dependencies: - "@atproto/caching" "https://gitpkg.now.sh/haileyok/atproto-oauth/packages/caching?feat-oauth-server" - "@atproto/fetch" "https://gitpkg.now.sh/haileyok/atproto-oauth/packages/fetch?feat-oauth-server" - "@atproto/oauth-server-metadata" "https://gitpkg.now.sh/haileyok/atproto-oauth/packages/oauth-server-metadata?feat-oauth-server" - zod "^3.22.4" - -"@atproto/oauth-server-metadata@https://gitpkg.now.sh/haileyok/atproto-oauth/packages/oauth-server-metadata?feat-oauth-server": - version "0.0.1" - resolved "https://gitpkg.now.sh/haileyok/atproto-oauth/packages/oauth-server-metadata?feat-oauth-server#9674d5d704a1662d22e675aa0d5b44ca1f27fdce5a5d3360032567fe895fe66f21c6c5f2d98dd3cd3be3cc90c814300d6ade3be03811a58a19196e91216ca434" - dependencies: - zod "^3.22.4" - "@atproto/ozone@^0.0.7": version "0.0.7" resolved "https://registry.yarnpkg.com/@atproto/ozone/-/ozone-0.0.7.tgz#bfad82bc1d0900e79401a82f13581f707415505a" @@ -461,18 +359,11 @@ dependencies: "@atproto/common-web" "^0.2.3" -"@atproto/syntax@^0.3.0", "@atproto/syntax@workspace:*": +"@atproto/syntax@^0.3.0": version "0.3.0" resolved "https://registry.yarnpkg.com/@atproto/syntax/-/syntax-0.3.0.tgz#fafa2dbea9add37253005cb663e7373e05e618b3" integrity sha512-Weq0ZBxffGHDXHl9U7BQc2BFJi/e23AL+k+i5+D9hUq/bzT4yjGsrCejkjq0xt82xXDjmhhvQSZ0LqxyZ5woxA== -"@atproto/transformer@https://gitpkg.now.sh/haileyok/atproto-oauth/packages/transformer?feat-oauth-server": - version "0.0.1" - uid ebc22db7baa265745f1021cbd95280c3ef0150eed0f1286a3e761d5a288af207d6878e5da450023613fa2cf66523e4f4708dbd755f1442e3624bf10e9df03fe9 - resolved "https://gitpkg.now.sh/haileyok/atproto-oauth/packages/transformer?feat-oauth-server#ebc22db7baa265745f1021cbd95280c3ef0150eed0f1286a3e761d5a288af207d6878e5da450023613fa2cf66523e4f4708dbd755f1442e3624bf10e9df03fe9" - dependencies: - tslib "^2.6.2" - "@atproto/xrpc-server@^0.4.2": version "0.4.2" resolved "https://registry.yarnpkg.com/@atproto/xrpc-server/-/xrpc-server-0.4.2.tgz#23efd89086b85933f1b0cc00c86e895adcaac315" @@ -15862,11 +15753,6 @@ lower-case@^2.0.2: dependencies: tslib "^2.0.3" -lru-cache@^10.2.0: - version "10.2.0" - resolved "https://registry.yarnpkg.com/lru-cache/-/lru-cache-10.2.0.tgz#0bd445ca57363465900f4d1f9bd8db343a4d95c3" - integrity "sha1-C9RFylc2NGWQD00fm9jbNDpNlcM= sha512-2bIM8x+VAf6JT4bKAljS1qUWgMsqZRPGJS6FSahIMPVvctcNhyVp7AJu7quxOW9jwkryBReKZY5tY5JYv2n/7Q==" - lru-cache@^4.0.1: version "4.1.5" resolved "https://registry.yarnpkg.com/lru-cache/-/lru-cache-4.1.5.tgz#8bbe50ea85bed59bc9e33dcab8235ee9bcf443cd" @@ -21113,7 +20999,7 @@ tslib@^1.11.1, tslib@^1.8.1, tslib@^1.9.0, tslib@^1.9.3: resolved "https://registry.yarnpkg.com/tslib/-/tslib-1.14.1.tgz#cf2d38bdc34a134bcaf1091c41f6619e2f672d00" integrity sha512-Xni35NKzjgMrwevysHTCArtLDpPvye8zV/0E4EyYn43P7/7qvQwPh9BGkHewbMulVntbigmcT7rdX3BNo9wRJg== -tslib@^2.0.0, tslib@^2.0.1, tslib@^2.0.3, tslib@^2.1.0, tslib@^2.3.1, tslib@^2.4.0, tslib@^2.4.1, tslib@^2.5.0, tslib@^2.5.3, tslib@^2.6.2: +tslib@^2.0.0, tslib@^2.0.1, tslib@^2.0.3, tslib@^2.1.0, tslib@^2.3.1, tslib@^2.4.0, tslib@^2.4.1, tslib@^2.5.0, tslib@^2.5.3: version "2.6.2" resolved "https://registry.yarnpkg.com/tslib/-/tslib-2.6.2.tgz#703ac29425e7b37cd6fd456e92404d46d1f3e4ae" integrity sha512-AEYxH93jGFPn/a2iVAwW87VuUIkR1FVUKB77NwMF7nBTDkDrrT/Hpt/IrCJ0QXhW27jTBDcf5ZY7w6RiqTMw2Q== @@ -22504,8 +22390,3 @@ zod@^3.14.2, zod@^3.20.2, zod@^3.21.4: version "3.22.2" resolved "https://registry.yarnpkg.com/zod/-/zod-3.22.2.tgz#3add8c682b7077c05ac6f979fea6998b573e157b" integrity sha512-wvWkphh5WQsJbVk1tbx1l1Ly4yg+XecD+Mq280uBGt9wa5BKSWf4Mhp6GmrkPixhMxmabYY7RbzlwVP32pbGCg== - -zod@^3.22.4: - version "3.22.4" - resolved "https://registry.yarnpkg.com/zod/-/zod-3.22.4.tgz#f31c3a9386f61b1f228af56faa9255e845cf3fff" - integrity "sha1-8xw6k4b2Gx8iivVvqpJV6EXPP/8= sha512-iC+8Io04lddc+mVqQ9AZ7OQ2MrUKGN+oIQyq1vemgt46jwCwLfhq7/pwnBnNXXXZb8VTVLKwp9EDkx+ryxIWmg==" From d61fc5f32e4752cd144c97fa1254055794e41a53 Mon Sep 17 00:00:00 2001 From: Hailey Date: Mon, 8 Apr 2024 12:02:22 -0700 Subject: [PATCH 13/54] add `expo-secure-store` --- package.json | 1 + yarn.lock | 5 +++++ 2 files changed, 6 insertions(+) diff --git a/package.json b/package.json index 55c5668544..c19452e316 100644 --- a/package.json +++ b/package.json @@ -122,6 +122,7 @@ "expo-localization": "~14.8.3", "expo-media-library": "~15.9.1", "expo-notifications": "~0.27.6", + "expo-secure-store": "^12.8.1", "expo-sharing": "^11.10.0", "expo-splash-screen": "~0.26.4", "expo-status-bar": "~1.11.1", diff --git a/yarn.lock b/yarn.lock index 4b6decea91..e7749a47b4 100644 --- a/yarn.lock +++ b/yarn.lock @@ -11994,6 +11994,11 @@ expo-pwa@0.0.127: commander "2.20.0" update-check "1.5.3" +expo-secure-store@^12.8.1: + version "12.8.1" + resolved "https://registry.yarnpkg.com/expo-secure-store/-/expo-secure-store-12.8.1.tgz#369a570702fa1dc0c49ea41a5ab18aca2a986d38" + integrity sha512-Ju3jmkHby4w7rIzdYAt9kQyQ7HhHJ0qRaiQOInknhOLIltftHjEgF4I1UmzKc7P5RCfGNmVbEH729Pncp/sHXQ== + expo-sharing@^11.10.0: version "11.10.0" resolved "https://registry.yarnpkg.com/expo-sharing/-/expo-sharing-11.10.0.tgz#0e85197ee4d2634b00fe201e571fbdc64cf83eef" From 71721b5943308c47ed82f3a142c9cd57099c62cb Mon Sep 17 00:00:00 2001 From: Hailey Date: Mon, 8 Apr 2024 16:30:46 -0700 Subject: [PATCH 14/54] copy over a few files for now --- package.json | 2 +- src/oauth-client-temp/b64/index.ts | 34 +++ src/oauth-client-temp/client/constants.ts | 1 + .../client/crypto-implementation.ts | 0 src/oauth-client-temp/jwk/alg.ts | 97 +++++++++ src/oauth-client-temp/jwk/index.ts | 9 + src/oauth-client-temp/jwk/jwk.ts | 153 ++++++++++++++ src/oauth-client-temp/jwk/jwks.ts | 19 ++ src/oauth-client-temp/jwk/jwt-decode.ts | 32 +++ src/oauth-client-temp/jwk/jwt-verify.ts | 20 ++ src/oauth-client-temp/jwk/jwt.ts | 172 +++++++++++++++ src/oauth-client-temp/jwk/key.ts | 95 +++++++++ src/oauth-client-temp/jwk/keyset.ts | 200 ++++++++++++++++++ src/oauth-client-temp/jwk/util.ts | 55 +++++ src/screens/Login/hooks/package.json | 14 ++ yarn.lock | 7 +- 16 files changed, 908 insertions(+), 2 deletions(-) create mode 100644 src/oauth-client-temp/b64/index.ts create mode 100644 src/oauth-client-temp/client/constants.ts create mode 100644 src/oauth-client-temp/client/crypto-implementation.ts create mode 100644 src/oauth-client-temp/jwk/alg.ts create mode 100644 src/oauth-client-temp/jwk/index.ts create mode 100644 src/oauth-client-temp/jwk/jwk.ts create mode 100644 src/oauth-client-temp/jwk/jwks.ts create mode 100644 src/oauth-client-temp/jwk/jwt-decode.ts create mode 100644 src/oauth-client-temp/jwk/jwt-verify.ts create mode 100644 src/oauth-client-temp/jwk/jwt.ts create mode 100644 src/oauth-client-temp/jwk/key.ts create mode 100644 src/oauth-client-temp/jwk/keyset.ts create mode 100644 src/oauth-client-temp/jwk/util.ts create mode 100644 src/screens/Login/hooks/package.json diff --git a/package.json b/package.json index c19452e316..59d402a5ec 100644 --- a/package.json +++ b/package.json @@ -185,7 +185,7 @@ "tippy.js": "^6.3.7", "tlds": "^1.234.0", "zeego": "^1.6.2", - "zod": "^3.20.2" + "zod": "^3.22.4" }, "devDependencies": { "@atproto/dev-env": "^0.2.28", diff --git a/src/oauth-client-temp/b64/index.ts b/src/oauth-client-temp/b64/index.ts new file mode 100644 index 0000000000..6acab5725e --- /dev/null +++ b/src/oauth-client-temp/b64/index.ts @@ -0,0 +1,34 @@ +import {fromByteArray, toByteArray} from 'base64-js' + +// Old Node implementations do not support "base64url" +const Buffer = (Buffer => { + if (typeof Buffer === 'function') { + try { + Buffer.from('', 'base64url') + return Buffer + } catch { + return undefined + } + } + return undefined +})(globalThis.Buffer) + +export const b64uDecode: (b64u: string) => Uint8Array = Buffer + ? b64u => Buffer.from(b64u, 'base64url') + : b64u => { + // toByteArray requires padding but not to replace '-' and '_' + const pad = b64u.length % 4 + const b64 = b64u.padEnd(b64u.length + (pad > 0 ? 4 - pad : 0), '=') + return toByteArray(b64) + } + +export const b64uEncode = Buffer + ? (bytes: Uint8Array) => { + const buffer = bytes instanceof Buffer ? bytes : Buffer.from(bytes) + return buffer.toString('base64url') + } + : (bytes: Uint8Array): string => + fromByteArray(bytes) + .replace(/\+/g, '-') + .replace(/\//g, '_') + .replace(/[=]+$/g, '') diff --git a/src/oauth-client-temp/client/constants.ts b/src/oauth-client-temp/client/constants.ts new file mode 100644 index 0000000000..dca10c5ce3 --- /dev/null +++ b/src/oauth-client-temp/client/constants.ts @@ -0,0 +1 @@ +export const FALLBACK_ALG = 'ES256' diff --git a/src/oauth-client-temp/client/crypto-implementation.ts b/src/oauth-client-temp/client/crypto-implementation.ts new file mode 100644 index 0000000000..e69de29bb2 diff --git a/src/oauth-client-temp/jwk/alg.ts b/src/oauth-client-temp/jwk/alg.ts new file mode 100644 index 0000000000..226a1bb662 --- /dev/null +++ b/src/oauth-client-temp/jwk/alg.ts @@ -0,0 +1,97 @@ +import { Jwk } from './jwk.js' + +declare const process: undefined | { versions?: { node?: string } } +const IS_NODE_RUNTIME = + typeof process !== 'undefined' && typeof process?.versions?.node === 'string' + +export function* jwkAlgorithms(jwk: Jwk): Generator { + // Ed25519, Ed448, and secp256k1 always have "alg" + // OKP always has "use" + if (jwk.alg) { + yield jwk.alg + return + } + + switch (jwk.kty) { + case 'EC': { + if (jwk.use === 'enc' || jwk.use === undefined) { + yield 'ECDH-ES' + yield 'ECDH-ES+A128KW' + yield 'ECDH-ES+A192KW' + yield 'ECDH-ES+A256KW' + } + + if (jwk.use === 'sig' || jwk.use === undefined) { + const crv = 'crv' in jwk ? jwk.crv : undefined + switch (crv) { + case 'P-256': + case 'P-384': + yield `ES${crv.slice(-3)}`.replace('21', '12') + break + case 'P-521': + yield 'ES512' + break + case 'secp256k1': + if (IS_NODE_RUNTIME) yield 'ES256K' + break + default: + throw new TypeError(`Unsupported crv "${crv}"`) + } + } + + return + } + + case 'OKP': { + if (!jwk.use) throw new TypeError('Missing "use" Parameter value') + yield 'ECDH-ES' + yield 'ECDH-ES+A128KW' + yield 'ECDH-ES+A192KW' + yield 'ECDH-ES+A256KW' + return + } + + case 'RSA': { + if (jwk.use === 'enc' || jwk.use === undefined) { + yield 'RSA-OAEP' + yield 'RSA-OAEP-256' + yield 'RSA-OAEP-384' + yield 'RSA-OAEP-512' + if (IS_NODE_RUNTIME) yield 'RSA1_5' + } + + if (jwk.use === 'sig' || jwk.use === undefined) { + yield 'PS256' + yield 'PS384' + yield 'PS512' + yield 'RS256' + yield 'RS384' + yield 'RS512' + } + + return + } + + case 'oct': { + if (jwk.use === 'enc' || jwk.use === undefined) { + yield 'A128GCMKW' + yield 'A192GCMKW' + yield 'A256GCMKW' + yield 'A128KW' + yield 'A192KW' + yield 'A256KW' + } + + if (jwk.use === 'sig' || jwk.use === undefined) { + yield 'HS256' + yield 'HS384' + yield 'HS512' + } + + return + } + + default: + throw new Error(`Unsupported kty "${jwk.kty}"`) + } +} diff --git a/src/oauth-client-temp/jwk/index.ts b/src/oauth-client-temp/jwk/index.ts new file mode 100644 index 0000000000..3e1c678233 --- /dev/null +++ b/src/oauth-client-temp/jwk/index.ts @@ -0,0 +1,9 @@ +export * from './alg.js' +export * from './jwk.js' +export * from './jwks.js' +export * from './jwt.js' +export * from './jwt-decode.js' +export * from './jwt-verify.js' +export * from './key.js' +export * from './keyset.js' +export * from './util.js' diff --git a/src/oauth-client-temp/jwk/jwk.ts b/src/oauth-client-temp/jwk/jwk.ts new file mode 100644 index 0000000000..f74a2ef377 --- /dev/null +++ b/src/oauth-client-temp/jwk/jwk.ts @@ -0,0 +1,153 @@ +import { z } from 'zod' + +export const keyUsageSchema = z.enum([ + 'sign', + 'verify', + 'encrypt', + 'decrypt', + 'wrapKey', + 'unwrapKey', + 'deriveKey', + 'deriveBits', +]) + +export type KeyUsage = z.infer + +/** + * The "use" and "key_ops" JWK members SHOULD NOT be used together; + * however, if both are used, the information they convey MUST be + * consistent. Applications should specify which of these members they + * use, if either is to be used by the application. + * + * @todo Actually check that "use" and "key_ops" are consistent when both are present. + * @see {@link https://datatracker.ietf.org/doc/html/rfc7517#section-4.3} + */ +export const jwkBaseSchema = z.object({ + kty: z.string().min(1), + alg: z.string().min(1).optional(), + kid: z.string().min(1).optional(), + ext: z.boolean().optional(), + use: z.enum(['sig', 'enc']).optional(), + key_ops: z.array(keyUsageSchema).readonly().optional(), + + x5c: z.array(z.string()).readonly().optional(), // X.509 Certificate Chain + x5t: z.string().min(1).optional(), // X.509 Certificate SHA-1 Thumbprint + 'x5t#S256': z.string().min(1).optional(), // X.509 Certificate SHA-256 Thumbprint + x5u: z.string().url().optional(), // X.509 URL +}) + +/** + * @todo: properly implement this + */ +export const jwkRsaKeySchema = jwkBaseSchema + .extend({ + kty: z.literal('RSA'), + alg: z + .enum(['RS256', 'RS384', 'RS512', 'PS256', 'PS384', 'PS512']) + .optional(), + + n: z.string().min(1), // Modulus + e: z.string().min(1), // Exponent + + d: z.string().min(1).optional(), // Private Exponent + p: z.string().min(1).optional(), // First Prime Factor + q: z.string().min(1).optional(), // Second Prime Factor + dp: z.string().min(1).optional(), // First Factor CRT Exponent + dq: z.string().min(1).optional(), // Second Factor CRT Exponent + qi: z.string().min(1).optional(), // First CRT Coefficient + oth: z + .array( + z + .object({ + r: z.string().optional(), + d: z.string().optional(), + t: z.string().optional(), + }) + .readonly(), + ) + .nonempty() + .readonly() + .optional(), // Other Primes Info + }) + .readonly() + +export const jwkEcKeySchema = jwkBaseSchema + .extend({ + kty: z.literal('EC'), + alg: z.enum(['ES256', 'ES384', 'ES512']).optional(), + crv: z.enum(['P-256', 'P-384', 'P-521']), + + x: z.string().min(1), + y: z.string().min(1), + + d: z.string().min(1).optional(), // ECC Private Key + }) + .readonly() + +export const jwkEcSecp256k1KeySchema = jwkBaseSchema + .extend({ + kty: z.literal('EC'), + alg: z.enum(['ES256K']).optional(), + crv: z.enum(['secp256k1']), + + x: z.string().min(1), + y: z.string().min(1), + + d: z.string().min(1).optional(), // ECC Private Key + }) + .readonly() + +export const jwkOkpKeySchema = jwkBaseSchema + .extend({ + kty: z.literal('OKP'), + alg: z.enum(['EdDSA']).optional(), + crv: z.enum(['Ed25519', 'Ed448']), + + x: z.string().min(1), + d: z.string().min(1).optional(), // ECC Private Key + }) + .readonly() + +export const jwkSymKeySchema = jwkBaseSchema + .extend({ + kty: z.literal('oct'), // Octet Sequence (used to represent symmetric keys) + alg: z.enum(['HS256', 'HS384', 'HS512']).optional(), + + k: z.string(), // Key Value (base64url encoded) + }) + .readonly() + +export const jwkUnknownKeySchema = jwkBaseSchema + .extend({ + kty: z + .string() + .refine((v) => v !== 'RSA' && v !== 'EC' && v !== 'OKP' && v !== 'oct'), + }) + .readonly() + +export const jwkSchema = z.union([ + jwkUnknownKeySchema, + jwkRsaKeySchema, + jwkEcKeySchema, + jwkEcSecp256k1KeySchema, + jwkOkpKeySchema, + jwkSymKeySchema, +]) + +export type Jwk = z.infer + +export const jwkPubSchema = jwkSchema + .refine((k) => k.kid != null, 'kid is required') + .refine((k) => k.use != null || k.key_ops != null, 'use or key_ops required') + .refine( + (k) => + !k.use || + !k.key_ops || + k.key_ops.every((o) => + k.use === 'sig' + ? o === 'sign' || o === 'verify' + : o === 'encrypt' || o === 'decrypt', + ), + 'use and key_ops must be consistent', + ) + .refine((k) => !('k' in k) && !('d' in k), 'private key not allowed') diff --git a/src/oauth-client-temp/jwk/jwks.ts b/src/oauth-client-temp/jwk/jwks.ts new file mode 100644 index 0000000000..1ec8d382ba --- /dev/null +++ b/src/oauth-client-temp/jwk/jwks.ts @@ -0,0 +1,19 @@ +import { z } from 'zod' + +import { jwkPubSchema, jwkSchema } from './jwk.js' + +export const jwksSchema = z + .object({ + keys: z.array(jwkSchema).readonly(), + }) + .readonly() + +export type Jwks = z.infer + +export const jwksPubSchema = z + .object({ + keys: z.array(jwkPubSchema).readonly(), + }) + .readonly() + +export type JwksPub = z.infer diff --git a/src/oauth-client-temp/jwk/jwt-decode.ts b/src/oauth-client-temp/jwk/jwt-decode.ts new file mode 100644 index 0000000000..287f8fbc08 --- /dev/null +++ b/src/oauth-client-temp/jwk/jwt-decode.ts @@ -0,0 +1,32 @@ +import { b64uDecode } from '@atproto/b64' + +import { ui8ToString } from './util.js' +import { + JwtHeader, + JwtPayload, + jwtHeaderSchema, + jwtPayloadSchema, +} from './jwt.js' + +export function unsafeDecodeJwt(jwt: string): { + header: JwtHeader + payload: JwtPayload +} { + const { 0: headerEnc, 1: payloadEnc, length } = jwt.split('.') + if (length > 3 || length < 2) { + throw new TypeError('invalid JWT input') + } + + const header = jwtHeaderSchema.parse( + JSON.parse(ui8ToString(b64uDecode(headerEnc!))), + ) + if (length === 2 && header?.alg !== 'none') { + throw new TypeError('invalid JWT input') + } + + const payload = jwtPayloadSchema.parse( + JSON.parse(ui8ToString(b64uDecode(payloadEnc!))), + ) + + return { header, payload } +} diff --git a/src/oauth-client-temp/jwk/jwt-verify.ts b/src/oauth-client-temp/jwk/jwt-verify.ts new file mode 100644 index 0000000000..5eeca81e53 --- /dev/null +++ b/src/oauth-client-temp/jwk/jwt-verify.ts @@ -0,0 +1,20 @@ +import { JwtHeader, JwtPayload } from './jwt.js' +import { RequiredKey } from './util.js' + +export type VerifyOptions = { + audience?: string | readonly string[] + clockTolerance?: string | number + issuer?: string | readonly string[] + maxTokenAge?: string | number + subject?: string + typ?: string + currentDate?: Date + requiredClaims?: readonly C[] +} + +export type VerifyPayload = Record + +export type VerifyResult

= { + payload: RequiredKey

+ protectedHeader: JwtHeader +} diff --git a/src/oauth-client-temp/jwk/jwt.ts b/src/oauth-client-temp/jwk/jwt.ts new file mode 100644 index 0000000000..c07af8cb73 --- /dev/null +++ b/src/oauth-client-temp/jwk/jwt.ts @@ -0,0 +1,172 @@ +import { z } from 'zod' + +import { jwkPubSchema } from './jwk.js' + +export const JWT_REGEXP = /^[A-Za-z0-9_-]{2,}(?:\.[A-Za-z0-9_-]{2,}){1,2}$/ +export const jwtSchema = z + .string() + .min(5) + .refinement( + (data: string): data is `${string}.${string}.${string}` => + JWT_REGEXP.test(data), + { + code: z.ZodIssueCode.custom, + message: 'Must be a JWT', + }, + ) + +export const isJwt = (data: unknown): data is Jwt => + jwtSchema.safeParse(data).success + +export type Jwt = z.infer + +/** + * @see {@link https://www.rfc-editor.org/rfc/rfc7515.html#section-4} + */ +export const jwtHeaderSchema = z.object({ + /** "alg" (Algorithm) Header Parameter */ + alg: z.string(), + /** "jku" (JWK Set URL) Header Parameter */ + jku: z.string().url().optional(), + /** "jwk" (JSON Web Key) Header Parameter */ + jwk: z + .object({ + kty: z.string(), + crv: z.string().optional(), + x: z.string().optional(), + y: z.string().optional(), + e: z.string().optional(), + n: z.string().optional(), + }) + .optional(), + /** "kid" (Key ID) Header Parameter */ + kid: z.string().optional(), + /** "x5u" (X.509 URL) Header Parameter */ + x5u: z.string().optional(), + /** "x5c" (X.509 Certificate Chain) Header Parameter */ + x5c: z.array(z.string()).optional(), + /** "x5t" (X.509 Certificate SHA-1 Thumbprint) Header Parameter */ + x5t: z.string().optional(), + /** "x5t#S256" (X.509 Certificate SHA-256 Thumbprint) Header Parameter */ + 'x5t#S256': z.string().optional(), + /** "typ" (Type) Header Parameter */ + typ: z.string().optional(), + /** "cty" (Content Type) Header Parameter */ + cty: z.string().optional(), + /** "crit" (Critical) Header Parameter */ + crit: z.array(z.string()).optional(), +}) + +export type JwtHeader = z.infer + +// https://www.iana.org/assignments/jwt/jwt.xhtml +export const jwtPayloadSchema = z.object({ + iss: z.string().optional(), + aud: z.union([z.string(), z.array(z.string()).nonempty()]).optional(), + sub: z.string().optional(), + exp: z.number().int().optional(), + nbf: z.number().int().optional(), + iat: z.number().int().optional(), + jti: z.string().optional(), + htm: z.string().optional(), + htu: z.string().optional(), + ath: z.string().optional(), + acr: z.string().optional(), + azp: z.string().optional(), + amr: z.array(z.string()).optional(), + // https://datatracker.ietf.org/doc/html/rfc7800 + cnf: z + .object({ + kid: z.string().optional(), // Key ID + jwk: jwkPubSchema.optional(), // JWK + jwe: z.string().optional(), // Encrypted key + jku: z.string().url().optional(), // JWK Set URI ("kid" should also be provided) + + // https://datatracker.ietf.org/doc/html/rfc9449#section-6.1 + jkt: z.string().optional(), + + // https://datatracker.ietf.org/doc/html/rfc8705 + 'x5t#S256': z.string().optional(), // X.509 Certificate SHA-256 Thumbprint + + // https://datatracker.ietf.org/doc/html/rfc9203 + osc: z.string().optional(), // OSCORE_Input_Material carrying the parameters for using OSCORE per-message security with implicit key confirmation + }) + .optional(), + + client_id: z.string().optional(), + + scope: z.string().optional(), + nonce: z.string().optional(), + + at_hash: z.string().optional(), + c_hash: z.string().optional(), + s_hash: z.string().optional(), + auth_time: z.number().int().optional(), + + // https://openid.net/specs/openid-connect-core-1_0.html#StandardClaims + + // OpenID: "profile" scope + name: z.string().optional(), + family_name: z.string().optional(), + given_name: z.string().optional(), + middle_name: z.string().optional(), + nickname: z.string().optional(), + preferred_username: z.string().optional(), + gender: z.string().optional(), // OpenID only defines "male" and "female" without forbidding other values + picture: z.string().url().optional(), + profile: z.string().url().optional(), + website: z.string().url().optional(), + birthdate: z + .string() + .regex(/\d{4}-\d{2}-\d{2}/) // YYYY-MM-DD + .optional(), + zoneinfo: z + .string() + .regex(/^[A-Za-z0-9_/]+$/) + .optional(), + locale: z + .string() + .regex(/^[a-z]{2}(-[A-Z]{2})?$/) + .optional(), + updated_at: z.number().int().optional(), + + // OpenID: "email" scope + email: z.string().optional(), + email_verified: z.boolean().optional(), + + // OpenID: "phone" scope + phone_number: z.string().optional(), + phone_number_verified: z.boolean().optional(), + + // OpenID: "address" scope + // https://openid.net/specs/openid-connect-core-1_0.html#AddressClaim + address: z + .object({ + formatted: z.string().optional(), + street_address: z.string().optional(), + locality: z.string().optional(), + region: z.string().optional(), + postal_code: z.string().optional(), + country: z.string().optional(), + }) + .optional(), + + // https://datatracker.ietf.org/doc/html/rfc9396#section-14.2 + authorization_details: z + .array( + z + .object({ + type: z.string(), + // https://datatracker.ietf.org/doc/html/rfc9396#section-2.2 + locations: z.array(z.string()).optional(), + actions: z.array(z.string()).optional(), + datatypes: z.array(z.string()).optional(), + identifier: z.string().optional(), + privileges: z.array(z.string()).optional(), + }) + .passthrough(), + ) + .optional(), +}) + +export type JwtPayload = z.infer diff --git a/src/oauth-client-temp/jwk/key.ts b/src/oauth-client-temp/jwk/key.ts new file mode 100644 index 0000000000..d8af69e0df --- /dev/null +++ b/src/oauth-client-temp/jwk/key.ts @@ -0,0 +1,95 @@ +import { jwkAlgorithms } from './alg.js' +import { Jwk, jwkSchema } from './jwk.js' +import { VerifyOptions, VerifyPayload, VerifyResult } from './jwt-verify.js' +import { Jwt, JwtHeader, JwtPayload } from './jwt.js' +import { cachedGetter } from './util.js' + +export abstract class Key { + constructor(protected jwk: Jwk) { + // A key should always be used either for signing or encryption. + if (!jwk.use) throw new TypeError('Missing "use" Parameter value') + } + + get isPrivate(): boolean { + const { jwk } = this + if ('d' in jwk && jwk.d !== undefined) return true + return this.isSymetric + } + + get isSymetric(): boolean { + const { jwk } = this + if ('k' in jwk && jwk.k !== undefined) return true + return false + } + + get privateJwk(): Jwk | undefined { + return this.isPrivate ? this.jwk : undefined + } + + @cachedGetter + get publicJwk(): Jwk | undefined { + if (this.isSymetric) return undefined + if (this.isPrivate) { + const { d: _, ...jwk } = this.jwk as any + return jwk + } + return this.jwk + } + + @cachedGetter + get bareJwk(): Jwk | undefined { + if (this.isSymetric) return undefined + const { kty, crv, e, n, x, y } = this.jwk as any + return jwkSchema.parse({ crv, e, kty, n, x, y }) + } + + get use() { + return this.jwk.use! + } + + /** + * The (forced) algorithm to use. If not provided, the key will be usable with + * any of the algorithms in {@link algorithms}. + */ + get alg() { + return this.jwk.alg + } + + get kid() { + return this.jwk.kid + } + + get crv() { + return (this.jwk as undefined | Extract)?.crv + } + + get canVerify() { + return this.use === 'sig' + } + + get canSign() { + return this.use === 'sig' && this.isPrivate && !this.isSymetric + } + + /** + * All the algorithms that this key can be used with. If `alg` is provided, + * this set will only contain that algorithm. + */ + @cachedGetter + get algorithms(): readonly string[] { + return Array.from(jwkAlgorithms(this.jwk)) + } + + /** + * Create a signed JWT + */ + abstract createJwt(header: JwtHeader, payload: JwtPayload): Promise + + /** + * Verify the signature, headers and payload of a JWT + */ + abstract verifyJwt< + P extends VerifyPayload = JwtPayload, + C extends string = string, + >(token: Jwt, options?: VerifyOptions): Promise> +} diff --git a/src/oauth-client-temp/jwk/keyset.ts b/src/oauth-client-temp/jwk/keyset.ts new file mode 100644 index 0000000000..9be83677d3 --- /dev/null +++ b/src/oauth-client-temp/jwk/keyset.ts @@ -0,0 +1,200 @@ +import { Jwk } from './jwk.js' +import { Jwks } from './jwks.js' +import { unsafeDecodeJwt } from './jwt-decode.js' +import { VerifyOptions } from './jwt-verify.js' +import { Jwt, JwtHeader, JwtPayload } from './jwt.js' +import { Key } from './key.js' +import { + Override, + cachedGetter, + isDefined, + matchesAny, + preferredOrderCmp, +} from './util.js' + +export type JwtSignHeader = Override> + +export type JwtPayloadGetter

= ( + header: JwtHeader, + key: Key, +) => P | PromiseLike

+ +export type KeySearch = { + use?: 'sig' | 'enc' + kid?: string | string[] + alg?: string | string[] +} + +const extractPrivateJwk = (key: Key): Jwk | undefined => key.privateJwk +const extractPublicJwk = (key: Key): Jwk | undefined => key.publicJwk + +export class Keyset implements Iterable { + constructor( + private readonly keys: readonly K[], + /** + * The preferred algorithms to use when signing a JWT using this keyset. + */ + readonly preferredSigningAlgorithms: readonly string[] = [ + 'EdDSA', + 'ES256K', + 'ES256', + // https://datatracker.ietf.org/doc/html/rfc7518#section-3.5 + 'PS256', + 'PS384', + 'PS512', + 'HS256', + 'HS384', + 'HS512', + ], + ) { + if (!keys.length) throw new Error('Keyset is empty') + + const kids = new Set() + for (const { kid } of keys) { + if (!kid) continue + + if (kids.has(kid)) throw new Error(`Duplicate key id: ${kid}`) + else kids.add(kid) + } + } + + @cachedGetter + get signAlgorithms(): readonly string[] { + const algorithms = new Set() + for (const key of this) { + if (key.use !== 'sig') continue + for (const alg of key.algorithms) { + algorithms.add(alg) + } + } + return Object.freeze( + [...algorithms].sort(preferredOrderCmp(this.preferredSigningAlgorithms)), + ) + } + + @cachedGetter + get publicJwks(): Jwks { + return { + keys: Array.from(this, extractPublicJwk).filter(isDefined), + } + } + + @cachedGetter + get privateJwks(): Jwks { + return { + keys: Array.from(this, extractPrivateJwk).filter(isDefined), + } + } + + has(kid: string): boolean { + return this.keys.some((key) => key.kid === kid) + } + + get(search: KeySearch): K { + for (const key of this.list(search)) { + return key + } + + throw new TypeError( + `Key not found ${search.kid || search.alg || ''}`, + ) + } + + *list(search: KeySearch): Generator { + // Optimization: Empty string or empty array will not match any key + if (search.kid?.length === 0) return + if (search.alg?.length === 0) return + + for (const key of this) { + if (search.use && key.use !== search.use) continue + + if (Array.isArray(search.kid)) { + if (!key.kid || !search.kid.includes(key.kid)) continue + } else if (search.kid) { + if (key.kid !== search.kid) continue + } + + if (Array.isArray(search.alg)) { + if (!search.alg.some((a) => key.algorithms.includes(a))) continue + } else if (typeof search.alg === 'string') { + if (!key.algorithms.includes(search.alg)) continue + } + + yield key + } + } + + findSigningKey(search: Omit): [key: Key, alg: string] { + const { kid, alg } = search + const matchingKeys: Key[] = [] + + for (const key of this.list({ kid, alg, use: 'sig' })) { + // Not a signing key + if (!key.canSign) continue + + // Skip negotiation if a specific "alg" was provided + if (typeof alg === 'string') return [key, alg] + + matchingKeys.push(key) + } + + const isAllowedAlg = matchesAny(alg) + const candidates = matchingKeys.map( + (key) => [key, key.algorithms.filter(isAllowedAlg)] as const, + ) + + // Return the first candidates that matches the preferred algorithms + for (const prefAlg of this.preferredSigningAlgorithms) { + for (const [matchingKey, matchingAlgs] of candidates) { + if (matchingAlgs.includes(prefAlg)) return [matchingKey, prefAlg] + } + } + + // Return any candidate + for (const [matchingKey, matchingAlgs] of candidates) { + for (const alg of matchingAlgs) { + return [matchingKey, alg] + } + } + + throw new TypeError(`No singing key found for ${kid || alg || ''}`) + } + + [Symbol.iterator](): IterableIterator { + return this.keys.values() + } + + async sign( + { alg: searchAlg, kid: searchKid, ...header }: JwtSignHeader, + payload: JwtPayload | JwtPayloadGetter, + ) { + const [key, alg] = this.findSigningKey({ alg: searchAlg, kid: searchKid }) + const protectedHeader = { ...header, alg, kid: key.kid } + + if (typeof payload === 'function') { + payload = await payload(protectedHeader, key) + } + + return key.createJwt(protectedHeader, payload) + } + + async verify< + P extends Record = JwtPayload, + C extends string = string, + >(token: Jwt, options?: VerifyOptions) { + const { header } = unsafeDecodeJwt(token) + const { kid, alg } = header + + const errors: unknown[] = [] + + for (const key of this.list({ use: 'sig', kid, alg })) { + try { + return await key.verifyJwt(token, options) + } catch (err) { + errors.push(err) + } + } + + throw new AggregateError(errors, 'Unable to verify signature') + } +} diff --git a/src/oauth-client-temp/jwk/util.ts b/src/oauth-client-temp/jwk/util.ts new file mode 100644 index 0000000000..12b5625cce --- /dev/null +++ b/src/oauth-client-temp/jwk/util.ts @@ -0,0 +1,55 @@ +// eslint-disable-next-line @typescript-eslint/ban-types +export type Simplify = { [K in keyof T]: T[K] } & {} +export type Override = Simplify> + +export type RequiredKey = Simplify< + string extends K + ? T + : { + [L in K]: Exclude + } & Omit +> + +export const isDefined = (i: T | undefined): i is T => i !== undefined + +export const preferredOrderCmp = + (order: readonly T[]) => + (a: T, b: T) => { + const aIdx = order.indexOf(a) + const bIdx = order.indexOf(b) + if (aIdx === bIdx) return 0 + if (aIdx === -1) return 1 + if (bIdx === -1) return -1 + return aIdx - bIdx + } + +export function matchesAny( + value: null | undefined | T | readonly T[], +): (v: unknown) => v is T { + return value == null + ? (v): v is T => true + : Array.isArray(value) + ? (v): v is T => value.includes(v) + : (v): v is T => v === value +} + +/** + * Decorator to cache the result of a getter on a class instance. + */ +export const cachedGetter = ( + target: (this: T) => V, + _context: ClassGetterDecoratorContext, +) => { + return function (this: T) { + const value = target.call(this) + Object.defineProperty(this, target.name, { + get: () => value, + enumerable: true, + configurable: true, + }) + return value + } +} + +export const decoder = new TextDecoder() +export const ui8ToString = (value: Uint8Array) => decoder.decode(value) diff --git a/src/screens/Login/hooks/package.json b/src/screens/Login/hooks/package.json new file mode 100644 index 0000000000..3a1fb0a9b1 --- /dev/null +++ b/src/screens/Login/hooks/package.json @@ -0,0 +1,14 @@ +{ + "name": "hooks", + "version": "1.0.0", + "description": "", + "main": "index.js", + "scripts": { + "test": "echo \"Error: no test specified\" && exit 1" + }, + "repository": { + "type": "git", + "url": "https://github.com/bluesky-social/social-app.git" + }, + "private": true +} diff --git a/yarn.lock b/yarn.lock index e7749a47b4..261339a1ae 100644 --- a/yarn.lock +++ b/yarn.lock @@ -22275,7 +22275,12 @@ zod@3.21.4: resolved "https://registry.yarnpkg.com/zod/-/zod-3.21.4.tgz#10882231d992519f0a10b5dd58a38c9dabbb64db" integrity sha512-m46AKbrzKVzOzs/DZgVnG5H55N1sv1M8qZU3A8RIKbs3mrACDNeIOeilDymVb2HdmP8uwshOCF4uJ8uM9rCqJw== -zod@^3.14.2, zod@^3.20.2, zod@^3.21.4: +zod@^3.14.2, zod@^3.21.4: version "3.22.2" resolved "https://registry.yarnpkg.com/zod/-/zod-3.22.2.tgz#3add8c682b7077c05ac6f979fea6998b573e157b" integrity sha512-wvWkphh5WQsJbVk1tbx1l1Ly4yg+XecD+Mq280uBGt9wa5BKSWf4Mhp6GmrkPixhMxmabYY7RbzlwVP32pbGCg== + +zod@^3.22.4: + version "3.22.4" + resolved "https://registry.yarnpkg.com/zod/-/zod-3.22.4.tgz#f31c3a9386f61b1f228af56faa9255e845cf3fff" + integrity sha512-iC+8Io04lddc+mVqQ9AZ7OQ2MrUKGN+oIQyq1vemgt46jwCwLfhq7/pwnBnNXXXZb8VTVLKwp9EDkx+ryxIWmg== From 352d37562d2c8787fc7d4210db5a27f1e45b4dec Mon Sep 17 00:00:00 2001 From: Hailey Date: Mon, 8 Apr 2024 16:48:49 -0700 Subject: [PATCH 15/54] save --- src/oauth-client-temp/client/crypto-implementation.ts | 11 +++++++++++ 1 file changed, 11 insertions(+) diff --git a/src/oauth-client-temp/client/crypto-implementation.ts b/src/oauth-client-temp/client/crypto-implementation.ts index e69de29bb2..bf3b340011 100644 --- a/src/oauth-client-temp/client/crypto-implementation.ts +++ b/src/oauth-client-temp/client/crypto-implementation.ts @@ -0,0 +1,11 @@ +import {Key} from '../jwk' + +export type DigestAlgorithm = { + name: 'sha256' | 'sha384' | 'sha512' +} + +export type {Key} + +export interface CryptoImplementation { + createKey(algs: ) +} From db750e39135031a37b7cea680f22c61c6c4f8e12 Mon Sep 17 00:00:00 2001 From: Hailey Date: Tue, 9 Apr 2024 10:31:44 -0700 Subject: [PATCH 16/54] add `rn-quick-crypto` and `rn-quick-base64` --- package.json | 2 + yarn.lock | 306 ++++++++++++++++++++++++++++++++++++++++++++++++++- 2 files changed, 302 insertions(+), 6 deletions(-) diff --git a/package.json b/package.json index 59d402a5ec..f3be0c0827 100644 --- a/package.json +++ b/package.json @@ -167,6 +167,8 @@ "react-native-pager-view": "6.2.3", "react-native-picker-select": "^8.1.0", "react-native-progress": "bluesky-social/react-native-progress", + "react-native-quick-base64": "^2.1.0", + "react-native-quick-crypto": "^0.6.1", "react-native-reanimated": "^3.6.0", "react-native-root-siblings": "^4.1.1", "react-native-safe-area-context": "4.8.2", diff --git a/yarn.lock b/yarn.lock index 261339a1ae..740c6257f5 100644 --- a/yarn.lock +++ b/yarn.lock @@ -2651,6 +2651,14 @@ resolved "https://registry.yarnpkg.com/@connectrpc/connect/-/connect-1.3.0.tgz#2894629f7f11b46fef883a898dab529f84171bf3" integrity sha512-kTeWxJnLLtxKc2ZSDN0rIBgwfP8RwcLknthX4AKlIAmN9ZC4gGnCbwp+3BKcP/WH5c8zGBAWqSY3zeqCM+ah7w== +"@craftzdog/react-native-buffer@^6.0.5": + version "6.0.5" + resolved "https://registry.yarnpkg.com/@craftzdog/react-native-buffer/-/react-native-buffer-6.0.5.tgz#0d4fbe0dd104186d2806655e3c0d25cebdae91d3" + integrity sha512-Av+YqfwA9e7jhgI9GFE/gTpwl/H+dRRLmZyJPOpKTy107j9Oj7oXlm3/YiMNz+C/CEGqcKAOqnXDLs4OL6AAFw== + dependencies: + ieee754 "^1.2.1" + react-native-quick-base64 "^2.0.5" + "@cspotcode/source-map-support@^0.8.0": version "0.8.1" resolved "https://registry.yarnpkg.com/@cspotcode/source-map-support/-/source-map-support-0.8.1.tgz#00629c35a688e05a88b1cda684fb9d5e73f000a1" @@ -7706,6 +7714,11 @@ resolved "https://registry.yarnpkg.com/@types/node/-/node-20.5.1.tgz#178d58ee7e4834152b0e8b4d30cbfab578b9bb30" integrity sha512-4tT2UrL5LBqDwoed9wZ6N3umC4Yhz3W3FloMmiiG4JwmUJWpie0c7lcnUNd4gtMKuDEO4wRVS8B6Xa0uMRsMKg== +"@types/node@^17.0.31": + version "17.0.45" + resolved "https://registry.yarnpkg.com/@types/node/-/node-17.0.45.tgz#2c0fafd78705e7a18b7906b5201a522719dc5190" + integrity sha512-w+tIMs3rq2afQdsPJlODhoUEKzFP1ayaoyl1CcnwtIlsVe7K7bA1NGm4s3PraqTLlXnbIN84zuBlxBWo1u9BLw== + "@types/node@^18.16.2": version "18.17.6" resolved "https://registry.yarnpkg.com/@types/node/-/node-18.17.6.tgz#0296e9a30b22d2a8fcaa48d3c45afe51474ca55b" @@ -8621,6 +8634,15 @@ asap@~2.0.3, asap@~2.0.6: resolved "https://registry.yarnpkg.com/asap/-/asap-2.0.6.tgz#e50347611d7e690943208bbdafebcbc2fb866d46" integrity sha512-BSHWgDSAiKs50o2Re8ppvp3seVHXSRM44cdSsT9FfNEUUZLOGWVCsiWaRPWM1Znn+mqZ1OfVZ3z3DWEzSp7hRA== +asn1.js@^4.10.1: + version "4.10.1" + resolved "https://registry.yarnpkg.com/asn1.js/-/asn1.js-4.10.1.tgz#b9c2bf5805f1e64aadeed6df3a2bfafb5a73f5a0" + integrity sha512-p32cOF5q0Zqs9uBiONKYLm6BClCoBCM5O9JfeUSlnQLBTxYdTK+pW+nXflm8UkKd2UYlEbYz5qEi0JuZR9ckSw== + dependencies: + bn.js "^4.0.0" + inherits "^2.0.1" + minimalistic-assert "^1.0.0" + asn1.js@^5.0.1: version "5.4.1" resolved "https://registry.yarnpkg.com/asn1.js/-/asn1.js-5.4.1.tgz#11a980b84ebb91781ce35b0fdc2ee294e3783f07" @@ -9131,11 +9153,16 @@ blueimp-md5@^2.10.0: resolved "https://registry.yarnpkg.com/blueimp-md5/-/blueimp-md5-2.19.0.tgz#b53feea5498dcb53dc6ec4b823adb84b729c4af0" integrity sha512-DRQrD6gJyy8FbiE4s+bDoXS9hiW3Vbx5uCdwvcCf3zLHL+Iv7LtGHLpr+GZV8rHG8tK766FGYBwRbu8pELTt+w== -bn.js@^4.0.0, bn.js@^4.11.8, bn.js@^4.11.9: +bn.js@^4.0.0, bn.js@^4.1.0, bn.js@^4.11.8, bn.js@^4.11.9: version "4.12.0" resolved "https://registry.yarnpkg.com/bn.js/-/bn.js-4.12.0.tgz#775b3f278efbb9718eec7361f483fb36fbbfea88" integrity sha512-c98Bf3tPniI+scsdk237ku1Dc3ujXQTSgyiPUDEOe7tRkhrqridvh8klBv0HCEso1OLOYcHuCv/cS6DNxKH+ZA== +bn.js@^5.0.0, bn.js@^5.2.1: + version "5.2.1" + resolved "https://registry.yarnpkg.com/bn.js/-/bn.js-5.2.1.tgz#0bc527a6a0d18d0aa8d5b0538ce4a77dccfa7b70" + integrity sha512-eXRvHzWyYPBuB4NBy0cmYQjGitUrtqwbvlzP3G6VFnNRbsZQIxQ10PbKKHt8gZ/HW/D/747aDl+QkDqg3KQLMQ== + body-parser@1.20.1: version "1.20.1" resolved "https://registry.yarnpkg.com/body-parser/-/body-parser-1.20.1.tgz#b1812a8912c195cd371a3ee5e66faa2338a5c668" @@ -9222,7 +9249,7 @@ braces@^3.0.1, braces@^3.0.2, braces@~3.0.2: dependencies: fill-range "^7.0.1" -brorand@^1.1.0: +brorand@^1.0.1, brorand@^1.1.0: version "1.1.0" resolved "https://registry.yarnpkg.com/brorand/-/brorand-1.1.0.tgz#12c25efe40a45e3c323eb8675a0a0ce57b22371f" integrity sha512-cKV8tMCEpQs4hK/ik71d6LrPOnpkpGBR0wzxqr68g2m/LB2GxVYQroAjMJZRVM1Y4BCjCKc3vAamxSzOY2RP+w== @@ -9232,6 +9259,61 @@ browser-process-hrtime@^1.0.0: resolved "https://registry.yarnpkg.com/browser-process-hrtime/-/browser-process-hrtime-1.0.0.tgz#3c9b4b7d782c8121e56f10106d84c0d0ffc94626" integrity sha512-9o5UecI3GhkpM6DrXr69PblIuWxPKk9Y0jHBRhdocZ2y7YECBFCsHm79Pr3OyR2AvjhDkabFJaDJMYRazHgsow== +browserify-aes@^1.0.4, browserify-aes@^1.2.0: + version "1.2.0" + resolved "https://registry.yarnpkg.com/browserify-aes/-/browserify-aes-1.2.0.tgz#326734642f403dabc3003209853bb70ad428ef48" + integrity sha512-+7CHXqGuspUn/Sl5aO7Ea0xWGAtETPXNSAjHo48JfLdPWcMng33Xe4znFvQweqc/uzk5zSOI3H52CYnjCfb5hA== + dependencies: + buffer-xor "^1.0.3" + cipher-base "^1.0.0" + create-hash "^1.1.0" + evp_bytestokey "^1.0.3" + inherits "^2.0.1" + safe-buffer "^5.0.1" + +browserify-cipher@^1.0.0: + version "1.0.1" + resolved "https://registry.yarnpkg.com/browserify-cipher/-/browserify-cipher-1.0.1.tgz#8d6474c1b870bfdabcd3bcfcc1934a10e94f15f0" + integrity sha512-sPhkz0ARKbf4rRQt2hTpAHqn47X3llLkUGn+xEJzLjwY8LRs2p0v7ljvI5EyoRO/mexrNunNECisZs+gw2zz1w== + dependencies: + browserify-aes "^1.0.4" + browserify-des "^1.0.0" + evp_bytestokey "^1.0.0" + +browserify-des@^1.0.0: + version "1.0.2" + resolved "https://registry.yarnpkg.com/browserify-des/-/browserify-des-1.0.2.tgz#3af4f1f59839403572f1c66204375f7a7f703e9c" + integrity sha512-BioO1xf3hFwz4kc6iBhI3ieDFompMhrMlnDFC4/0/vd5MokpuAc3R+LYbwTA9A5Yc9pq9UYPqffKpW2ObuwX5A== + dependencies: + cipher-base "^1.0.1" + des.js "^1.0.0" + inherits "^2.0.1" + safe-buffer "^5.1.2" + +browserify-rsa@^4.0.0, browserify-rsa@^4.1.0: + version "4.1.0" + resolved "https://registry.yarnpkg.com/browserify-rsa/-/browserify-rsa-4.1.0.tgz#b2fd06b5b75ae297f7ce2dc651f918f5be158c8d" + integrity sha512-AdEER0Hkspgno2aR97SAf6vi0y0k8NuOpGnVH3O99rcA5Q6sh8QxcngtHuJ6uXwnfAXNM4Gn1Gb7/MV1+Ymbog== + dependencies: + bn.js "^5.0.0" + randombytes "^2.0.1" + +browserify-sign@^4.0.0: + version "4.2.3" + resolved "https://registry.yarnpkg.com/browserify-sign/-/browserify-sign-4.2.3.tgz#7afe4c01ec7ee59a89a558a4b75bd85ae62d4208" + integrity sha512-JWCZW6SKhfhjJxO8Tyiiy+XYB7cqd2S5/+WeYHsKdNKFlCBhKbblba1A/HN/90YwtxKc8tCErjffZl++UNmGiw== + dependencies: + bn.js "^5.2.1" + browserify-rsa "^4.1.0" + create-hash "^1.2.0" + create-hmac "^1.1.7" + elliptic "^6.5.5" + hash-base "~3.0" + inherits "^2.0.4" + parse-asn1 "^5.1.7" + readable-stream "^2.3.8" + safe-buffer "^5.2.1" + browserslist@^4.0.0, browserslist@^4.14.5, browserslist@^4.18.1, browserslist@^4.21.10, browserslist@^4.21.4, browserslist@^4.21.9: version "4.21.10" resolved "https://registry.yarnpkg.com/browserslist/-/browserslist-4.21.10.tgz#dbbac576628c13d3b2231332cb2ec5a46e015bb0" @@ -9277,6 +9359,11 @@ buffer-writer@2.0.0: resolved "https://registry.yarnpkg.com/buffer-writer/-/buffer-writer-2.0.0.tgz#ce7eb81a38f7829db09c873f2fbb792c0c98ec04" integrity sha512-a7ZpuTZU1TRtnwyCNW3I5dc0wWNC3VR9S++Ewyk2HHZdrO3CQJqSpd+95Us590V6AL7JqUAH2IwZ/398PmNFgw== +buffer-xor@^1.0.3: + version "1.0.3" + resolved "https://registry.yarnpkg.com/buffer-xor/-/buffer-xor-1.0.3.tgz#26e61ed1422fb70dd42e6e36729ed51d855fe8d9" + integrity sha512-571s0T7nZWK6vB67HI5dyUF7wXiNcfaPPPTl6zYCNApANjIvYJTg7hlud/+cJpdAhS7dVzqMLmfhfHR3rAcOjQ== + buffer@5.6.0: version "5.6.0" resolved "https://registry.yarnpkg.com/buffer/-/buffer-5.6.0.tgz#a31749dc7d81d84db08abf937b6b8c4033f62786" @@ -9636,6 +9723,14 @@ ci-info@^3.2.0, ci-info@^3.3.0: resolved "https://registry.yarnpkg.com/ci-info/-/ci-info-3.8.0.tgz#81408265a5380c929f0bc665d62256628ce9ef91" integrity sha512-eXTggHWSooYhq49F2opQhuHWgzucfF2YgODK4e1566GQs5BIfP30B0oenwBJHfWxAs2fyPB1s7Mg949zLf61Yw== +cipher-base@^1.0.0, cipher-base@^1.0.1, cipher-base@^1.0.3: + version "1.0.4" + resolved "https://registry.yarnpkg.com/cipher-base/-/cipher-base-1.0.4.tgz#8760e4ecc272f4c363532f926d874aae2c1397de" + integrity sha512-Kkht5ye6ZGmwv40uUDZztayT2ThLQGfnj/T71N/XzeZeo3nf8foyW7zGTsPYkEya3m5f3cAypH+qe7YOrM1U2Q== + dependencies: + inherits "^2.0.1" + safe-buffer "^5.0.1" + cjs-module-lexer@^1.0.0: version "1.2.3" resolved "https://registry.yarnpkg.com/cjs-module-lexer/-/cjs-module-lexer-1.2.3.tgz#6c370ab19f8a3394e318fe682686ec0ac684d107" @@ -10076,6 +10171,37 @@ cosmiconfig@^8.0.0: parse-json "^5.2.0" path-type "^4.0.0" +create-ecdh@^4.0.0: + version "4.0.4" + resolved "https://registry.yarnpkg.com/create-ecdh/-/create-ecdh-4.0.4.tgz#d6e7f4bffa66736085a0762fd3a632684dabcc4e" + integrity sha512-mf+TCx8wWc9VpuxfP2ht0iSISLZnt0JgWlrOKZiNqyUZWnjIaCIVNQArMHnCZKfEYRg6IM7A+NeJoN8gf/Ws0A== + dependencies: + bn.js "^4.1.0" + elliptic "^6.5.3" + +create-hash@^1.1.0, create-hash@^1.1.2, create-hash@^1.2.0: + version "1.2.0" + resolved "https://registry.yarnpkg.com/create-hash/-/create-hash-1.2.0.tgz#889078af11a63756bcfb59bd221996be3a9ef196" + integrity sha512-z00bCGNHDG8mHAkP7CtT1qVu+bFQUPjYq/4Iv3C3kWjTFV10zIjfSoeqXo9Asws8gwSHDGj/hl2u4OGIjapeCg== + dependencies: + cipher-base "^1.0.1" + inherits "^2.0.1" + md5.js "^1.3.4" + ripemd160 "^2.0.1" + sha.js "^2.4.0" + +create-hmac@^1.1.0, create-hmac@^1.1.4, create-hmac@^1.1.7: + version "1.1.7" + resolved "https://registry.yarnpkg.com/create-hmac/-/create-hmac-1.1.7.tgz#69170c78b3ab957147b2b8b04572e47ead2243ff" + integrity sha512-MJG9liiZ+ogc4TzUwuvbER1JRdgvUFSB5+VR/g5h82fGaIRWMWddtKBHi7/sVhfjQZ6SehlyhvQYrcYkaUIpLg== + dependencies: + cipher-base "^1.0.3" + create-hash "^1.1.0" + inherits "^2.0.1" + ripemd160 "^2.0.0" + safe-buffer "^5.0.1" + sha.js "^2.4.8" + create-jest@^29.7.0: version "29.7.0" resolved "https://registry.yarnpkg.com/create-jest/-/create-jest-29.7.0.tgz#a355c5b3cb1e1af02ba177fe7afd7feee49a5320" @@ -10139,6 +10265,23 @@ crypt@0.0.2, crypt@~0.0.1: resolved "https://registry.yarnpkg.com/crypt/-/crypt-0.0.2.tgz#88d7ff7ec0dfb86f713dc87bbb42d044d3e6c41b" integrity sha512-mCxBlsHFYh9C+HVpiEacem8FEBnMXgU9gy4zmNC+SXAZNB/1idgp/aulFJ4FgCi7GPEVbfyng092GqL2k2rmow== +crypto-browserify@^3.12.0: + version "3.12.0" + resolved "https://registry.yarnpkg.com/crypto-browserify/-/crypto-browserify-3.12.0.tgz#396cf9f3137f03e4b8e532c58f698254e00f80ec" + integrity sha512-fz4spIh+znjO2VjL+IdhEpRJ3YN6sMzITSBijk6FK2UvTqruSQW+/cCZTSNsMiZNvUeq0CqurF+dAbyiGOY6Wg== + dependencies: + browserify-cipher "^1.0.0" + browserify-sign "^4.0.0" + create-ecdh "^4.0.0" + create-hash "^1.1.0" + create-hmac "^1.1.0" + diffie-hellman "^5.0.0" + inherits "^2.0.1" + pbkdf2 "^3.0.3" + public-encrypt "^4.0.0" + randombytes "^2.0.0" + randomfill "^1.0.3" + crypto-random-string@^1.0.0: version "1.0.0" resolved "https://registry.yarnpkg.com/crypto-random-string/-/crypto-random-string-1.0.0.tgz#a230f64f568310e1498009940790ec99545bca7e" @@ -10605,6 +10748,14 @@ dequal@^2.0.3: resolved "https://registry.yarnpkg.com/dequal/-/dequal-2.0.3.tgz#2644214f1997d39ed0ee0ece72335490a7ac67be" integrity sha512-0je+qPKHEMohvfRTCEo3CrPG6cAzAYgmzKyxRiYSSDkS6eGJdyVJm7WaYA5ECaAD9wLB2T4EEeymA5aFVcYXCA== +des.js@^1.0.0: + version "1.1.0" + resolved "https://registry.yarnpkg.com/des.js/-/des.js-1.1.0.tgz#1d37f5766f3bbff4ee9638e871a8768c173b81da" + integrity sha512-r17GxjhUCjSRy8aiJpr8/UadFIzMzJGexI3Nmz4ADi9LYSFx4gTBp80+NaX/YsXWWLhpZ7v/v/ubEc/bCNfKwg== + dependencies: + inherits "^2.0.1" + minimalistic-assert "^1.0.0" + destroy@1.2.0: version "1.2.0" resolved "https://registry.yarnpkg.com/destroy/-/destroy-1.2.0.tgz#4803735509ad8be552934c67df614f94e66fa015" @@ -10704,6 +10855,15 @@ diff@^4.0.1: resolved "https://registry.yarnpkg.com/diff/-/diff-4.0.2.tgz#60f3aecb89d5fae520c11aa19efc2bb982aade7d" integrity sha512-58lmxKSA4BNyLz+HHMUzlOEpg09FV+ev6ZMe3vJihgdxzgcwZ8VoEEPmALCZG9LmqfVoNMMKpttIYTVG6uDY7A== +diffie-hellman@^5.0.0: + version "5.0.3" + resolved "https://registry.yarnpkg.com/diffie-hellman/-/diffie-hellman-5.0.3.tgz#40e8ee98f55a2149607146921c63e1ae5f3d2875" + integrity sha512-kqag/Nl+f3GwyK25fhUMYj81BUOrZ9IuJsjIcDE5icNM9FJHAVm3VcUDxdLPoQtTuUylWm6ZIknYJwwaPxsUzg== + dependencies: + bn.js "^4.1.0" + miller-rabin "^4.0.0" + randombytes "^2.0.0" + dir-glob@^3.0.1: version "3.0.1" resolved "https://registry.yarnpkg.com/dir-glob/-/dir-glob-3.0.1.tgz#56dbf73d992a4a93ba1584f4534063fd2e41717f" @@ -10941,6 +11101,19 @@ elliptic@^6.4.1: minimalistic-assert "^1.0.1" minimalistic-crypto-utils "^1.0.1" +elliptic@^6.5.3, elliptic@^6.5.5: + version "6.5.5" + resolved "https://registry.yarnpkg.com/elliptic/-/elliptic-6.5.5.tgz#c715e09f78b6923977610d4c2346d6ce22e6dded" + integrity sha512-7EjbcmUm17NQFu4Pmgmq2olYMj8nwMnpcddByChSUjArp8F5DQWcIcpriwO4ZToLNAJig0yiyjswfyGNje/ixw== + dependencies: + bn.js "^4.11.9" + brorand "^1.1.0" + hash.js "^1.0.0" + hmac-drbg "^1.0.1" + inherits "^2.0.4" + minimalistic-assert "^1.0.1" + minimalistic-crypto-utils "^1.0.1" + email-validator@^2.0.4: version "2.0.4" resolved "https://registry.yarnpkg.com/email-validator/-/email-validator-2.0.4.tgz#b8dfaa5d0dae28f1b03c95881d904d4e40bfe7ed" @@ -11640,6 +11813,14 @@ events@3.3.0, events@^3.2.0, events@^3.3.0: resolved "https://registry.yarnpkg.com/events/-/events-3.3.0.tgz#31a95ad0a924e2d2c419a813aeb2c4e878ea7400" integrity sha512-mQw+2fkQbALzQ7V0MY0IqdnXNOeTtP4r0lN9z7AAawCXgqea7bDii20AYrIBrFd/Hx0M2Ocz6S111CaFkUcb0Q== +evp_bytestokey@^1.0.0, evp_bytestokey@^1.0.3: + version "1.0.3" + resolved "https://registry.yarnpkg.com/evp_bytestokey/-/evp_bytestokey-1.0.3.tgz#7fcbdb198dc71959432efe13842684e0525acb02" + integrity sha512-/f2Go4TognH/KvCISP7OUsHn85hT9nUkxxA9BEWxFn+Oj9o8ZNLm/40hdlgSLyuOimsrTKLUMEorQexp/aPQeA== + dependencies: + md5.js "^1.3.4" + safe-buffer "^5.1.1" + exec-async@^2.2.0: version "2.2.0" resolved "https://registry.yarnpkg.com/exec-async/-/exec-async-2.2.0.tgz#c7c5ad2eef3478d38390c6dd3acfe8af0efc8301" @@ -12995,6 +13176,23 @@ has@^1.0.3: dependencies: function-bind "^1.1.1" +hash-base@^3.0.0: + version "3.1.0" + resolved "https://registry.yarnpkg.com/hash-base/-/hash-base-3.1.0.tgz#55c381d9e06e1d2997a883b4a3fddfe7f0d3af33" + integrity sha512-1nmYp/rhMDiE7AYkDw+lLwlAzz0AntGIe51F3RfFfEqyQ3feY2eI/NcwC6umIQVOASPMsWJLJScWKSSvzL9IVA== + dependencies: + inherits "^2.0.4" + readable-stream "^3.6.0" + safe-buffer "^5.2.0" + +hash-base@~3.0: + version "3.0.4" + resolved "https://registry.yarnpkg.com/hash-base/-/hash-base-3.0.4.tgz#5fc8686847ecd73499403319a6b0a3f3f6ae4918" + integrity sha512-EeeoJKjTyt868liAlVmcv2ZsUfGHlE3Q+BICOXcZiwN3osr5Q/zFGYmTJpoIzuaSTAwndFy+GqhEwlU4L3j4Ow== + dependencies: + inherits "^2.0.1" + safe-buffer "^5.0.1" + hash.js@^1.0.0, hash.js@^1.0.3: version "1.1.7" resolved "https://registry.yarnpkg.com/hash.js/-/hash.js-1.1.7.tgz#0babca538e8d4ee4a0f8988d68866537a003cf42" @@ -15782,6 +15980,15 @@ md5-file@^3.2.3: dependencies: buffer-alloc "^1.1.0" +md5.js@^1.3.4: + version "1.3.5" + resolved "https://registry.yarnpkg.com/md5.js/-/md5.js-1.3.5.tgz#b5d07b8e3216e3e27cd728d72f70d1e6a342005f" + integrity sha512-xitP+WxNPcTTOgnTJcrhM0xvdPepipPSf3I8EIpGKeFLjt3PlJLIDG3u8EX53ZIubkb+5U2+3rELYpEhHhzdkg== + dependencies: + hash-base "^3.0.0" + inherits "^2.0.1" + safe-buffer "^5.1.2" + md5@^2.2.1: version "2.3.0" resolved "https://registry.yarnpkg.com/md5/-/md5-2.3.0.tgz#c3da9a6aae3a30b46b7b0c349b87b110dc3bda4f" @@ -16118,6 +16325,14 @@ micromatch@4.0.5, micromatch@^4.0.2, micromatch@^4.0.4, micromatch@^4.0.5: braces "^3.0.2" picomatch "^2.3.1" +miller-rabin@^4.0.0: + version "4.0.1" + resolved "https://registry.yarnpkg.com/miller-rabin/-/miller-rabin-4.0.1.tgz#f080351c865b0dc562a8462966daa53543c78a4d" + integrity sha512-115fLhvZVqWwHPbClyntxEVfVDfl9DLLTuJvq3g2O/Oxi8AiNouAHvDSzHS0viUJc+V5vm3eq91Xwqn9dp4jRA== + dependencies: + bn.js "^4.0.0" + brorand "^1.0.1" + mime-db@1.52.0, "mime-db@>= 1.43.0 < 2": version "1.52.0" resolved "https://registry.yarnpkg.com/mime-db/-/mime-db-1.52.0.tgz#bbabcdc02859f4987301c856e3387ce5ec43bf70" @@ -17004,6 +17219,18 @@ parent-module@^1.0.0: dependencies: callsites "^3.0.0" +parse-asn1@^5.0.0, parse-asn1@^5.1.7: + version "5.1.7" + resolved "https://registry.yarnpkg.com/parse-asn1/-/parse-asn1-5.1.7.tgz#73cdaaa822125f9647165625eb45f8a051d2df06" + integrity sha512-CTM5kuWR3sx9IFamcl5ErfPl6ea/N8IYwiJ+vpeB2g+1iknv7zBl5uPwbMbRVznRVbrNY6lGuDoE5b30grmbqg== + dependencies: + asn1.js "^4.10.1" + browserify-aes "^1.2.0" + evp_bytestokey "^1.0.3" + hash-base "~3.0" + pbkdf2 "^3.1.2" + safe-buffer "^5.2.1" + parse-json@^4.0.0: version "4.0.0" resolved "https://registry.yarnpkg.com/parse-json/-/parse-json-4.0.0.tgz#be35f5425be1f7f6c747184f98a788cb99477ee0" @@ -17142,6 +17369,17 @@ pathe@^1.1.0: resolved "https://registry.yarnpkg.com/pathe/-/pathe-1.1.1.tgz#1dd31d382b974ba69809adc9a7a347e65d84829a" integrity sha512-d+RQGp0MAYTIaDBIMmOfMwz3E+LOZnxx1HZd5R18mmCZY0QBlK0LDZfPc8FW8Ed2DlvsuE6PRjroDY+wg4+j/Q== +pbkdf2@^3.0.3, pbkdf2@^3.1.2: + version "3.1.2" + resolved "https://registry.yarnpkg.com/pbkdf2/-/pbkdf2-3.1.2.tgz#dd822aa0887580e52f1a039dc3eda108efae3075" + integrity sha512-iuh7L6jA7JEGu2WxDwtQP1ddOpaJNC4KlDEFfdQajSGgGPNi4OyDc2R7QnbY2bR9QjBVGwgvTdNJZoE7RaxUMA== + dependencies: + create-hash "^1.1.2" + create-hmac "^1.1.4" + ripemd160 "^2.0.1" + safe-buffer "^5.0.1" + sha.js "^2.4.8" + peek-readable@^4.1.0: version "4.1.0" resolved "https://registry.yarnpkg.com/peek-readable/-/peek-readable-4.1.0.tgz#4ece1111bf5c2ad8867c314c81356847e8a62e72" @@ -18325,6 +18563,18 @@ psl@^1.1.33, psl@^1.9.0: resolved "https://registry.yarnpkg.com/psl/-/psl-1.9.0.tgz#d0df2a137f00794565fcaf3b2c00cd09f8d5a5a7" integrity sha512-E/ZsdU4HLs/68gYzgGTkMicWTLPdAftJLfJFlLUAAKZGkStNU72sZjT66SnMDVOfOWY/YAoiD7Jxa9iHvngcag== +public-encrypt@^4.0.0: + version "4.0.3" + resolved "https://registry.yarnpkg.com/public-encrypt/-/public-encrypt-4.0.3.tgz#4fcc9d77a07e48ba7527e7cbe0de33d0701331e0" + integrity sha512-zVpa8oKZSz5bTMTFClc1fQOnyyEzpl5ozpi1B5YcvBrdohMjH2rfsBtyXcuNuwjsDIXmBYlF2N5FlJYhR29t8Q== + dependencies: + bn.js "^4.1.0" + browserify-rsa "^4.0.0" + create-hash "^1.1.0" + parse-asn1 "^5.0.0" + randombytes "^2.0.1" + safe-buffer "^5.1.2" + pump@^3.0.0: version "3.0.0" resolved "https://registry.yarnpkg.com/pump/-/pump-3.0.0.tgz#b4a2116815bde2f4e1ea602354e8c75565107a64" @@ -18430,13 +18680,21 @@ ramda@^0.27.1: resolved "https://registry.yarnpkg.com/ramda/-/ramda-0.27.2.tgz#84463226f7f36dc33592f6f4ed6374c48306c3f1" integrity sha512-SbiLPU40JuJniHexQSAgad32hfwd+DRUdwF2PlVuI5RZD0/vahUco7R8vD86J/tcEKKF9vZrUVwgtmGCqlCKyA== -randombytes@^2.1.0: +randombytes@^2.0.0, randombytes@^2.0.1, randombytes@^2.0.5, randombytes@^2.1.0: version "2.1.0" resolved "https://registry.yarnpkg.com/randombytes/-/randombytes-2.1.0.tgz#df6f84372f0270dc65cdf6291349ab7a473d4f2a" integrity sha512-vYl3iOX+4CKUWuxGi9Ukhie6fsqXqS9FE2Zaic4tNFD2N2QQaXOMFbuKK4QmDHC0JO6B1Zp41J0LpT0oR68amQ== dependencies: safe-buffer "^5.1.0" +randomfill@^1.0.3: + version "1.0.4" + resolved "https://registry.yarnpkg.com/randomfill/-/randomfill-1.0.4.tgz#c92196fc86ab42be983f1bf31778224931d61458" + integrity sha512-87lcbR8+MhcWcUiQ+9e+Rwx8MyR2P7qnt15ynUlbm3TU/fjbgz4GsvfSUDTemtCCtVCqb4ZcEFlyPNTh9bBTLw== + dependencies: + randombytes "^2.0.5" + safe-buffer "^5.1.0" + range-parser@^1.2.1, range-parser@~1.2.1: version "1.2.1" resolved "https://registry.yarnpkg.com/range-parser/-/range-parser-1.2.1.tgz#3cf37023d199e1c24d1a55b84800c2f3e6468031" @@ -18649,6 +18907,26 @@ react-native-progress@bluesky-social/react-native-progress: dependencies: prop-types "^15.7.2" +react-native-quick-base64@^2.0.5, react-native-quick-base64@^2.1.0: + version "2.1.0" + resolved "https://registry.yarnpkg.com/react-native-quick-base64/-/react-native-quick-base64-2.1.0.tgz#6759a9f9b94b2b7c951917ce7d1970afdf62865d" + integrity sha512-5T5qhEuHcqeP/GGAOaeNsz0c5jZsMemy2svlgRc7PfUQSH6ABakwTT0tYNZ1XImJZWc8najYgVG8mrJgml5DNw== + dependencies: + base64-js "^1.5.1" + +react-native-quick-crypto@^0.6.1: + version "0.6.1" + resolved "https://registry.yarnpkg.com/react-native-quick-crypto/-/react-native-quick-crypto-0.6.1.tgz#7b89c67c4a5d3669c4491fe7884621c1c74d01bc" + integrity sha512-s6uFo7tcI3syo8/y5j+t6Rf+KVSuRKDp6tH04A0vjaHptJC6Iu7DVgkNYO7aqtfrYn8ZUgQ/Kqaq+m4i9TxgIQ== + dependencies: + "@craftzdog/react-native-buffer" "^6.0.5" + "@types/node" "^17.0.31" + crypto-browserify "^3.12.0" + events "^3.3.0" + react-native-quick-base64 "^2.0.5" + stream-browserify "^3.0.0" + string_decoder "^1.3.0" + react-native-reanimated@^3.6.0: version "3.6.0" resolved "https://registry.yarnpkg.com/react-native-reanimated/-/react-native-reanimated-3.6.0.tgz#d2ca5f4c234f592af3d63bc749806e36d6e0a755" @@ -18913,7 +19191,7 @@ read-cache@^1.0.0: dependencies: pify "^2.3.0" -readable-stream@^2.0.1, readable-stream@^2.0.2, readable-stream@~2.3.6: +readable-stream@^2.0.1, readable-stream@^2.0.2, readable-stream@^2.3.8, readable-stream@~2.3.6: version "2.3.8" resolved "https://registry.yarnpkg.com/readable-stream/-/readable-stream-2.3.8.tgz#91125e8042bba1b9887f49345f6277027ce8be9b" integrity sha512-8p0AUk4XODgIewSi0l8Epjs+EVnWiK7NoDIEGU0HhE7+ZyY8D1IMY7odu5lRrFXGg71L15KG8QrPmum45RTtdA== @@ -19319,6 +19597,14 @@ rimraf@~2.6.2: dependencies: glob "^7.1.3" +ripemd160@^2.0.0, ripemd160@^2.0.1: + version "2.0.2" + resolved "https://registry.yarnpkg.com/ripemd160/-/ripemd160-2.0.2.tgz#a1c1a6f624751577ba5d07914cbc92850585890c" + integrity sha512-ii4iagi25WusVoiC4B4lq7pbXfAp3D9v5CwfkY33vffw2+pkDjY1D8GaN7spsxvCSx8dkPqOZCEZyfxcmJG2IA== + dependencies: + hash-base "^3.0.0" + inherits "^2.0.1" + rn-fetch-blob@^0.12.0: version "0.12.0" resolved "https://registry.yarnpkg.com/rn-fetch-blob/-/rn-fetch-blob-0.12.0.tgz#ec610d2f9b3f1065556b58ab9c106eeb256f3cba" @@ -19417,7 +19703,7 @@ safe-buffer@5.1.2, safe-buffer@~5.1.0, safe-buffer@~5.1.1: resolved "https://registry.yarnpkg.com/safe-buffer/-/safe-buffer-5.1.2.tgz#991ec69d296e0313747d59bdfd2b745c35f8828d" integrity sha512-Gd2UZBJDkXlY7GbJxfsE8/nvKkUEU1G38c1siN6QP6a9PT9MmHB8GnpscSmMJSoF8LOIrt8ud/wPtojys4G6+g== -safe-buffer@5.2.1, safe-buffer@>=5.1.0, safe-buffer@^5.0.1, safe-buffer@^5.1.0, safe-buffer@~5.2.0: +safe-buffer@5.2.1, safe-buffer@>=5.1.0, safe-buffer@^5.0.1, safe-buffer@^5.1.0, safe-buffer@^5.1.1, safe-buffer@^5.1.2, safe-buffer@^5.2.0, safe-buffer@^5.2.1, safe-buffer@~5.2.0: version "5.2.1" resolved "https://registry.yarnpkg.com/safe-buffer/-/safe-buffer-5.2.1.tgz#1eaf9fa9bdb1fdd4ec75f58f9cdb4e6b7827eec6" integrity sha512-rp3So07KcdmmKbGvgaNxQSJr7bGVSVk5S9Eq1F+ppbRo70+YeaDxkw5Dd8NPN+GD6bjnYm2VuPuCXmpuYvmCXQ== @@ -19704,6 +19990,14 @@ sf-symbols-typescript@^1.0.0: resolved "https://registry.yarnpkg.com/sf-symbols-typescript/-/sf-symbols-typescript-1.0.0.tgz#94e9210bf27e7583f9749a0d07bd4f4937ea488f" integrity sha512-DkS7q3nN68dEMb4E18HFPDAvyrjDZK9YAQQF2QxeFu9gp2xRDXFMF8qLJ1EmQ/qeEGQmop4lmMM1WtYJTIcCMw== +sha.js@^2.4.0, sha.js@^2.4.8: + version "2.4.11" + resolved "https://registry.yarnpkg.com/sha.js/-/sha.js-2.4.11.tgz#37a5cf0b81ecbc6943de109ba2960d1b26584ae7" + integrity sha512-QMEp5B7cftE7APOjk5Y6xgrbWu+WkLVQwk8JNjZ8nKRciZaByEW6MubieAiToS7+dwvrjGhH8jRXz3MVd0AYqQ== + dependencies: + inherits "^2.0.1" + safe-buffer "^5.0.1" + shallow-clone@^3.0.0: version "3.0.1" resolved "https://registry.yarnpkg.com/shallow-clone/-/shallow-clone-3.0.1.tgz#8f2981ad92531f55035b01fb230769a40e02efa3" @@ -20112,7 +20406,7 @@ statuses@2.0.1: resolved "https://registry.yarnpkg.com/statuses/-/statuses-1.5.0.tgz#161c7dac177659fd9811f43771fa99381478628c" integrity sha512-OpZ3zP+jT1PI7I8nemJX4AKmAX070ZkYPVWV/AaKTJl+tXCTGyVdC1a4SL8RUQYEwk/f34ZX8UTykN68FwrqAA== -stream-browserify@3.0.0: +stream-browserify@3.0.0, stream-browserify@^3.0.0: version "3.0.0" resolved "https://registry.yarnpkg.com/stream-browserify/-/stream-browserify-3.0.0.tgz#22b0a2850cdf6503e73085da1fc7b7d0c2122f2f" integrity sha512-H73RAHsVBapbim0tU2JwwOiXUj+fikfiaoYAKHF3VJfA0pe2BCzkhAHBlLG6REzE+2WNZcxOXjK7lkso+9euLA== From e165d496c9a85efdcf0c74bc333ee6cdcbf7f043 Mon Sep 17 00:00:00 2001 From: Hailey Date: Tue, 9 Apr 2024 10:38:40 -0700 Subject: [PATCH 17/54] metro config "polyfill" --- metro.config.js | 11 +++++++++++ 1 file changed, 11 insertions(+) diff --git a/metro.config.js b/metro.config.js index a49d95f9aa..c3cd6e9649 100644 --- a/metro.config.js +++ b/metro.config.js @@ -6,6 +6,17 @@ cfg.resolver.sourceExts = process.env.RN_SRC_EXT ? process.env.RN_SRC_EXT.split(',').concat(cfg.resolver.sourceExts) : cfg.resolver.sourceExts +cfg.resolver.resolveRequest = (context, moduleName, platform) => { + if (moduleName === 'crypto' && platform !== 'web') { + return context.resolveRequest( + context, + 'react-native-quick-crypto', + platform, + ) + } + return context.resolveRequest(context, moduleName, platform) +} + cfg.transformer.getTransformOptions = async () => ({ transform: { experimentalImportSupport: true, From ec58082dbbe989cda7e13edb22c3b7c01dc516be Mon Sep 17 00:00:00 2001 From: Hailey Date: Tue, 9 Apr 2024 11:17:30 -0700 Subject: [PATCH 18/54] add jwk lib --- package.json | 1 + yarn.lock | 8 ++++++++ 2 files changed, 9 insertions(+) diff --git a/package.json b/package.json index f3be0c0827..0269c754cc 100644 --- a/package.json +++ b/package.json @@ -63,6 +63,7 @@ "@lingui/react": "^4.5.0", "@mattermost/react-native-paste-input": "^0.6.4", "@miblanchard/react-native-slider": "^2.3.1", + "@pagopa/io-react-native-jwt": "^1.1.0", "@radix-ui/react-dropdown-menu": "^2.0.6", "@react-native-async-storage/async-storage": "1.23.1", "@react-native-masked-view/masked-view": "0.3.0", diff --git a/yarn.lock b/yarn.lock index 740c6257f5..a3e634f6aa 100644 --- a/yarn.lock +++ b/yarn.lock @@ -4478,6 +4478,14 @@ mkdirp "^1.0.4" rimraf "^3.0.2" +"@pagopa/io-react-native-jwt@^1.1.0": + version "1.1.0" + resolved "https://registry.yarnpkg.com/@pagopa/io-react-native-jwt/-/io-react-native-jwt-1.1.0.tgz#9a8e99672a683a32a27785eb01a7f108561ca8dd" + integrity sha512-R/Cgiu3Qb/7LnzQstUTGkNnsKfQ5lc/O3eSAkzZPGQqQb4hoSch4N/2JgqXRZPeTNfFA2vDmYbV6fSidMDL2xA== + dependencies: + abab "^2.0.6" + zod "^3.21.4" + "@pmmmwh/react-refresh-webpack-plugin@^0.5.11", "@pmmmwh/react-refresh-webpack-plugin@^0.5.3": version "0.5.11" resolved "https://registry.yarnpkg.com/@pmmmwh/react-refresh-webpack-plugin/-/react-refresh-webpack-plugin-0.5.11.tgz#7c2268cedaa0644d677e8c4f377bc8fb304f714a" From 1461c0a34f671d5814bb93625a7646c3c9c0ee9c Mon Sep 17 00:00:00 2001 From: Hailey Date: Tue, 9 Apr 2024 13:04:33 -0700 Subject: [PATCH 19/54] decent base --- package.json | 2 +- .../client/crypto-implementation.ts | 44 ++- .../client/crypto-wrapper.ts | 173 +++++++++++ src/oauth-client-temp/client/db.ts | 0 src/oauth-client-temp/client/db.web.ts | 0 src/oauth-client-temp/client/key.ts | 78 +++++ src/oauth-client-temp/client/keyset.ts | 0 .../client/oauth-callback-error.ts | 16 + .../client/oauth-client-factory.ts | 290 ++++++++++++++++++ src/oauth-client-temp/client/oauth-client.ts | 89 ++++++ .../client/oauth-database.ts | 6 + .../client/oauth-resolver.ts | 28 ++ .../client/oauth-server-factory.ts | 81 +++++ src/oauth-client-temp/client/oauth-server.ts | 287 +++++++++++++++++ src/oauth-client-temp/client/oauth-types.ts | 37 +++ .../client/session-getter.ts | 139 +++++++++ src/oauth-client-temp/client/util.ts | 160 ++++++++++ .../client/validate-client-metadata.ts | 81 +++++ .../disposable-polyfill/index.ts | 10 + src/oauth-client-temp/fetch-dpop/index.ts | 174 +++++++++++ .../identity-resolver/identity-resolver.ts | 35 +++ .../identity-resolver/index.ts | 2 + .../universal-identity-resolver.ts | 52 ++++ .../indexed-db/db-index.web.ts | 44 +++ .../indexed-db/db-object-store.web.ts | 47 +++ .../indexed-db/db-transaction.web.ts | 52 ++++ src/oauth-client-temp/indexed-db/db.ts | 9 + src/oauth-client-temp/indexed-db/db.web.ts | 113 +++++++ src/oauth-client-temp/indexed-db/index.web.ts | 6 + src/oauth-client-temp/indexed-db/schema.ts | 2 + src/oauth-client-temp/indexed-db/util.web.ts | 20 ++ src/oauth-client-temp/jwk-jose/index.ts | 2 + src/oauth-client-temp/jwk-jose/jose-key.ts | 115 +++++++ src/oauth-client-temp/jwk-jose/jose-keyset.ts | 16 + src/oauth-client-temp/jwk-jose/util.ts | 9 + src/oauth-client-temp/jwk/alg.ts | 4 +- src/oauth-client-temp/jwk/index.ts | 18 +- src/oauth-client-temp/jwk/jwks.ts | 4 +- src/oauth-client-temp/jwk/jwt-decode.ts | 16 +- src/oauth-client-temp/jwk/jwt-verify.ts | 4 +- src/oauth-client-temp/jwk/jwt.ts | 4 +- src/oauth-client-temp/jwk/key.ts | 22 +- src/oauth-client-temp/jwk/keyset.ts | 40 +-- src/screens/Login/hooks/useLogin.ts | 6 +- yarn.lock | 13 +- 45 files changed, 2276 insertions(+), 74 deletions(-) create mode 100644 src/oauth-client-temp/client/crypto-wrapper.ts create mode 100644 src/oauth-client-temp/client/db.ts create mode 100644 src/oauth-client-temp/client/db.web.ts create mode 100644 src/oauth-client-temp/client/key.ts create mode 100644 src/oauth-client-temp/client/keyset.ts create mode 100644 src/oauth-client-temp/client/oauth-callback-error.ts create mode 100644 src/oauth-client-temp/client/oauth-client-factory.ts create mode 100644 src/oauth-client-temp/client/oauth-client.ts create mode 100644 src/oauth-client-temp/client/oauth-database.ts create mode 100644 src/oauth-client-temp/client/oauth-resolver.ts create mode 100644 src/oauth-client-temp/client/oauth-server-factory.ts create mode 100644 src/oauth-client-temp/client/oauth-server.ts create mode 100644 src/oauth-client-temp/client/oauth-types.ts create mode 100644 src/oauth-client-temp/client/session-getter.ts create mode 100644 src/oauth-client-temp/client/util.ts create mode 100644 src/oauth-client-temp/client/validate-client-metadata.ts create mode 100644 src/oauth-client-temp/disposable-polyfill/index.ts create mode 100644 src/oauth-client-temp/fetch-dpop/index.ts create mode 100644 src/oauth-client-temp/identity-resolver/identity-resolver.ts create mode 100644 src/oauth-client-temp/identity-resolver/index.ts create mode 100644 src/oauth-client-temp/identity-resolver/universal-identity-resolver.ts create mode 100644 src/oauth-client-temp/indexed-db/db-index.web.ts create mode 100644 src/oauth-client-temp/indexed-db/db-object-store.web.ts create mode 100644 src/oauth-client-temp/indexed-db/db-transaction.web.ts create mode 100644 src/oauth-client-temp/indexed-db/db.ts create mode 100644 src/oauth-client-temp/indexed-db/db.web.ts create mode 100644 src/oauth-client-temp/indexed-db/index.web.ts create mode 100644 src/oauth-client-temp/indexed-db/schema.ts create mode 100644 src/oauth-client-temp/indexed-db/util.web.ts create mode 100644 src/oauth-client-temp/jwk-jose/index.ts create mode 100644 src/oauth-client-temp/jwk-jose/jose-key.ts create mode 100644 src/oauth-client-temp/jwk-jose/jose-keyset.ts create mode 100644 src/oauth-client-temp/jwk-jose/util.ts diff --git a/package.json b/package.json index 0269c754cc..235cadaa48 100644 --- a/package.json +++ b/package.json @@ -63,7 +63,6 @@ "@lingui/react": "^4.5.0", "@mattermost/react-native-paste-input": "^0.6.4", "@miblanchard/react-native-slider": "^2.3.1", - "@pagopa/io-react-native-jwt": "^1.1.0", "@radix-ui/react-dropdown-menu": "^2.0.6", "@react-native-async-storage/async-storage": "1.23.1", "@react-native-masked-view/masked-view": "0.3.0", @@ -133,6 +132,7 @@ "expo-web-browser": "~12.8.2", "fast-text-encoding": "^1.0.6", "history": "^5.3.0", + "jose": "^5.2.4", "js-sha256": "^0.9.0", "jwt-decode": "^4.0.0", "lande": "^1.0.10", diff --git a/src/oauth-client-temp/client/crypto-implementation.ts b/src/oauth-client-temp/client/crypto-implementation.ts index bf3b340011..237485068d 100644 --- a/src/oauth-client-temp/client/crypto-implementation.ts +++ b/src/oauth-client-temp/client/crypto-implementation.ts @@ -1,11 +1,47 @@ -import {Key} from '../jwk' +import {CryptoKey} from '#/oauth-client-temp/client/key' +import {Key} from '#/oauth-client-temp/jwk' + +// TODO this might not be necessary with this setup, we will see export type DigestAlgorithm = { name: 'sha256' | 'sha384' | 'sha512' } -export type {Key} +export class CryptoImplementation { + public async createKey(algs: string[]): Promise { + return CryptoKey.generate(undefined, algs) + } + + getRandomValues(byteLength: number): Uint8Array { + const bytes = new Uint8Array(byteLength) + // TODO REVIEW POLYFILL + // @ts-ignore Polyfilled + return crypto.getRandomValues(bytes) + } + + async digest( + bytes: Uint8Array, + algorithm: DigestAlgorithm, + ): Promise { + // TODO REVIEW POLYFILL + // @ts-ignore Polyfilled + const buffer = await this.crypto.subtle.digest( + digestAlgorithmToSubtle(algorithm), + bytes, + ) + return new Uint8Array(buffer) + } +} -export interface CryptoImplementation { - createKey(algs: ) +// TODO OAUTH types +// @ts-ignore +function digestAlgorithmToSubtle({name}: DigestAlgorithm): AlgorithmIdentifier { + switch (name) { + case 'sha256': + case 'sha384': + case 'sha512': + return `SHA-${name.slice(-3)}` + default: + throw new Error(`Unknown hash algorithm ${name}`) + } } diff --git a/src/oauth-client-temp/client/crypto-wrapper.ts b/src/oauth-client-temp/client/crypto-wrapper.ts new file mode 100644 index 0000000000..d893319c61 --- /dev/null +++ b/src/oauth-client-temp/client/crypto-wrapper.ts @@ -0,0 +1,173 @@ +import {b64uEncode} from '#/oauth-client-temp/b64' +import { + JwtHeader, + JwtPayload, + Key, + unsafeDecodeJwt, +} from '#/oauth-client-temp/jwk' +import {CryptoImplementation, DigestAlgorithm} from './crypto-implementation' + +export class CryptoWrapper { + constructor(protected implementation: CryptoImplementation) {} + + public async generateKey(algs: string[]): Promise { + return this.implementation.createKey(algs) + } + + public async sha256(text: string): Promise { + // TODO REVIEW POLYFILL + // @ts-ignore Polyfilled + const bytes = new TextEncoder().encode(text) + const digest = await this.implementation.digest(bytes, {name: 'sha256'}) + return b64uEncode(digest) + } + + public async generateNonce(length = 16): Promise { + const bytes = await this.implementation.getRandomValues(length) + return b64uEncode(bytes) + } + + public async validateIdTokenClaims( + token: string, + state: string, + nonce: string, + code?: string, + accessToken?: string, + ): Promise<{ + header: JwtHeader + payload: JwtPayload + }> { + // It's fine to use unsafeDecodeJwt here because the token was received from + // the server's token endpoint. The following checks are to ensure that the + // oauth flow was indeed initiated by the client. + const {header, payload} = unsafeDecodeJwt(token) + if (!payload.nonce || payload.nonce !== nonce) { + throw new TypeError('Nonce mismatch') + } + if (payload.c_hash) { + await this.validateHashClaim(payload.c_hash, code, header) + } + if (payload.s_hash) { + await this.validateHashClaim(payload.s_hash, state, header) + } + if (payload.at_hash) { + await this.validateHashClaim(payload.at_hash, accessToken, header) + } + return {header, payload} + } + + private async validateHashClaim( + claim: unknown, + source: unknown, + header: {alg: string; crv?: string}, + ): Promise { + if (typeof claim !== 'string' || !claim) { + throw new TypeError(`string "_hash" claim expected`) + } + if (typeof source !== 'string' || !source) { + throw new TypeError(`string value expected`) + } + const expected = await this.generateHashClaim(source, header) + if (expected !== claim) { + throw new TypeError(`"_hash" does not match`) + } + } + + protected async generateHashClaim( + source: string, + header: {alg: string; crv?: string}, + ) { + const algo = getHashAlgo(header) + // TODO REVIEW POLYFILL + // @ts-ignore Polyfilled + const bytes = new TextEncoder().encode(source) + const digest = await this.implementation.digest(bytes, algo) + return b64uEncode(digest.slice(0, digest.length / 2)) + } + + public async generatePKCE(byteLength?: number) { + const verifier = await this.generateVerifier(byteLength) + return { + verifier, + challenge: await this.sha256(verifier), + method: 'S256', + } + } + + // TODO OAUTH types + public async calculateJwkThumbprint(jwk: any) { + const components = extractJktComponents(jwk) + const data = JSON.stringify(components) + return this.sha256(data) + } + + /** + * @see {@link https://datatracker.ietf.org/doc/html/rfc7636#section-4.1} + * @note It is RECOMMENDED that the output of a suitable random number generator + * be used to create a 32-octet sequence. The octet sequence is then + * base64url-encoded to produce a 43-octet URL safe string to use as the code + * verifier. + */ + protected async generateVerifier(byteLength = 32) { + if (byteLength < 32 || byteLength > 96) { + throw new TypeError('Invalid code_verifier length') + } + const bytes = await this.implementation.getRandomValues(byteLength) + return b64uEncode(bytes) + } +} + +function getHashAlgo(header: {alg: string; crv?: string}): DigestAlgorithm { + switch (header.alg) { + case 'HS256': + case 'RS256': + case 'PS256': + case 'ES256': + case 'ES256K': + return {name: 'sha256'} + case 'HS384': + case 'RS384': + case 'PS384': + case 'ES384': + return {name: 'sha384'} + case 'HS512': + case 'RS512': + case 'PS512': + case 'ES512': + return {name: 'sha512'} + case 'EdDSA': + switch (header.crv) { + case 'Ed25519': + return {name: 'sha512'} + default: + throw new TypeError('unrecognized or invalid EdDSA curve provided') + } + default: + throw new TypeError('unrecognized or invalid JWS algorithm provided') + } +} + +// TODO OAUTH types +function extractJktComponents(jwk: {[x: string]: any; kty: any}) { + // TODO OAUTH types + const get = (field: string) => { + const value = jwk[field] + if (typeof value !== 'string' || !value) { + throw new TypeError(`"${field}" Parameter missing or invalid`) + } + return value + } + + switch (jwk.kty) { + case 'EC': + return {crv: get('crv'), kty: get('kty'), x: get('x'), y: get('y')} + case 'OKP': + return {crv: get('crv'), kty: get('kty'), x: get('x')} + case 'RSA': + return {e: get('e'), kty: get('kty'), n: get('n')} + case 'oct': + return {k: get('k'), kty: get('kty')} + default: + throw new TypeError('"kty" (Key Type) Parameter missing or unsupported') + } +} diff --git a/src/oauth-client-temp/client/db.ts b/src/oauth-client-temp/client/db.ts new file mode 100644 index 0000000000..e69de29bb2 diff --git a/src/oauth-client-temp/client/db.web.ts b/src/oauth-client-temp/client/db.web.ts new file mode 100644 index 0000000000..e69de29bb2 diff --git a/src/oauth-client-temp/client/key.ts b/src/oauth-client-temp/client/key.ts new file mode 100644 index 0000000000..228e0471d1 --- /dev/null +++ b/src/oauth-client-temp/client/key.ts @@ -0,0 +1,78 @@ +import { + fromSubtleAlgorithm, + generateKeypair, + isSignatureKeyPair, +} from '#/oauth-client-temp/client/util' +import {Jwk, jwkSchema} from '#/oauth-client-temp/jwk' +import {JoseKey} from '#/oauth-client-temp/jwk-jose' + +export class CryptoKey extends JoseKey { + // static async fromIndexedDB(kid: string, allowedAlgos: string[] = ['ES384']) { + // const cryptoKeyPair = await loadCryptoKeyPair(kid, allowedAlgos) + // return this.fromKeypair(kid, cryptoKeyPair) + // } + + static async generate( + // TODO REVIEW POLYFILL + // @ts-ignore Polyfilled + kid: string = crypto.randomUUID(), + allowedAlgos: string[] = ['ES384'], + exportable = false, + ) { + const cryptoKeyPair = await generateKeypair(allowedAlgos, exportable) + return this.fromKeypair(kid, cryptoKeyPair) + } + + // TODO REVIEW POLYFILL + // @ts-ignore Polyfilled + static async fromKeypair(kid: string, cryptoKeyPair: CryptoKeyPair) { + if (!isSignatureKeyPair(cryptoKeyPair)) { + throw new TypeError('CryptoKeyPair must be compatible with sign/verify') + } + + // https://datatracker.ietf.org/doc/html/rfc7517 + // > The "use" and "key_ops" JWK members SHOULD NOT be used together; [...] + // > Applications should specify which of these members they use. + + // TODO REVIEW POLYFILL + // @ts-ignore Polyfilled + const {key_ops: _, ...jwk} = await crypto.subtle.exportKey( + 'jwk', + cryptoKeyPair.privateKey.extractable + ? cryptoKeyPair.privateKey + : cryptoKeyPair.publicKey, + ) + + const use = jwk.use ?? 'sig' + const alg = + jwk.alg ?? fromSubtleAlgorithm(cryptoKeyPair.privateKey.algorithm) + + if (use !== 'sig') { + throw new TypeError('Unsupported JWK use') + } + + return new CryptoKey( + jwkSchema.parse({...jwk, use, kid, alg}), + cryptoKeyPair, + ) + } + + // TODO REVIEW POLYFILL + // @ts-ignore Polyfilled + constructor(jwk: Jwk, readonly cryptoKeyPair: CryptoKeyPair) { + super(jwk) + } + + get isPrivate() { + return true + } + + get privateJwk(): Jwk | undefined { + if (super.isPrivate) return this.jwk + throw new Error('Private Webcrypto Key not exportable') + } + + protected async getKey() { + return this.cryptoKeyPair.privateKey + } +} diff --git a/src/oauth-client-temp/client/keyset.ts b/src/oauth-client-temp/client/keyset.ts new file mode 100644 index 0000000000..e69de29bb2 diff --git a/src/oauth-client-temp/client/oauth-callback-error.ts b/src/oauth-client-temp/client/oauth-callback-error.ts new file mode 100644 index 0000000000..9c9c26d19d --- /dev/null +++ b/src/oauth-client-temp/client/oauth-callback-error.ts @@ -0,0 +1,16 @@ +export class OAuthCallbackError extends Error { + static from(err: unknown, params: URLSearchParams, state?: string) { + if (err instanceof OAuthCallbackError) return err + const message = err instanceof Error ? err.message : undefined + return new OAuthCallbackError(params, message, state, err) + } + + constructor( + public readonly params: URLSearchParams, + message = params.get('error_description') || 'OAuth callback error', + public readonly state?: string, + cause?: unknown, + ) { + super(message, { cause }) + } +} diff --git a/src/oauth-client-temp/client/oauth-client-factory.ts b/src/oauth-client-temp/client/oauth-client-factory.ts new file mode 100644 index 0000000000..8ca685a94a --- /dev/null +++ b/src/oauth-client-temp/client/oauth-client-factory.ts @@ -0,0 +1,290 @@ +import {GenericStore} from '@atproto/caching' + +import {Key} from '#/oauth-client-temp/jwk' +import {FALLBACK_ALG} from './constants' +import {OAuthCallbackError} from './oauth-callback-error' +import {OAuthClient} from './oauth-client' +import {OAuthServer} from './oauth-server' +import { + OAuthServerFactory, + OAuthServerFactoryOptions, +} from './oauth-server-factory' +import { + OAuthAuthorizeOptions, + OAuthResponseMode, + OAuthResponseType, +} from './oauth-types' +import {Session, SessionGetter} from './session-getter' + +export type InternalStateData = { + iss: string + nonce: string + dpopKey: Key + verifier?: string + + /** + * @note This could be parametrized to be of any type. This wasn't done for + * the sake of simplicity but could be added in a later development. + */ + appState?: string +} + +export type OAuthClientOptions = OAuthServerFactoryOptions & { + stateStore: GenericStore + sessionStore: GenericStore + + /** + * "form_post" will typically be used for server-side applications. + */ + responseMode?: OAuthResponseMode + responseType?: OAuthResponseType +} + +export class OAuthClientFactory { + readonly serverFactory: OAuthServerFactory + + readonly stateStore: GenericStore + readonly sessionGetter: SessionGetter + + readonly responseMode?: OAuthResponseMode + readonly responseType?: OAuthResponseType + + constructor(options: OAuthClientOptions) { + this.responseMode = options?.responseMode + this.responseType = options?.responseType + this.serverFactory = new OAuthServerFactory(options) + this.stateStore = options.stateStore + this.sessionGetter = new SessionGetter( + options.sessionStore, + this.serverFactory, + ) + } + + get clientMetadata() { + return this.serverFactory.clientMetadata + } + + async authorize( + input: string, + options?: OAuthAuthorizeOptions, + ): Promise { + const {did, metadata} = await this.serverFactory.resolver.resolve(input) + + const nonce = await this.serverFactory.crypto.generateNonce() + const pkce = await this.serverFactory.crypto.generatePKCE() + const dpopKey = await this.serverFactory.crypto.generateKey( + metadata.dpop_signing_alg_values_supported || [FALLBACK_ALG], + ) + + const state = await this.serverFactory.crypto.generateNonce() + + await this.stateStore.set(state, { + iss: metadata.issuer, + dpopKey, + nonce, + verifier: pkce?.verifier, + appState: options?.state, + }) + + const parameters = { + client_id: this.clientMetadata.client_id, + redirect_uri: this.clientMetadata.redirect_uris[0], + code_challenge: pkce?.challenge, + code_challenge_method: pkce?.method, + nonce, + state, + login_hint: did || undefined, + response_mode: this.responseMode, + response_type: + this.responseType != null && + metadata.response_types_supported?.includes(this.responseType) + ? this.responseType + : 'code', + + display: options?.display, + id_token_hint: options?.id_token_hint, + max_age: options?.max_age, // this.clientMetadata.default_max_age + prompt: options?.prompt, + scope: options?.scope + ?.split(' ') + .filter(s => metadata.scopes_supported?.includes(s)) + .join(' '), + ui_locales: options?.ui_locales, + } + + if (metadata.pushed_authorization_request_endpoint) { + const server = await this.serverFactory.fromMetadata(metadata, dpopKey) + const {json} = await server.request( + 'pushed_authorization_request', + parameters, + ) + + const authorizationUrl = new URL(metadata.authorization_endpoint) + authorizationUrl.searchParams.set( + 'client_id', + this.clientMetadata.client_id, + ) + authorizationUrl.searchParams.set('request_uri', json.request_uri) + return authorizationUrl + } else if (metadata.require_pushed_authorization_requests) { + throw new Error( + 'Server requires pushed authorization requests (PAR) but no PAR endpoint is available', + ) + } else { + const authorizationUrl = new URL(metadata.authorization_endpoint) + for (const [key, value] of Object.entries(parameters)) { + if (value) authorizationUrl.searchParams.set(key, String(value)) + } + + // Length of the URL that will be sent to the server + const urlLength = + authorizationUrl.pathname.length + authorizationUrl.search.length + if (urlLength < 2048) { + return authorizationUrl + } else if (!metadata.pushed_authorization_request_endpoint) { + throw new Error('Login URL too long') + } + } + + throw new Error( + 'Server does not support pushed authorization requests (PAR)', + ) + } + + async callback(params: URLSearchParams): Promise<{ + client: OAuthClient + state?: string + }> { + // TODO: better errors + + const responseJwt = params.get('response') + if (responseJwt != null) { + // https://openid.net/specs/oauth-v2-jarm.html + throw new OAuthCallbackError(params, 'JARM not supported') + } + + const issuerParam = params.get('iss') + const stateParam = params.get('state') + const errorParam = params.get('error') + const codeParam = params.get('code') + + if (!stateParam) { + throw new OAuthCallbackError(params, 'Missing "state" parameter') + } + const stateData = await this.stateStore.get(stateParam) + if (stateData) { + // Prevent any kind of replay + await this.stateStore.del(stateParam) + } else { + throw new OAuthCallbackError(params, 'Invalid state') + } + + try { + if (errorParam != null) { + throw new OAuthCallbackError(params, undefined, stateData.appState) + } + + if (!codeParam) { + throw new OAuthCallbackError( + params, + 'Missing "code" query param', + stateData.appState, + ) + } + + const server = await this.serverFactory.fromIssuer( + stateData.iss, + stateData.dpopKey, + ) + + if (issuerParam != null) { + if (!server.serverMetadata.issuer) { + throw new OAuthCallbackError( + params, + 'Issuer not found in metadata', + stateData.appState, + ) + } + if (server.serverMetadata.issuer !== issuerParam) { + throw new OAuthCallbackError( + params, + 'Issuer mismatch', + stateData.appState, + ) + } + } else if ( + server.serverMetadata.authorization_response_iss_parameter_supported + ) { + throw new OAuthCallbackError( + params, + 'iss missing from the response', + stateData.appState, + ) + } + + const tokenSet = await server.exchangeCode(codeParam, stateData.verifier) + try { + if (tokenSet.id_token) { + await this.serverFactory.crypto.validateIdTokenClaims( + tokenSet.id_token, + stateParam, + stateData.nonce, + codeParam, + tokenSet.access_token, + ) + } + + const sessionId = await this.serverFactory.crypto.generateNonce(4) + + await this.sessionGetter.setStored(sessionId, { + dpopKey: stateData.dpopKey, + tokenSet, + }) + + const client = this.createClient(server, sessionId) + + return {client, state: stateData.appState} + } catch (err) { + await server.revoke(tokenSet.access_token) + + throw err + } + } catch (err) { + // Make sure, whatever the underlying error, that the appState is + // available in the calling code + throw OAuthCallbackError.from(err, params, stateData.appState) + } + } + + /** + * Build a client from a stored session. This will refresh the token only if + * needed (about to expire) by default. + * + * @param refresh See {@link SessionGetter.getSession} + */ + async restore(sessionId: string, refresh?: boolean): Promise { + const {dpopKey, tokenSet} = await this.sessionGetter.getSession( + sessionId, + refresh, + ) + + const server = await this.serverFactory.fromIssuer(tokenSet.iss, dpopKey) + + return this.createClient(server, sessionId) + } + + async revoke(sessionId: string) { + const {dpopKey, tokenSet} = await this.sessionGetter.get(sessionId, { + allowStale: true, + }) + + const server = await this.serverFactory.fromIssuer(tokenSet.iss, dpopKey) + + await server.revoke(tokenSet.access_token) + await this.sessionGetter.delStored(sessionId) + } + + createClient(server: OAuthServer, sessionId: string): OAuthClient { + return new OAuthClient(server, sessionId, this.sessionGetter) + } +} diff --git a/src/oauth-client-temp/client/oauth-client.ts b/src/oauth-client-temp/client/oauth-client.ts new file mode 100644 index 0000000000..aee2d9d81b --- /dev/null +++ b/src/oauth-client-temp/client/oauth-client.ts @@ -0,0 +1,89 @@ +import {JwtPayload, unsafeDecodeJwt} from '#/oauth-client-temp/jwk' +import {OAuthServer, TokenSet} from './oauth-server' +import {SessionGetter} from './session-getter' + +export class OAuthClient { + constructor( + private readonly server: OAuthServer, + public readonly sessionId: string, + private readonly sessionGetter: SessionGetter, + ) {} + + /** + * @param refresh See {@link SessionGetter.getSession} + */ + async getTokenSet(refresh?: boolean): Promise { + const {tokenSet} = await this.sessionGetter.getSession( + this.sessionId, + refresh, + ) + return tokenSet + } + + async getUserinfo(): Promise<{ + userinfo?: JwtPayload + expired?: boolean + scope?: string + iss: string + aud: string + sub: string + }> { + const tokenSet = await this.getTokenSet() + + return { + userinfo: tokenSet.id_token + ? unsafeDecodeJwt(tokenSet.id_token).payload + : undefined, + expired: + tokenSet.expires_at == null + ? undefined + : tokenSet.expires_at < Date.now() - 5e3, + scope: tokenSet.scope, + iss: tokenSet.iss, + aud: tokenSet.aud, + sub: tokenSet.sub, + } + } + + async signOut() { + try { + const tokenSet = await this.getTokenSet(false) + await this.server.revoke(tokenSet.access_token) + } finally { + await this.sessionGetter.delStored(this.sessionId) + } + } + + async request( + pathname: string, + init?: RequestInit, + refreshCredentials?: boolean, + ): Promise { + const tokenSet = await this.getTokenSet(refreshCredentials) + const headers = new Headers(init?.headers) + headers.set( + 'Authorization', + `${tokenSet.token_type} ${tokenSet.access_token}`, + ) + const request = new Request(new URL(pathname, tokenSet.aud), { + ...init, + headers, + }) + + return this.server.dpopFetch(request).catch(err => { + if (!refreshCredentials && isTokenExpiredError(err)) { + return this.request(pathname, init, true) + } + + throw err + }) + } +} + +/** + * @todo Actually implement this + */ +function isTokenExpiredError(_err: unknown) { + // TODO: Detect access_token expired 401 + return false +} diff --git a/src/oauth-client-temp/client/oauth-database.ts b/src/oauth-client-temp/client/oauth-database.ts new file mode 100644 index 0000000000..026a04af75 --- /dev/null +++ b/src/oauth-client-temp/client/oauth-database.ts @@ -0,0 +1,6 @@ +import {DidDocument} from '@atproto/did' +import {ResolvedHandle} from '@atproto/handle-resolver' +import {OAuthServerMetadata} from '@atproto/oauth-server-metadata' +import IsomorphicOAuthServerMetadataResolver from '@atproto/oauth-server-metadata-resolver' + +import {CryptoKey} from '#/oauth-client-temp/client/key' diff --git a/src/oauth-client-temp/client/oauth-resolver.ts b/src/oauth-client-temp/client/oauth-resolver.ts new file mode 100644 index 0000000000..4ca5798435 --- /dev/null +++ b/src/oauth-client-temp/client/oauth-resolver.ts @@ -0,0 +1,28 @@ +import {IdentityResolver, ResolvedIdentity} from '@atproto/identity-resolver' +import { + OAuthServerMetadata, + OAuthServerMetadataResolver, +} from '@atproto/oauth-server-metadata-resolver' + +export class OAuthResolver { + constructor( + readonly metadataResolver: OAuthServerMetadataResolver, + readonly identityResolver: IdentityResolver, + ) {} + + public async resolve(input: string): Promise< + Partial & { + url: URL + metadata: OAuthServerMetadata + } + > { + const identity = /^https?:\/\//.test(input) + ? // Allow using a PDS url directly as login input (e.g. when the handle does not resolve to a DID) + {url: new URL(input)} + : await this.identityResolver.resolve(input, 'AtprotoPersonalDataServer') + + const metadata = await this.metadataResolver.resolve(identity.url.origin) + + return {...identity, metadata} + } +} diff --git a/src/oauth-client-temp/client/oauth-server-factory.ts b/src/oauth-client-temp/client/oauth-server-factory.ts new file mode 100644 index 0000000000..265dda9c39 --- /dev/null +++ b/src/oauth-client-temp/client/oauth-server-factory.ts @@ -0,0 +1,81 @@ +import {GenericStore, MemoryStore} from '@atproto/caching' +import {Fetch} from '@atproto/fetch' +import {IdentityResolver} from '@atproto/identity-resolver' +import {OAuthClientMetadata} from '@atproto/oauth-client-metadata' +import {OAuthServerMetadata} from '@atproto/oauth-server-metadata' +import {OAuthServerMetadataResolver} from '@atproto/oauth-server-metadata-resolver' + +import {Key, Keyset} from '#/oauth-client-temp/jwk' +import {CryptoImplementation} from './crypto-implementation' +import {CryptoWrapper} from './crypto-wrapper' +import {OAuthResolver} from './oauth-resolver' +import {OAuthServer} from './oauth-server' +import {OAuthClientMetadataId} from './oauth-types' +import {validateClientMetadata} from './validate-client-metadata' + +export type OAuthServerFactoryOptions = { + clientMetadata: OAuthClientMetadata + metadataResolver: OAuthServerMetadataResolver + cryptoImplementation: CryptoImplementation + identityResolver: IdentityResolver + fetch?: Fetch + keyset?: Keyset + dpopNonceCache?: GenericStore +} + +export class OAuthServerFactory { + readonly clientMetadata: OAuthClientMetadataId + readonly metadataResolver: OAuthServerMetadataResolver + readonly crypto: CryptoWrapper + readonly resolver: OAuthResolver + readonly fetch: Fetch + readonly keyset?: Keyset + readonly dpopNonceCache: GenericStore + + constructor({ + metadataResolver, + identityResolver, + clientMetadata, + cryptoImplementation, + keyset, + fetch = globalThis.fetch, + dpopNonceCache = new MemoryStore({ + ttl: 60e3, + max: 100, + }), + }: OAuthServerFactoryOptions) { + validateClientMetadata(clientMetadata, keyset) + + if (!clientMetadata.client_id) { + throw new TypeError('A client_id property must be specified') + } + + this.clientMetadata = clientMetadata + this.metadataResolver = metadataResolver + this.keyset = keyset + this.fetch = fetch + this.dpopNonceCache = dpopNonceCache + + this.crypto = new CryptoWrapper(cryptoImplementation) + this.resolver = new OAuthResolver(metadataResolver, identityResolver) + } + + async fromIssuer(issuer: string, dpopKey: Key) { + const {origin} = new URL(issuer) + const serverMetadata = await this.metadataResolver.resolve(origin) + return this.fromMetadata(serverMetadata, dpopKey) + } + + async fromMetadata(serverMetadata: OAuthServerMetadata, dpopKey: Key) { + return new OAuthServer( + dpopKey, + serverMetadata, + this.clientMetadata, + this.dpopNonceCache, + this.resolver, + this.crypto, + this.keyset, + this.fetch, + ) + } +} diff --git a/src/oauth-client-temp/client/oauth-server.ts b/src/oauth-client-temp/client/oauth-server.ts new file mode 100644 index 0000000000..fdae49a451 --- /dev/null +++ b/src/oauth-client-temp/client/oauth-server.ts @@ -0,0 +1,287 @@ +import {GenericStore} from '@atproto/caching' +import { + Fetch, + fetchFailureHandler, + fetchJsonProcessor, + fetchOkProcessor, +} from '@atproto/fetch' +import {OAuthClientMetadata} from '@atproto/oauth-client-metadata' +import {OAuthServerMetadata} from '@atproto/oauth-server-metadata' + +import {dpopFetchWrapper} from '#/oauth-client-temp/fetch-dpop' +import {Jwt, Key, Keyset} from '#/oauth-client-temp/jwk' +import {FALLBACK_ALG} from './constants' +import {CryptoWrapper} from './crypto-wrapper' +import {OAuthResolver} from './oauth-resolver' +import { + OAuthEndpointName, + OAuthTokenResponse, + OAuthTokenType, +} from './oauth-types' + +export type TokenSet = { + iss: string + sub: string + aud: string + scope?: string + + id_token?: Jwt + refresh_token?: string + access_token: string + token_type: OAuthTokenType + expires_at?: number +} + +export class OAuthServer { + readonly dpopFetch: (request: Request) => Promise + + constructor( + readonly dpopKey: Key, + readonly serverMetadata: OAuthServerMetadata, + readonly clientMetadata: OAuthClientMetadata & {client_id: string}, + readonly dpopNonceCache: GenericStore, + readonly resolver: OAuthResolver, + readonly crypto: CryptoWrapper, + readonly keyset?: Keyset, + fetch?: Fetch, + ) { + const dpopFetch = dpopFetchWrapper({ + fetch, + iss: this.clientMetadata.client_id, + key: dpopKey, + alg: negotiateAlg( + dpopKey, + serverMetadata.dpop_signing_alg_values_supported, + ), + sha256: async v => crypto.sha256(v), + nonceCache: dpopNonceCache, + }) + + this.dpopFetch = request => dpopFetch(request).catch(fetchFailureHandler) + } + + async revoke(token: string) { + try { + await this.request('revocation', {token}) + } catch { + // Don't care + } + } + + async exchangeCode(code: string, verifier?: string): Promise { + const {json: tokenResponse} = await this.request('token', { + grant_type: 'authorization_code', + redirect_uri: this.clientMetadata.redirect_uris[0]!, + code, + code_verifier: verifier, + }) + + try { + if (!tokenResponse.sub) { + throw new TypeError(`Missing "sub" in token response`) + } + + // VERY IMPORTANT ! + const resolved = await this.checkSubIssuer(tokenResponse.sub) + + return { + sub: tokenResponse.sub, + aud: resolved.url.href, + iss: resolved.metadata.issuer, + + scope: tokenResponse.scope, + id_token: tokenResponse.id_token, + refresh_token: tokenResponse.refresh_token, + access_token: tokenResponse.access_token, + token_type: tokenResponse.token_type ?? 'Bearer', + expires_at: + typeof tokenResponse.expires_in === 'number' + ? Date.now() + tokenResponse.expires_in * 1000 + : undefined, + } + } catch (err) { + await this.revoke(tokenResponse.access_token) + + throw err + } + } + + async refresh(tokenSet: TokenSet): Promise { + if (!tokenSet.refresh_token) { + throw new Error('No refresh token available') + } + + const {json: tokenResponse} = await this.request('token', { + grant_type: 'refresh_token', + refresh_token: tokenSet.refresh_token, + }) + + try { + if (tokenSet.sub !== tokenResponse.sub) { + throw new TypeError(`Unexpected "sub" in token response`) + } + if (tokenSet.iss !== this.serverMetadata.issuer) { + throw new TypeError('Issuer mismatch') + } + + // VERY IMPORTANT ! + const resolved = await this.checkSubIssuer(tokenResponse.sub) + + return { + sub: tokenResponse.sub, + aud: resolved.url.href, + iss: resolved.metadata.issuer, + + id_token: tokenResponse.id_token, + refresh_token: tokenResponse.refresh_token, + access_token: tokenResponse.access_token, + token_type: tokenResponse.token_type ?? 'Bearer', + expires_at: Date.now() + (tokenResponse.expires_in ?? 60) * 1000, + } + } catch (err) { + await this.revoke(tokenResponse.access_token) + + throw err + } + } + + /** + * Whenever an OAuth token response is received, we **MUST** verify that the + * "sub" is a DID, whose issuer authority is indeed the server we just + * obtained credentials from. This check is a critical step to actually be + * able to use the "sub" (DID) as being the actual user's identifier. + */ + protected async checkSubIssuer(sub: string) { + const resolved = await this.resolver.resolve(sub) + if (resolved.metadata.issuer !== this.serverMetadata.issuer) { + // Maybe the user switched PDS. + throw new TypeError('Issuer mismatch') + } + return resolved + } + + async request( + endpoint: E, + payload: Record, + ) { + const url = this.serverMetadata[`${endpoint}_endpoint`] + if (!url) throw new Error(`No ${endpoint} endpoint available`) + const auth = await this.buildClientAuth(endpoint) + + const request = new Request(url, { + method: 'POST', + headers: {...auth.headers, 'Content-Type': 'application/json'}, + body: JSON.stringify({...payload, ...auth.payload}), + }) + + const response = await this.dpopFetch(request) + .then(fetchOkProcessor()) + .then( + fetchJsonProcessor< + E extends 'pushed_authorization_request' + ? {request_uri: string} + : E extends 'token' + ? OAuthTokenResponse + : unknown + >(), + ) + + // TODO: validate using zod ? + if (endpoint === 'token') { + if (!response.json.access_token) { + throw new TypeError('No access token in token response') + } + } + + return response + } + + async buildClientAuth(endpoint: OAuthEndpointName): Promise<{ + headers?: Record + payload: + | { + client_id: string + } + | { + client_id: string + client_assertion_type: string + client_assertion: string + } + }> { + const methodSupported = + this.serverMetadata[`${endpoint}_endpoint_auth_methods_supported`] || + this.serverMetadata.token_endpoint_auth_methods_supported + + const method = + this.clientMetadata[`${endpoint}_endpoint_auth_method`] || + this.clientMetadata.token_endpoint_auth_method + + if ( + method === 'private_key_jwt' || + (this.keyset && + !method && + (methodSupported?.includes('private_key_jwt') ?? false)) + ) { + if (!this.keyset) throw new Error('No keyset available') + + try { + const alg = + this.serverMetadata[ + `${endpoint}_endpoint_auth_signing_alg_values_supported` + ] ?? + this.serverMetadata + .token_endpoint_auth_signing_alg_values_supported ?? + FALLBACK_ALG + + // If jwks is defined, make sure to only sign using a key that exists in + // the jwks. If jwks_uri is defined, we can't be sure that the key we're + // looking for is in there so we will just assume it is. + const kid = this.clientMetadata.jwks?.keys + .map(({kid}) => kid) + .filter((v): v is string => !!v) + + return { + payload: { + client_id: this.clientMetadata.client_id, + client_assertion_type: + 'urn:ietf:params:oauth:client-assertion-type:jwt-bearer', + client_assertion: await this.keyset.sign( + {alg, kid}, + { + iss: this.clientMetadata.client_id, + sub: this.clientMetadata.client_id, + aud: this.serverMetadata.issuer, + jti: await this.crypto.generateNonce(), + iat: Math.floor(Date.now() / 1000), + }, + ), + }, + } + } catch (err) { + if (method === 'private_key_jwt') throw err + + // Else try next method + } + } + + if ( + method === 'none' || + (!method && (methodSupported?.includes('none') ?? true)) + ) { + return { + payload: { + client_id: this.clientMetadata.client_id, + }, + } + } + + throw new Error(`Unsupported ${endpoint} authentication method`) + } +} + +function negotiateAlg(key: Key, supportedAlgs: string[] | undefined): string { + const alg = key.algorithms.find(a => supportedAlgs?.includes(a) ?? true) + if (alg) return alg + + throw new Error('Key does not match any alg supported by the server') +} diff --git a/src/oauth-client-temp/client/oauth-types.ts b/src/oauth-client-temp/client/oauth-types.ts new file mode 100644 index 0000000000..922abf9067 --- /dev/null +++ b/src/oauth-client-temp/client/oauth-types.ts @@ -0,0 +1,37 @@ +import {OAuthClientMetadata} from '@atproto/oauth-client-metadata' + +import {Jwt} from '#/oauth-client-temp/jwk' + +export type OAuthResponseMode = 'query' | 'fragment' | 'form_post' +export type OAuthResponseType = 'code' | 'code id_token' + +export type OAuthEndpointName = + | 'token' + | 'revocation' + | 'introspection' + | 'pushed_authorization_request' + +export type OAuthTokenType = 'Bearer' | 'DPoP' + +export type OAuthAuthorizeOptions = { + display?: 'page' | 'popup' | 'touch' | 'wap' + id_token_hint?: string + max_age?: number + prompt?: 'login' | 'none' | 'consent' | 'select_account' + scope?: string + state?: string + ui_locales?: string +} + +export type OAuthTokenResponse = { + issuer?: string + sub?: string + scope?: string + id_token?: Jwt + refresh_token?: string + access_token: string + token_type?: OAuthTokenType + expires_in?: number +} + +export type OAuthClientMetadataId = OAuthClientMetadata & {client_id: string} diff --git a/src/oauth-client-temp/client/session-getter.ts b/src/oauth-client-temp/client/session-getter.ts new file mode 100644 index 0000000000..c2889c9673 --- /dev/null +++ b/src/oauth-client-temp/client/session-getter.ts @@ -0,0 +1,139 @@ +import {CachedGetter, GenericStore} from '@atproto/caching' +import {FetchResponseError} from '@atproto/fetch' + +import {Key} from '#/oauth-client-temp/jwk' +import {TokenSet} from './oauth-server' +import {OAuthServerFactory} from './oauth-server-factory' + +export type Session = { + dpopKey: Key + tokenSet: TokenSet +} + +/** + * There are several advantages to wrapping the sessionStore in a (single) + * CachedGetter, the main of which is that the cached getter will ensure that at + * most one fresh call is ever being made. Another advantage, is that it + * contains the logic for reading from the cache which, if the cache is based on + * localStorage/indexedDB, will sync across multiple tabs (for a given + * sessionId). + */ +export class SessionGetter extends CachedGetter { + constructor( + sessionStore: GenericStore, + serverFactory: OAuthServerFactory, + ) { + super( + async (sessionId, options, storedSession) => { + // There needs to be a previous session to be able to refresh + if (storedSession === undefined) { + throw new Error('The session was revoked') + } + + // Since refresh tokens can only be used once, we might run into + // concurrency issues if multiple tabs/instances are trying to refresh + // the same token. The chances of this happening when multiple instances + // are started simultaneously is reduced by randomizing the expiry time + // (see isStale() bellow). Even so, There still exist chances that + // multiple tabs will try to refresh the token at the same time. The + // best solution would be to use a mutex/lock to ensure that only one + // instance is refreshing the token at a time. A simpler workaround is + // to check if the value stored in the session store is the same as the + // one in memory. If it isn't, then another instance has already + // refreshed the token. + + const {tokenSet, dpopKey} = storedSession + const server = await serverFactory.fromIssuer(tokenSet.iss, dpopKey) + const newTokenSet = await server.refresh(tokenSet).catch(async err => { + if (await isRefreshDeniedError(err)) { + // Allow some time for the concurrent request to be stored before + // we try to get it. + await new Promise(r => setTimeout(r, 500)) + + const stored = await this.getStored(sessionId) + if (stored !== undefined) { + if ( + stored.tokenSet.access_token !== tokenSet.access_token || + stored.tokenSet.refresh_token !== tokenSet.refresh_token + ) { + // A concurrent refresh occurred. Pretend this one succeeded. + return stored.tokenSet + } else { + // The session data will be deleted from the sessionStore by + // the "deleteOnError" callback. + } + } + } + + throw err + }) + return {...storedSession, tokenSet: newTokenSet} + }, + sessionStore, + { + isStale: (sessionId, {tokenSet}) => { + return ( + tokenSet.expires_at != null && + tokenSet.expires_at < + Date.now() + + // Add some lee way to ensure the token is not expired when it + // reaches the server. + 30e3 + + // Add some randomness to prevent all instances from trying to + // refreshing at the exact same time, when they are started at + // the same time. + 60e3 * Math.random() + ) + }, + onStoreError: async (err, sessionId, {tokenSet, dpopKey}) => { + // If the token data cannot be stored, let's revoke it + const server = await serverFactory.fromIssuer(tokenSet.iss, dpopKey) + await server.revoke(tokenSet.access_token) + throw err + }, + deleteOnError: async (err, sessionId, {tokenSet}) => { + // Not possible to refresh without a refresh token + if (!tokenSet.refresh_token) return true + + // If fetching a refresh token fails because they are no longer valid, + // delete the session from the sessionStore. + if (await isRefreshDeniedError(err)) return true + + // Unknown cause, keep the session in the store + return false + }, + }, + ) + } + + /** + * @param refresh When `true`, the credentials will be refreshed even if they + * are not expired. When `false`, the credentials will not be refreshed even + * if they are expired. When `undefined`, the credentials will be refreshed + * if, and only if, they are (about to be) expired. Defaults to `undefined`. + */ + async getSession(sessionId: string, refresh?: boolean) { + return this.get(sessionId, { + noCache: refresh === true, + allowStale: refresh === false, + }) + } +} + +async function isRefreshDeniedError(err: unknown) { + if (err instanceof FetchResponseError && err.statusCode === 400) { + if (err.response?.bodyUsed === false) { + try { + const json = await err.response.clone().json() + return ( + json.error === 'invalid_request' && + json.error_description === 'Invalid refresh token' + ) + } catch { + // falls through + } + } + } + + return false +} diff --git a/src/oauth-client-temp/client/util.ts b/src/oauth-client-temp/client/util.ts new file mode 100644 index 0000000000..5e238ac3b7 --- /dev/null +++ b/src/oauth-client-temp/client/util.ts @@ -0,0 +1,160 @@ +export type JWSAlgorithm = + // HMAC + | 'HS256' + | 'HS384' + | 'HS512' + // RSA + | 'PS256' + | 'PS384' + | 'PS512' + | 'RS256' + | 'RS384' + | 'RS512' + // EC + | 'ES256' + | 'ES256K' + | 'ES384' + | 'ES512' + // OKP + | 'EdDSA' + +// TODO REVIEW POLYFILL +// @ts-ignore Polyfilled +export type SubtleAlgorithm = RsaHashedKeyGenParams | EcKeyGenParams + +export function toSubtleAlgorithm( + alg: string, + crv?: string, + options?: {modulusLength?: number}, +): SubtleAlgorithm { + switch (alg) { + case 'PS256': + case 'PS384': + case 'PS512': + return { + name: 'RSA-PSS', + hash: `SHA-${alg.slice(-3) as '256' | '384' | '512'}`, + modulusLength: options?.modulusLength ?? 2048, + publicExponent: new Uint8Array([0x01, 0x00, 0x01]), + } + case 'RS256': + case 'RS384': + case 'RS512': + return { + name: 'RSASSA-PKCS1-v1_5', + hash: `SHA-${alg.slice(-3) as '256' | '384' | '512'}`, + modulusLength: options?.modulusLength ?? 2048, + publicExponent: new Uint8Array([0x01, 0x00, 0x01]), + } + case 'ES256': + case 'ES384': + return { + name: 'ECDSA', + namedCurve: `P-${alg.slice(-3) as '256' | '384'}`, + } + case 'ES512': + return { + name: 'ECDSA', + namedCurve: 'P-521', + } + default: + // https://github.com/w3c/webcrypto/issues/82#issuecomment-849856773 + + throw new TypeError(`Unsupported alg "${alg}"`) + } +} + +// TODO REVIEW POLYFILL +// @ts-ignore Polyfilled +export function fromSubtleAlgorithm(algorithm: KeyAlgorithm): JWSAlgorithm { + switch (algorithm.name) { + case 'RSA-PSS': + case 'RSASSA-PKCS1-v1_5': { + // TODO REVIEW POLYFILL + // @ts-ignore Polyfilled + const hash = (algorithm).hash.name + switch (hash) { + case 'SHA-256': + case 'SHA-384': + case 'SHA-512': { + const prefix = algorithm.name === 'RSA-PSS' ? 'PS' : 'RS' + return `${prefix}${hash.slice(-3) as '256' | '384' | '512'}` + } + default: + throw new TypeError('unsupported RsaHashedKeyAlgorithm hash') + } + } + case 'ECDSA': { + // TODO REVIEW POLYFILL + // @ts-ignore Polyfilled + const namedCurve = (algorithm).namedCurve + switch (namedCurve) { + case 'P-256': + case 'P-384': + case 'P-512': + return `ES${namedCurve.slice(-3) as '256' | '384' | '512'}` + case 'P-521': + return 'ES512' + default: + throw new TypeError('unsupported EcKeyAlgorithm namedCurve') + } + } + case 'Ed448': + case 'Ed25519': + return 'EdDSA' + default: + // https://github.com/w3c/webcrypto/issues/82#issuecomment-849856773 + + throw new TypeError(`Unexpected algorithm "${algorithm.name}"`) + } +} + +export function isSignatureKeyPair( + v: unknown, + extractable?: boolean, + // TODO REVIEW POLYFILL + // @ts-ignore Polyfilled +): v is CryptoKeyPair { + return ( + typeof v === 'object' && + v !== null && + 'privateKey' in v && + // TODO REVIEW POLYFILL + // @ts-ignore Polyfilled + v.privateKey instanceof CryptoKey && + v.privateKey.type === 'private' && + (extractable == null || v.privateKey.extractable === extractable) && + v.privateKey.usages.includes('sign') && + 'publicKey' in v && + // TODO REVIEW POLYFILL + // @ts-ignore Polyfilled + v.publicKey instanceof CryptoKey && + v.publicKey.type === 'public' && + v.publicKey.extractable === true && + v.publicKey.usages.includes('verify') + ) +} + +export async function generateKeypair( + algs: string[], + extractable = false, + // TODO REVIEW POLYFILL + // @ts-ignore Polyfilled +): Promise { + const errors: unknown[] = [] + for (const alg of algs) { + try { + // TODO REVIEW POLYFILL + // @ts-ignore Polyfilled + return await crypto.subtle.generateKey( + toSubtleAlgorithm(alg), + extractable, + ['sign', 'verify'], + ) + } catch (err) { + errors.push(err) + } + } + + throw new AggregateError(errors, 'Failed to generate keypair') +} diff --git a/src/oauth-client-temp/client/validate-client-metadata.ts b/src/oauth-client-temp/client/validate-client-metadata.ts new file mode 100644 index 0000000000..fdc3aece4d --- /dev/null +++ b/src/oauth-client-temp/client/validate-client-metadata.ts @@ -0,0 +1,81 @@ +import {OAuthClientMetadata} from '@atproto/oauth-client-metadata' + +import {Keyset} from '#/oauth-client-temp/jwk' + +export function validateClientMetadata( + metadata: OAuthClientMetadata, + keyset?: Keyset, +): asserts metadata is OAuthClientMetadata & {client_id: string} { + if (!metadata.client_id) { + throw new TypeError('client_id must be provided') + } + + const url = new URL(metadata.client_id) + if (url.pathname !== '/') { + throw new TypeError('origin must be a URL root') + } + if (url.username || url.password) { + throw new TypeError('client_id URI must not contain a username or password') + } + if (url.search || url.hash) { + throw new TypeError('client_id URI must not contain a query or fragment') + } + if (url.href !== metadata.client_id) { + throw new TypeError('client_id URI must be a normalized URL') + } + + if ( + url.hostname === 'localhost' || + url.hostname === '[::1]' || + url.hostname === '127.0.0.1' + ) { + if (url.protocol !== 'http:' || url.port) { + throw new TypeError('loopback clients must use "http:" and port "80"') + } + } + + if (metadata.client_uri && metadata.client_uri !== metadata.client_id) { + throw new TypeError('client_uri must match client_id') + } + + if (!metadata.redirect_uris.length) { + throw new TypeError('At least one redirect_uri must be provided') + } + for (const u of metadata.redirect_uris) { + const redirectUrl = new URL(u) + // Loopback redirect_uris require special handling + if ( + redirectUrl.hostname === 'localhost' || + redirectUrl.hostname === '[::1]' || + redirectUrl.hostname === '127.0.0.1' + ) { + if (redirectUrl.protocol !== 'http:') { + throw new TypeError('loopback redirect_uris must use "http:"') + } + } else { + // Not a loopback client + if (redirectUrl.origin !== url.origin) { + throw new TypeError('redirect_uris must have the same origin') + } + } + } + + for (const endpoint of [ + 'token', + 'revocation', + 'introspection', + 'pushed_authorization_request', + ] as const) { + const method = metadata[`${endpoint}_endpoint_auth_method`] + if (method && method !== 'none') { + if (!keyset) { + throw new TypeError(`Keyset is required for ${method} method`) + } + if (!metadata[`${endpoint}_endpoint_auth_signing_alg`]) { + throw new TypeError( + `${endpoint}_endpoint_auth_signing_alg must be provided`, + ) + } + } + } +} diff --git a/src/oauth-client-temp/disposable-polyfill/index.ts b/src/oauth-client-temp/disposable-polyfill/index.ts new file mode 100644 index 0000000000..ddb9073b16 --- /dev/null +++ b/src/oauth-client-temp/disposable-polyfill/index.ts @@ -0,0 +1,10 @@ +// Code compiled with tsc supports "using" and "await using" syntax. This +// features is supported by downleveling the code to ES2017. The downleveling +// relies on `Symbol.dispose` and `Symbol.asyncDispose` symbols. These symbols +// might not be available in all environments. This package provides a polyfill +// for these symbols. + +// @ts-expect-error +Symbol.dispose ??= Symbol('@@dispose') +// @ts-expect-error +Symbol.asyncDispose ??= Symbol('@@asyncDispose') diff --git a/src/oauth-client-temp/fetch-dpop/index.ts b/src/oauth-client-temp/fetch-dpop/index.ts new file mode 100644 index 0000000000..eb3f3e4a45 --- /dev/null +++ b/src/oauth-client-temp/fetch-dpop/index.ts @@ -0,0 +1,174 @@ +import {GenericStore} from '@atproto/caching' +import {Fetch} from '@atproto/fetch' + +import {b64uEncode} from '#/oauth-client-temp/b64' +import {Key} from '#/oauth-client-temp/jwk' + +export function dpopFetchWrapper({ + key, + iss, + alg, + // TODO REVIEW POLYFILL + // @ts-ignore Polyfilled + sha256 = typeof crypto !== 'undefined' && crypto.subtle != null + ? subtleSha256 + : undefined, + nonceCache, +}: { + key: Key + iss: string + alg?: string + sha256?: (input: string) => Promise + nonceCache?: GenericStore + fetch?: Fetch +}): Fetch { + if (!sha256) { + throw new Error( + `crypto.subtle is not available in this environment. Please provide a sha256 function.`, + ) + } + + return async function (request) { + return dpopFetch.call( + this, + request, + key, + iss, + alg, + sha256, + nonceCache, + fetch, + ) + } +} + +export async function dpopFetch( + this: ThisParameterType, + request: Request, + key: Key, + iss: string, + alg: string = key.alg || 'ES256', + sha256: (input: string) => string | PromiseLike = subtleSha256, + nonceCache?: GenericStore, + fetch = globalThis.fetch as Fetch, +): Promise { + const authorizationHeader = request.headers.get('Authorization') + const ath = authorizationHeader?.startsWith('DPoP ') + ? await sha256(authorizationHeader.slice(5)) + : undefined + + const {origin} = new URL(request.url) + + // Clone request for potential retry + const clonedRequest = request.clone() + + // Try with the previously known nonce + const oldNonce = await Promise.resolve() + .then(() => nonceCache?.get(origin)) + .catch(() => undefined) // Ignore cache.get errors + + request.headers.set( + 'DPoP', + await buildProof(key, alg, iss, request.method, request.url, oldNonce, ath), + ) + + const response = await fetch(request) + + const nonce = response.headers.get('DPoP-Nonce') + if (!nonce) return response + + // Store the fresh nonce for future requests + try { + await nonceCache?.set(origin, nonce) + } catch { + // Ignore cache.set errors + } + + if (!(await isUseDpopNonceError(response))) { + return response + } + + clonedRequest.headers.set( + 'DPoP', + await buildProof(key, alg, iss, request.method, request.url, nonce, ath), + ) + + return fetch(clonedRequest) +} + +async function buildProof( + key: Key, + alg: string, + iss: string, + htm: string, + htu: string, + nonce?: string, + ath?: string, +) { + if (!key.bareJwk) { + throw new Error('Only asymetric keys can be used as DPoP proofs') + } + + const now = Math.floor(Date.now() / 1e3) + + return key.createJwt( + { + alg, + typ: 'dpop+jwt', + jwk: key.bareJwk, + }, + { + iss, + iat: now, + exp: now + 10, + // Any collision will cause the request to be rejected by the server. no biggie. + jti: Math.random().toString(36).slice(2), + htm, + htu, + nonce, + ath, + }, + ) +} + +async function isUseDpopNonceError(response: Response): Promise { + if (response.status !== 400) { + return false + } + + const ct = response.headers.get('Content-Type') + const mime = ct?.split(';')[0]?.trim() + if (mime !== 'application/json') { + return false + } + + try { + const body = await response.clone().json() + return body?.error === 'use_dpop_nonce' + } catch { + return false + } +} + +function subtleSha256(input: string): Promise { + // TODO REVIEW POLYFILL + // @ts-ignore Polyfilled + if (typeof crypto === 'undefined' || crypto.subtle == null) { + throw new Error( + `crypto.subtle is not available in this environment. Please provide a sha256 function.`, + ) + } + + // TODO REVIEW POLYFILL + // @ts-ignore Polyfilled + return ( + // TODO REVIEW POLYFILL + // @ts-ignore Polyfilled + crypto.subtle + // TODO REVIEW POLYFILL + // @ts-ignore Polyfilled + .digest('SHA-256', new TextEncoder().encode(input)) + // TODO OAUTH types + .then((digest: Iterable) => b64uEncode(new Uint8Array(digest))) + ) +} diff --git a/src/oauth-client-temp/identity-resolver/identity-resolver.ts b/src/oauth-client-temp/identity-resolver/identity-resolver.ts new file mode 100644 index 0000000000..88e592ff2b --- /dev/null +++ b/src/oauth-client-temp/identity-resolver/identity-resolver.ts @@ -0,0 +1,35 @@ +import { DidResolver } from '@atproto/did' +import { + HandleResolver, + ResolvedHandle, + isResolvedHandle, +} from '@atproto/handle-resolver' +import { normalizeAndEnsureValidHandle } from '@atproto/syntax' + +export type ResolvedIdentity = { + did: NonNullable + url: URL +} + +export class IdentityResolver { + constructor( + readonly handleResolver: HandleResolver, + readonly didResolver: DidResolver<'plc' | 'web'>, + ) {} + + public async resolve( + input: string, + serviceType = 'AtprotoPersonalDataServer', + ): Promise { + const did = isResolvedHandle(input) + ? input // Already a did + : await this.handleResolver.resolve(normalizeAndEnsureValidHandle(input)) + if (!did) throw new Error(`Handle ${input} does not resolve to a DID`) + + const url = await this.didResolver.resolveServiceEndpoint(did, { + type: serviceType, + }) + + return { did, url } + } +} diff --git a/src/oauth-client-temp/identity-resolver/index.ts b/src/oauth-client-temp/identity-resolver/index.ts new file mode 100644 index 0000000000..bccd3ae900 --- /dev/null +++ b/src/oauth-client-temp/identity-resolver/index.ts @@ -0,0 +1,2 @@ +export * from './identity-resolver' +export * from './universal-identity-resolver' diff --git a/src/oauth-client-temp/identity-resolver/universal-identity-resolver.ts b/src/oauth-client-temp/identity-resolver/universal-identity-resolver.ts new file mode 100644 index 0000000000..a9201d93c2 --- /dev/null +++ b/src/oauth-client-temp/identity-resolver/universal-identity-resolver.ts @@ -0,0 +1,52 @@ +import { + DidCache, + IsomorphicDidResolver, + IsomorphicDidResolverOptions, +} from '@atproto/did' +import {Fetch} from '@atproto/fetch' +import UniversalHandleResolver, { + HandleResolverCache, + UniversalHandleResolverOptions, +} from '@atproto/handle-resolver' + +import {IdentityResolver} from './identity-resolver' + +export type UniversalIdentityResolverOptions = { + fetch?: Fetch + + didCache?: DidCache + handleCache?: HandleResolverCache + + /** + * @see {@link IsomorphicDidResolverOptions.plcDirectoryUrl} + */ + plcDirectoryUrl?: IsomorphicDidResolverOptions['plcDirectoryUrl'] + + /** + * @see {@link UniversalHandleResolverOptions.atprotoLexiconUrl} + */ + atprotoLexiconUrl?: UniversalHandleResolverOptions['atprotoLexiconUrl'] +} + +export class UniversalIdentityResolver extends IdentityResolver { + static from({ + fetch = globalThis.fetch, + didCache, + handleCache, + plcDirectoryUrl, + atprotoLexiconUrl, + }: UniversalIdentityResolverOptions) { + return new this( + new UniversalHandleResolver({ + fetch, + cache: handleCache, + atprotoLexiconUrl, + }), + new IsomorphicDidResolver({ + fetch, // + cache: didCache, + plcDirectoryUrl, + }), + ) + } +} diff --git a/src/oauth-client-temp/indexed-db/db-index.web.ts b/src/oauth-client-temp/indexed-db/db-index.web.ts new file mode 100644 index 0000000000..b06fdd5844 --- /dev/null +++ b/src/oauth-client-temp/indexed-db/db-index.web.ts @@ -0,0 +1,44 @@ +import {ObjectStoreSchema} from './schema' +import {promisify} from './util' + +export class DbIndexWeb { + constructor(private idbIndex: IDBIndex) {} + + count(query?: IDBValidKey | IDBKeyRange) { + return promisify(this.idbIndex.count(query)) + } + + get(query: IDBValidKey | IDBKeyRange) { + return promisify(this.idbIndex.get(query)) + } + + getKey(query: IDBValidKey | IDBKeyRange) { + return promisify(this.idbIndex.getKey(query)) + } + + getAll(query?: IDBValidKey | IDBKeyRange | null, count?: number) { + return promisify(this.idbIndex.getAll(query, count)) + } + + getAllKeys(query?: IDBValidKey | IDBKeyRange | null, count?: number) { + return promisify(this.idbIndex.getAllKeys(query, count)) + } + + deleteAll(query?: IDBValidKey | IDBKeyRange | null): Promise { + return new Promise((resolve, reject) => { + const result = this.idbIndex.openCursor(query) + result.onsuccess = function (event) { + const cursor = (event as any).target.result as IDBCursorWithValue + if (cursor) { + cursor.delete() + cursor.continue() + } else { + resolve() + } + } + result.onerror = function (event) { + reject((event.target as any)?.error || new Error('Unexpected error')) + } + }) + } +} diff --git a/src/oauth-client-temp/indexed-db/db-object-store.web.ts b/src/oauth-client-temp/indexed-db/db-object-store.web.ts new file mode 100644 index 0000000000..44c20a7004 --- /dev/null +++ b/src/oauth-client-temp/indexed-db/db-object-store.web.ts @@ -0,0 +1,47 @@ +import {DbIndexWeb} from './db-index.web' +import {ObjectStoreSchema} from './schema' +import {promisify} from './util' + +export class DbObjectStoreWeb { + constructor(private idbObjStore: IDBObjectStore) {} + + get name() { + return this.idbObjStore.name + } + + index(name: string) { + return new DbIndexWeb(this.idbObjStore.index(name)) + } + + get(key: IDBValidKey | IDBKeyRange) { + return promisify(this.idbObjStore.get(key)) + } + + getKey(query: IDBValidKey | IDBKeyRange) { + return promisify(this.idbObjStore.getKey(query)) + } + + getAll(query?: IDBValidKey | IDBKeyRange | null, count?: number) { + return promisify(this.idbObjStore.getAll(query, count)) + } + + getAllKeys(query?: IDBValidKey | IDBKeyRange | null, count?: number) { + return promisify(this.idbObjStore.getAllKeys(query, count)) + } + + add(value: Schema, key?: IDBValidKey) { + return promisify(this.idbObjStore.add(value, key)) + } + + put(value: Schema, key?: IDBValidKey) { + return promisify(this.idbObjStore.put(value, key)) + } + + delete(key: IDBValidKey | IDBKeyRange) { + return promisify(this.idbObjStore.delete(key)) + } + + clear() { + return promisify(this.idbObjStore.clear()) + } +} diff --git a/src/oauth-client-temp/indexed-db/db-transaction.web.ts b/src/oauth-client-temp/indexed-db/db-transaction.web.ts new file mode 100644 index 0000000000..08fddc9afe --- /dev/null +++ b/src/oauth-client-temp/indexed-db/db-transaction.web.ts @@ -0,0 +1,52 @@ +import {DbObjectStoreWeb} from './db-object-store.web' +import {DatabaseSchema} from './schema' + +export class DbTransactionWeb + implements Disposable +{ + #tx: IDBTransaction | null + + constructor(tx: IDBTransaction) { + this.#tx = tx + + const onAbort = () => { + cleanup() + } + const onComplete = () => { + cleanup() + } + const cleanup = () => { + this.#tx = null + tx.removeEventListener('abort', onAbort) + tx.removeEventListener('complete', onComplete) + } + tx.addEventListener('abort', onAbort) + tx.addEventListener('complete', onComplete) + } + + protected get tx(): IDBTransaction { + if (!this.#tx) throw new Error('Transaction already ended') + return this.#tx + } + + async abort() { + const {tx} = this + this.#tx = null + tx.abort() + } + + async commit() { + const {tx} = this + this.#tx = null + tx.commit?.() + } + + objectStore(name: T) { + const store = this.tx.objectStore(name) + return new DbObjectStoreWeb(store) + } + + [Symbol.dispose](): void { + if (this.#tx) this.commit() + } +} diff --git a/src/oauth-client-temp/indexed-db/db.ts b/src/oauth-client-temp/indexed-db/db.ts new file mode 100644 index 0000000000..5d83361657 --- /dev/null +++ b/src/oauth-client-temp/indexed-db/db.ts @@ -0,0 +1,9 @@ +import {DatabaseSchema} from '#/oauth-client-temp/indexed-db/schema' + +export class Db implements Disposable { + static async open( + dbname: string, + migrations: ReadonlyArray<(db: IDBDatabase) => void>, + txOptions?: IDBTransactionOptions, + ) +} diff --git a/src/oauth-client-temp/indexed-db/db.web.ts b/src/oauth-client-temp/indexed-db/db.web.ts new file mode 100644 index 0000000000..a983b7f530 --- /dev/null +++ b/src/oauth-client-temp/indexed-db/db.web.ts @@ -0,0 +1,113 @@ +import {DbTransactionWeb} from './db-transaction.web' +import {DatabaseSchema} from './schema' + +export class Db implements Disposable { + static async open( + dbName: string, + migrations: ReadonlyArray<(db: IDBDatabase) => void>, + txOptions?: IDBTransactionOptions, + ) { + const db = await new Promise((resolve, reject) => { + const request = indexedDB.open(dbName, migrations.length) + + request.onerror = () => reject(request.error) + request.onsuccess = () => resolve(request.result) + request.onupgradeneeded = ({oldVersion, newVersion}) => { + const db = request.result + try { + for ( + let version = oldVersion; + version < (newVersion ?? migrations.length); + ++version + ) { + const migration = migrations[version] + if (migration) migration(db) + else throw new Error(`Missing migration for version ${version}`) + } + } catch (err) { + db.close() + reject(err) + } + } + }) + + return new DbWeb(db, txOptions) + } + + #db: null | IDBDatabase + + constructor( + db: IDBDatabase, + protected readonly txOptions?: IDBTransactionOptions, + ) { + this.#db = db + + const cleanup = () => { + this.#db = null + db.removeEventListener('versionchange', cleanup) + db.removeEventListener('close', cleanup) + db.close() // Can we call close on a "closed" database? + } + + db.addEventListener('versionchange', cleanup) + db.addEventListener('close', cleanup) + } + + protected get db(): IDBDatabase { + if (!this.#db) throw new Error('Database closed') + return this.#db + } + + get name() { + return this.db.name + } + + get objectStoreNames() { + return this.db.objectStoreNames + } + + get version() { + return this.db.version + } + + async transaction( + storeNames: T, + mode: IDBTransactionMode, + run: (tx: DbTransactionWeb>) => R | PromiseLike, + ): Promise { + return new Promise(async (resolve, reject) => { + try { + const tx = this.db.transaction(storeNames, mode, this.txOptions) + let result: {done: false} | {done: true; value: R} = {done: false} + + tx.oncomplete = () => { + if (result.done) resolve(result.value) + else reject(new Error('Transaction completed without result')) + } + tx.onerror = () => reject(tx.error) + tx.onabort = () => reject(tx.error || new Error('Transaction aborted')) + + try { + const value = await run(new DbTransactionWeb(tx)) + result = {done: true, value} + tx.commit() + } catch (err) { + tx.abort() + throw err + } + } catch (err) { + reject(err) + } + }) + } + + close() { + const {db} = this + this.#db = null + db.close() + } + + [Symbol.dispose]() { + if (this.#db) return this.close() + } +} diff --git a/src/oauth-client-temp/indexed-db/index.web.ts b/src/oauth-client-temp/indexed-db/index.web.ts new file mode 100644 index 0000000000..d1b6d4e0dd --- /dev/null +++ b/src/oauth-client-temp/indexed-db/index.web.ts @@ -0,0 +1,6 @@ +import '#/oauth-client-temp/disposable-polyfill' + +export * from './db.web' +export * from './db.web' +export * from './db-index.web' +export * from './db-object-store.web' diff --git a/src/oauth-client-temp/indexed-db/schema.ts b/src/oauth-client-temp/indexed-db/schema.ts new file mode 100644 index 0000000000..f8736b2a19 --- /dev/null +++ b/src/oauth-client-temp/indexed-db/schema.ts @@ -0,0 +1,2 @@ +export type ObjectStoreSchema = NonNullable +export type DatabaseSchema = Record diff --git a/src/oauth-client-temp/indexed-db/util.web.ts b/src/oauth-client-temp/indexed-db/util.web.ts new file mode 100644 index 0000000000..6e52b5919c --- /dev/null +++ b/src/oauth-client-temp/indexed-db/util.web.ts @@ -0,0 +1,20 @@ +export function promisify(request: IDBRequest) { + const promise = new Promise((resolve, reject) => { + const cleanup = () => { + request.removeEventListener('success', success) + request.removeEventListener('error', error) + } + const success = () => { + resolve(request.result) + cleanup() + } + const error = () => { + reject(request.error) + cleanup() + } + request.addEventListener('success', success) + request.addEventListener('error', error) + }) + + return promise +} diff --git a/src/oauth-client-temp/jwk-jose/index.ts b/src/oauth-client-temp/jwk-jose/index.ts new file mode 100644 index 0000000000..179625dd51 --- /dev/null +++ b/src/oauth-client-temp/jwk-jose/index.ts @@ -0,0 +1,2 @@ +export * from './jose-key' +export * from './jose-keyset' diff --git a/src/oauth-client-temp/jwk-jose/jose-key.ts b/src/oauth-client-temp/jwk-jose/jose-key.ts new file mode 100644 index 0000000000..baeb31ff2c --- /dev/null +++ b/src/oauth-client-temp/jwk-jose/jose-key.ts @@ -0,0 +1,115 @@ +import { + exportJWK, + importJWK, + importPKCS8, + JWK, + jwtVerify, + JWTVerifyOptions, + KeyLike, + SignJWT, +} from 'jose' + +import { + Jwk, + jwkSchema, + Jwt, + JwtHeader, + JwtPayload, + Key, + VerifyOptions, + VerifyPayload, + VerifyResult, +} from '#/oauth-client-temp/jwk' +import {either} from './util' + +export type Importable = string | KeyLike | Jwk + +export class JoseKey extends Key { + #keyObj?: KeyLike | Uint8Array + + protected async getKey() { + return (this.#keyObj ||= await importJWK(this.jwk as JWK)) + } + + async createJwt(header: JwtHeader, payload: JwtPayload) { + if (header.kid && header.kid !== this.kid) { + throw new TypeError( + `Invalid "kid" (${header.kid}) used to sign with key "${this.kid}"`, + ) + } + + if (!header.alg || !this.algorithms.includes(header.alg)) { + throw new TypeError( + `Invalid "alg" (${header.alg}) used to sign with key "${this.kid}"`, + ) + } + + const keyObj = await this.getKey() + return new SignJWT(payload) + .setProtectedHeader({...header, kid: this.kid}) + .sign(keyObj) as Promise + } + + async verifyJwt< + P extends VerifyPayload = JwtPayload, + C extends string = string, + >(token: Jwt, options?: VerifyOptions): Promise> { + const keyObj = await this.getKey() + const result = await jwtVerify(token, keyObj, { + ...options, + algorithms: this.algorithms, + } as JWTVerifyOptions) + return result as VerifyResult + } + + static async fromImportable( + input: Importable, + kid?: string, + ): Promise { + if (typeof input === 'string') { + // PKCS8 + if (input.startsWith('-----')) { + return this.fromPKCS8(input, kid) + } + + // Jwk (string) + if (input.startsWith('{')) { + return this.fromJWK(input, kid) + } + + throw new TypeError('Invalid input') + } + + if (typeof input === 'object') { + // Jwk + if ('kty' in input || 'alg' in input) { + return this.fromJWK(input, kid) + } + + // KeyLike + return this.fromJWK(await exportJWK(input), kid) + } + + throw new TypeError('Invalid input') + } + + static async fromPKCS8(pem: string, kid?: string): Promise { + const keyLike = await importPKCS8(pem, '', {extractable: true}) + return this.fromJWK(await exportJWK(keyLike), kid) + } + + static async fromJWK( + input: string | Record, + inputKid?: string, + ): Promise { + const jwk = jwkSchema.parse( + typeof input === 'string' ? JSON.parse(input) : input, + ) + + const kid = either(jwk.kid, inputKid) + const alg = jwk.alg + const use = jwk.use || 'sig' + + return new JoseKey({...jwk, kid, alg, use}) + } +} diff --git a/src/oauth-client-temp/jwk-jose/jose-keyset.ts b/src/oauth-client-temp/jwk-jose/jose-keyset.ts new file mode 100644 index 0000000000..27baefcfda --- /dev/null +++ b/src/oauth-client-temp/jwk-jose/jose-keyset.ts @@ -0,0 +1,16 @@ +import {Key, Keyset} from '#/oauth-client-temp/jwk' +import {Importable, JoseKey} from './jose-key' + +export class JoseKeyset extends Keyset { + static async fromImportables( + input: Record, + ) { + return new JoseKeyset( + await Promise.all( + Object.entries(input).map(([kid, secret]) => + secret instanceof Key ? secret : JoseKey.fromImportable(secret, kid), + ), + ), + ) + } +} diff --git a/src/oauth-client-temp/jwk-jose/util.ts b/src/oauth-client-temp/jwk-jose/util.ts new file mode 100644 index 0000000000..f75cdb6671 --- /dev/null +++ b/src/oauth-client-temp/jwk-jose/util.ts @@ -0,0 +1,9 @@ +export function either( + a?: T, + b?: T, +): T | undefined { + if (a != null && b != null && a !== b) { + throw new TypeError(`Expected "${b}", got "${a}"`) + } + return a ?? b ?? undefined +} diff --git a/src/oauth-client-temp/jwk/alg.ts b/src/oauth-client-temp/jwk/alg.ts index 226a1bb662..8be3555018 100644 --- a/src/oauth-client-temp/jwk/alg.ts +++ b/src/oauth-client-temp/jwk/alg.ts @@ -1,6 +1,6 @@ -import { Jwk } from './jwk.js' +import {Jwk} from './jwk' -declare const process: undefined | { versions?: { node?: string } } +declare const process: undefined | {versions?: {node?: string}} const IS_NODE_RUNTIME = typeof process !== 'undefined' && typeof process?.versions?.node === 'string' diff --git a/src/oauth-client-temp/jwk/index.ts b/src/oauth-client-temp/jwk/index.ts index 3e1c678233..79393d6eea 100644 --- a/src/oauth-client-temp/jwk/index.ts +++ b/src/oauth-client-temp/jwk/index.ts @@ -1,9 +1,9 @@ -export * from './alg.js' -export * from './jwk.js' -export * from './jwks.js' -export * from './jwt.js' -export * from './jwt-decode.js' -export * from './jwt-verify.js' -export * from './key.js' -export * from './keyset.js' -export * from './util.js' +export * from './alg' +export * from './jwk' +export * from './jwks' +export * from './jwt' +export * from './jwt-decode' +export * from './jwt-verify' +export * from './key' +export * from './keyset' +export * from './util' diff --git a/src/oauth-client-temp/jwk/jwks.ts b/src/oauth-client-temp/jwk/jwks.ts index 1ec8d382ba..b1b333d986 100644 --- a/src/oauth-client-temp/jwk/jwks.ts +++ b/src/oauth-client-temp/jwk/jwks.ts @@ -1,6 +1,6 @@ -import { z } from 'zod' +import {z} from 'zod' -import { jwkPubSchema, jwkSchema } from './jwk.js' +import {jwkPubSchema, jwkSchema} from './jwk' export const jwksSchema = z .object({ diff --git a/src/oauth-client-temp/jwk/jwt-decode.ts b/src/oauth-client-temp/jwk/jwt-decode.ts index 287f8fbc08..7fc5f3ef3f 100644 --- a/src/oauth-client-temp/jwk/jwt-decode.ts +++ b/src/oauth-client-temp/jwk/jwt-decode.ts @@ -1,18 +1,12 @@ -import { b64uDecode } from '@atproto/b64' - -import { ui8ToString } from './util.js' -import { - JwtHeader, - JwtPayload, - jwtHeaderSchema, - jwtPayloadSchema, -} from './jwt.js' +import {b64uDecode} from '#/oauth-client-temp/b64' +import {JwtHeader, jwtHeaderSchema, JwtPayload, jwtPayloadSchema} from './jwt' +import {ui8ToString} from './util' export function unsafeDecodeJwt(jwt: string): { header: JwtHeader payload: JwtPayload } { - const { 0: headerEnc, 1: payloadEnc, length } = jwt.split('.') + const {0: headerEnc, 1: payloadEnc, length} = jwt.split('.') if (length > 3 || length < 2) { throw new TypeError('invalid JWT input') } @@ -28,5 +22,5 @@ export function unsafeDecodeJwt(jwt: string): { JSON.parse(ui8ToString(b64uDecode(payloadEnc!))), ) - return { header, payload } + return {header, payload} } diff --git a/src/oauth-client-temp/jwk/jwt-verify.ts b/src/oauth-client-temp/jwk/jwt-verify.ts index 5eeca81e53..3e05f60ae5 100644 --- a/src/oauth-client-temp/jwk/jwt-verify.ts +++ b/src/oauth-client-temp/jwk/jwt-verify.ts @@ -1,5 +1,5 @@ -import { JwtHeader, JwtPayload } from './jwt.js' -import { RequiredKey } from './util.js' +import {JwtHeader, JwtPayload} from './jwt' +import {RequiredKey} from './util' export type VerifyOptions = { audience?: string | readonly string[] diff --git a/src/oauth-client-temp/jwk/jwt.ts b/src/oauth-client-temp/jwk/jwt.ts index c07af8cb73..51de57916c 100644 --- a/src/oauth-client-temp/jwk/jwt.ts +++ b/src/oauth-client-temp/jwk/jwt.ts @@ -1,6 +1,6 @@ -import { z } from 'zod' +import {z} from 'zod' -import { jwkPubSchema } from './jwk.js' +import {jwkPubSchema} from './jwk' export const JWT_REGEXP = /^[A-Za-z0-9_-]{2,}(?:\.[A-Za-z0-9_-]{2,}){1,2}$/ export const jwtSchema = z diff --git a/src/oauth-client-temp/jwk/key.ts b/src/oauth-client-temp/jwk/key.ts index d8af69e0df..c9923f043e 100644 --- a/src/oauth-client-temp/jwk/key.ts +++ b/src/oauth-client-temp/jwk/key.ts @@ -1,8 +1,8 @@ -import { jwkAlgorithms } from './alg.js' -import { Jwk, jwkSchema } from './jwk.js' -import { VerifyOptions, VerifyPayload, VerifyResult } from './jwt-verify.js' -import { Jwt, JwtHeader, JwtPayload } from './jwt.js' -import { cachedGetter } from './util.js' +import {jwkAlgorithms} from './alg' +import {Jwk, jwkSchema} from './jwk' +import {Jwt, JwtHeader, JwtPayload} from './jwt' +import {VerifyOptions, VerifyPayload, VerifyResult} from './jwt-verify' +import {cachedGetter} from './util' export abstract class Key { constructor(protected jwk: Jwk) { @@ -11,13 +11,13 @@ export abstract class Key { } get isPrivate(): boolean { - const { jwk } = this + const {jwk} = this if ('d' in jwk && jwk.d !== undefined) return true return this.isSymetric } get isSymetric(): boolean { - const { jwk } = this + const {jwk} = this if ('k' in jwk && jwk.k !== undefined) return true return false } @@ -30,7 +30,7 @@ export abstract class Key { get publicJwk(): Jwk | undefined { if (this.isSymetric) return undefined if (this.isPrivate) { - const { d: _, ...jwk } = this.jwk as any + const {d: _, ...jwk} = this.jwk as any return jwk } return this.jwk @@ -39,8 +39,8 @@ export abstract class Key { @cachedGetter get bareJwk(): Jwk | undefined { if (this.isSymetric) return undefined - const { kty, crv, e, n, x, y } = this.jwk as any - return jwkSchema.parse({ crv, e, kty, n, x, y }) + const {kty, crv, e, n, x, y} = this.jwk as any + return jwkSchema.parse({crv, e, kty, n, x, y}) } get use() { @@ -60,7 +60,7 @@ export abstract class Key { } get crv() { - return (this.jwk as undefined | Extract)?.crv + return (this.jwk as undefined | Extract)?.crv } get canVerify() { diff --git a/src/oauth-client-temp/jwk/keyset.ts b/src/oauth-client-temp/jwk/keyset.ts index 9be83677d3..09137aaba5 100644 --- a/src/oauth-client-temp/jwk/keyset.ts +++ b/src/oauth-client-temp/jwk/keyset.ts @@ -1,16 +1,16 @@ -import { Jwk } from './jwk.js' -import { Jwks } from './jwks.js' -import { unsafeDecodeJwt } from './jwt-decode.js' -import { VerifyOptions } from './jwt-verify.js' -import { Jwt, JwtHeader, JwtPayload } from './jwt.js' -import { Key } from './key.js' +import {Jwk} from './jwk' +import {Jwks} from './jwks' +import {Jwt, JwtHeader, JwtPayload} from './jwt' +import {unsafeDecodeJwt} from './jwt-decode' +import {VerifyOptions} from './jwt-verify' +import {Key} from './key' import { - Override, cachedGetter, isDefined, matchesAny, + Override, preferredOrderCmp, -} from './util.js' +} from './util' export type JwtSignHeader = Override> @@ -50,7 +50,7 @@ export class Keyset implements Iterable { if (!keys.length) throw new Error('Keyset is empty') const kids = new Set() - for (const { kid } of keys) { + for (const {kid} of keys) { if (!kid) continue if (kids.has(kid)) throw new Error(`Duplicate key id: ${kid}`) @@ -87,7 +87,7 @@ export class Keyset implements Iterable { } has(kid: string): boolean { - return this.keys.some((key) => key.kid === kid) + return this.keys.some(key => key.kid === kid) } get(search: KeySearch): K { @@ -115,7 +115,7 @@ export class Keyset implements Iterable { } if (Array.isArray(search.alg)) { - if (!search.alg.some((a) => key.algorithms.includes(a))) continue + if (!search.alg.some(a => key.algorithms.includes(a))) continue } else if (typeof search.alg === 'string') { if (!key.algorithms.includes(search.alg)) continue } @@ -125,10 +125,10 @@ export class Keyset implements Iterable { } findSigningKey(search: Omit): [key: Key, alg: string] { - const { kid, alg } = search + const {kid, alg} = search const matchingKeys: Key[] = [] - for (const key of this.list({ kid, alg, use: 'sig' })) { + for (const key of this.list({kid, alg, use: 'sig'})) { // Not a signing key if (!key.canSign) continue @@ -140,7 +140,7 @@ export class Keyset implements Iterable { const isAllowedAlg = matchesAny(alg) const candidates = matchingKeys.map( - (key) => [key, key.algorithms.filter(isAllowedAlg)] as const, + key => [key, key.algorithms.filter(isAllowedAlg)] as const, ) // Return the first candidates that matches the preferred algorithms @@ -165,11 +165,11 @@ export class Keyset implements Iterable { } async sign( - { alg: searchAlg, kid: searchKid, ...header }: JwtSignHeader, + {alg: searchAlg, kid: searchKid, ...header}: JwtSignHeader, payload: JwtPayload | JwtPayloadGetter, ) { - const [key, alg] = this.findSigningKey({ alg: searchAlg, kid: searchKid }) - const protectedHeader = { ...header, alg, kid: key.kid } + const [key, alg] = this.findSigningKey({alg: searchAlg, kid: searchKid}) + const protectedHeader = {...header, alg, kid: key.kid} if (typeof payload === 'function') { payload = await payload(protectedHeader, key) @@ -182,12 +182,12 @@ export class Keyset implements Iterable { P extends Record = JwtPayload, C extends string = string, >(token: Jwt, options?: VerifyOptions) { - const { header } = unsafeDecodeJwt(token) - const { kid, alg } = header + const {header} = unsafeDecodeJwt(token) + const {kid, alg} = header const errors: unknown[] = [] - for (const key of this.list({ use: 'sig', kid, alg })) { + for (const key of this.list({use: 'sig', kid, alg})) { try { return await key.verifyJwt(token, options) } catch (err) { diff --git a/src/screens/Login/hooks/useLogin.ts b/src/screens/Login/hooks/useLogin.ts index b5c9dbc4b1..f1626d7d9e 100644 --- a/src/screens/Login/hooks/useLogin.ts +++ b/src/screens/Login/hooks/useLogin.ts @@ -1,6 +1,5 @@ import React from 'react' import * as Browser from 'expo-web-browser' -import {OAuthClientFactory} from '@atproto/oauth-client' import { buildOAuthUrl, @@ -12,9 +11,11 @@ import { OAUTH_RESPONSE_TYPES, OAUTH_SCOPE, } from 'lib/oauth' +import {CryptoImplementation} from '#/oauth-client-temp/client/crypto-implementation' +import {OAuthClientFactory} from '#/oauth-client-temp/client/oauth-client-factory' // TODO remove hack -const serviceUrl = 'http://localhost:2583/oauth/authorize' +const serviceUrl = 'http://localhost' // Service URL here is just a placeholder, this isn't how it will actually work export function useLogin(serviceUrl: string | undefined) { @@ -29,6 +30,7 @@ export function useLogin(serviceUrl: string | undefined) { dpop_bound_access_tokens: DPOP_BOUND_ACCESS_TOKENS, application_type: OAUTH_APPLICATION_TYPE, }, + cryptoImplementation: new CryptoImplementation(crypto), }) if (!serviceUrl) return diff --git a/yarn.lock b/yarn.lock index a3e634f6aa..d05d3aa797 100644 --- a/yarn.lock +++ b/yarn.lock @@ -4478,14 +4478,6 @@ mkdirp "^1.0.4" rimraf "^3.0.2" -"@pagopa/io-react-native-jwt@^1.1.0": - version "1.1.0" - resolved "https://registry.yarnpkg.com/@pagopa/io-react-native-jwt/-/io-react-native-jwt-1.1.0.tgz#9a8e99672a683a32a27785eb01a7f108561ca8dd" - integrity sha512-R/Cgiu3Qb/7LnzQstUTGkNnsKfQ5lc/O3eSAkzZPGQqQb4hoSch4N/2JgqXRZPeTNfFA2vDmYbV6fSidMDL2xA== - dependencies: - abab "^2.0.6" - zod "^3.21.4" - "@pmmmwh/react-refresh-webpack-plugin@^0.5.11", "@pmmmwh/react-refresh-webpack-plugin@^0.5.3": version "0.5.11" resolved "https://registry.yarnpkg.com/@pmmmwh/react-refresh-webpack-plugin/-/react-refresh-webpack-plugin-0.5.11.tgz#7c2268cedaa0644d677e8c4f377bc8fb304f714a" @@ -15244,6 +15236,11 @@ jose@^5.0.1: resolved "https://registry.yarnpkg.com/jose/-/jose-5.1.3.tgz#303959d85c51b5cb14725f930270b72be56abdca" integrity sha512-GPExOkcMsCLBTi1YetY2LmkoY559fss0+0KVa6kOfb2YFe84nAM7Nm/XzuZozah4iHgmBGrCOHL5/cy670SBRw== +jose@^5.2.4: + version "5.2.4" + resolved "https://registry.yarnpkg.com/jose/-/jose-5.2.4.tgz#c0d296caeeed0b8444a8b8c3b68403d61aa4ed72" + integrity sha512-6ScbIk2WWCeXkmzF6bRPmEuaqy1m8SbsRFMa/FLrSCkGIhj8OLVG/IH+XHVmNMx/KUo8cVWEE6oKR4dJ+S0Rkg== + js-base64@^3.7.2: version "3.7.5" resolved "https://registry.yarnpkg.com/js-base64/-/js-base64-3.7.5.tgz#21e24cf6b886f76d6f5f165bfcd69cc55b9e3fca" From c649795f055de05bbafbee2b86472c8e4daf8629 Mon Sep 17 00:00:00 2001 From: Hailey Date: Tue, 9 Apr 2024 14:30:59 -0700 Subject: [PATCH 20/54] remove all the test files --- src/oauth-client-temp/b64/index.ts | 34 -- src/oauth-client-temp/client/constants.ts | 1 - .../client/crypto-implementation.ts | 47 --- .../client/crypto-wrapper.ts | 173 ----------- src/oauth-client-temp/client/db.ts | 0 src/oauth-client-temp/client/db.web.ts | 0 src/oauth-client-temp/client/key.ts | 78 ----- src/oauth-client-temp/client/keyset.ts | 0 .../client/oauth-callback-error.ts | 16 - .../client/oauth-client-factory.ts | 290 ------------------ src/oauth-client-temp/client/oauth-client.ts | 89 ------ .../client/oauth-database.ts | 6 - .../client/oauth-resolver.ts | 28 -- .../client/oauth-server-factory.ts | 81 ----- src/oauth-client-temp/client/oauth-server.ts | 287 ----------------- src/oauth-client-temp/client/oauth-types.ts | 37 --- .../client/session-getter.ts | 139 --------- src/oauth-client-temp/client/util.ts | 160 ---------- .../client/validate-client-metadata.ts | 81 ----- .../disposable-polyfill/index.ts | 10 - src/oauth-client-temp/fetch-dpop/index.ts | 174 ----------- .../identity-resolver/identity-resolver.ts | 35 --- .../identity-resolver/index.ts | 2 - .../universal-identity-resolver.ts | 52 ---- .../indexed-db/db-index.web.ts | 44 --- .../indexed-db/db-object-store.web.ts | 47 --- .../indexed-db/db-transaction.web.ts | 52 ---- src/oauth-client-temp/indexed-db/db.ts | 9 - src/oauth-client-temp/indexed-db/db.web.ts | 113 ------- src/oauth-client-temp/indexed-db/index.web.ts | 6 - src/oauth-client-temp/indexed-db/schema.ts | 2 - src/oauth-client-temp/indexed-db/util.web.ts | 20 -- src/oauth-client-temp/jwk-jose/index.ts | 2 - src/oauth-client-temp/jwk-jose/jose-key.ts | 115 ------- src/oauth-client-temp/jwk-jose/jose-keyset.ts | 16 - src/oauth-client-temp/jwk-jose/util.ts | 9 - src/oauth-client-temp/jwk/alg.ts | 97 ------ src/oauth-client-temp/jwk/index.ts | 9 - src/oauth-client-temp/jwk/jwk.ts | 153 --------- src/oauth-client-temp/jwk/jwks.ts | 19 -- src/oauth-client-temp/jwk/jwt-decode.ts | 26 -- src/oauth-client-temp/jwk/jwt-verify.ts | 20 -- src/oauth-client-temp/jwk/jwt.ts | 172 ----------- src/oauth-client-temp/jwk/key.ts | 95 ------ src/oauth-client-temp/jwk/keyset.ts | 200 ------------ src/oauth-client-temp/jwk/util.ts | 55 ---- 46 files changed, 3101 deletions(-) delete mode 100644 src/oauth-client-temp/b64/index.ts delete mode 100644 src/oauth-client-temp/client/constants.ts delete mode 100644 src/oauth-client-temp/client/crypto-implementation.ts delete mode 100644 src/oauth-client-temp/client/crypto-wrapper.ts delete mode 100644 src/oauth-client-temp/client/db.ts delete mode 100644 src/oauth-client-temp/client/db.web.ts delete mode 100644 src/oauth-client-temp/client/key.ts delete mode 100644 src/oauth-client-temp/client/keyset.ts delete mode 100644 src/oauth-client-temp/client/oauth-callback-error.ts delete mode 100644 src/oauth-client-temp/client/oauth-client-factory.ts delete mode 100644 src/oauth-client-temp/client/oauth-client.ts delete mode 100644 src/oauth-client-temp/client/oauth-database.ts delete mode 100644 src/oauth-client-temp/client/oauth-resolver.ts delete mode 100644 src/oauth-client-temp/client/oauth-server-factory.ts delete mode 100644 src/oauth-client-temp/client/oauth-server.ts delete mode 100644 src/oauth-client-temp/client/oauth-types.ts delete mode 100644 src/oauth-client-temp/client/session-getter.ts delete mode 100644 src/oauth-client-temp/client/util.ts delete mode 100644 src/oauth-client-temp/client/validate-client-metadata.ts delete mode 100644 src/oauth-client-temp/disposable-polyfill/index.ts delete mode 100644 src/oauth-client-temp/fetch-dpop/index.ts delete mode 100644 src/oauth-client-temp/identity-resolver/identity-resolver.ts delete mode 100644 src/oauth-client-temp/identity-resolver/index.ts delete mode 100644 src/oauth-client-temp/identity-resolver/universal-identity-resolver.ts delete mode 100644 src/oauth-client-temp/indexed-db/db-index.web.ts delete mode 100644 src/oauth-client-temp/indexed-db/db-object-store.web.ts delete mode 100644 src/oauth-client-temp/indexed-db/db-transaction.web.ts delete mode 100644 src/oauth-client-temp/indexed-db/db.ts delete mode 100644 src/oauth-client-temp/indexed-db/db.web.ts delete mode 100644 src/oauth-client-temp/indexed-db/index.web.ts delete mode 100644 src/oauth-client-temp/indexed-db/schema.ts delete mode 100644 src/oauth-client-temp/indexed-db/util.web.ts delete mode 100644 src/oauth-client-temp/jwk-jose/index.ts delete mode 100644 src/oauth-client-temp/jwk-jose/jose-key.ts delete mode 100644 src/oauth-client-temp/jwk-jose/jose-keyset.ts delete mode 100644 src/oauth-client-temp/jwk-jose/util.ts delete mode 100644 src/oauth-client-temp/jwk/alg.ts delete mode 100644 src/oauth-client-temp/jwk/index.ts delete mode 100644 src/oauth-client-temp/jwk/jwk.ts delete mode 100644 src/oauth-client-temp/jwk/jwks.ts delete mode 100644 src/oauth-client-temp/jwk/jwt-decode.ts delete mode 100644 src/oauth-client-temp/jwk/jwt-verify.ts delete mode 100644 src/oauth-client-temp/jwk/jwt.ts delete mode 100644 src/oauth-client-temp/jwk/key.ts delete mode 100644 src/oauth-client-temp/jwk/keyset.ts delete mode 100644 src/oauth-client-temp/jwk/util.ts diff --git a/src/oauth-client-temp/b64/index.ts b/src/oauth-client-temp/b64/index.ts deleted file mode 100644 index 6acab5725e..0000000000 --- a/src/oauth-client-temp/b64/index.ts +++ /dev/null @@ -1,34 +0,0 @@ -import {fromByteArray, toByteArray} from 'base64-js' - -// Old Node implementations do not support "base64url" -const Buffer = (Buffer => { - if (typeof Buffer === 'function') { - try { - Buffer.from('', 'base64url') - return Buffer - } catch { - return undefined - } - } - return undefined -})(globalThis.Buffer) - -export const b64uDecode: (b64u: string) => Uint8Array = Buffer - ? b64u => Buffer.from(b64u, 'base64url') - : b64u => { - // toByteArray requires padding but not to replace '-' and '_' - const pad = b64u.length % 4 - const b64 = b64u.padEnd(b64u.length + (pad > 0 ? 4 - pad : 0), '=') - return toByteArray(b64) - } - -export const b64uEncode = Buffer - ? (bytes: Uint8Array) => { - const buffer = bytes instanceof Buffer ? bytes : Buffer.from(bytes) - return buffer.toString('base64url') - } - : (bytes: Uint8Array): string => - fromByteArray(bytes) - .replace(/\+/g, '-') - .replace(/\//g, '_') - .replace(/[=]+$/g, '') diff --git a/src/oauth-client-temp/client/constants.ts b/src/oauth-client-temp/client/constants.ts deleted file mode 100644 index dca10c5ce3..0000000000 --- a/src/oauth-client-temp/client/constants.ts +++ /dev/null @@ -1 +0,0 @@ -export const FALLBACK_ALG = 'ES256' diff --git a/src/oauth-client-temp/client/crypto-implementation.ts b/src/oauth-client-temp/client/crypto-implementation.ts deleted file mode 100644 index 237485068d..0000000000 --- a/src/oauth-client-temp/client/crypto-implementation.ts +++ /dev/null @@ -1,47 +0,0 @@ -import {CryptoKey} from '#/oauth-client-temp/client/key' -import {Key} from '#/oauth-client-temp/jwk' - -// TODO this might not be necessary with this setup, we will see - -export type DigestAlgorithm = { - name: 'sha256' | 'sha384' | 'sha512' -} - -export class CryptoImplementation { - public async createKey(algs: string[]): Promise { - return CryptoKey.generate(undefined, algs) - } - - getRandomValues(byteLength: number): Uint8Array { - const bytes = new Uint8Array(byteLength) - // TODO REVIEW POLYFILL - // @ts-ignore Polyfilled - return crypto.getRandomValues(bytes) - } - - async digest( - bytes: Uint8Array, - algorithm: DigestAlgorithm, - ): Promise { - // TODO REVIEW POLYFILL - // @ts-ignore Polyfilled - const buffer = await this.crypto.subtle.digest( - digestAlgorithmToSubtle(algorithm), - bytes, - ) - return new Uint8Array(buffer) - } -} - -// TODO OAUTH types -// @ts-ignore -function digestAlgorithmToSubtle({name}: DigestAlgorithm): AlgorithmIdentifier { - switch (name) { - case 'sha256': - case 'sha384': - case 'sha512': - return `SHA-${name.slice(-3)}` - default: - throw new Error(`Unknown hash algorithm ${name}`) - } -} diff --git a/src/oauth-client-temp/client/crypto-wrapper.ts b/src/oauth-client-temp/client/crypto-wrapper.ts deleted file mode 100644 index d893319c61..0000000000 --- a/src/oauth-client-temp/client/crypto-wrapper.ts +++ /dev/null @@ -1,173 +0,0 @@ -import {b64uEncode} from '#/oauth-client-temp/b64' -import { - JwtHeader, - JwtPayload, - Key, - unsafeDecodeJwt, -} from '#/oauth-client-temp/jwk' -import {CryptoImplementation, DigestAlgorithm} from './crypto-implementation' - -export class CryptoWrapper { - constructor(protected implementation: CryptoImplementation) {} - - public async generateKey(algs: string[]): Promise { - return this.implementation.createKey(algs) - } - - public async sha256(text: string): Promise { - // TODO REVIEW POLYFILL - // @ts-ignore Polyfilled - const bytes = new TextEncoder().encode(text) - const digest = await this.implementation.digest(bytes, {name: 'sha256'}) - return b64uEncode(digest) - } - - public async generateNonce(length = 16): Promise { - const bytes = await this.implementation.getRandomValues(length) - return b64uEncode(bytes) - } - - public async validateIdTokenClaims( - token: string, - state: string, - nonce: string, - code?: string, - accessToken?: string, - ): Promise<{ - header: JwtHeader - payload: JwtPayload - }> { - // It's fine to use unsafeDecodeJwt here because the token was received from - // the server's token endpoint. The following checks are to ensure that the - // oauth flow was indeed initiated by the client. - const {header, payload} = unsafeDecodeJwt(token) - if (!payload.nonce || payload.nonce !== nonce) { - throw new TypeError('Nonce mismatch') - } - if (payload.c_hash) { - await this.validateHashClaim(payload.c_hash, code, header) - } - if (payload.s_hash) { - await this.validateHashClaim(payload.s_hash, state, header) - } - if (payload.at_hash) { - await this.validateHashClaim(payload.at_hash, accessToken, header) - } - return {header, payload} - } - - private async validateHashClaim( - claim: unknown, - source: unknown, - header: {alg: string; crv?: string}, - ): Promise { - if (typeof claim !== 'string' || !claim) { - throw new TypeError(`string "_hash" claim expected`) - } - if (typeof source !== 'string' || !source) { - throw new TypeError(`string value expected`) - } - const expected = await this.generateHashClaim(source, header) - if (expected !== claim) { - throw new TypeError(`"_hash" does not match`) - } - } - - protected async generateHashClaim( - source: string, - header: {alg: string; crv?: string}, - ) { - const algo = getHashAlgo(header) - // TODO REVIEW POLYFILL - // @ts-ignore Polyfilled - const bytes = new TextEncoder().encode(source) - const digest = await this.implementation.digest(bytes, algo) - return b64uEncode(digest.slice(0, digest.length / 2)) - } - - public async generatePKCE(byteLength?: number) { - const verifier = await this.generateVerifier(byteLength) - return { - verifier, - challenge: await this.sha256(verifier), - method: 'S256', - } - } - - // TODO OAUTH types - public async calculateJwkThumbprint(jwk: any) { - const components = extractJktComponents(jwk) - const data = JSON.stringify(components) - return this.sha256(data) - } - - /** - * @see {@link https://datatracker.ietf.org/doc/html/rfc7636#section-4.1} - * @note It is RECOMMENDED that the output of a suitable random number generator - * be used to create a 32-octet sequence. The octet sequence is then - * base64url-encoded to produce a 43-octet URL safe string to use as the code - * verifier. - */ - protected async generateVerifier(byteLength = 32) { - if (byteLength < 32 || byteLength > 96) { - throw new TypeError('Invalid code_verifier length') - } - const bytes = await this.implementation.getRandomValues(byteLength) - return b64uEncode(bytes) - } -} - -function getHashAlgo(header: {alg: string; crv?: string}): DigestAlgorithm { - switch (header.alg) { - case 'HS256': - case 'RS256': - case 'PS256': - case 'ES256': - case 'ES256K': - return {name: 'sha256'} - case 'HS384': - case 'RS384': - case 'PS384': - case 'ES384': - return {name: 'sha384'} - case 'HS512': - case 'RS512': - case 'PS512': - case 'ES512': - return {name: 'sha512'} - case 'EdDSA': - switch (header.crv) { - case 'Ed25519': - return {name: 'sha512'} - default: - throw new TypeError('unrecognized or invalid EdDSA curve provided') - } - default: - throw new TypeError('unrecognized or invalid JWS algorithm provided') - } -} - -// TODO OAUTH types -function extractJktComponents(jwk: {[x: string]: any; kty: any}) { - // TODO OAUTH types - const get = (field: string) => { - const value = jwk[field] - if (typeof value !== 'string' || !value) { - throw new TypeError(`"${field}" Parameter missing or invalid`) - } - return value - } - - switch (jwk.kty) { - case 'EC': - return {crv: get('crv'), kty: get('kty'), x: get('x'), y: get('y')} - case 'OKP': - return {crv: get('crv'), kty: get('kty'), x: get('x')} - case 'RSA': - return {e: get('e'), kty: get('kty'), n: get('n')} - case 'oct': - return {k: get('k'), kty: get('kty')} - default: - throw new TypeError('"kty" (Key Type) Parameter missing or unsupported') - } -} diff --git a/src/oauth-client-temp/client/db.ts b/src/oauth-client-temp/client/db.ts deleted file mode 100644 index e69de29bb2..0000000000 diff --git a/src/oauth-client-temp/client/db.web.ts b/src/oauth-client-temp/client/db.web.ts deleted file mode 100644 index e69de29bb2..0000000000 diff --git a/src/oauth-client-temp/client/key.ts b/src/oauth-client-temp/client/key.ts deleted file mode 100644 index 228e0471d1..0000000000 --- a/src/oauth-client-temp/client/key.ts +++ /dev/null @@ -1,78 +0,0 @@ -import { - fromSubtleAlgorithm, - generateKeypair, - isSignatureKeyPair, -} from '#/oauth-client-temp/client/util' -import {Jwk, jwkSchema} from '#/oauth-client-temp/jwk' -import {JoseKey} from '#/oauth-client-temp/jwk-jose' - -export class CryptoKey extends JoseKey { - // static async fromIndexedDB(kid: string, allowedAlgos: string[] = ['ES384']) { - // const cryptoKeyPair = await loadCryptoKeyPair(kid, allowedAlgos) - // return this.fromKeypair(kid, cryptoKeyPair) - // } - - static async generate( - // TODO REVIEW POLYFILL - // @ts-ignore Polyfilled - kid: string = crypto.randomUUID(), - allowedAlgos: string[] = ['ES384'], - exportable = false, - ) { - const cryptoKeyPair = await generateKeypair(allowedAlgos, exportable) - return this.fromKeypair(kid, cryptoKeyPair) - } - - // TODO REVIEW POLYFILL - // @ts-ignore Polyfilled - static async fromKeypair(kid: string, cryptoKeyPair: CryptoKeyPair) { - if (!isSignatureKeyPair(cryptoKeyPair)) { - throw new TypeError('CryptoKeyPair must be compatible with sign/verify') - } - - // https://datatracker.ietf.org/doc/html/rfc7517 - // > The "use" and "key_ops" JWK members SHOULD NOT be used together; [...] - // > Applications should specify which of these members they use. - - // TODO REVIEW POLYFILL - // @ts-ignore Polyfilled - const {key_ops: _, ...jwk} = await crypto.subtle.exportKey( - 'jwk', - cryptoKeyPair.privateKey.extractable - ? cryptoKeyPair.privateKey - : cryptoKeyPair.publicKey, - ) - - const use = jwk.use ?? 'sig' - const alg = - jwk.alg ?? fromSubtleAlgorithm(cryptoKeyPair.privateKey.algorithm) - - if (use !== 'sig') { - throw new TypeError('Unsupported JWK use') - } - - return new CryptoKey( - jwkSchema.parse({...jwk, use, kid, alg}), - cryptoKeyPair, - ) - } - - // TODO REVIEW POLYFILL - // @ts-ignore Polyfilled - constructor(jwk: Jwk, readonly cryptoKeyPair: CryptoKeyPair) { - super(jwk) - } - - get isPrivate() { - return true - } - - get privateJwk(): Jwk | undefined { - if (super.isPrivate) return this.jwk - throw new Error('Private Webcrypto Key not exportable') - } - - protected async getKey() { - return this.cryptoKeyPair.privateKey - } -} diff --git a/src/oauth-client-temp/client/keyset.ts b/src/oauth-client-temp/client/keyset.ts deleted file mode 100644 index e69de29bb2..0000000000 diff --git a/src/oauth-client-temp/client/oauth-callback-error.ts b/src/oauth-client-temp/client/oauth-callback-error.ts deleted file mode 100644 index 9c9c26d19d..0000000000 --- a/src/oauth-client-temp/client/oauth-callback-error.ts +++ /dev/null @@ -1,16 +0,0 @@ -export class OAuthCallbackError extends Error { - static from(err: unknown, params: URLSearchParams, state?: string) { - if (err instanceof OAuthCallbackError) return err - const message = err instanceof Error ? err.message : undefined - return new OAuthCallbackError(params, message, state, err) - } - - constructor( - public readonly params: URLSearchParams, - message = params.get('error_description') || 'OAuth callback error', - public readonly state?: string, - cause?: unknown, - ) { - super(message, { cause }) - } -} diff --git a/src/oauth-client-temp/client/oauth-client-factory.ts b/src/oauth-client-temp/client/oauth-client-factory.ts deleted file mode 100644 index 8ca685a94a..0000000000 --- a/src/oauth-client-temp/client/oauth-client-factory.ts +++ /dev/null @@ -1,290 +0,0 @@ -import {GenericStore} from '@atproto/caching' - -import {Key} from '#/oauth-client-temp/jwk' -import {FALLBACK_ALG} from './constants' -import {OAuthCallbackError} from './oauth-callback-error' -import {OAuthClient} from './oauth-client' -import {OAuthServer} from './oauth-server' -import { - OAuthServerFactory, - OAuthServerFactoryOptions, -} from './oauth-server-factory' -import { - OAuthAuthorizeOptions, - OAuthResponseMode, - OAuthResponseType, -} from './oauth-types' -import {Session, SessionGetter} from './session-getter' - -export type InternalStateData = { - iss: string - nonce: string - dpopKey: Key - verifier?: string - - /** - * @note This could be parametrized to be of any type. This wasn't done for - * the sake of simplicity but could be added in a later development. - */ - appState?: string -} - -export type OAuthClientOptions = OAuthServerFactoryOptions & { - stateStore: GenericStore - sessionStore: GenericStore - - /** - * "form_post" will typically be used for server-side applications. - */ - responseMode?: OAuthResponseMode - responseType?: OAuthResponseType -} - -export class OAuthClientFactory { - readonly serverFactory: OAuthServerFactory - - readonly stateStore: GenericStore - readonly sessionGetter: SessionGetter - - readonly responseMode?: OAuthResponseMode - readonly responseType?: OAuthResponseType - - constructor(options: OAuthClientOptions) { - this.responseMode = options?.responseMode - this.responseType = options?.responseType - this.serverFactory = new OAuthServerFactory(options) - this.stateStore = options.stateStore - this.sessionGetter = new SessionGetter( - options.sessionStore, - this.serverFactory, - ) - } - - get clientMetadata() { - return this.serverFactory.clientMetadata - } - - async authorize( - input: string, - options?: OAuthAuthorizeOptions, - ): Promise { - const {did, metadata} = await this.serverFactory.resolver.resolve(input) - - const nonce = await this.serverFactory.crypto.generateNonce() - const pkce = await this.serverFactory.crypto.generatePKCE() - const dpopKey = await this.serverFactory.crypto.generateKey( - metadata.dpop_signing_alg_values_supported || [FALLBACK_ALG], - ) - - const state = await this.serverFactory.crypto.generateNonce() - - await this.stateStore.set(state, { - iss: metadata.issuer, - dpopKey, - nonce, - verifier: pkce?.verifier, - appState: options?.state, - }) - - const parameters = { - client_id: this.clientMetadata.client_id, - redirect_uri: this.clientMetadata.redirect_uris[0], - code_challenge: pkce?.challenge, - code_challenge_method: pkce?.method, - nonce, - state, - login_hint: did || undefined, - response_mode: this.responseMode, - response_type: - this.responseType != null && - metadata.response_types_supported?.includes(this.responseType) - ? this.responseType - : 'code', - - display: options?.display, - id_token_hint: options?.id_token_hint, - max_age: options?.max_age, // this.clientMetadata.default_max_age - prompt: options?.prompt, - scope: options?.scope - ?.split(' ') - .filter(s => metadata.scopes_supported?.includes(s)) - .join(' '), - ui_locales: options?.ui_locales, - } - - if (metadata.pushed_authorization_request_endpoint) { - const server = await this.serverFactory.fromMetadata(metadata, dpopKey) - const {json} = await server.request( - 'pushed_authorization_request', - parameters, - ) - - const authorizationUrl = new URL(metadata.authorization_endpoint) - authorizationUrl.searchParams.set( - 'client_id', - this.clientMetadata.client_id, - ) - authorizationUrl.searchParams.set('request_uri', json.request_uri) - return authorizationUrl - } else if (metadata.require_pushed_authorization_requests) { - throw new Error( - 'Server requires pushed authorization requests (PAR) but no PAR endpoint is available', - ) - } else { - const authorizationUrl = new URL(metadata.authorization_endpoint) - for (const [key, value] of Object.entries(parameters)) { - if (value) authorizationUrl.searchParams.set(key, String(value)) - } - - // Length of the URL that will be sent to the server - const urlLength = - authorizationUrl.pathname.length + authorizationUrl.search.length - if (urlLength < 2048) { - return authorizationUrl - } else if (!metadata.pushed_authorization_request_endpoint) { - throw new Error('Login URL too long') - } - } - - throw new Error( - 'Server does not support pushed authorization requests (PAR)', - ) - } - - async callback(params: URLSearchParams): Promise<{ - client: OAuthClient - state?: string - }> { - // TODO: better errors - - const responseJwt = params.get('response') - if (responseJwt != null) { - // https://openid.net/specs/oauth-v2-jarm.html - throw new OAuthCallbackError(params, 'JARM not supported') - } - - const issuerParam = params.get('iss') - const stateParam = params.get('state') - const errorParam = params.get('error') - const codeParam = params.get('code') - - if (!stateParam) { - throw new OAuthCallbackError(params, 'Missing "state" parameter') - } - const stateData = await this.stateStore.get(stateParam) - if (stateData) { - // Prevent any kind of replay - await this.stateStore.del(stateParam) - } else { - throw new OAuthCallbackError(params, 'Invalid state') - } - - try { - if (errorParam != null) { - throw new OAuthCallbackError(params, undefined, stateData.appState) - } - - if (!codeParam) { - throw new OAuthCallbackError( - params, - 'Missing "code" query param', - stateData.appState, - ) - } - - const server = await this.serverFactory.fromIssuer( - stateData.iss, - stateData.dpopKey, - ) - - if (issuerParam != null) { - if (!server.serverMetadata.issuer) { - throw new OAuthCallbackError( - params, - 'Issuer not found in metadata', - stateData.appState, - ) - } - if (server.serverMetadata.issuer !== issuerParam) { - throw new OAuthCallbackError( - params, - 'Issuer mismatch', - stateData.appState, - ) - } - } else if ( - server.serverMetadata.authorization_response_iss_parameter_supported - ) { - throw new OAuthCallbackError( - params, - 'iss missing from the response', - stateData.appState, - ) - } - - const tokenSet = await server.exchangeCode(codeParam, stateData.verifier) - try { - if (tokenSet.id_token) { - await this.serverFactory.crypto.validateIdTokenClaims( - tokenSet.id_token, - stateParam, - stateData.nonce, - codeParam, - tokenSet.access_token, - ) - } - - const sessionId = await this.serverFactory.crypto.generateNonce(4) - - await this.sessionGetter.setStored(sessionId, { - dpopKey: stateData.dpopKey, - tokenSet, - }) - - const client = this.createClient(server, sessionId) - - return {client, state: stateData.appState} - } catch (err) { - await server.revoke(tokenSet.access_token) - - throw err - } - } catch (err) { - // Make sure, whatever the underlying error, that the appState is - // available in the calling code - throw OAuthCallbackError.from(err, params, stateData.appState) - } - } - - /** - * Build a client from a stored session. This will refresh the token only if - * needed (about to expire) by default. - * - * @param refresh See {@link SessionGetter.getSession} - */ - async restore(sessionId: string, refresh?: boolean): Promise { - const {dpopKey, tokenSet} = await this.sessionGetter.getSession( - sessionId, - refresh, - ) - - const server = await this.serverFactory.fromIssuer(tokenSet.iss, dpopKey) - - return this.createClient(server, sessionId) - } - - async revoke(sessionId: string) { - const {dpopKey, tokenSet} = await this.sessionGetter.get(sessionId, { - allowStale: true, - }) - - const server = await this.serverFactory.fromIssuer(tokenSet.iss, dpopKey) - - await server.revoke(tokenSet.access_token) - await this.sessionGetter.delStored(sessionId) - } - - createClient(server: OAuthServer, sessionId: string): OAuthClient { - return new OAuthClient(server, sessionId, this.sessionGetter) - } -} diff --git a/src/oauth-client-temp/client/oauth-client.ts b/src/oauth-client-temp/client/oauth-client.ts deleted file mode 100644 index aee2d9d81b..0000000000 --- a/src/oauth-client-temp/client/oauth-client.ts +++ /dev/null @@ -1,89 +0,0 @@ -import {JwtPayload, unsafeDecodeJwt} from '#/oauth-client-temp/jwk' -import {OAuthServer, TokenSet} from './oauth-server' -import {SessionGetter} from './session-getter' - -export class OAuthClient { - constructor( - private readonly server: OAuthServer, - public readonly sessionId: string, - private readonly sessionGetter: SessionGetter, - ) {} - - /** - * @param refresh See {@link SessionGetter.getSession} - */ - async getTokenSet(refresh?: boolean): Promise { - const {tokenSet} = await this.sessionGetter.getSession( - this.sessionId, - refresh, - ) - return tokenSet - } - - async getUserinfo(): Promise<{ - userinfo?: JwtPayload - expired?: boolean - scope?: string - iss: string - aud: string - sub: string - }> { - const tokenSet = await this.getTokenSet() - - return { - userinfo: tokenSet.id_token - ? unsafeDecodeJwt(tokenSet.id_token).payload - : undefined, - expired: - tokenSet.expires_at == null - ? undefined - : tokenSet.expires_at < Date.now() - 5e3, - scope: tokenSet.scope, - iss: tokenSet.iss, - aud: tokenSet.aud, - sub: tokenSet.sub, - } - } - - async signOut() { - try { - const tokenSet = await this.getTokenSet(false) - await this.server.revoke(tokenSet.access_token) - } finally { - await this.sessionGetter.delStored(this.sessionId) - } - } - - async request( - pathname: string, - init?: RequestInit, - refreshCredentials?: boolean, - ): Promise { - const tokenSet = await this.getTokenSet(refreshCredentials) - const headers = new Headers(init?.headers) - headers.set( - 'Authorization', - `${tokenSet.token_type} ${tokenSet.access_token}`, - ) - const request = new Request(new URL(pathname, tokenSet.aud), { - ...init, - headers, - }) - - return this.server.dpopFetch(request).catch(err => { - if (!refreshCredentials && isTokenExpiredError(err)) { - return this.request(pathname, init, true) - } - - throw err - }) - } -} - -/** - * @todo Actually implement this - */ -function isTokenExpiredError(_err: unknown) { - // TODO: Detect access_token expired 401 - return false -} diff --git a/src/oauth-client-temp/client/oauth-database.ts b/src/oauth-client-temp/client/oauth-database.ts deleted file mode 100644 index 026a04af75..0000000000 --- a/src/oauth-client-temp/client/oauth-database.ts +++ /dev/null @@ -1,6 +0,0 @@ -import {DidDocument} from '@atproto/did' -import {ResolvedHandle} from '@atproto/handle-resolver' -import {OAuthServerMetadata} from '@atproto/oauth-server-metadata' -import IsomorphicOAuthServerMetadataResolver from '@atproto/oauth-server-metadata-resolver' - -import {CryptoKey} from '#/oauth-client-temp/client/key' diff --git a/src/oauth-client-temp/client/oauth-resolver.ts b/src/oauth-client-temp/client/oauth-resolver.ts deleted file mode 100644 index 4ca5798435..0000000000 --- a/src/oauth-client-temp/client/oauth-resolver.ts +++ /dev/null @@ -1,28 +0,0 @@ -import {IdentityResolver, ResolvedIdentity} from '@atproto/identity-resolver' -import { - OAuthServerMetadata, - OAuthServerMetadataResolver, -} from '@atproto/oauth-server-metadata-resolver' - -export class OAuthResolver { - constructor( - readonly metadataResolver: OAuthServerMetadataResolver, - readonly identityResolver: IdentityResolver, - ) {} - - public async resolve(input: string): Promise< - Partial & { - url: URL - metadata: OAuthServerMetadata - } - > { - const identity = /^https?:\/\//.test(input) - ? // Allow using a PDS url directly as login input (e.g. when the handle does not resolve to a DID) - {url: new URL(input)} - : await this.identityResolver.resolve(input, 'AtprotoPersonalDataServer') - - const metadata = await this.metadataResolver.resolve(identity.url.origin) - - return {...identity, metadata} - } -} diff --git a/src/oauth-client-temp/client/oauth-server-factory.ts b/src/oauth-client-temp/client/oauth-server-factory.ts deleted file mode 100644 index 265dda9c39..0000000000 --- a/src/oauth-client-temp/client/oauth-server-factory.ts +++ /dev/null @@ -1,81 +0,0 @@ -import {GenericStore, MemoryStore} from '@atproto/caching' -import {Fetch} from '@atproto/fetch' -import {IdentityResolver} from '@atproto/identity-resolver' -import {OAuthClientMetadata} from '@atproto/oauth-client-metadata' -import {OAuthServerMetadata} from '@atproto/oauth-server-metadata' -import {OAuthServerMetadataResolver} from '@atproto/oauth-server-metadata-resolver' - -import {Key, Keyset} from '#/oauth-client-temp/jwk' -import {CryptoImplementation} from './crypto-implementation' -import {CryptoWrapper} from './crypto-wrapper' -import {OAuthResolver} from './oauth-resolver' -import {OAuthServer} from './oauth-server' -import {OAuthClientMetadataId} from './oauth-types' -import {validateClientMetadata} from './validate-client-metadata' - -export type OAuthServerFactoryOptions = { - clientMetadata: OAuthClientMetadata - metadataResolver: OAuthServerMetadataResolver - cryptoImplementation: CryptoImplementation - identityResolver: IdentityResolver - fetch?: Fetch - keyset?: Keyset - dpopNonceCache?: GenericStore -} - -export class OAuthServerFactory { - readonly clientMetadata: OAuthClientMetadataId - readonly metadataResolver: OAuthServerMetadataResolver - readonly crypto: CryptoWrapper - readonly resolver: OAuthResolver - readonly fetch: Fetch - readonly keyset?: Keyset - readonly dpopNonceCache: GenericStore - - constructor({ - metadataResolver, - identityResolver, - clientMetadata, - cryptoImplementation, - keyset, - fetch = globalThis.fetch, - dpopNonceCache = new MemoryStore({ - ttl: 60e3, - max: 100, - }), - }: OAuthServerFactoryOptions) { - validateClientMetadata(clientMetadata, keyset) - - if (!clientMetadata.client_id) { - throw new TypeError('A client_id property must be specified') - } - - this.clientMetadata = clientMetadata - this.metadataResolver = metadataResolver - this.keyset = keyset - this.fetch = fetch - this.dpopNonceCache = dpopNonceCache - - this.crypto = new CryptoWrapper(cryptoImplementation) - this.resolver = new OAuthResolver(metadataResolver, identityResolver) - } - - async fromIssuer(issuer: string, dpopKey: Key) { - const {origin} = new URL(issuer) - const serverMetadata = await this.metadataResolver.resolve(origin) - return this.fromMetadata(serverMetadata, dpopKey) - } - - async fromMetadata(serverMetadata: OAuthServerMetadata, dpopKey: Key) { - return new OAuthServer( - dpopKey, - serverMetadata, - this.clientMetadata, - this.dpopNonceCache, - this.resolver, - this.crypto, - this.keyset, - this.fetch, - ) - } -} diff --git a/src/oauth-client-temp/client/oauth-server.ts b/src/oauth-client-temp/client/oauth-server.ts deleted file mode 100644 index fdae49a451..0000000000 --- a/src/oauth-client-temp/client/oauth-server.ts +++ /dev/null @@ -1,287 +0,0 @@ -import {GenericStore} from '@atproto/caching' -import { - Fetch, - fetchFailureHandler, - fetchJsonProcessor, - fetchOkProcessor, -} from '@atproto/fetch' -import {OAuthClientMetadata} from '@atproto/oauth-client-metadata' -import {OAuthServerMetadata} from '@atproto/oauth-server-metadata' - -import {dpopFetchWrapper} from '#/oauth-client-temp/fetch-dpop' -import {Jwt, Key, Keyset} from '#/oauth-client-temp/jwk' -import {FALLBACK_ALG} from './constants' -import {CryptoWrapper} from './crypto-wrapper' -import {OAuthResolver} from './oauth-resolver' -import { - OAuthEndpointName, - OAuthTokenResponse, - OAuthTokenType, -} from './oauth-types' - -export type TokenSet = { - iss: string - sub: string - aud: string - scope?: string - - id_token?: Jwt - refresh_token?: string - access_token: string - token_type: OAuthTokenType - expires_at?: number -} - -export class OAuthServer { - readonly dpopFetch: (request: Request) => Promise - - constructor( - readonly dpopKey: Key, - readonly serverMetadata: OAuthServerMetadata, - readonly clientMetadata: OAuthClientMetadata & {client_id: string}, - readonly dpopNonceCache: GenericStore, - readonly resolver: OAuthResolver, - readonly crypto: CryptoWrapper, - readonly keyset?: Keyset, - fetch?: Fetch, - ) { - const dpopFetch = dpopFetchWrapper({ - fetch, - iss: this.clientMetadata.client_id, - key: dpopKey, - alg: negotiateAlg( - dpopKey, - serverMetadata.dpop_signing_alg_values_supported, - ), - sha256: async v => crypto.sha256(v), - nonceCache: dpopNonceCache, - }) - - this.dpopFetch = request => dpopFetch(request).catch(fetchFailureHandler) - } - - async revoke(token: string) { - try { - await this.request('revocation', {token}) - } catch { - // Don't care - } - } - - async exchangeCode(code: string, verifier?: string): Promise { - const {json: tokenResponse} = await this.request('token', { - grant_type: 'authorization_code', - redirect_uri: this.clientMetadata.redirect_uris[0]!, - code, - code_verifier: verifier, - }) - - try { - if (!tokenResponse.sub) { - throw new TypeError(`Missing "sub" in token response`) - } - - // VERY IMPORTANT ! - const resolved = await this.checkSubIssuer(tokenResponse.sub) - - return { - sub: tokenResponse.sub, - aud: resolved.url.href, - iss: resolved.metadata.issuer, - - scope: tokenResponse.scope, - id_token: tokenResponse.id_token, - refresh_token: tokenResponse.refresh_token, - access_token: tokenResponse.access_token, - token_type: tokenResponse.token_type ?? 'Bearer', - expires_at: - typeof tokenResponse.expires_in === 'number' - ? Date.now() + tokenResponse.expires_in * 1000 - : undefined, - } - } catch (err) { - await this.revoke(tokenResponse.access_token) - - throw err - } - } - - async refresh(tokenSet: TokenSet): Promise { - if (!tokenSet.refresh_token) { - throw new Error('No refresh token available') - } - - const {json: tokenResponse} = await this.request('token', { - grant_type: 'refresh_token', - refresh_token: tokenSet.refresh_token, - }) - - try { - if (tokenSet.sub !== tokenResponse.sub) { - throw new TypeError(`Unexpected "sub" in token response`) - } - if (tokenSet.iss !== this.serverMetadata.issuer) { - throw new TypeError('Issuer mismatch') - } - - // VERY IMPORTANT ! - const resolved = await this.checkSubIssuer(tokenResponse.sub) - - return { - sub: tokenResponse.sub, - aud: resolved.url.href, - iss: resolved.metadata.issuer, - - id_token: tokenResponse.id_token, - refresh_token: tokenResponse.refresh_token, - access_token: tokenResponse.access_token, - token_type: tokenResponse.token_type ?? 'Bearer', - expires_at: Date.now() + (tokenResponse.expires_in ?? 60) * 1000, - } - } catch (err) { - await this.revoke(tokenResponse.access_token) - - throw err - } - } - - /** - * Whenever an OAuth token response is received, we **MUST** verify that the - * "sub" is a DID, whose issuer authority is indeed the server we just - * obtained credentials from. This check is a critical step to actually be - * able to use the "sub" (DID) as being the actual user's identifier. - */ - protected async checkSubIssuer(sub: string) { - const resolved = await this.resolver.resolve(sub) - if (resolved.metadata.issuer !== this.serverMetadata.issuer) { - // Maybe the user switched PDS. - throw new TypeError('Issuer mismatch') - } - return resolved - } - - async request( - endpoint: E, - payload: Record, - ) { - const url = this.serverMetadata[`${endpoint}_endpoint`] - if (!url) throw new Error(`No ${endpoint} endpoint available`) - const auth = await this.buildClientAuth(endpoint) - - const request = new Request(url, { - method: 'POST', - headers: {...auth.headers, 'Content-Type': 'application/json'}, - body: JSON.stringify({...payload, ...auth.payload}), - }) - - const response = await this.dpopFetch(request) - .then(fetchOkProcessor()) - .then( - fetchJsonProcessor< - E extends 'pushed_authorization_request' - ? {request_uri: string} - : E extends 'token' - ? OAuthTokenResponse - : unknown - >(), - ) - - // TODO: validate using zod ? - if (endpoint === 'token') { - if (!response.json.access_token) { - throw new TypeError('No access token in token response') - } - } - - return response - } - - async buildClientAuth(endpoint: OAuthEndpointName): Promise<{ - headers?: Record - payload: - | { - client_id: string - } - | { - client_id: string - client_assertion_type: string - client_assertion: string - } - }> { - const methodSupported = - this.serverMetadata[`${endpoint}_endpoint_auth_methods_supported`] || - this.serverMetadata.token_endpoint_auth_methods_supported - - const method = - this.clientMetadata[`${endpoint}_endpoint_auth_method`] || - this.clientMetadata.token_endpoint_auth_method - - if ( - method === 'private_key_jwt' || - (this.keyset && - !method && - (methodSupported?.includes('private_key_jwt') ?? false)) - ) { - if (!this.keyset) throw new Error('No keyset available') - - try { - const alg = - this.serverMetadata[ - `${endpoint}_endpoint_auth_signing_alg_values_supported` - ] ?? - this.serverMetadata - .token_endpoint_auth_signing_alg_values_supported ?? - FALLBACK_ALG - - // If jwks is defined, make sure to only sign using a key that exists in - // the jwks. If jwks_uri is defined, we can't be sure that the key we're - // looking for is in there so we will just assume it is. - const kid = this.clientMetadata.jwks?.keys - .map(({kid}) => kid) - .filter((v): v is string => !!v) - - return { - payload: { - client_id: this.clientMetadata.client_id, - client_assertion_type: - 'urn:ietf:params:oauth:client-assertion-type:jwt-bearer', - client_assertion: await this.keyset.sign( - {alg, kid}, - { - iss: this.clientMetadata.client_id, - sub: this.clientMetadata.client_id, - aud: this.serverMetadata.issuer, - jti: await this.crypto.generateNonce(), - iat: Math.floor(Date.now() / 1000), - }, - ), - }, - } - } catch (err) { - if (method === 'private_key_jwt') throw err - - // Else try next method - } - } - - if ( - method === 'none' || - (!method && (methodSupported?.includes('none') ?? true)) - ) { - return { - payload: { - client_id: this.clientMetadata.client_id, - }, - } - } - - throw new Error(`Unsupported ${endpoint} authentication method`) - } -} - -function negotiateAlg(key: Key, supportedAlgs: string[] | undefined): string { - const alg = key.algorithms.find(a => supportedAlgs?.includes(a) ?? true) - if (alg) return alg - - throw new Error('Key does not match any alg supported by the server') -} diff --git a/src/oauth-client-temp/client/oauth-types.ts b/src/oauth-client-temp/client/oauth-types.ts deleted file mode 100644 index 922abf9067..0000000000 --- a/src/oauth-client-temp/client/oauth-types.ts +++ /dev/null @@ -1,37 +0,0 @@ -import {OAuthClientMetadata} from '@atproto/oauth-client-metadata' - -import {Jwt} from '#/oauth-client-temp/jwk' - -export type OAuthResponseMode = 'query' | 'fragment' | 'form_post' -export type OAuthResponseType = 'code' | 'code id_token' - -export type OAuthEndpointName = - | 'token' - | 'revocation' - | 'introspection' - | 'pushed_authorization_request' - -export type OAuthTokenType = 'Bearer' | 'DPoP' - -export type OAuthAuthorizeOptions = { - display?: 'page' | 'popup' | 'touch' | 'wap' - id_token_hint?: string - max_age?: number - prompt?: 'login' | 'none' | 'consent' | 'select_account' - scope?: string - state?: string - ui_locales?: string -} - -export type OAuthTokenResponse = { - issuer?: string - sub?: string - scope?: string - id_token?: Jwt - refresh_token?: string - access_token: string - token_type?: OAuthTokenType - expires_in?: number -} - -export type OAuthClientMetadataId = OAuthClientMetadata & {client_id: string} diff --git a/src/oauth-client-temp/client/session-getter.ts b/src/oauth-client-temp/client/session-getter.ts deleted file mode 100644 index c2889c9673..0000000000 --- a/src/oauth-client-temp/client/session-getter.ts +++ /dev/null @@ -1,139 +0,0 @@ -import {CachedGetter, GenericStore} from '@atproto/caching' -import {FetchResponseError} from '@atproto/fetch' - -import {Key} from '#/oauth-client-temp/jwk' -import {TokenSet} from './oauth-server' -import {OAuthServerFactory} from './oauth-server-factory' - -export type Session = { - dpopKey: Key - tokenSet: TokenSet -} - -/** - * There are several advantages to wrapping the sessionStore in a (single) - * CachedGetter, the main of which is that the cached getter will ensure that at - * most one fresh call is ever being made. Another advantage, is that it - * contains the logic for reading from the cache which, if the cache is based on - * localStorage/indexedDB, will sync across multiple tabs (for a given - * sessionId). - */ -export class SessionGetter extends CachedGetter { - constructor( - sessionStore: GenericStore, - serverFactory: OAuthServerFactory, - ) { - super( - async (sessionId, options, storedSession) => { - // There needs to be a previous session to be able to refresh - if (storedSession === undefined) { - throw new Error('The session was revoked') - } - - // Since refresh tokens can only be used once, we might run into - // concurrency issues if multiple tabs/instances are trying to refresh - // the same token. The chances of this happening when multiple instances - // are started simultaneously is reduced by randomizing the expiry time - // (see isStale() bellow). Even so, There still exist chances that - // multiple tabs will try to refresh the token at the same time. The - // best solution would be to use a mutex/lock to ensure that only one - // instance is refreshing the token at a time. A simpler workaround is - // to check if the value stored in the session store is the same as the - // one in memory. If it isn't, then another instance has already - // refreshed the token. - - const {tokenSet, dpopKey} = storedSession - const server = await serverFactory.fromIssuer(tokenSet.iss, dpopKey) - const newTokenSet = await server.refresh(tokenSet).catch(async err => { - if (await isRefreshDeniedError(err)) { - // Allow some time for the concurrent request to be stored before - // we try to get it. - await new Promise(r => setTimeout(r, 500)) - - const stored = await this.getStored(sessionId) - if (stored !== undefined) { - if ( - stored.tokenSet.access_token !== tokenSet.access_token || - stored.tokenSet.refresh_token !== tokenSet.refresh_token - ) { - // A concurrent refresh occurred. Pretend this one succeeded. - return stored.tokenSet - } else { - // The session data will be deleted from the sessionStore by - // the "deleteOnError" callback. - } - } - } - - throw err - }) - return {...storedSession, tokenSet: newTokenSet} - }, - sessionStore, - { - isStale: (sessionId, {tokenSet}) => { - return ( - tokenSet.expires_at != null && - tokenSet.expires_at < - Date.now() + - // Add some lee way to ensure the token is not expired when it - // reaches the server. - 30e3 + - // Add some randomness to prevent all instances from trying to - // refreshing at the exact same time, when they are started at - // the same time. - 60e3 * Math.random() - ) - }, - onStoreError: async (err, sessionId, {tokenSet, dpopKey}) => { - // If the token data cannot be stored, let's revoke it - const server = await serverFactory.fromIssuer(tokenSet.iss, dpopKey) - await server.revoke(tokenSet.access_token) - throw err - }, - deleteOnError: async (err, sessionId, {tokenSet}) => { - // Not possible to refresh without a refresh token - if (!tokenSet.refresh_token) return true - - // If fetching a refresh token fails because they are no longer valid, - // delete the session from the sessionStore. - if (await isRefreshDeniedError(err)) return true - - // Unknown cause, keep the session in the store - return false - }, - }, - ) - } - - /** - * @param refresh When `true`, the credentials will be refreshed even if they - * are not expired. When `false`, the credentials will not be refreshed even - * if they are expired. When `undefined`, the credentials will be refreshed - * if, and only if, they are (about to be) expired. Defaults to `undefined`. - */ - async getSession(sessionId: string, refresh?: boolean) { - return this.get(sessionId, { - noCache: refresh === true, - allowStale: refresh === false, - }) - } -} - -async function isRefreshDeniedError(err: unknown) { - if (err instanceof FetchResponseError && err.statusCode === 400) { - if (err.response?.bodyUsed === false) { - try { - const json = await err.response.clone().json() - return ( - json.error === 'invalid_request' && - json.error_description === 'Invalid refresh token' - ) - } catch { - // falls through - } - } - } - - return false -} diff --git a/src/oauth-client-temp/client/util.ts b/src/oauth-client-temp/client/util.ts deleted file mode 100644 index 5e238ac3b7..0000000000 --- a/src/oauth-client-temp/client/util.ts +++ /dev/null @@ -1,160 +0,0 @@ -export type JWSAlgorithm = - // HMAC - | 'HS256' - | 'HS384' - | 'HS512' - // RSA - | 'PS256' - | 'PS384' - | 'PS512' - | 'RS256' - | 'RS384' - | 'RS512' - // EC - | 'ES256' - | 'ES256K' - | 'ES384' - | 'ES512' - // OKP - | 'EdDSA' - -// TODO REVIEW POLYFILL -// @ts-ignore Polyfilled -export type SubtleAlgorithm = RsaHashedKeyGenParams | EcKeyGenParams - -export function toSubtleAlgorithm( - alg: string, - crv?: string, - options?: {modulusLength?: number}, -): SubtleAlgorithm { - switch (alg) { - case 'PS256': - case 'PS384': - case 'PS512': - return { - name: 'RSA-PSS', - hash: `SHA-${alg.slice(-3) as '256' | '384' | '512'}`, - modulusLength: options?.modulusLength ?? 2048, - publicExponent: new Uint8Array([0x01, 0x00, 0x01]), - } - case 'RS256': - case 'RS384': - case 'RS512': - return { - name: 'RSASSA-PKCS1-v1_5', - hash: `SHA-${alg.slice(-3) as '256' | '384' | '512'}`, - modulusLength: options?.modulusLength ?? 2048, - publicExponent: new Uint8Array([0x01, 0x00, 0x01]), - } - case 'ES256': - case 'ES384': - return { - name: 'ECDSA', - namedCurve: `P-${alg.slice(-3) as '256' | '384'}`, - } - case 'ES512': - return { - name: 'ECDSA', - namedCurve: 'P-521', - } - default: - // https://github.com/w3c/webcrypto/issues/82#issuecomment-849856773 - - throw new TypeError(`Unsupported alg "${alg}"`) - } -} - -// TODO REVIEW POLYFILL -// @ts-ignore Polyfilled -export function fromSubtleAlgorithm(algorithm: KeyAlgorithm): JWSAlgorithm { - switch (algorithm.name) { - case 'RSA-PSS': - case 'RSASSA-PKCS1-v1_5': { - // TODO REVIEW POLYFILL - // @ts-ignore Polyfilled - const hash = (algorithm).hash.name - switch (hash) { - case 'SHA-256': - case 'SHA-384': - case 'SHA-512': { - const prefix = algorithm.name === 'RSA-PSS' ? 'PS' : 'RS' - return `${prefix}${hash.slice(-3) as '256' | '384' | '512'}` - } - default: - throw new TypeError('unsupported RsaHashedKeyAlgorithm hash') - } - } - case 'ECDSA': { - // TODO REVIEW POLYFILL - // @ts-ignore Polyfilled - const namedCurve = (algorithm).namedCurve - switch (namedCurve) { - case 'P-256': - case 'P-384': - case 'P-512': - return `ES${namedCurve.slice(-3) as '256' | '384' | '512'}` - case 'P-521': - return 'ES512' - default: - throw new TypeError('unsupported EcKeyAlgorithm namedCurve') - } - } - case 'Ed448': - case 'Ed25519': - return 'EdDSA' - default: - // https://github.com/w3c/webcrypto/issues/82#issuecomment-849856773 - - throw new TypeError(`Unexpected algorithm "${algorithm.name}"`) - } -} - -export function isSignatureKeyPair( - v: unknown, - extractable?: boolean, - // TODO REVIEW POLYFILL - // @ts-ignore Polyfilled -): v is CryptoKeyPair { - return ( - typeof v === 'object' && - v !== null && - 'privateKey' in v && - // TODO REVIEW POLYFILL - // @ts-ignore Polyfilled - v.privateKey instanceof CryptoKey && - v.privateKey.type === 'private' && - (extractable == null || v.privateKey.extractable === extractable) && - v.privateKey.usages.includes('sign') && - 'publicKey' in v && - // TODO REVIEW POLYFILL - // @ts-ignore Polyfilled - v.publicKey instanceof CryptoKey && - v.publicKey.type === 'public' && - v.publicKey.extractable === true && - v.publicKey.usages.includes('verify') - ) -} - -export async function generateKeypair( - algs: string[], - extractable = false, - // TODO REVIEW POLYFILL - // @ts-ignore Polyfilled -): Promise { - const errors: unknown[] = [] - for (const alg of algs) { - try { - // TODO REVIEW POLYFILL - // @ts-ignore Polyfilled - return await crypto.subtle.generateKey( - toSubtleAlgorithm(alg), - extractable, - ['sign', 'verify'], - ) - } catch (err) { - errors.push(err) - } - } - - throw new AggregateError(errors, 'Failed to generate keypair') -} diff --git a/src/oauth-client-temp/client/validate-client-metadata.ts b/src/oauth-client-temp/client/validate-client-metadata.ts deleted file mode 100644 index fdc3aece4d..0000000000 --- a/src/oauth-client-temp/client/validate-client-metadata.ts +++ /dev/null @@ -1,81 +0,0 @@ -import {OAuthClientMetadata} from '@atproto/oauth-client-metadata' - -import {Keyset} from '#/oauth-client-temp/jwk' - -export function validateClientMetadata( - metadata: OAuthClientMetadata, - keyset?: Keyset, -): asserts metadata is OAuthClientMetadata & {client_id: string} { - if (!metadata.client_id) { - throw new TypeError('client_id must be provided') - } - - const url = new URL(metadata.client_id) - if (url.pathname !== '/') { - throw new TypeError('origin must be a URL root') - } - if (url.username || url.password) { - throw new TypeError('client_id URI must not contain a username or password') - } - if (url.search || url.hash) { - throw new TypeError('client_id URI must not contain a query or fragment') - } - if (url.href !== metadata.client_id) { - throw new TypeError('client_id URI must be a normalized URL') - } - - if ( - url.hostname === 'localhost' || - url.hostname === '[::1]' || - url.hostname === '127.0.0.1' - ) { - if (url.protocol !== 'http:' || url.port) { - throw new TypeError('loopback clients must use "http:" and port "80"') - } - } - - if (metadata.client_uri && metadata.client_uri !== metadata.client_id) { - throw new TypeError('client_uri must match client_id') - } - - if (!metadata.redirect_uris.length) { - throw new TypeError('At least one redirect_uri must be provided') - } - for (const u of metadata.redirect_uris) { - const redirectUrl = new URL(u) - // Loopback redirect_uris require special handling - if ( - redirectUrl.hostname === 'localhost' || - redirectUrl.hostname === '[::1]' || - redirectUrl.hostname === '127.0.0.1' - ) { - if (redirectUrl.protocol !== 'http:') { - throw new TypeError('loopback redirect_uris must use "http:"') - } - } else { - // Not a loopback client - if (redirectUrl.origin !== url.origin) { - throw new TypeError('redirect_uris must have the same origin') - } - } - } - - for (const endpoint of [ - 'token', - 'revocation', - 'introspection', - 'pushed_authorization_request', - ] as const) { - const method = metadata[`${endpoint}_endpoint_auth_method`] - if (method && method !== 'none') { - if (!keyset) { - throw new TypeError(`Keyset is required for ${method} method`) - } - if (!metadata[`${endpoint}_endpoint_auth_signing_alg`]) { - throw new TypeError( - `${endpoint}_endpoint_auth_signing_alg must be provided`, - ) - } - } - } -} diff --git a/src/oauth-client-temp/disposable-polyfill/index.ts b/src/oauth-client-temp/disposable-polyfill/index.ts deleted file mode 100644 index ddb9073b16..0000000000 --- a/src/oauth-client-temp/disposable-polyfill/index.ts +++ /dev/null @@ -1,10 +0,0 @@ -// Code compiled with tsc supports "using" and "await using" syntax. This -// features is supported by downleveling the code to ES2017. The downleveling -// relies on `Symbol.dispose` and `Symbol.asyncDispose` symbols. These symbols -// might not be available in all environments. This package provides a polyfill -// for these symbols. - -// @ts-expect-error -Symbol.dispose ??= Symbol('@@dispose') -// @ts-expect-error -Symbol.asyncDispose ??= Symbol('@@asyncDispose') diff --git a/src/oauth-client-temp/fetch-dpop/index.ts b/src/oauth-client-temp/fetch-dpop/index.ts deleted file mode 100644 index eb3f3e4a45..0000000000 --- a/src/oauth-client-temp/fetch-dpop/index.ts +++ /dev/null @@ -1,174 +0,0 @@ -import {GenericStore} from '@atproto/caching' -import {Fetch} from '@atproto/fetch' - -import {b64uEncode} from '#/oauth-client-temp/b64' -import {Key} from '#/oauth-client-temp/jwk' - -export function dpopFetchWrapper({ - key, - iss, - alg, - // TODO REVIEW POLYFILL - // @ts-ignore Polyfilled - sha256 = typeof crypto !== 'undefined' && crypto.subtle != null - ? subtleSha256 - : undefined, - nonceCache, -}: { - key: Key - iss: string - alg?: string - sha256?: (input: string) => Promise - nonceCache?: GenericStore - fetch?: Fetch -}): Fetch { - if (!sha256) { - throw new Error( - `crypto.subtle is not available in this environment. Please provide a sha256 function.`, - ) - } - - return async function (request) { - return dpopFetch.call( - this, - request, - key, - iss, - alg, - sha256, - nonceCache, - fetch, - ) - } -} - -export async function dpopFetch( - this: ThisParameterType, - request: Request, - key: Key, - iss: string, - alg: string = key.alg || 'ES256', - sha256: (input: string) => string | PromiseLike = subtleSha256, - nonceCache?: GenericStore, - fetch = globalThis.fetch as Fetch, -): Promise { - const authorizationHeader = request.headers.get('Authorization') - const ath = authorizationHeader?.startsWith('DPoP ') - ? await sha256(authorizationHeader.slice(5)) - : undefined - - const {origin} = new URL(request.url) - - // Clone request for potential retry - const clonedRequest = request.clone() - - // Try with the previously known nonce - const oldNonce = await Promise.resolve() - .then(() => nonceCache?.get(origin)) - .catch(() => undefined) // Ignore cache.get errors - - request.headers.set( - 'DPoP', - await buildProof(key, alg, iss, request.method, request.url, oldNonce, ath), - ) - - const response = await fetch(request) - - const nonce = response.headers.get('DPoP-Nonce') - if (!nonce) return response - - // Store the fresh nonce for future requests - try { - await nonceCache?.set(origin, nonce) - } catch { - // Ignore cache.set errors - } - - if (!(await isUseDpopNonceError(response))) { - return response - } - - clonedRequest.headers.set( - 'DPoP', - await buildProof(key, alg, iss, request.method, request.url, nonce, ath), - ) - - return fetch(clonedRequest) -} - -async function buildProof( - key: Key, - alg: string, - iss: string, - htm: string, - htu: string, - nonce?: string, - ath?: string, -) { - if (!key.bareJwk) { - throw new Error('Only asymetric keys can be used as DPoP proofs') - } - - const now = Math.floor(Date.now() / 1e3) - - return key.createJwt( - { - alg, - typ: 'dpop+jwt', - jwk: key.bareJwk, - }, - { - iss, - iat: now, - exp: now + 10, - // Any collision will cause the request to be rejected by the server. no biggie. - jti: Math.random().toString(36).slice(2), - htm, - htu, - nonce, - ath, - }, - ) -} - -async function isUseDpopNonceError(response: Response): Promise { - if (response.status !== 400) { - return false - } - - const ct = response.headers.get('Content-Type') - const mime = ct?.split(';')[0]?.trim() - if (mime !== 'application/json') { - return false - } - - try { - const body = await response.clone().json() - return body?.error === 'use_dpop_nonce' - } catch { - return false - } -} - -function subtleSha256(input: string): Promise { - // TODO REVIEW POLYFILL - // @ts-ignore Polyfilled - if (typeof crypto === 'undefined' || crypto.subtle == null) { - throw new Error( - `crypto.subtle is not available in this environment. Please provide a sha256 function.`, - ) - } - - // TODO REVIEW POLYFILL - // @ts-ignore Polyfilled - return ( - // TODO REVIEW POLYFILL - // @ts-ignore Polyfilled - crypto.subtle - // TODO REVIEW POLYFILL - // @ts-ignore Polyfilled - .digest('SHA-256', new TextEncoder().encode(input)) - // TODO OAUTH types - .then((digest: Iterable) => b64uEncode(new Uint8Array(digest))) - ) -} diff --git a/src/oauth-client-temp/identity-resolver/identity-resolver.ts b/src/oauth-client-temp/identity-resolver/identity-resolver.ts deleted file mode 100644 index 88e592ff2b..0000000000 --- a/src/oauth-client-temp/identity-resolver/identity-resolver.ts +++ /dev/null @@ -1,35 +0,0 @@ -import { DidResolver } from '@atproto/did' -import { - HandleResolver, - ResolvedHandle, - isResolvedHandle, -} from '@atproto/handle-resolver' -import { normalizeAndEnsureValidHandle } from '@atproto/syntax' - -export type ResolvedIdentity = { - did: NonNullable - url: URL -} - -export class IdentityResolver { - constructor( - readonly handleResolver: HandleResolver, - readonly didResolver: DidResolver<'plc' | 'web'>, - ) {} - - public async resolve( - input: string, - serviceType = 'AtprotoPersonalDataServer', - ): Promise { - const did = isResolvedHandle(input) - ? input // Already a did - : await this.handleResolver.resolve(normalizeAndEnsureValidHandle(input)) - if (!did) throw new Error(`Handle ${input} does not resolve to a DID`) - - const url = await this.didResolver.resolveServiceEndpoint(did, { - type: serviceType, - }) - - return { did, url } - } -} diff --git a/src/oauth-client-temp/identity-resolver/index.ts b/src/oauth-client-temp/identity-resolver/index.ts deleted file mode 100644 index bccd3ae900..0000000000 --- a/src/oauth-client-temp/identity-resolver/index.ts +++ /dev/null @@ -1,2 +0,0 @@ -export * from './identity-resolver' -export * from './universal-identity-resolver' diff --git a/src/oauth-client-temp/identity-resolver/universal-identity-resolver.ts b/src/oauth-client-temp/identity-resolver/universal-identity-resolver.ts deleted file mode 100644 index a9201d93c2..0000000000 --- a/src/oauth-client-temp/identity-resolver/universal-identity-resolver.ts +++ /dev/null @@ -1,52 +0,0 @@ -import { - DidCache, - IsomorphicDidResolver, - IsomorphicDidResolverOptions, -} from '@atproto/did' -import {Fetch} from '@atproto/fetch' -import UniversalHandleResolver, { - HandleResolverCache, - UniversalHandleResolverOptions, -} from '@atproto/handle-resolver' - -import {IdentityResolver} from './identity-resolver' - -export type UniversalIdentityResolverOptions = { - fetch?: Fetch - - didCache?: DidCache - handleCache?: HandleResolverCache - - /** - * @see {@link IsomorphicDidResolverOptions.plcDirectoryUrl} - */ - plcDirectoryUrl?: IsomorphicDidResolverOptions['plcDirectoryUrl'] - - /** - * @see {@link UniversalHandleResolverOptions.atprotoLexiconUrl} - */ - atprotoLexiconUrl?: UniversalHandleResolverOptions['atprotoLexiconUrl'] -} - -export class UniversalIdentityResolver extends IdentityResolver { - static from({ - fetch = globalThis.fetch, - didCache, - handleCache, - plcDirectoryUrl, - atprotoLexiconUrl, - }: UniversalIdentityResolverOptions) { - return new this( - new UniversalHandleResolver({ - fetch, - cache: handleCache, - atprotoLexiconUrl, - }), - new IsomorphicDidResolver({ - fetch, // - cache: didCache, - plcDirectoryUrl, - }), - ) - } -} diff --git a/src/oauth-client-temp/indexed-db/db-index.web.ts b/src/oauth-client-temp/indexed-db/db-index.web.ts deleted file mode 100644 index b06fdd5844..0000000000 --- a/src/oauth-client-temp/indexed-db/db-index.web.ts +++ /dev/null @@ -1,44 +0,0 @@ -import {ObjectStoreSchema} from './schema' -import {promisify} from './util' - -export class DbIndexWeb { - constructor(private idbIndex: IDBIndex) {} - - count(query?: IDBValidKey | IDBKeyRange) { - return promisify(this.idbIndex.count(query)) - } - - get(query: IDBValidKey | IDBKeyRange) { - return promisify(this.idbIndex.get(query)) - } - - getKey(query: IDBValidKey | IDBKeyRange) { - return promisify(this.idbIndex.getKey(query)) - } - - getAll(query?: IDBValidKey | IDBKeyRange | null, count?: number) { - return promisify(this.idbIndex.getAll(query, count)) - } - - getAllKeys(query?: IDBValidKey | IDBKeyRange | null, count?: number) { - return promisify(this.idbIndex.getAllKeys(query, count)) - } - - deleteAll(query?: IDBValidKey | IDBKeyRange | null): Promise { - return new Promise((resolve, reject) => { - const result = this.idbIndex.openCursor(query) - result.onsuccess = function (event) { - const cursor = (event as any).target.result as IDBCursorWithValue - if (cursor) { - cursor.delete() - cursor.continue() - } else { - resolve() - } - } - result.onerror = function (event) { - reject((event.target as any)?.error || new Error('Unexpected error')) - } - }) - } -} diff --git a/src/oauth-client-temp/indexed-db/db-object-store.web.ts b/src/oauth-client-temp/indexed-db/db-object-store.web.ts deleted file mode 100644 index 44c20a7004..0000000000 --- a/src/oauth-client-temp/indexed-db/db-object-store.web.ts +++ /dev/null @@ -1,47 +0,0 @@ -import {DbIndexWeb} from './db-index.web' -import {ObjectStoreSchema} from './schema' -import {promisify} from './util' - -export class DbObjectStoreWeb { - constructor(private idbObjStore: IDBObjectStore) {} - - get name() { - return this.idbObjStore.name - } - - index(name: string) { - return new DbIndexWeb(this.idbObjStore.index(name)) - } - - get(key: IDBValidKey | IDBKeyRange) { - return promisify(this.idbObjStore.get(key)) - } - - getKey(query: IDBValidKey | IDBKeyRange) { - return promisify(this.idbObjStore.getKey(query)) - } - - getAll(query?: IDBValidKey | IDBKeyRange | null, count?: number) { - return promisify(this.idbObjStore.getAll(query, count)) - } - - getAllKeys(query?: IDBValidKey | IDBKeyRange | null, count?: number) { - return promisify(this.idbObjStore.getAllKeys(query, count)) - } - - add(value: Schema, key?: IDBValidKey) { - return promisify(this.idbObjStore.add(value, key)) - } - - put(value: Schema, key?: IDBValidKey) { - return promisify(this.idbObjStore.put(value, key)) - } - - delete(key: IDBValidKey | IDBKeyRange) { - return promisify(this.idbObjStore.delete(key)) - } - - clear() { - return promisify(this.idbObjStore.clear()) - } -} diff --git a/src/oauth-client-temp/indexed-db/db-transaction.web.ts b/src/oauth-client-temp/indexed-db/db-transaction.web.ts deleted file mode 100644 index 08fddc9afe..0000000000 --- a/src/oauth-client-temp/indexed-db/db-transaction.web.ts +++ /dev/null @@ -1,52 +0,0 @@ -import {DbObjectStoreWeb} from './db-object-store.web' -import {DatabaseSchema} from './schema' - -export class DbTransactionWeb - implements Disposable -{ - #tx: IDBTransaction | null - - constructor(tx: IDBTransaction) { - this.#tx = tx - - const onAbort = () => { - cleanup() - } - const onComplete = () => { - cleanup() - } - const cleanup = () => { - this.#tx = null - tx.removeEventListener('abort', onAbort) - tx.removeEventListener('complete', onComplete) - } - tx.addEventListener('abort', onAbort) - tx.addEventListener('complete', onComplete) - } - - protected get tx(): IDBTransaction { - if (!this.#tx) throw new Error('Transaction already ended') - return this.#tx - } - - async abort() { - const {tx} = this - this.#tx = null - tx.abort() - } - - async commit() { - const {tx} = this - this.#tx = null - tx.commit?.() - } - - objectStore(name: T) { - const store = this.tx.objectStore(name) - return new DbObjectStoreWeb(store) - } - - [Symbol.dispose](): void { - if (this.#tx) this.commit() - } -} diff --git a/src/oauth-client-temp/indexed-db/db.ts b/src/oauth-client-temp/indexed-db/db.ts deleted file mode 100644 index 5d83361657..0000000000 --- a/src/oauth-client-temp/indexed-db/db.ts +++ /dev/null @@ -1,9 +0,0 @@ -import {DatabaseSchema} from '#/oauth-client-temp/indexed-db/schema' - -export class Db implements Disposable { - static async open( - dbname: string, - migrations: ReadonlyArray<(db: IDBDatabase) => void>, - txOptions?: IDBTransactionOptions, - ) -} diff --git a/src/oauth-client-temp/indexed-db/db.web.ts b/src/oauth-client-temp/indexed-db/db.web.ts deleted file mode 100644 index a983b7f530..0000000000 --- a/src/oauth-client-temp/indexed-db/db.web.ts +++ /dev/null @@ -1,113 +0,0 @@ -import {DbTransactionWeb} from './db-transaction.web' -import {DatabaseSchema} from './schema' - -export class Db implements Disposable { - static async open( - dbName: string, - migrations: ReadonlyArray<(db: IDBDatabase) => void>, - txOptions?: IDBTransactionOptions, - ) { - const db = await new Promise((resolve, reject) => { - const request = indexedDB.open(dbName, migrations.length) - - request.onerror = () => reject(request.error) - request.onsuccess = () => resolve(request.result) - request.onupgradeneeded = ({oldVersion, newVersion}) => { - const db = request.result - try { - for ( - let version = oldVersion; - version < (newVersion ?? migrations.length); - ++version - ) { - const migration = migrations[version] - if (migration) migration(db) - else throw new Error(`Missing migration for version ${version}`) - } - } catch (err) { - db.close() - reject(err) - } - } - }) - - return new DbWeb(db, txOptions) - } - - #db: null | IDBDatabase - - constructor( - db: IDBDatabase, - protected readonly txOptions?: IDBTransactionOptions, - ) { - this.#db = db - - const cleanup = () => { - this.#db = null - db.removeEventListener('versionchange', cleanup) - db.removeEventListener('close', cleanup) - db.close() // Can we call close on a "closed" database? - } - - db.addEventListener('versionchange', cleanup) - db.addEventListener('close', cleanup) - } - - protected get db(): IDBDatabase { - if (!this.#db) throw new Error('Database closed') - return this.#db - } - - get name() { - return this.db.name - } - - get objectStoreNames() { - return this.db.objectStoreNames - } - - get version() { - return this.db.version - } - - async transaction( - storeNames: T, - mode: IDBTransactionMode, - run: (tx: DbTransactionWeb>) => R | PromiseLike, - ): Promise { - return new Promise(async (resolve, reject) => { - try { - const tx = this.db.transaction(storeNames, mode, this.txOptions) - let result: {done: false} | {done: true; value: R} = {done: false} - - tx.oncomplete = () => { - if (result.done) resolve(result.value) - else reject(new Error('Transaction completed without result')) - } - tx.onerror = () => reject(tx.error) - tx.onabort = () => reject(tx.error || new Error('Transaction aborted')) - - try { - const value = await run(new DbTransactionWeb(tx)) - result = {done: true, value} - tx.commit() - } catch (err) { - tx.abort() - throw err - } - } catch (err) { - reject(err) - } - }) - } - - close() { - const {db} = this - this.#db = null - db.close() - } - - [Symbol.dispose]() { - if (this.#db) return this.close() - } -} diff --git a/src/oauth-client-temp/indexed-db/index.web.ts b/src/oauth-client-temp/indexed-db/index.web.ts deleted file mode 100644 index d1b6d4e0dd..0000000000 --- a/src/oauth-client-temp/indexed-db/index.web.ts +++ /dev/null @@ -1,6 +0,0 @@ -import '#/oauth-client-temp/disposable-polyfill' - -export * from './db.web' -export * from './db.web' -export * from './db-index.web' -export * from './db-object-store.web' diff --git a/src/oauth-client-temp/indexed-db/schema.ts b/src/oauth-client-temp/indexed-db/schema.ts deleted file mode 100644 index f8736b2a19..0000000000 --- a/src/oauth-client-temp/indexed-db/schema.ts +++ /dev/null @@ -1,2 +0,0 @@ -export type ObjectStoreSchema = NonNullable -export type DatabaseSchema = Record diff --git a/src/oauth-client-temp/indexed-db/util.web.ts b/src/oauth-client-temp/indexed-db/util.web.ts deleted file mode 100644 index 6e52b5919c..0000000000 --- a/src/oauth-client-temp/indexed-db/util.web.ts +++ /dev/null @@ -1,20 +0,0 @@ -export function promisify(request: IDBRequest) { - const promise = new Promise((resolve, reject) => { - const cleanup = () => { - request.removeEventListener('success', success) - request.removeEventListener('error', error) - } - const success = () => { - resolve(request.result) - cleanup() - } - const error = () => { - reject(request.error) - cleanup() - } - request.addEventListener('success', success) - request.addEventListener('error', error) - }) - - return promise -} diff --git a/src/oauth-client-temp/jwk-jose/index.ts b/src/oauth-client-temp/jwk-jose/index.ts deleted file mode 100644 index 179625dd51..0000000000 --- a/src/oauth-client-temp/jwk-jose/index.ts +++ /dev/null @@ -1,2 +0,0 @@ -export * from './jose-key' -export * from './jose-keyset' diff --git a/src/oauth-client-temp/jwk-jose/jose-key.ts b/src/oauth-client-temp/jwk-jose/jose-key.ts deleted file mode 100644 index baeb31ff2c..0000000000 --- a/src/oauth-client-temp/jwk-jose/jose-key.ts +++ /dev/null @@ -1,115 +0,0 @@ -import { - exportJWK, - importJWK, - importPKCS8, - JWK, - jwtVerify, - JWTVerifyOptions, - KeyLike, - SignJWT, -} from 'jose' - -import { - Jwk, - jwkSchema, - Jwt, - JwtHeader, - JwtPayload, - Key, - VerifyOptions, - VerifyPayload, - VerifyResult, -} from '#/oauth-client-temp/jwk' -import {either} from './util' - -export type Importable = string | KeyLike | Jwk - -export class JoseKey extends Key { - #keyObj?: KeyLike | Uint8Array - - protected async getKey() { - return (this.#keyObj ||= await importJWK(this.jwk as JWK)) - } - - async createJwt(header: JwtHeader, payload: JwtPayload) { - if (header.kid && header.kid !== this.kid) { - throw new TypeError( - `Invalid "kid" (${header.kid}) used to sign with key "${this.kid}"`, - ) - } - - if (!header.alg || !this.algorithms.includes(header.alg)) { - throw new TypeError( - `Invalid "alg" (${header.alg}) used to sign with key "${this.kid}"`, - ) - } - - const keyObj = await this.getKey() - return new SignJWT(payload) - .setProtectedHeader({...header, kid: this.kid}) - .sign(keyObj) as Promise - } - - async verifyJwt< - P extends VerifyPayload = JwtPayload, - C extends string = string, - >(token: Jwt, options?: VerifyOptions): Promise> { - const keyObj = await this.getKey() - const result = await jwtVerify(token, keyObj, { - ...options, - algorithms: this.algorithms, - } as JWTVerifyOptions) - return result as VerifyResult - } - - static async fromImportable( - input: Importable, - kid?: string, - ): Promise { - if (typeof input === 'string') { - // PKCS8 - if (input.startsWith('-----')) { - return this.fromPKCS8(input, kid) - } - - // Jwk (string) - if (input.startsWith('{')) { - return this.fromJWK(input, kid) - } - - throw new TypeError('Invalid input') - } - - if (typeof input === 'object') { - // Jwk - if ('kty' in input || 'alg' in input) { - return this.fromJWK(input, kid) - } - - // KeyLike - return this.fromJWK(await exportJWK(input), kid) - } - - throw new TypeError('Invalid input') - } - - static async fromPKCS8(pem: string, kid?: string): Promise { - const keyLike = await importPKCS8(pem, '', {extractable: true}) - return this.fromJWK(await exportJWK(keyLike), kid) - } - - static async fromJWK( - input: string | Record, - inputKid?: string, - ): Promise { - const jwk = jwkSchema.parse( - typeof input === 'string' ? JSON.parse(input) : input, - ) - - const kid = either(jwk.kid, inputKid) - const alg = jwk.alg - const use = jwk.use || 'sig' - - return new JoseKey({...jwk, kid, alg, use}) - } -} diff --git a/src/oauth-client-temp/jwk-jose/jose-keyset.ts b/src/oauth-client-temp/jwk-jose/jose-keyset.ts deleted file mode 100644 index 27baefcfda..0000000000 --- a/src/oauth-client-temp/jwk-jose/jose-keyset.ts +++ /dev/null @@ -1,16 +0,0 @@ -import {Key, Keyset} from '#/oauth-client-temp/jwk' -import {Importable, JoseKey} from './jose-key' - -export class JoseKeyset extends Keyset { - static async fromImportables( - input: Record, - ) { - return new JoseKeyset( - await Promise.all( - Object.entries(input).map(([kid, secret]) => - secret instanceof Key ? secret : JoseKey.fromImportable(secret, kid), - ), - ), - ) - } -} diff --git a/src/oauth-client-temp/jwk-jose/util.ts b/src/oauth-client-temp/jwk-jose/util.ts deleted file mode 100644 index f75cdb6671..0000000000 --- a/src/oauth-client-temp/jwk-jose/util.ts +++ /dev/null @@ -1,9 +0,0 @@ -export function either( - a?: T, - b?: T, -): T | undefined { - if (a != null && b != null && a !== b) { - throw new TypeError(`Expected "${b}", got "${a}"`) - } - return a ?? b ?? undefined -} diff --git a/src/oauth-client-temp/jwk/alg.ts b/src/oauth-client-temp/jwk/alg.ts deleted file mode 100644 index 8be3555018..0000000000 --- a/src/oauth-client-temp/jwk/alg.ts +++ /dev/null @@ -1,97 +0,0 @@ -import {Jwk} from './jwk' - -declare const process: undefined | {versions?: {node?: string}} -const IS_NODE_RUNTIME = - typeof process !== 'undefined' && typeof process?.versions?.node === 'string' - -export function* jwkAlgorithms(jwk: Jwk): Generator { - // Ed25519, Ed448, and secp256k1 always have "alg" - // OKP always has "use" - if (jwk.alg) { - yield jwk.alg - return - } - - switch (jwk.kty) { - case 'EC': { - if (jwk.use === 'enc' || jwk.use === undefined) { - yield 'ECDH-ES' - yield 'ECDH-ES+A128KW' - yield 'ECDH-ES+A192KW' - yield 'ECDH-ES+A256KW' - } - - if (jwk.use === 'sig' || jwk.use === undefined) { - const crv = 'crv' in jwk ? jwk.crv : undefined - switch (crv) { - case 'P-256': - case 'P-384': - yield `ES${crv.slice(-3)}`.replace('21', '12') - break - case 'P-521': - yield 'ES512' - break - case 'secp256k1': - if (IS_NODE_RUNTIME) yield 'ES256K' - break - default: - throw new TypeError(`Unsupported crv "${crv}"`) - } - } - - return - } - - case 'OKP': { - if (!jwk.use) throw new TypeError('Missing "use" Parameter value') - yield 'ECDH-ES' - yield 'ECDH-ES+A128KW' - yield 'ECDH-ES+A192KW' - yield 'ECDH-ES+A256KW' - return - } - - case 'RSA': { - if (jwk.use === 'enc' || jwk.use === undefined) { - yield 'RSA-OAEP' - yield 'RSA-OAEP-256' - yield 'RSA-OAEP-384' - yield 'RSA-OAEP-512' - if (IS_NODE_RUNTIME) yield 'RSA1_5' - } - - if (jwk.use === 'sig' || jwk.use === undefined) { - yield 'PS256' - yield 'PS384' - yield 'PS512' - yield 'RS256' - yield 'RS384' - yield 'RS512' - } - - return - } - - case 'oct': { - if (jwk.use === 'enc' || jwk.use === undefined) { - yield 'A128GCMKW' - yield 'A192GCMKW' - yield 'A256GCMKW' - yield 'A128KW' - yield 'A192KW' - yield 'A256KW' - } - - if (jwk.use === 'sig' || jwk.use === undefined) { - yield 'HS256' - yield 'HS384' - yield 'HS512' - } - - return - } - - default: - throw new Error(`Unsupported kty "${jwk.kty}"`) - } -} diff --git a/src/oauth-client-temp/jwk/index.ts b/src/oauth-client-temp/jwk/index.ts deleted file mode 100644 index 79393d6eea..0000000000 --- a/src/oauth-client-temp/jwk/index.ts +++ /dev/null @@ -1,9 +0,0 @@ -export * from './alg' -export * from './jwk' -export * from './jwks' -export * from './jwt' -export * from './jwt-decode' -export * from './jwt-verify' -export * from './key' -export * from './keyset' -export * from './util' diff --git a/src/oauth-client-temp/jwk/jwk.ts b/src/oauth-client-temp/jwk/jwk.ts deleted file mode 100644 index f74a2ef377..0000000000 --- a/src/oauth-client-temp/jwk/jwk.ts +++ /dev/null @@ -1,153 +0,0 @@ -import { z } from 'zod' - -export const keyUsageSchema = z.enum([ - 'sign', - 'verify', - 'encrypt', - 'decrypt', - 'wrapKey', - 'unwrapKey', - 'deriveKey', - 'deriveBits', -]) - -export type KeyUsage = z.infer - -/** - * The "use" and "key_ops" JWK members SHOULD NOT be used together; - * however, if both are used, the information they convey MUST be - * consistent. Applications should specify which of these members they - * use, if either is to be used by the application. - * - * @todo Actually check that "use" and "key_ops" are consistent when both are present. - * @see {@link https://datatracker.ietf.org/doc/html/rfc7517#section-4.3} - */ -export const jwkBaseSchema = z.object({ - kty: z.string().min(1), - alg: z.string().min(1).optional(), - kid: z.string().min(1).optional(), - ext: z.boolean().optional(), - use: z.enum(['sig', 'enc']).optional(), - key_ops: z.array(keyUsageSchema).readonly().optional(), - - x5c: z.array(z.string()).readonly().optional(), // X.509 Certificate Chain - x5t: z.string().min(1).optional(), // X.509 Certificate SHA-1 Thumbprint - 'x5t#S256': z.string().min(1).optional(), // X.509 Certificate SHA-256 Thumbprint - x5u: z.string().url().optional(), // X.509 URL -}) - -/** - * @todo: properly implement this - */ -export const jwkRsaKeySchema = jwkBaseSchema - .extend({ - kty: z.literal('RSA'), - alg: z - .enum(['RS256', 'RS384', 'RS512', 'PS256', 'PS384', 'PS512']) - .optional(), - - n: z.string().min(1), // Modulus - e: z.string().min(1), // Exponent - - d: z.string().min(1).optional(), // Private Exponent - p: z.string().min(1).optional(), // First Prime Factor - q: z.string().min(1).optional(), // Second Prime Factor - dp: z.string().min(1).optional(), // First Factor CRT Exponent - dq: z.string().min(1).optional(), // Second Factor CRT Exponent - qi: z.string().min(1).optional(), // First CRT Coefficient - oth: z - .array( - z - .object({ - r: z.string().optional(), - d: z.string().optional(), - t: z.string().optional(), - }) - .readonly(), - ) - .nonempty() - .readonly() - .optional(), // Other Primes Info - }) - .readonly() - -export const jwkEcKeySchema = jwkBaseSchema - .extend({ - kty: z.literal('EC'), - alg: z.enum(['ES256', 'ES384', 'ES512']).optional(), - crv: z.enum(['P-256', 'P-384', 'P-521']), - - x: z.string().min(1), - y: z.string().min(1), - - d: z.string().min(1).optional(), // ECC Private Key - }) - .readonly() - -export const jwkEcSecp256k1KeySchema = jwkBaseSchema - .extend({ - kty: z.literal('EC'), - alg: z.enum(['ES256K']).optional(), - crv: z.enum(['secp256k1']), - - x: z.string().min(1), - y: z.string().min(1), - - d: z.string().min(1).optional(), // ECC Private Key - }) - .readonly() - -export const jwkOkpKeySchema = jwkBaseSchema - .extend({ - kty: z.literal('OKP'), - alg: z.enum(['EdDSA']).optional(), - crv: z.enum(['Ed25519', 'Ed448']), - - x: z.string().min(1), - d: z.string().min(1).optional(), // ECC Private Key - }) - .readonly() - -export const jwkSymKeySchema = jwkBaseSchema - .extend({ - kty: z.literal('oct'), // Octet Sequence (used to represent symmetric keys) - alg: z.enum(['HS256', 'HS384', 'HS512']).optional(), - - k: z.string(), // Key Value (base64url encoded) - }) - .readonly() - -export const jwkUnknownKeySchema = jwkBaseSchema - .extend({ - kty: z - .string() - .refine((v) => v !== 'RSA' && v !== 'EC' && v !== 'OKP' && v !== 'oct'), - }) - .readonly() - -export const jwkSchema = z.union([ - jwkUnknownKeySchema, - jwkRsaKeySchema, - jwkEcKeySchema, - jwkEcSecp256k1KeySchema, - jwkOkpKeySchema, - jwkSymKeySchema, -]) - -export type Jwk = z.infer - -export const jwkPubSchema = jwkSchema - .refine((k) => k.kid != null, 'kid is required') - .refine((k) => k.use != null || k.key_ops != null, 'use or key_ops required') - .refine( - (k) => - !k.use || - !k.key_ops || - k.key_ops.every((o) => - k.use === 'sig' - ? o === 'sign' || o === 'verify' - : o === 'encrypt' || o === 'decrypt', - ), - 'use and key_ops must be consistent', - ) - .refine((k) => !('k' in k) && !('d' in k), 'private key not allowed') diff --git a/src/oauth-client-temp/jwk/jwks.ts b/src/oauth-client-temp/jwk/jwks.ts deleted file mode 100644 index b1b333d986..0000000000 --- a/src/oauth-client-temp/jwk/jwks.ts +++ /dev/null @@ -1,19 +0,0 @@ -import {z} from 'zod' - -import {jwkPubSchema, jwkSchema} from './jwk' - -export const jwksSchema = z - .object({ - keys: z.array(jwkSchema).readonly(), - }) - .readonly() - -export type Jwks = z.infer - -export const jwksPubSchema = z - .object({ - keys: z.array(jwkPubSchema).readonly(), - }) - .readonly() - -export type JwksPub = z.infer diff --git a/src/oauth-client-temp/jwk/jwt-decode.ts b/src/oauth-client-temp/jwk/jwt-decode.ts deleted file mode 100644 index 7fc5f3ef3f..0000000000 --- a/src/oauth-client-temp/jwk/jwt-decode.ts +++ /dev/null @@ -1,26 +0,0 @@ -import {b64uDecode} from '#/oauth-client-temp/b64' -import {JwtHeader, jwtHeaderSchema, JwtPayload, jwtPayloadSchema} from './jwt' -import {ui8ToString} from './util' - -export function unsafeDecodeJwt(jwt: string): { - header: JwtHeader - payload: JwtPayload -} { - const {0: headerEnc, 1: payloadEnc, length} = jwt.split('.') - if (length > 3 || length < 2) { - throw new TypeError('invalid JWT input') - } - - const header = jwtHeaderSchema.parse( - JSON.parse(ui8ToString(b64uDecode(headerEnc!))), - ) - if (length === 2 && header?.alg !== 'none') { - throw new TypeError('invalid JWT input') - } - - const payload = jwtPayloadSchema.parse( - JSON.parse(ui8ToString(b64uDecode(payloadEnc!))), - ) - - return {header, payload} -} diff --git a/src/oauth-client-temp/jwk/jwt-verify.ts b/src/oauth-client-temp/jwk/jwt-verify.ts deleted file mode 100644 index 3e05f60ae5..0000000000 --- a/src/oauth-client-temp/jwk/jwt-verify.ts +++ /dev/null @@ -1,20 +0,0 @@ -import {JwtHeader, JwtPayload} from './jwt' -import {RequiredKey} from './util' - -export type VerifyOptions = { - audience?: string | readonly string[] - clockTolerance?: string | number - issuer?: string | readonly string[] - maxTokenAge?: string | number - subject?: string - typ?: string - currentDate?: Date - requiredClaims?: readonly C[] -} - -export type VerifyPayload = Record - -export type VerifyResult

= { - payload: RequiredKey

- protectedHeader: JwtHeader -} diff --git a/src/oauth-client-temp/jwk/jwt.ts b/src/oauth-client-temp/jwk/jwt.ts deleted file mode 100644 index 51de57916c..0000000000 --- a/src/oauth-client-temp/jwk/jwt.ts +++ /dev/null @@ -1,172 +0,0 @@ -import {z} from 'zod' - -import {jwkPubSchema} from './jwk' - -export const JWT_REGEXP = /^[A-Za-z0-9_-]{2,}(?:\.[A-Za-z0-9_-]{2,}){1,2}$/ -export const jwtSchema = z - .string() - .min(5) - .refinement( - (data: string): data is `${string}.${string}.${string}` => - JWT_REGEXP.test(data), - { - code: z.ZodIssueCode.custom, - message: 'Must be a JWT', - }, - ) - -export const isJwt = (data: unknown): data is Jwt => - jwtSchema.safeParse(data).success - -export type Jwt = z.infer - -/** - * @see {@link https://www.rfc-editor.org/rfc/rfc7515.html#section-4} - */ -export const jwtHeaderSchema = z.object({ - /** "alg" (Algorithm) Header Parameter */ - alg: z.string(), - /** "jku" (JWK Set URL) Header Parameter */ - jku: z.string().url().optional(), - /** "jwk" (JSON Web Key) Header Parameter */ - jwk: z - .object({ - kty: z.string(), - crv: z.string().optional(), - x: z.string().optional(), - y: z.string().optional(), - e: z.string().optional(), - n: z.string().optional(), - }) - .optional(), - /** "kid" (Key ID) Header Parameter */ - kid: z.string().optional(), - /** "x5u" (X.509 URL) Header Parameter */ - x5u: z.string().optional(), - /** "x5c" (X.509 Certificate Chain) Header Parameter */ - x5c: z.array(z.string()).optional(), - /** "x5t" (X.509 Certificate SHA-1 Thumbprint) Header Parameter */ - x5t: z.string().optional(), - /** "x5t#S256" (X.509 Certificate SHA-256 Thumbprint) Header Parameter */ - 'x5t#S256': z.string().optional(), - /** "typ" (Type) Header Parameter */ - typ: z.string().optional(), - /** "cty" (Content Type) Header Parameter */ - cty: z.string().optional(), - /** "crit" (Critical) Header Parameter */ - crit: z.array(z.string()).optional(), -}) - -export type JwtHeader = z.infer - -// https://www.iana.org/assignments/jwt/jwt.xhtml -export const jwtPayloadSchema = z.object({ - iss: z.string().optional(), - aud: z.union([z.string(), z.array(z.string()).nonempty()]).optional(), - sub: z.string().optional(), - exp: z.number().int().optional(), - nbf: z.number().int().optional(), - iat: z.number().int().optional(), - jti: z.string().optional(), - htm: z.string().optional(), - htu: z.string().optional(), - ath: z.string().optional(), - acr: z.string().optional(), - azp: z.string().optional(), - amr: z.array(z.string()).optional(), - // https://datatracker.ietf.org/doc/html/rfc7800 - cnf: z - .object({ - kid: z.string().optional(), // Key ID - jwk: jwkPubSchema.optional(), // JWK - jwe: z.string().optional(), // Encrypted key - jku: z.string().url().optional(), // JWK Set URI ("kid" should also be provided) - - // https://datatracker.ietf.org/doc/html/rfc9449#section-6.1 - jkt: z.string().optional(), - - // https://datatracker.ietf.org/doc/html/rfc8705 - 'x5t#S256': z.string().optional(), // X.509 Certificate SHA-256 Thumbprint - - // https://datatracker.ietf.org/doc/html/rfc9203 - osc: z.string().optional(), // OSCORE_Input_Material carrying the parameters for using OSCORE per-message security with implicit key confirmation - }) - .optional(), - - client_id: z.string().optional(), - - scope: z.string().optional(), - nonce: z.string().optional(), - - at_hash: z.string().optional(), - c_hash: z.string().optional(), - s_hash: z.string().optional(), - auth_time: z.number().int().optional(), - - // https://openid.net/specs/openid-connect-core-1_0.html#StandardClaims - - // OpenID: "profile" scope - name: z.string().optional(), - family_name: z.string().optional(), - given_name: z.string().optional(), - middle_name: z.string().optional(), - nickname: z.string().optional(), - preferred_username: z.string().optional(), - gender: z.string().optional(), // OpenID only defines "male" and "female" without forbidding other values - picture: z.string().url().optional(), - profile: z.string().url().optional(), - website: z.string().url().optional(), - birthdate: z - .string() - .regex(/\d{4}-\d{2}-\d{2}/) // YYYY-MM-DD - .optional(), - zoneinfo: z - .string() - .regex(/^[A-Za-z0-9_/]+$/) - .optional(), - locale: z - .string() - .regex(/^[a-z]{2}(-[A-Z]{2})?$/) - .optional(), - updated_at: z.number().int().optional(), - - // OpenID: "email" scope - email: z.string().optional(), - email_verified: z.boolean().optional(), - - // OpenID: "phone" scope - phone_number: z.string().optional(), - phone_number_verified: z.boolean().optional(), - - // OpenID: "address" scope - // https://openid.net/specs/openid-connect-core-1_0.html#AddressClaim - address: z - .object({ - formatted: z.string().optional(), - street_address: z.string().optional(), - locality: z.string().optional(), - region: z.string().optional(), - postal_code: z.string().optional(), - country: z.string().optional(), - }) - .optional(), - - // https://datatracker.ietf.org/doc/html/rfc9396#section-14.2 - authorization_details: z - .array( - z - .object({ - type: z.string(), - // https://datatracker.ietf.org/doc/html/rfc9396#section-2.2 - locations: z.array(z.string()).optional(), - actions: z.array(z.string()).optional(), - datatypes: z.array(z.string()).optional(), - identifier: z.string().optional(), - privileges: z.array(z.string()).optional(), - }) - .passthrough(), - ) - .optional(), -}) - -export type JwtPayload = z.infer diff --git a/src/oauth-client-temp/jwk/key.ts b/src/oauth-client-temp/jwk/key.ts deleted file mode 100644 index c9923f043e..0000000000 --- a/src/oauth-client-temp/jwk/key.ts +++ /dev/null @@ -1,95 +0,0 @@ -import {jwkAlgorithms} from './alg' -import {Jwk, jwkSchema} from './jwk' -import {Jwt, JwtHeader, JwtPayload} from './jwt' -import {VerifyOptions, VerifyPayload, VerifyResult} from './jwt-verify' -import {cachedGetter} from './util' - -export abstract class Key { - constructor(protected jwk: Jwk) { - // A key should always be used either for signing or encryption. - if (!jwk.use) throw new TypeError('Missing "use" Parameter value') - } - - get isPrivate(): boolean { - const {jwk} = this - if ('d' in jwk && jwk.d !== undefined) return true - return this.isSymetric - } - - get isSymetric(): boolean { - const {jwk} = this - if ('k' in jwk && jwk.k !== undefined) return true - return false - } - - get privateJwk(): Jwk | undefined { - return this.isPrivate ? this.jwk : undefined - } - - @cachedGetter - get publicJwk(): Jwk | undefined { - if (this.isSymetric) return undefined - if (this.isPrivate) { - const {d: _, ...jwk} = this.jwk as any - return jwk - } - return this.jwk - } - - @cachedGetter - get bareJwk(): Jwk | undefined { - if (this.isSymetric) return undefined - const {kty, crv, e, n, x, y} = this.jwk as any - return jwkSchema.parse({crv, e, kty, n, x, y}) - } - - get use() { - return this.jwk.use! - } - - /** - * The (forced) algorithm to use. If not provided, the key will be usable with - * any of the algorithms in {@link algorithms}. - */ - get alg() { - return this.jwk.alg - } - - get kid() { - return this.jwk.kid - } - - get crv() { - return (this.jwk as undefined | Extract)?.crv - } - - get canVerify() { - return this.use === 'sig' - } - - get canSign() { - return this.use === 'sig' && this.isPrivate && !this.isSymetric - } - - /** - * All the algorithms that this key can be used with. If `alg` is provided, - * this set will only contain that algorithm. - */ - @cachedGetter - get algorithms(): readonly string[] { - return Array.from(jwkAlgorithms(this.jwk)) - } - - /** - * Create a signed JWT - */ - abstract createJwt(header: JwtHeader, payload: JwtPayload): Promise - - /** - * Verify the signature, headers and payload of a JWT - */ - abstract verifyJwt< - P extends VerifyPayload = JwtPayload, - C extends string = string, - >(token: Jwt, options?: VerifyOptions): Promise> -} diff --git a/src/oauth-client-temp/jwk/keyset.ts b/src/oauth-client-temp/jwk/keyset.ts deleted file mode 100644 index 09137aaba5..0000000000 --- a/src/oauth-client-temp/jwk/keyset.ts +++ /dev/null @@ -1,200 +0,0 @@ -import {Jwk} from './jwk' -import {Jwks} from './jwks' -import {Jwt, JwtHeader, JwtPayload} from './jwt' -import {unsafeDecodeJwt} from './jwt-decode' -import {VerifyOptions} from './jwt-verify' -import {Key} from './key' -import { - cachedGetter, - isDefined, - matchesAny, - Override, - preferredOrderCmp, -} from './util' - -export type JwtSignHeader = Override> - -export type JwtPayloadGetter

= ( - header: JwtHeader, - key: Key, -) => P | PromiseLike

- -export type KeySearch = { - use?: 'sig' | 'enc' - kid?: string | string[] - alg?: string | string[] -} - -const extractPrivateJwk = (key: Key): Jwk | undefined => key.privateJwk -const extractPublicJwk = (key: Key): Jwk | undefined => key.publicJwk - -export class Keyset implements Iterable { - constructor( - private readonly keys: readonly K[], - /** - * The preferred algorithms to use when signing a JWT using this keyset. - */ - readonly preferredSigningAlgorithms: readonly string[] = [ - 'EdDSA', - 'ES256K', - 'ES256', - // https://datatracker.ietf.org/doc/html/rfc7518#section-3.5 - 'PS256', - 'PS384', - 'PS512', - 'HS256', - 'HS384', - 'HS512', - ], - ) { - if (!keys.length) throw new Error('Keyset is empty') - - const kids = new Set() - for (const {kid} of keys) { - if (!kid) continue - - if (kids.has(kid)) throw new Error(`Duplicate key id: ${kid}`) - else kids.add(kid) - } - } - - @cachedGetter - get signAlgorithms(): readonly string[] { - const algorithms = new Set() - for (const key of this) { - if (key.use !== 'sig') continue - for (const alg of key.algorithms) { - algorithms.add(alg) - } - } - return Object.freeze( - [...algorithms].sort(preferredOrderCmp(this.preferredSigningAlgorithms)), - ) - } - - @cachedGetter - get publicJwks(): Jwks { - return { - keys: Array.from(this, extractPublicJwk).filter(isDefined), - } - } - - @cachedGetter - get privateJwks(): Jwks { - return { - keys: Array.from(this, extractPrivateJwk).filter(isDefined), - } - } - - has(kid: string): boolean { - return this.keys.some(key => key.kid === kid) - } - - get(search: KeySearch): K { - for (const key of this.list(search)) { - return key - } - - throw new TypeError( - `Key not found ${search.kid || search.alg || ''}`, - ) - } - - *list(search: KeySearch): Generator { - // Optimization: Empty string or empty array will not match any key - if (search.kid?.length === 0) return - if (search.alg?.length === 0) return - - for (const key of this) { - if (search.use && key.use !== search.use) continue - - if (Array.isArray(search.kid)) { - if (!key.kid || !search.kid.includes(key.kid)) continue - } else if (search.kid) { - if (key.kid !== search.kid) continue - } - - if (Array.isArray(search.alg)) { - if (!search.alg.some(a => key.algorithms.includes(a))) continue - } else if (typeof search.alg === 'string') { - if (!key.algorithms.includes(search.alg)) continue - } - - yield key - } - } - - findSigningKey(search: Omit): [key: Key, alg: string] { - const {kid, alg} = search - const matchingKeys: Key[] = [] - - for (const key of this.list({kid, alg, use: 'sig'})) { - // Not a signing key - if (!key.canSign) continue - - // Skip negotiation if a specific "alg" was provided - if (typeof alg === 'string') return [key, alg] - - matchingKeys.push(key) - } - - const isAllowedAlg = matchesAny(alg) - const candidates = matchingKeys.map( - key => [key, key.algorithms.filter(isAllowedAlg)] as const, - ) - - // Return the first candidates that matches the preferred algorithms - for (const prefAlg of this.preferredSigningAlgorithms) { - for (const [matchingKey, matchingAlgs] of candidates) { - if (matchingAlgs.includes(prefAlg)) return [matchingKey, prefAlg] - } - } - - // Return any candidate - for (const [matchingKey, matchingAlgs] of candidates) { - for (const alg of matchingAlgs) { - return [matchingKey, alg] - } - } - - throw new TypeError(`No singing key found for ${kid || alg || ''}`) - } - - [Symbol.iterator](): IterableIterator { - return this.keys.values() - } - - async sign( - {alg: searchAlg, kid: searchKid, ...header}: JwtSignHeader, - payload: JwtPayload | JwtPayloadGetter, - ) { - const [key, alg] = this.findSigningKey({alg: searchAlg, kid: searchKid}) - const protectedHeader = {...header, alg, kid: key.kid} - - if (typeof payload === 'function') { - payload = await payload(protectedHeader, key) - } - - return key.createJwt(protectedHeader, payload) - } - - async verify< - P extends Record = JwtPayload, - C extends string = string, - >(token: Jwt, options?: VerifyOptions) { - const {header} = unsafeDecodeJwt(token) - const {kid, alg} = header - - const errors: unknown[] = [] - - for (const key of this.list({use: 'sig', kid, alg})) { - try { - return await key.verifyJwt(token, options) - } catch (err) { - errors.push(err) - } - } - - throw new AggregateError(errors, 'Unable to verify signature') - } -} diff --git a/src/oauth-client-temp/jwk/util.ts b/src/oauth-client-temp/jwk/util.ts deleted file mode 100644 index 12b5625cce..0000000000 --- a/src/oauth-client-temp/jwk/util.ts +++ /dev/null @@ -1,55 +0,0 @@ -// eslint-disable-next-line @typescript-eslint/ban-types -export type Simplify = { [K in keyof T]: T[K] } & {} -export type Override = Simplify> - -export type RequiredKey = Simplify< - string extends K - ? T - : { - [L in K]: Exclude - } & Omit -> - -export const isDefined = (i: T | undefined): i is T => i !== undefined - -export const preferredOrderCmp = - (order: readonly T[]) => - (a: T, b: T) => { - const aIdx = order.indexOf(a) - const bIdx = order.indexOf(b) - if (aIdx === bIdx) return 0 - if (aIdx === -1) return 1 - if (bIdx === -1) return -1 - return aIdx - bIdx - } - -export function matchesAny( - value: null | undefined | T | readonly T[], -): (v: unknown) => v is T { - return value == null - ? (v): v is T => true - : Array.isArray(value) - ? (v): v is T => value.includes(v) - : (v): v is T => v === value -} - -/** - * Decorator to cache the result of a getter on a class instance. - */ -export const cachedGetter = ( - target: (this: T) => V, - _context: ClassGetterDecoratorContext, -) => { - return function (this: T) { - const value = target.call(this) - Object.defineProperty(this, target.name, { - get: () => value, - enumerable: true, - configurable: true, - }) - return value - } -} - -export const decoder = new TextDecoder() -export const ui8ToString = (value: Uint8Array) => decoder.decode(value) From 6522853d99746b25fdeaed011554c962b9df3375 Mon Sep 17 00:00:00 2001 From: Hailey Date: Tue, 9 Apr 2024 19:07:58 -0700 Subject: [PATCH 21/54] add `expo-sqlite` --- package.json | 1 + yarn.lock | 43 +++++++++++++++++++++++++++++++++++++++++++ 2 files changed, 44 insertions(+) diff --git a/package.json b/package.json index 235cadaa48..c611093294 100644 --- a/package.json +++ b/package.json @@ -125,6 +125,7 @@ "expo-secure-store": "^12.8.1", "expo-sharing": "^11.10.0", "expo-splash-screen": "~0.26.4", + "expo-sqlite": "^13.4.0", "expo-status-bar": "~1.11.1", "expo-system-ui": "~2.9.3", "expo-task-manager": "~11.7.2", diff --git a/yarn.lock b/yarn.lock index d05d3aa797..a7d5d01f97 100644 --- a/yarn.lock +++ b/yarn.lock @@ -3555,6 +3555,17 @@ webpack-dev-server "^4.11.1" webpack-manifest-plugin "^4.1.1" +"@expo/websql@^1.0.1": + version "1.0.1" + resolved "https://registry.yarnpkg.com/@expo/websql/-/websql-1.0.1.tgz#fff0cf9c1baa1f70f9e1d658b7c39a420d9b10a9" + integrity sha512-H9/t1V7XXyKC343FJz/LwaVBfDhs6IqhDtSYWpt8LNSQDVjf5NvVJLc5wp+KCpRidZx8+0+YeHJN45HOXmqjFA== + dependencies: + argsarray "^0.0.1" + immediate "^3.2.2" + noop-fn "^1.0.0" + pouchdb-collections "^1.0.1" + tiny-queue "^0.2.1" + "@expo/xcpretty@^4.3.0": version "4.3.0" resolved "https://registry.yarnpkg.com/@expo/xcpretty/-/xcpretty-4.3.0.tgz#d745c2c5ec38fc6acd451112bb05c6ae952a2c3a" @@ -8475,6 +8486,11 @@ argparse@^2.0.1: resolved "https://registry.yarnpkg.com/argparse/-/argparse-2.0.1.tgz#246f50f3ca78a3240f6c997e8a9bd1eac49e4b38" integrity sha512-8+9WqebbFzpX9OR+Wa6O29asIogeRMzcGtAINdpMHHyAg10f05aSFVBbcEqGf/PXw1EjAZ+q2/bEBg3DvurK3Q== +argsarray@^0.0.1: + version "0.0.1" + resolved "https://registry.yarnpkg.com/argsarray/-/argsarray-0.0.1.tgz#6e7207b4ecdb39b0af88303fa5ae22bda8df61cb" + integrity sha512-u96dg2GcAKtpTrBdDoFIM7PjcBA+6rSP0OR94MOReNRyUECL6MtQt5XXmRr4qrftYaef9+l5hcpO5te7sML1Cg== + aria-hidden@^1.1.1: version "1.2.3" resolved "https://registry.yarnpkg.com/aria-hidden/-/aria-hidden-1.2.3.tgz#14aeb7fb692bbb72d69bebfa47279c1fd725e954" @@ -12192,6 +12208,13 @@ expo-splash-screen@~0.26.4: dependencies: "@expo/prebuild-config" "6.7.4" +expo-sqlite@^13.4.0: + version "13.4.0" + resolved "https://registry.yarnpkg.com/expo-sqlite/-/expo-sqlite-13.4.0.tgz#ae945622662263431aa9daee59b659255cb6a9fd" + integrity sha512-5f7d2EDM+pgerM33KndtX4gWw2nuVaXY68nnqx7PhkiYeyEmeNfZ29bIFtpBzNb/L5l0/DTtRxuSqftxbknFtw== + dependencies: + "@expo/websql" "^1.0.1" + expo-status-bar@~1.11.1: version "1.11.1" resolved "https://registry.yarnpkg.com/expo-status-bar/-/expo-status-bar-1.11.1.tgz#a11318741d361048c11db2b16c4364a79a74af30" @@ -13510,6 +13533,11 @@ image-size@^1.0.2: dependencies: queue "6.0.2" +immediate@^3.2.2: + version "3.3.0" + resolved "https://registry.yarnpkg.com/immediate/-/immediate-3.3.0.tgz#1aef225517836bcdf7f2a2de2600c79ff0269266" + integrity sha512-HR7EVodfFUdQCTIeySw+WDRFJlPcLOJbXfwwZ7Oom6tjsvZ3bOkCDJHehQC3nxJrv7+f9XecwazynjU8e4Vw3Q== + immediate@~3.0.5: version "3.0.6" resolved "https://registry.yarnpkg.com/immediate/-/immediate-3.0.6.tgz#9db1dbd0faf8de6fbe0f5dd5e56bb606280de69b" @@ -16748,6 +16776,11 @@ nodemailer@^6.8.0: resolved "https://registry.yarnpkg.com/nodemailer/-/nodemailer-6.9.4.tgz#93bd4a60eb0be6fa088a0483340551ebabfd2abf" integrity sha512-CXjQvrQZV4+6X5wP6ZIgdehJamI63MFoYFGGPtHudWym9qaEHDNdPzaj5bfMCvxG1vhAileSWW90q7nL0N36mA== +noop-fn@^1.0.0: + version "1.0.0" + resolved "https://registry.yarnpkg.com/noop-fn/-/noop-fn-1.0.0.tgz#5f33d47f13d2150df93e0cb036699e982f78ffbf" + integrity sha512-pQ8vODlgXt2e7A3mIbFDlizkr46r75V+BJxVAyat8Jl7YmI513gG5cfyRL0FedKraoZ+VAouI1h4/IWpus5pcQ== + normalize-path@^3.0.0, normalize-path@~3.0.0: version "3.0.0" resolved "https://registry.yarnpkg.com/normalize-path/-/normalize-path-3.0.0.tgz#0dcd69ff23a1c9b11fd0978316644a0388216a65" @@ -18214,6 +18247,11 @@ postinstall-postinstall@^2.1.0: resolved "https://registry.yarnpkg.com/postinstall-postinstall/-/postinstall-postinstall-2.1.0.tgz#4f7f77441ef539d1512c40bd04c71b06a4704ca3" integrity sha512-7hQX6ZlZXIoRiWNrbMQaLzUUfH+sSx39u8EJ9HYuDc1kLo9IXKWjM5RSquZN1ad5GnH8CGFM78fsAAQi3OKEEQ== +pouchdb-collections@^1.0.1: + version "1.0.1" + resolved "https://registry.yarnpkg.com/pouchdb-collections/-/pouchdb-collections-1.0.1.tgz#fe63a17da977611abef7cb8026cb1a9553fd8359" + integrity sha512-31db6JRg4+4D5Yzc2nqsRqsA2oOkZS8DpFav3jf/qVNBxusKa2ClkEIZ2bJNpaDbMfWtnuSq59p6Bn+CipPMdg== + prebuild-install@^7.1.0, prebuild-install@^7.1.1: version "7.1.1" resolved "https://registry.yarnpkg.com/prebuild-install/-/prebuild-install-7.1.1.tgz#de97d5b34a70a0c81334fd24641f2a1702352e45" @@ -21050,6 +21088,11 @@ tiny-hashes@^1.0.1: resolved "https://registry.yarnpkg.com/tiny-hashes/-/tiny-hashes-1.0.1.tgz#ddbe9060312ddb4efe0a174bb3a27e1331c425a1" integrity sha512-knIN5zj4fl7kW4EBU5sLP20DWUvi/rVouvJezV0UAym2DkQaqm365Nyc8F3QEiOvunNDMxR8UhcXd1d5g+Wg1g== +tiny-queue@^0.2.1: + version "0.2.1" + resolved "https://registry.yarnpkg.com/tiny-queue/-/tiny-queue-0.2.1.tgz#25a67f2c6e253b2ca941977b5ef7442ef97a6046" + integrity sha512-EijGsv7kzd9I9g0ByCl6h42BWNGUZrlCSejfrb3AKeHC33SGbASu1VDf5O3rRiiUOhAC9CHdZxFPbZu0HmR70A== + tippy.js@^6.3.7: version "6.3.7" resolved "https://registry.yarnpkg.com/tippy.js/-/tippy.js-6.3.7.tgz#8ccfb651d642010ed9a32ff29b0e9e19c5b8c61c" From ba4e95f0b1fb38fcdbc695c62ba4cc984caafbc4 Mon Sep 17 00:00:00 2001 From: Hailey Date: Tue, 9 Apr 2024 20:32:07 -0700 Subject: [PATCH 22/54] update deps (last time) --- package.json | 2 +- yarn.lock | 10 +++++----- 2 files changed, 6 insertions(+), 6 deletions(-) diff --git a/package.json b/package.json index c611093294..41af0a0606 100644 --- a/package.json +++ b/package.json @@ -122,7 +122,6 @@ "expo-localization": "~14.8.3", "expo-media-library": "~15.9.1", "expo-notifications": "~0.27.6", - "expo-secure-store": "^12.8.1", "expo-sharing": "^11.10.0", "expo-splash-screen": "~0.26.4", "expo-sqlite": "^13.4.0", @@ -166,6 +165,7 @@ "react-native-get-random-values": "~1.11.0", "react-native-image-crop-picker": "^0.38.1", "react-native-ios-context-menu": "^1.15.3", + "react-native-mmkv": "^2.12.2", "react-native-pager-view": "6.2.3", "react-native-picker-select": "^8.1.0", "react-native-progress": "bluesky-social/react-native-progress", diff --git a/yarn.lock b/yarn.lock index a7d5d01f97..9dd5ecb02d 100644 --- a/yarn.lock +++ b/yarn.lock @@ -12191,11 +12191,6 @@ expo-pwa@0.0.127: commander "2.20.0" update-check "1.5.3" -expo-secure-store@^12.8.1: - version "12.8.1" - resolved "https://registry.yarnpkg.com/expo-secure-store/-/expo-secure-store-12.8.1.tgz#369a570702fa1dc0c49ea41a5ab18aca2a986d38" - integrity sha512-Ju3jmkHby4w7rIzdYAt9kQyQ7HhHJ0qRaiQOInknhOLIltftHjEgF4I1UmzKc7P5RCfGNmVbEH729Pncp/sHXQ== - expo-sharing@^11.10.0: version "11.10.0" resolved "https://registry.yarnpkg.com/expo-sharing/-/expo-sharing-11.10.0.tgz#0e85197ee4d2634b00fe201e571fbdc64cf83eef" @@ -18931,6 +18926,11 @@ react-native-ios-context-menu@^1.15.3: dependencies: "@dominicstop/ts-event-emitter" "^1.1.0" +react-native-mmkv@^2.12.2: + version "2.12.2" + resolved "https://registry.yarnpkg.com/react-native-mmkv/-/react-native-mmkv-2.12.2.tgz#4bba0f5f04e2cf222494cce3a9794ba6a4894dee" + integrity sha512-6058Aq0p57chPrUutLGe9fYoiDVDNMU2PKV+lLFUJ3GhoHvUrLdsS1PDSCLr00yqzL4WJQ7TTzH+V8cpyrNcfg== + react-native-pager-view@6.2.3: version "6.2.3" resolved "https://registry.yarnpkg.com/react-native-pager-view/-/react-native-pager-view-6.2.3.tgz#698f6387fdf06cecc3d8d4792604419cb89cb775" From e831bc1a9a5814486d1bbe4f546b1063b9d7a71c Mon Sep 17 00:00:00 2001 From: Hailey Date: Wed, 10 Apr 2024 08:32:01 -0700 Subject: [PATCH 23/54] native crypto setup --- babel.config.js | 4 +++- package.json | 3 ++- src/lib/oauth.ts | 4 ++-- src/screens/Login/hooks/useLogin.ts | 13 ++++++------- yarn.lock | 30 ++++++++++++----------------- 5 files changed, 25 insertions(+), 29 deletions(-) diff --git a/babel.config.js b/babel.config.js index 43b2c7bce3..785aee02cb 100644 --- a/babel.config.js +++ b/babel.config.js @@ -42,7 +42,9 @@ module.exports = function (api) { platform: './src/platform', state: './src/state', view: './src/view', - crypto: './src/platform/crypto.ts', + crypto: 'react-native-quick-crypto', + stream: 'stream-browserify', + buffer: '@craftzdog/react-native-buffer', }, }, ], diff --git a/package.json b/package.json index d1cd1369b6..f770cfa912 100644 --- a/package.json +++ b/package.json @@ -263,7 +263,8 @@ }, "resolutions": { "@types/react": "^18", - "**/zeed-dom": "0.10.9" + "**/zeed-dom": "0.10.9", + "browserify-sign": "4.2.2" }, "jest": { "preset": "jest-expo/ios", diff --git a/src/lib/oauth.ts b/src/lib/oauth.ts index fc25980012..d1152a6e6f 100644 --- a/src/lib/oauth.ts +++ b/src/lib/oauth.ts @@ -1,7 +1,7 @@ import {isWeb} from 'platform/detection' -export const OAUTH_CLIENT_ID = 'https://bsky.app' -export const OAUTH_REDIRECT_URI = 'https://bsky.app/auth/callback' +export const OAUTH_CLIENT_ID = 'http://localhost/' +export const OAUTH_REDIRECT_URI = 'http://127.0.0.1:5173/' export const OAUTH_SCOPE = 'openid profile email phone offline_access' export const OAUTH_GRANT_TYPES = [ 'authorization_code', diff --git a/src/screens/Login/hooks/useLogin.ts b/src/screens/Login/hooks/useLogin.ts index f1626d7d9e..13840e1ac2 100644 --- a/src/screens/Login/hooks/useLogin.ts +++ b/src/screens/Login/hooks/useLogin.ts @@ -1,5 +1,6 @@ import React from 'react' import * as Browser from 'expo-web-browser' +import {BrowserOAuthClientFactory} from '@atproto/oauth-client-react-native' import { buildOAuthUrl, @@ -11,16 +12,11 @@ import { OAUTH_RESPONSE_TYPES, OAUTH_SCOPE, } from 'lib/oauth' -import {CryptoImplementation} from '#/oauth-client-temp/client/crypto-implementation' -import {OAuthClientFactory} from '#/oauth-client-temp/client/oauth-client-factory' - -// TODO remove hack -const serviceUrl = 'http://localhost' // Service URL here is just a placeholder, this isn't how it will actually work export function useLogin(serviceUrl: string | undefined) { const openAuthSession = React.useCallback(async () => { - const oauthFactory = new OAuthClientFactory({ + const oauthFactory = new BrowserOAuthClientFactory({ clientMetadata: { client_id: OAUTH_CLIENT_ID, redirect_uris: [OAUTH_REDIRECT_URI], @@ -30,9 +26,12 @@ export function useLogin(serviceUrl: string | undefined) { dpop_bound_access_tokens: DPOP_BOUND_ACCESS_TOKENS, application_type: OAUTH_APPLICATION_TYPE, }, - cryptoImplementation: new CryptoImplementation(crypto), }) + await oauthFactory.signIn('afepwasfojefpaowejfpwef') + + return + if (!serviceUrl) return const url = buildOAuthUrl(serviceUrl, '123') // TODO replace '123' with the appropriate state diff --git a/yarn.lock b/yarn.lock index f9ab27b55f..d0a88c7ff6 100644 --- a/yarn.lock +++ b/yarn.lock @@ -9279,20 +9279,19 @@ browserify-rsa@^4.0.0, browserify-rsa@^4.1.0: bn.js "^5.0.0" randombytes "^2.0.1" -browserify-sign@^4.0.0: - version "4.2.3" - resolved "https://registry.yarnpkg.com/browserify-sign/-/browserify-sign-4.2.3.tgz#7afe4c01ec7ee59a89a558a4b75bd85ae62d4208" - integrity sha512-JWCZW6SKhfhjJxO8Tyiiy+XYB7cqd2S5/+WeYHsKdNKFlCBhKbblba1A/HN/90YwtxKc8tCErjffZl++UNmGiw== +browserify-sign@4.2.2, browserify-sign@^4.0.0: + version "4.2.2" + resolved "https://registry.yarnpkg.com/browserify-sign/-/browserify-sign-4.2.2.tgz#e78d4b69816d6e3dd1c747e64e9947f9ad79bc7e" + integrity sha512-1rudGyeYY42Dk6texmv7c4VcQ0EsvVbLwZkA+AQB7SxvXxmcD93jcHie8bzecJ+ChDlmAm2Qyu0+Ccg5uhZXCg== dependencies: bn.js "^5.2.1" browserify-rsa "^4.1.0" create-hash "^1.2.0" create-hmac "^1.1.7" - elliptic "^6.5.5" - hash-base "~3.0" + elliptic "^6.5.4" inherits "^2.0.4" - parse-asn1 "^5.1.7" - readable-stream "^2.3.8" + parse-asn1 "^5.1.6" + readable-stream "^3.6.2" safe-buffer "^5.2.1" browserslist@^4.0.0, browserslist@^4.14.5, browserslist@^4.18.1, browserslist@^4.21.10, browserslist@^4.21.4, browserslist@^4.21.9: @@ -11082,7 +11081,7 @@ elliptic@^6.4.1: minimalistic-assert "^1.0.1" minimalistic-crypto-utils "^1.0.1" -elliptic@^6.5.3, elliptic@^6.5.5: +elliptic@^6.5.3, elliptic@^6.5.4: version "6.5.5" resolved "https://registry.yarnpkg.com/elliptic/-/elliptic-6.5.5.tgz#c715e09f78b6923977610d4c2346d6ce22e6dded" integrity sha512-7EjbcmUm17NQFu4Pmgmq2olYMj8nwMnpcddByChSUjArp8F5DQWcIcpriwO4ZToLNAJig0yiyjswfyGNje/ixw== @@ -17262,7 +17261,7 @@ parent-module@^1.0.0: dependencies: callsites "^3.0.0" -parse-asn1@^5.0.0, parse-asn1@^5.1.7: +parse-asn1@^5.0.0, parse-asn1@^5.1.6: version "5.1.7" resolved "https://registry.yarnpkg.com/parse-asn1/-/parse-asn1-5.1.7.tgz#73cdaaa822125f9647165625eb45f8a051d2df06" integrity sha512-CTM5kuWR3sx9IFamcl5ErfPl6ea/N8IYwiJ+vpeB2g+1iknv7zBl5uPwbMbRVznRVbrNY6lGuDoE5b30grmbqg== @@ -18265,7 +18264,7 @@ pouchdb-collections@^1.0.1: resolved "https://registry.yarnpkg.com/pouchdb-collections/-/pouchdb-collections-1.0.1.tgz#fe63a17da977611abef7cb8026cb1a9553fd8359" integrity sha512-31db6JRg4+4D5Yzc2nqsRqsA2oOkZS8DpFav3jf/qVNBxusKa2ClkEIZ2bJNpaDbMfWtnuSq59p6Bn+CipPMdg== -prebuild-install@^7.1.0, prebuild-install@^7.1.1: +prebuild-install@^7.1.1: version "7.1.1" resolved "https://registry.yarnpkg.com/prebuild-install/-/prebuild-install-7.1.1.tgz#de97d5b34a70a0c81334fd24641f2a1702352e45" integrity sha512-jAXscXWMcCK8GgCoHOfIr0ODh5ai8mj63L2nWrjuAgXE6tDyYGnx4/8o/rCgU+B4JSyZBKbeZqzhtwtC3ovxjw== @@ -19252,7 +19251,7 @@ read-cache@^1.0.0: dependencies: pify "^2.3.0" -readable-stream@^2.0.1, readable-stream@^2.0.2, readable-stream@^2.3.8, readable-stream@~2.3.6: +readable-stream@^2.0.1, readable-stream@^2.0.2, readable-stream@~2.3.6: version "2.3.8" resolved "https://registry.yarnpkg.com/readable-stream/-/readable-stream-2.3.8.tgz#91125e8042bba1b9887f49345f6277027ce8be9b" integrity sha512-8p0AUk4XODgIewSi0l8Epjs+EVnWiK7NoDIEGU0HhE7+ZyY8D1IMY7odu5lRrFXGg71L15KG8QrPmum45RTtdA== @@ -19265,7 +19264,7 @@ readable-stream@^2.0.1, readable-stream@^2.0.2, readable-stream@^2.3.8, readable string_decoder "~1.1.1" util-deprecate "~1.0.1" -readable-stream@^3.0.6, readable-stream@^3.1.1, readable-stream@^3.4.0, readable-stream@^3.5.0, readable-stream@^3.6.0: +readable-stream@^3.0.6, readable-stream@^3.1.1, readable-stream@^3.4.0, readable-stream@^3.5.0, readable-stream@^3.6.0, readable-stream@^3.6.2: version "3.6.2" resolved "https://registry.yarnpkg.com/readable-stream/-/readable-stream-3.6.2.tgz#56a9b36ea965c00c5a93ef31eb111a0f11056967" integrity sha512-9u/sniCrY3D5WdsERHzHE4G2YCXqoG5FTHUiCC4SIbr6XcLZBY05ya9EKjYek9O5xOAwjGq+1JdGBAS7Q9ScoA== @@ -22640,11 +22639,6 @@ zeego@^1.6.2: "@radix-ui/react-dropdown-menu" "^2.0.1" sf-symbols-typescript "^1.0.0" -zod@3.21.4: - version "3.21.4" - resolved "https://registry.yarnpkg.com/zod/-/zod-3.21.4.tgz#10882231d992519f0a10b5dd58a38c9dabbb64db" - integrity sha512-m46AKbrzKVzOzs/DZgVnG5H55N1sv1M8qZU3A8RIKbs3mrACDNeIOeilDymVb2HdmP8uwshOCF4uJ8uM9rCqJw== - zod@^3.14.2, zod@^3.21.4: version "3.22.2" resolved "https://registry.yarnpkg.com/zod/-/zod-3.22.2.tgz#3add8c682b7077c05ac6f979fea6998b573e157b" From 25e5ee7f97d429ec0a27fb612d0bfb91048a2077 Mon Sep 17 00:00:00 2001 From: Hailey Date: Thu, 11 Apr 2024 22:03:31 -0700 Subject: [PATCH 24/54] remove bogus packages --- package.json | 4 -- yarn.lock | 194 ++++----------------------------------------------- 2 files changed, 12 insertions(+), 186 deletions(-) diff --git a/package.json b/package.json index f770cfa912..e84e9b8146 100644 --- a/package.json +++ b/package.json @@ -124,7 +124,6 @@ "expo-notifications": "~0.27.6", "expo-sharing": "^11.10.0", "expo-splash-screen": "~0.26.4", - "expo-sqlite": "^13.4.0", "expo-status-bar": "~1.11.1", "expo-system-ui": "~2.9.3", "expo-task-manager": "~11.7.2", @@ -165,12 +164,9 @@ "react-native-get-random-values": "~1.11.0", "react-native-image-crop-picker": "^0.38.1", "react-native-ios-context-menu": "^1.15.3", - "react-native-mmkv": "^2.12.2", "react-native-pager-view": "6.2.3", "react-native-picker-select": "^8.1.0", "react-native-progress": "bluesky-social/react-native-progress", - "react-native-quick-base64": "^2.1.0", - "react-native-quick-crypto": "^0.6.1", "react-native-reanimated": "^3.6.0", "react-native-root-siblings": "^4.1.1", "react-native-safe-area-context": "4.8.2", diff --git a/yarn.lock b/yarn.lock index d0a88c7ff6..d2bb186c60 100644 --- a/yarn.lock +++ b/yarn.lock @@ -2590,14 +2590,6 @@ resolved "https://registry.yarnpkg.com/@connectrpc/connect/-/connect-1.3.0.tgz#2894629f7f11b46fef883a898dab529f84171bf3" integrity sha512-kTeWxJnLLtxKc2ZSDN0rIBgwfP8RwcLknthX4AKlIAmN9ZC4gGnCbwp+3BKcP/WH5c8zGBAWqSY3zeqCM+ah7w== -"@craftzdog/react-native-buffer@^6.0.5": - version "6.0.5" - resolved "https://registry.yarnpkg.com/@craftzdog/react-native-buffer/-/react-native-buffer-6.0.5.tgz#0d4fbe0dd104186d2806655e3c0d25cebdae91d3" - integrity sha512-Av+YqfwA9e7jhgI9GFE/gTpwl/H+dRRLmZyJPOpKTy107j9Oj7oXlm3/YiMNz+C/CEGqcKAOqnXDLs4OL6AAFw== - dependencies: - ieee754 "^1.2.1" - react-native-quick-base64 "^2.0.5" - "@cspotcode/source-map-support@^0.8.0": version "0.8.1" resolved "https://registry.yarnpkg.com/@cspotcode/source-map-support/-/source-map-support-0.8.1.tgz#00629c35a688e05a88b1cda684fb9d5e73f000a1" @@ -3494,17 +3486,6 @@ webpack-dev-server "^4.11.1" webpack-manifest-plugin "^4.1.1" -"@expo/websql@^1.0.1": - version "1.0.1" - resolved "https://registry.yarnpkg.com/@expo/websql/-/websql-1.0.1.tgz#fff0cf9c1baa1f70f9e1d658b7c39a420d9b10a9" - integrity sha512-H9/t1V7XXyKC343FJz/LwaVBfDhs6IqhDtSYWpt8LNSQDVjf5NvVJLc5wp+KCpRidZx8+0+YeHJN45HOXmqjFA== - dependencies: - argsarray "^0.0.1" - immediate "^3.2.2" - noop-fn "^1.0.0" - pouchdb-collections "^1.0.1" - tiny-queue "^0.2.1" - "@expo/xcpretty@^4.3.0": version "4.3.0" resolved "https://registry.yarnpkg.com/@expo/xcpretty/-/xcpretty-4.3.0.tgz#d745c2c5ec38fc6acd451112bb05c6ae952a2c3a" @@ -7681,11 +7662,6 @@ resolved "https://registry.yarnpkg.com/@types/node/-/node-20.5.1.tgz#178d58ee7e4834152b0e8b4d30cbfab578b9bb30" integrity sha512-4tT2UrL5LBqDwoed9wZ6N3umC4Yhz3W3FloMmiiG4JwmUJWpie0c7lcnUNd4gtMKuDEO4wRVS8B6Xa0uMRsMKg== -"@types/node@^17.0.31": - version "17.0.45" - resolved "https://registry.yarnpkg.com/@types/node/-/node-17.0.45.tgz#2c0fafd78705e7a18b7906b5201a522719dc5190" - integrity sha512-w+tIMs3rq2afQdsPJlODhoUEKzFP1ayaoyl1CcnwtIlsVe7K7bA1NGm4s3PraqTLlXnbIN84zuBlxBWo1u9BLw== - "@types/node@^18.16.2": version "18.17.6" resolved "https://registry.yarnpkg.com/@types/node/-/node-18.17.6.tgz#0296e9a30b22d2a8fcaa48d3c45afe51474ca55b" @@ -8442,11 +8418,6 @@ argparse@^2.0.1: resolved "https://registry.yarnpkg.com/argparse/-/argparse-2.0.1.tgz#246f50f3ca78a3240f6c997e8a9bd1eac49e4b38" integrity sha512-8+9WqebbFzpX9OR+Wa6O29asIogeRMzcGtAINdpMHHyAg10f05aSFVBbcEqGf/PXw1EjAZ+q2/bEBg3DvurK3Q== -argsarray@^0.0.1: - version "0.0.1" - resolved "https://registry.yarnpkg.com/argsarray/-/argsarray-0.0.1.tgz#6e7207b4ecdb39b0af88303fa5ae22bda8df61cb" - integrity sha512-u96dg2GcAKtpTrBdDoFIM7PjcBA+6rSP0OR94MOReNRyUECL6MtQt5XXmRr4qrftYaef9+l5hcpO5te7sML1Cg== - aria-hidden@^1.1.1: version "1.2.3" resolved "https://registry.yarnpkg.com/aria-hidden/-/aria-hidden-1.2.3.tgz#14aeb7fb692bbb72d69bebfa47279c1fd725e954" @@ -9134,7 +9105,7 @@ blueimp-md5@^2.10.0: resolved "https://registry.yarnpkg.com/blueimp-md5/-/blueimp-md5-2.19.0.tgz#b53feea5498dcb53dc6ec4b823adb84b729c4af0" integrity sha512-DRQrD6gJyy8FbiE4s+bDoXS9hiW3Vbx5uCdwvcCf3zLHL+Iv7LtGHLpr+GZV8rHG8tK766FGYBwRbu8pELTt+w== -bn.js@^4.0.0, bn.js@^4.1.0, bn.js@^4.11.8, bn.js@^4.11.9: +bn.js@^4.0.0, bn.js@^4.11.8, bn.js@^4.11.9: version "4.12.0" resolved "https://registry.yarnpkg.com/bn.js/-/bn.js-4.12.0.tgz#775b3f278efbb9718eec7361f483fb36fbbfea88" integrity sha512-c98Bf3tPniI+scsdk237ku1Dc3ujXQTSgyiPUDEOe7tRkhrqridvh8klBv0HCEso1OLOYcHuCv/cS6DNxKH+ZA== @@ -9230,7 +9201,7 @@ braces@^3.0.1, braces@^3.0.2, braces@~3.0.2: dependencies: fill-range "^7.0.1" -brorand@^1.0.1, brorand@^1.1.0: +brorand@^1.1.0: version "1.1.0" resolved "https://registry.yarnpkg.com/brorand/-/brorand-1.1.0.tgz#12c25efe40a45e3c323eb8675a0a0ce57b22371f" integrity sha512-cKV8tMCEpQs4hK/ik71d6LrPOnpkpGBR0wzxqr68g2m/LB2GxVYQroAjMJZRVM1Y4BCjCKc3vAamxSzOY2RP+w== @@ -9240,7 +9211,7 @@ browser-process-hrtime@^1.0.0: resolved "https://registry.yarnpkg.com/browser-process-hrtime/-/browser-process-hrtime-1.0.0.tgz#3c9b4b7d782c8121e56f10106d84c0d0ffc94626" integrity sha512-9o5UecI3GhkpM6DrXr69PblIuWxPKk9Y0jHBRhdocZ2y7YECBFCsHm79Pr3OyR2AvjhDkabFJaDJMYRazHgsow== -browserify-aes@^1.0.4, browserify-aes@^1.2.0: +browserify-aes@^1.2.0: version "1.2.0" resolved "https://registry.yarnpkg.com/browserify-aes/-/browserify-aes-1.2.0.tgz#326734642f403dabc3003209853bb70ad428ef48" integrity sha512-+7CHXqGuspUn/Sl5aO7Ea0xWGAtETPXNSAjHo48JfLdPWcMng33Xe4znFvQweqc/uzk5zSOI3H52CYnjCfb5hA== @@ -9252,26 +9223,7 @@ browserify-aes@^1.0.4, browserify-aes@^1.2.0: inherits "^2.0.1" safe-buffer "^5.0.1" -browserify-cipher@^1.0.0: - version "1.0.1" - resolved "https://registry.yarnpkg.com/browserify-cipher/-/browserify-cipher-1.0.1.tgz#8d6474c1b870bfdabcd3bcfcc1934a10e94f15f0" - integrity sha512-sPhkz0ARKbf4rRQt2hTpAHqn47X3llLkUGn+xEJzLjwY8LRs2p0v7ljvI5EyoRO/mexrNunNECisZs+gw2zz1w== - dependencies: - browserify-aes "^1.0.4" - browserify-des "^1.0.0" - evp_bytestokey "^1.0.0" - -browserify-des@^1.0.0: - version "1.0.2" - resolved "https://registry.yarnpkg.com/browserify-des/-/browserify-des-1.0.2.tgz#3af4f1f59839403572f1c66204375f7a7f703e9c" - integrity sha512-BioO1xf3hFwz4kc6iBhI3ieDFompMhrMlnDFC4/0/vd5MokpuAc3R+LYbwTA9A5Yc9pq9UYPqffKpW2ObuwX5A== - dependencies: - cipher-base "^1.0.1" - des.js "^1.0.0" - inherits "^2.0.1" - safe-buffer "^5.1.2" - -browserify-rsa@^4.0.0, browserify-rsa@^4.1.0: +browserify-rsa@^4.1.0: version "4.1.0" resolved "https://registry.yarnpkg.com/browserify-rsa/-/browserify-rsa-4.1.0.tgz#b2fd06b5b75ae297f7ce2dc651f918f5be158c8d" integrity sha512-AdEER0Hkspgno2aR97SAf6vi0y0k8NuOpGnVH3O99rcA5Q6sh8QxcngtHuJ6uXwnfAXNM4Gn1Gb7/MV1+Ymbog== @@ -9279,7 +9231,7 @@ browserify-rsa@^4.0.0, browserify-rsa@^4.1.0: bn.js "^5.0.0" randombytes "^2.0.1" -browserify-sign@4.2.2, browserify-sign@^4.0.0: +browserify-sign@4.2.2: version "4.2.2" resolved "https://registry.yarnpkg.com/browserify-sign/-/browserify-sign-4.2.2.tgz#e78d4b69816d6e3dd1c747e64e9947f9ad79bc7e" integrity sha512-1rudGyeYY42Dk6texmv7c4VcQ0EsvVbLwZkA+AQB7SxvXxmcD93jcHie8bzecJ+ChDlmAm2Qyu0+Ccg5uhZXCg== @@ -10151,14 +10103,6 @@ cosmiconfig@^8.0.0: parse-json "^5.2.0" path-type "^4.0.0" -create-ecdh@^4.0.0: - version "4.0.4" - resolved "https://registry.yarnpkg.com/create-ecdh/-/create-ecdh-4.0.4.tgz#d6e7f4bffa66736085a0762fd3a632684dabcc4e" - integrity sha512-mf+TCx8wWc9VpuxfP2ht0iSISLZnt0JgWlrOKZiNqyUZWnjIaCIVNQArMHnCZKfEYRg6IM7A+NeJoN8gf/Ws0A== - dependencies: - bn.js "^4.1.0" - elliptic "^6.5.3" - create-hash@^1.1.0, create-hash@^1.1.2, create-hash@^1.2.0: version "1.2.0" resolved "https://registry.yarnpkg.com/create-hash/-/create-hash-1.2.0.tgz#889078af11a63756bcfb59bd221996be3a9ef196" @@ -10170,7 +10114,7 @@ create-hash@^1.1.0, create-hash@^1.1.2, create-hash@^1.2.0: ripemd160 "^2.0.1" sha.js "^2.4.0" -create-hmac@^1.1.0, create-hmac@^1.1.4, create-hmac@^1.1.7: +create-hmac@^1.1.4, create-hmac@^1.1.7: version "1.1.7" resolved "https://registry.yarnpkg.com/create-hmac/-/create-hmac-1.1.7.tgz#69170c78b3ab957147b2b8b04572e47ead2243ff" integrity sha512-MJG9liiZ+ogc4TzUwuvbER1JRdgvUFSB5+VR/g5h82fGaIRWMWddtKBHi7/sVhfjQZ6SehlyhvQYrcYkaUIpLg== @@ -10245,23 +10189,6 @@ crypt@0.0.2, crypt@~0.0.1: resolved "https://registry.yarnpkg.com/crypt/-/crypt-0.0.2.tgz#88d7ff7ec0dfb86f713dc87bbb42d044d3e6c41b" integrity sha512-mCxBlsHFYh9C+HVpiEacem8FEBnMXgU9gy4zmNC+SXAZNB/1idgp/aulFJ4FgCi7GPEVbfyng092GqL2k2rmow== -crypto-browserify@^3.12.0: - version "3.12.0" - resolved "https://registry.yarnpkg.com/crypto-browserify/-/crypto-browserify-3.12.0.tgz#396cf9f3137f03e4b8e532c58f698254e00f80ec" - integrity sha512-fz4spIh+znjO2VjL+IdhEpRJ3YN6sMzITSBijk6FK2UvTqruSQW+/cCZTSNsMiZNvUeq0CqurF+dAbyiGOY6Wg== - dependencies: - browserify-cipher "^1.0.0" - browserify-sign "^4.0.0" - create-ecdh "^4.0.0" - create-hash "^1.1.0" - create-hmac "^1.1.0" - diffie-hellman "^5.0.0" - inherits "^2.0.1" - pbkdf2 "^3.0.3" - public-encrypt "^4.0.0" - randombytes "^2.0.0" - randomfill "^1.0.3" - crypto-random-string@^1.0.0: version "1.0.0" resolved "https://registry.yarnpkg.com/crypto-random-string/-/crypto-random-string-1.0.0.tgz#a230f64f568310e1498009940790ec99545bca7e" @@ -10728,14 +10655,6 @@ dequal@^2.0.3: resolved "https://registry.yarnpkg.com/dequal/-/dequal-2.0.3.tgz#2644214f1997d39ed0ee0ece72335490a7ac67be" integrity sha512-0je+qPKHEMohvfRTCEo3CrPG6cAzAYgmzKyxRiYSSDkS6eGJdyVJm7WaYA5ECaAD9wLB2T4EEeymA5aFVcYXCA== -des.js@^1.0.0: - version "1.1.0" - resolved "https://registry.yarnpkg.com/des.js/-/des.js-1.1.0.tgz#1d37f5766f3bbff4ee9638e871a8768c173b81da" - integrity sha512-r17GxjhUCjSRy8aiJpr8/UadFIzMzJGexI3Nmz4ADi9LYSFx4gTBp80+NaX/YsXWWLhpZ7v/v/ubEc/bCNfKwg== - dependencies: - inherits "^2.0.1" - minimalistic-assert "^1.0.0" - destroy@1.2.0: version "1.2.0" resolved "https://registry.yarnpkg.com/destroy/-/destroy-1.2.0.tgz#4803735509ad8be552934c67df614f94e66fa015" @@ -10835,15 +10754,6 @@ diff@^4.0.1: resolved "https://registry.yarnpkg.com/diff/-/diff-4.0.2.tgz#60f3aecb89d5fae520c11aa19efc2bb982aade7d" integrity sha512-58lmxKSA4BNyLz+HHMUzlOEpg09FV+ev6ZMe3vJihgdxzgcwZ8VoEEPmALCZG9LmqfVoNMMKpttIYTVG6uDY7A== -diffie-hellman@^5.0.0: - version "5.0.3" - resolved "https://registry.yarnpkg.com/diffie-hellman/-/diffie-hellman-5.0.3.tgz#40e8ee98f55a2149607146921c63e1ae5f3d2875" - integrity sha512-kqag/Nl+f3GwyK25fhUMYj81BUOrZ9IuJsjIcDE5icNM9FJHAVm3VcUDxdLPoQtTuUylWm6ZIknYJwwaPxsUzg== - dependencies: - bn.js "^4.1.0" - miller-rabin "^4.0.0" - randombytes "^2.0.0" - dir-glob@^3.0.1: version "3.0.1" resolved "https://registry.yarnpkg.com/dir-glob/-/dir-glob-3.0.1.tgz#56dbf73d992a4a93ba1584f4534063fd2e41717f" @@ -11081,7 +10991,7 @@ elliptic@^6.4.1: minimalistic-assert "^1.0.1" minimalistic-crypto-utils "^1.0.1" -elliptic@^6.5.3, elliptic@^6.5.4: +elliptic@^6.5.4: version "6.5.5" resolved "https://registry.yarnpkg.com/elliptic/-/elliptic-6.5.5.tgz#c715e09f78b6923977610d4c2346d6ce22e6dded" integrity sha512-7EjbcmUm17NQFu4Pmgmq2olYMj8nwMnpcddByChSUjArp8F5DQWcIcpriwO4ZToLNAJig0yiyjswfyGNje/ixw== @@ -11793,7 +11703,7 @@ events@3.3.0, events@^3.2.0, events@^3.3.0: resolved "https://registry.yarnpkg.com/events/-/events-3.3.0.tgz#31a95ad0a924e2d2c419a813aeb2c4e878ea7400" integrity sha512-mQw+2fkQbALzQ7V0MY0IqdnXNOeTtP4r0lN9z7AAawCXgqea7bDii20AYrIBrFd/Hx0M2Ocz6S111CaFkUcb0Q== -evp_bytestokey@^1.0.0, evp_bytestokey@^1.0.3: +evp_bytestokey@^1.0.3: version "1.0.3" resolved "https://registry.yarnpkg.com/evp_bytestokey/-/evp_bytestokey-1.0.3.tgz#7fcbdb198dc71959432efe13842684e0525acb02" integrity sha512-/f2Go4TognH/KvCISP7OUsHn85hT9nUkxxA9BEWxFn+Oj9o8ZNLm/40hdlgSLyuOimsrTKLUMEorQexp/aPQeA== @@ -12167,13 +12077,6 @@ expo-splash-screen@~0.26.4: dependencies: "@expo/prebuild-config" "6.7.4" -expo-sqlite@^13.4.0: - version "13.4.0" - resolved "https://registry.yarnpkg.com/expo-sqlite/-/expo-sqlite-13.4.0.tgz#ae945622662263431aa9daee59b659255cb6a9fd" - integrity sha512-5f7d2EDM+pgerM33KndtX4gWw2nuVaXY68nnqx7PhkiYeyEmeNfZ29bIFtpBzNb/L5l0/DTtRxuSqftxbknFtw== - dependencies: - "@expo/websql" "^1.0.1" - expo-status-bar@~1.11.1: version "1.11.1" resolved "https://registry.yarnpkg.com/expo-status-bar/-/expo-status-bar-1.11.1.tgz#a11318741d361048c11db2b16c4364a79a74af30" @@ -13516,11 +13419,6 @@ image-size@^1.0.2: dependencies: queue "6.0.2" -immediate@^3.2.2: - version "3.3.0" - resolved "https://registry.yarnpkg.com/immediate/-/immediate-3.3.0.tgz#1aef225517836bcdf7f2a2de2600c79ff0269266" - integrity sha512-HR7EVodfFUdQCTIeySw+WDRFJlPcLOJbXfwwZ7Oom6tjsvZ3bOkCDJHehQC3nxJrv7+f9XecwazynjU8e4Vw3Q== - immediate@~3.0.5: version "3.0.6" resolved "https://registry.yarnpkg.com/immediate/-/immediate-3.0.6.tgz#9db1dbd0faf8de6fbe0f5dd5e56bb606280de69b" @@ -16355,14 +16253,6 @@ micromatch@4.0.5, micromatch@^4.0.2, micromatch@^4.0.4, micromatch@^4.0.5: braces "^3.0.2" picomatch "^2.3.1" -miller-rabin@^4.0.0: - version "4.0.1" - resolved "https://registry.yarnpkg.com/miller-rabin/-/miller-rabin-4.0.1.tgz#f080351c865b0dc562a8462966daa53543c78a4d" - integrity sha512-115fLhvZVqWwHPbClyntxEVfVDfl9DLLTuJvq3g2O/Oxi8AiNouAHvDSzHS0viUJc+V5vm3eq91Xwqn9dp4jRA== - dependencies: - bn.js "^4.0.0" - brorand "^1.0.1" - mime-db@1.52.0, "mime-db@>= 1.43.0 < 2": version "1.52.0" resolved "https://registry.yarnpkg.com/mime-db/-/mime-db-1.52.0.tgz#bbabcdc02859f4987301c856e3387ce5ec43bf70" @@ -16780,11 +16670,6 @@ nodemailer@^6.8.0: resolved "https://registry.yarnpkg.com/nodemailer/-/nodemailer-6.9.4.tgz#93bd4a60eb0be6fa088a0483340551ebabfd2abf" integrity sha512-CXjQvrQZV4+6X5wP6ZIgdehJamI63MFoYFGGPtHudWym9qaEHDNdPzaj5bfMCvxG1vhAileSWW90q7nL0N36mA== -noop-fn@^1.0.0: - version "1.0.0" - resolved "https://registry.yarnpkg.com/noop-fn/-/noop-fn-1.0.0.tgz#5f33d47f13d2150df93e0cb036699e982f78ffbf" - integrity sha512-pQ8vODlgXt2e7A3mIbFDlizkr46r75V+BJxVAyat8Jl7YmI513gG5cfyRL0FedKraoZ+VAouI1h4/IWpus5pcQ== - normalize-path@^3.0.0, normalize-path@~3.0.0: version "3.0.0" resolved "https://registry.yarnpkg.com/normalize-path/-/normalize-path-3.0.0.tgz#0dcd69ff23a1c9b11fd0978316644a0388216a65" @@ -17261,7 +17146,7 @@ parent-module@^1.0.0: dependencies: callsites "^3.0.0" -parse-asn1@^5.0.0, parse-asn1@^5.1.6: +parse-asn1@^5.1.6: version "5.1.7" resolved "https://registry.yarnpkg.com/parse-asn1/-/parse-asn1-5.1.7.tgz#73cdaaa822125f9647165625eb45f8a051d2df06" integrity sha512-CTM5kuWR3sx9IFamcl5ErfPl6ea/N8IYwiJ+vpeB2g+1iknv7zBl5uPwbMbRVznRVbrNY6lGuDoE5b30grmbqg== @@ -17419,7 +17304,7 @@ pathe@^1.1.0: resolved "https://registry.yarnpkg.com/pathe/-/pathe-1.1.1.tgz#1dd31d382b974ba69809adc9a7a347e65d84829a" integrity sha512-d+RQGp0MAYTIaDBIMmOfMwz3E+LOZnxx1HZd5R18mmCZY0QBlK0LDZfPc8FW8Ed2DlvsuE6PRjroDY+wg4+j/Q== -pbkdf2@^3.0.3, pbkdf2@^3.1.2: +pbkdf2@^3.1.2: version "3.1.2" resolved "https://registry.yarnpkg.com/pbkdf2/-/pbkdf2-3.1.2.tgz#dd822aa0887580e52f1a039dc3eda108efae3075" integrity sha512-iuh7L6jA7JEGu2WxDwtQP1ddOpaJNC4KlDEFfdQajSGgGPNi4OyDc2R7QnbY2bR9QjBVGwgvTdNJZoE7RaxUMA== @@ -18259,11 +18144,6 @@ postinstall-postinstall@^2.1.0: resolved "https://registry.yarnpkg.com/postinstall-postinstall/-/postinstall-postinstall-2.1.0.tgz#4f7f77441ef539d1512c40bd04c71b06a4704ca3" integrity sha512-7hQX6ZlZXIoRiWNrbMQaLzUUfH+sSx39u8EJ9HYuDc1kLo9IXKWjM5RSquZN1ad5GnH8CGFM78fsAAQi3OKEEQ== -pouchdb-collections@^1.0.1: - version "1.0.1" - resolved "https://registry.yarnpkg.com/pouchdb-collections/-/pouchdb-collections-1.0.1.tgz#fe63a17da977611abef7cb8026cb1a9553fd8359" - integrity sha512-31db6JRg4+4D5Yzc2nqsRqsA2oOkZS8DpFav3jf/qVNBxusKa2ClkEIZ2bJNpaDbMfWtnuSq59p6Bn+CipPMdg== - prebuild-install@^7.1.1: version "7.1.1" resolved "https://registry.yarnpkg.com/prebuild-install/-/prebuild-install-7.1.1.tgz#de97d5b34a70a0c81334fd24641f2a1702352e45" @@ -18618,18 +18498,6 @@ psl@^1.1.33, psl@^1.9.0: resolved "https://registry.yarnpkg.com/psl/-/psl-1.9.0.tgz#d0df2a137f00794565fcaf3b2c00cd09f8d5a5a7" integrity sha512-E/ZsdU4HLs/68gYzgGTkMicWTLPdAftJLfJFlLUAAKZGkStNU72sZjT66SnMDVOfOWY/YAoiD7Jxa9iHvngcag== -public-encrypt@^4.0.0: - version "4.0.3" - resolved "https://registry.yarnpkg.com/public-encrypt/-/public-encrypt-4.0.3.tgz#4fcc9d77a07e48ba7527e7cbe0de33d0701331e0" - integrity sha512-zVpa8oKZSz5bTMTFClc1fQOnyyEzpl5ozpi1B5YcvBrdohMjH2rfsBtyXcuNuwjsDIXmBYlF2N5FlJYhR29t8Q== - dependencies: - bn.js "^4.1.0" - browserify-rsa "^4.0.0" - create-hash "^1.1.0" - parse-asn1 "^5.0.0" - randombytes "^2.0.1" - safe-buffer "^5.1.2" - pump@^3.0.0: version "3.0.0" resolved "https://registry.yarnpkg.com/pump/-/pump-3.0.0.tgz#b4a2116815bde2f4e1ea602354e8c75565107a64" @@ -18735,21 +18603,13 @@ ramda@^0.27.1: resolved "https://registry.yarnpkg.com/ramda/-/ramda-0.27.2.tgz#84463226f7f36dc33592f6f4ed6374c48306c3f1" integrity sha512-SbiLPU40JuJniHexQSAgad32hfwd+DRUdwF2PlVuI5RZD0/vahUco7R8vD86J/tcEKKF9vZrUVwgtmGCqlCKyA== -randombytes@^2.0.0, randombytes@^2.0.1, randombytes@^2.0.5, randombytes@^2.1.0: +randombytes@^2.0.1, randombytes@^2.1.0: version "2.1.0" resolved "https://registry.yarnpkg.com/randombytes/-/randombytes-2.1.0.tgz#df6f84372f0270dc65cdf6291349ab7a473d4f2a" integrity sha512-vYl3iOX+4CKUWuxGi9Ukhie6fsqXqS9FE2Zaic4tNFD2N2QQaXOMFbuKK4QmDHC0JO6B1Zp41J0LpT0oR68amQ== dependencies: safe-buffer "^5.1.0" -randomfill@^1.0.3: - version "1.0.4" - resolved "https://registry.yarnpkg.com/randomfill/-/randomfill-1.0.4.tgz#c92196fc86ab42be983f1bf31778224931d61458" - integrity sha512-87lcbR8+MhcWcUiQ+9e+Rwx8MyR2P7qnt15ynUlbm3TU/fjbgz4GsvfSUDTemtCCtVCqb4ZcEFlyPNTh9bBTLw== - dependencies: - randombytes "^2.0.5" - safe-buffer "^5.1.0" - range-parser@^1.2.1, range-parser@~1.2.1: version "1.2.1" resolved "https://registry.yarnpkg.com/range-parser/-/range-parser-1.2.1.tgz#3cf37023d199e1c24d1a55b84800c2f3e6468031" @@ -18943,11 +18803,6 @@ react-native-ios-context-menu@^1.15.3: dependencies: "@dominicstop/ts-event-emitter" "^1.1.0" -react-native-mmkv@^2.12.2: - version "2.12.2" - resolved "https://registry.yarnpkg.com/react-native-mmkv/-/react-native-mmkv-2.12.2.tgz#4bba0f5f04e2cf222494cce3a9794ba6a4894dee" - integrity sha512-6058Aq0p57chPrUutLGe9fYoiDVDNMU2PKV+lLFUJ3GhoHvUrLdsS1PDSCLr00yqzL4WJQ7TTzH+V8cpyrNcfg== - react-native-pager-view@6.2.3: version "6.2.3" resolved "https://registry.yarnpkg.com/react-native-pager-view/-/react-native-pager-view-6.2.3.tgz#698f6387fdf06cecc3d8d4792604419cb89cb775" @@ -18967,26 +18822,6 @@ react-native-progress@bluesky-social/react-native-progress: dependencies: prop-types "^15.7.2" -react-native-quick-base64@^2.0.5, react-native-quick-base64@^2.1.0: - version "2.1.0" - resolved "https://registry.yarnpkg.com/react-native-quick-base64/-/react-native-quick-base64-2.1.0.tgz#6759a9f9b94b2b7c951917ce7d1970afdf62865d" - integrity sha512-5T5qhEuHcqeP/GGAOaeNsz0c5jZsMemy2svlgRc7PfUQSH6ABakwTT0tYNZ1XImJZWc8najYgVG8mrJgml5DNw== - dependencies: - base64-js "^1.5.1" - -react-native-quick-crypto@^0.6.1: - version "0.6.1" - resolved "https://registry.yarnpkg.com/react-native-quick-crypto/-/react-native-quick-crypto-0.6.1.tgz#7b89c67c4a5d3669c4491fe7884621c1c74d01bc" - integrity sha512-s6uFo7tcI3syo8/y5j+t6Rf+KVSuRKDp6tH04A0vjaHptJC6Iu7DVgkNYO7aqtfrYn8ZUgQ/Kqaq+m4i9TxgIQ== - dependencies: - "@craftzdog/react-native-buffer" "^6.0.5" - "@types/node" "^17.0.31" - crypto-browserify "^3.12.0" - events "^3.3.0" - react-native-quick-base64 "^2.0.5" - stream-browserify "^3.0.0" - string_decoder "^1.3.0" - react-native-reanimated@^3.6.0: version "3.6.0" resolved "https://registry.yarnpkg.com/react-native-reanimated/-/react-native-reanimated-3.6.0.tgz#d2ca5f4c234f592af3d63bc749806e36d6e0a755" @@ -20471,7 +20306,7 @@ statuses@2.0.1: resolved "https://registry.yarnpkg.com/statuses/-/statuses-1.5.0.tgz#161c7dac177659fd9811f43771fa99381478628c" integrity sha512-OpZ3zP+jT1PI7I8nemJX4AKmAX070ZkYPVWV/AaKTJl+tXCTGyVdC1a4SL8RUQYEwk/f34ZX8UTykN68FwrqAA== -stream-browserify@3.0.0, stream-browserify@^3.0.0: +stream-browserify@3.0.0: version "3.0.0" resolved "https://registry.yarnpkg.com/stream-browserify/-/stream-browserify-3.0.0.tgz#22b0a2850cdf6503e73085da1fc7b7d0c2122f2f" integrity sha512-H73RAHsVBapbim0tU2JwwOiXUj+fikfiaoYAKHF3VJfA0pe2BCzkhAHBlLG6REzE+2WNZcxOXjK7lkso+9euLA== @@ -21115,11 +20950,6 @@ tiny-hashes@^1.0.1: resolved "https://registry.yarnpkg.com/tiny-hashes/-/tiny-hashes-1.0.1.tgz#ddbe9060312ddb4efe0a174bb3a27e1331c425a1" integrity sha512-knIN5zj4fl7kW4EBU5sLP20DWUvi/rVouvJezV0UAym2DkQaqm365Nyc8F3QEiOvunNDMxR8UhcXd1d5g+Wg1g== -tiny-queue@^0.2.1: - version "0.2.1" - resolved "https://registry.yarnpkg.com/tiny-queue/-/tiny-queue-0.2.1.tgz#25a67f2c6e253b2ca941977b5ef7442ef97a6046" - integrity sha512-EijGsv7kzd9I9g0ByCl6h42BWNGUZrlCSejfrb3AKeHC33SGbASu1VDf5O3rRiiUOhAC9CHdZxFPbZu0HmR70A== - tippy.js@^6.3.7: version "6.3.7" resolved "https://registry.yarnpkg.com/tippy.js/-/tippy.js-6.3.7.tgz#8ccfb651d642010ed9a32ff29b0e9e19c5b8c61c" From 13607edd43fa93031271cb937023edb4690a807b Mon Sep 17 00:00:00 2001 From: Hailey Date: Wed, 10 Apr 2024 12:58:41 -0700 Subject: [PATCH 25/54] squash fix android module name import fixes fix some imports use `@atproto/jwk` over local zod schema preview oauth client rename package alignment more alignment database implementation cleanup some types alignment switch implementation for es256 half of `verifyJwt` for android half of `verifyJwt` for iOS rm log `createJwt` android `createJwt` ios rm print cleanup native impl silence some warnings reorg, finish web impl follow the `CryptoImplementation` move some more things around normalize some names rename func add generate on android add android dep rm bool add alg better implementation ios jwk generation fix index.ts add `getRandomValues()` on native web `getRandomValues` silence ts error for missing crypto on web add `digest` for web add `digest` add module --- .../android/build.gradle | 93 ++++++ .../android/src/main/AndroidManifest.xml | 2 + .../modules/blueskyoauthclient/CryptoUtil.kt | 52 ++++ .../ExpoBlueskyOAuthClientModule.kt | 35 +++ .../modules/blueskyoauthclient/JWTUtil.kt | 35 +++ .../expo-module.config.json | 9 + modules/expo-bluesky-oauth-client/index.ts | 6 + .../ios/CryptoUtil.swift | 37 +++ .../ios/ExpoBlueskyOAuthClient.podspec | 22 ++ .../ios/ExpoBlueskyOAuthClientModule.swift | 43 +++ .../expo-bluesky-oauth-client/ios/JWK.swift | 29 ++ .../ios/JWTUtil.swift | 81 ++++++ .../src/crypto-subtle.ts | 24 ++ .../src/crypto-subtle.web.ts | 48 ++++ .../expo-bluesky-oauth-client/src/jose-key.ts | 106 +++++++ .../src/native-types.ts | 20 ++ .../src/rn-crypto-key.ts | 86 ++++++ .../src/rn-crypto-key.web.ts | 78 +++++ .../src/rn-oauth-client-factory.ts | 153 ++++++++++ .../src/rn-oauth-database.native.ts | 214 ++++++++++++++ .../src/rn-oauth-database.ts | 269 ++++++++++++++++++ .../expo-bluesky-oauth-client/src/store.ts | 0 .../src/store.web.ts | 0 .../expo-bluesky-oauth-client/src/util.web.ts | 41 +++ package.json | 1 + src/view/screens/Home.tsx | 13 + yarn.lock | 5 + 27 files changed, 1502 insertions(+) create mode 100644 modules/expo-bluesky-oauth-client/android/build.gradle create mode 100644 modules/expo-bluesky-oauth-client/android/src/main/AndroidManifest.xml create mode 100644 modules/expo-bluesky-oauth-client/android/src/main/java/expo/modules/blueskyoauthclient/CryptoUtil.kt create mode 100644 modules/expo-bluesky-oauth-client/android/src/main/java/expo/modules/blueskyoauthclient/ExpoBlueskyOAuthClientModule.kt create mode 100644 modules/expo-bluesky-oauth-client/android/src/main/java/expo/modules/blueskyoauthclient/JWTUtil.kt create mode 100644 modules/expo-bluesky-oauth-client/expo-module.config.json create mode 100644 modules/expo-bluesky-oauth-client/index.ts create mode 100644 modules/expo-bluesky-oauth-client/ios/CryptoUtil.swift create mode 100644 modules/expo-bluesky-oauth-client/ios/ExpoBlueskyOAuthClient.podspec create mode 100644 modules/expo-bluesky-oauth-client/ios/ExpoBlueskyOAuthClientModule.swift create mode 100644 modules/expo-bluesky-oauth-client/ios/JWK.swift create mode 100644 modules/expo-bluesky-oauth-client/ios/JWTUtil.swift create mode 100644 modules/expo-bluesky-oauth-client/src/crypto-subtle.ts create mode 100644 modules/expo-bluesky-oauth-client/src/crypto-subtle.web.ts create mode 100644 modules/expo-bluesky-oauth-client/src/jose-key.ts create mode 100644 modules/expo-bluesky-oauth-client/src/native-types.ts create mode 100644 modules/expo-bluesky-oauth-client/src/rn-crypto-key.ts create mode 100644 modules/expo-bluesky-oauth-client/src/rn-crypto-key.web.ts create mode 100644 modules/expo-bluesky-oauth-client/src/rn-oauth-client-factory.ts create mode 100644 modules/expo-bluesky-oauth-client/src/rn-oauth-database.native.ts create mode 100644 modules/expo-bluesky-oauth-client/src/rn-oauth-database.ts create mode 100644 modules/expo-bluesky-oauth-client/src/store.ts create mode 100644 modules/expo-bluesky-oauth-client/src/store.web.ts create mode 100644 modules/expo-bluesky-oauth-client/src/util.web.ts diff --git a/modules/expo-bluesky-oauth-client/android/build.gradle b/modules/expo-bluesky-oauth-client/android/build.gradle new file mode 100644 index 0000000000..f64e824a26 --- /dev/null +++ b/modules/expo-bluesky-oauth-client/android/build.gradle @@ -0,0 +1,93 @@ +apply plugin: 'com.android.library' +apply plugin: 'kotlin-android' +apply plugin: 'maven-publish' + +group = 'expo.modules.blueskyoauthclient' +version = '0.0.1' + +buildscript { + def expoModulesCorePlugin = new File(project(":expo-modules-core").projectDir.absolutePath, "ExpoModulesCorePlugin.gradle") + if (expoModulesCorePlugin.exists()) { + apply from: expoModulesCorePlugin + applyKotlinExpoModulesCorePlugin() + } + + // Simple helper that allows the root project to override versions declared by this library. + ext.safeExtGet = { prop, fallback -> + rootProject.ext.has(prop) ? rootProject.ext.get(prop) : fallback + } + + // Ensures backward compatibility + ext.getKotlinVersion = { + if (ext.has("kotlinVersion")) { + ext.kotlinVersion() + } else { + ext.safeExtGet("kotlinVersion", "1.8.10") + } + } + + repositories { + mavenCentral() + } + + dependencies { + classpath("org.jetbrains.kotlin:kotlin-gradle-plugin:${getKotlinVersion()}") + } +} + +afterEvaluate { + publishing { + publications { + release(MavenPublication) { + from components.release + } + } + repositories { + maven { + url = mavenLocal().url + } + } + } +} + +android { + compileSdkVersion safeExtGet("compileSdkVersion", 33) + + def agpVersion = com.android.Version.ANDROID_GRADLE_PLUGIN_VERSION + if (agpVersion.tokenize('.')[0].toInteger() < 8) { + compileOptions { + sourceCompatibility JavaVersion.VERSION_11 + targetCompatibility JavaVersion.VERSION_11 + } + + kotlinOptions { + jvmTarget = JavaVersion.VERSION_11.majorVersion + } + } + + namespace "expo.modules.blueskyoauthclient" + defaultConfig { + minSdkVersion safeExtGet("minSdkVersion", 21) + targetSdkVersion safeExtGet("targetSdkVersion", 34) + versionCode 1 + versionName "0.0.1" + } + lintOptions { + abortOnError false + } + publishing { + singleVariant("release") { + withSourcesJar() + } + } +} + +repositories { + mavenCentral() +} + +dependencies { + implementation project(':expo-modules-core') + implementation "org.jetbrains.kotlin:kotlin-stdlib-jdk7:${getKotlinVersion()}" + implementation "com.nimbusds:nimbus-jose-jwt:9.38-rc3" +} diff --git a/modules/expo-bluesky-oauth-client/android/src/main/AndroidManifest.xml b/modules/expo-bluesky-oauth-client/android/src/main/AndroidManifest.xml new file mode 100644 index 0000000000..bdae66c8f5 --- /dev/null +++ b/modules/expo-bluesky-oauth-client/android/src/main/AndroidManifest.xml @@ -0,0 +1,2 @@ + + diff --git a/modules/expo-bluesky-oauth-client/android/src/main/java/expo/modules/blueskyoauthclient/CryptoUtil.kt b/modules/expo-bluesky-oauth-client/android/src/main/java/expo/modules/blueskyoauthclient/CryptoUtil.kt new file mode 100644 index 0000000000..2abce43009 --- /dev/null +++ b/modules/expo-bluesky-oauth-client/android/src/main/java/expo/modules/blueskyoauthclient/CryptoUtil.kt @@ -0,0 +1,52 @@ +package expo.modules.blueskyoauthclient + +import com.nimbusds.jose.Algorithm +import java.security.KeyPairGenerator +import java.security.MessageDigest +import java.security.interfaces.ECPublicKey +import java.security.interfaces.ECPrivateKey +import com.nimbusds.jose.jwk.Curve +import com.nimbusds.jose.jwk.ECKey +import com.nimbusds.jose.jwk.KeyUse +import java.util.UUID + +class CryptoUtil { + fun digest(data: ByteArray): ByteArray { + val digest = MessageDigest.getInstance("sha256") + return digest.digest(data) + } + + fun getRandomValues(byteLength: Int): ByteArray { + val random = ByteArray(byteLength) + java.security.SecureRandom().nextBytes(random) + return random + } + + fun generateKeyPair(keyId: String?): Pair { + val keyIdString = keyId ?: UUID.randomUUID().toString() + + val keyPairGen = KeyPairGenerator.getInstance("EC") + keyPairGen.initialize(Curve.P_256.toECParameterSpec()) + val keyPair = keyPairGen.generateKeyPair() + + val publicKey = keyPair.public as ECPublicKey + val privateKey = keyPair.private as ECPrivateKey + + val publicJwk = ECKey.Builder(Curve.P_256, publicKey) + .keyUse(KeyUse.SIGNATURE) + .algorithm(Algorithm.parse("ES256")) + .keyID(keyIdString) + .build() + val privateJwk = ECKey.Builder(Curve.P_256, publicKey) + .privateKey(privateKey) + .keyUse(KeyUse.SIGNATURE) + .keyID(keyIdString) + .algorithm(Algorithm.parse("ES256")) + .build() + + return Pair( + publicJwk.toString(), + privateJwk.toString() + ) + } +} diff --git a/modules/expo-bluesky-oauth-client/android/src/main/java/expo/modules/blueskyoauthclient/ExpoBlueskyOAuthClientModule.kt b/modules/expo-bluesky-oauth-client/android/src/main/java/expo/modules/blueskyoauthclient/ExpoBlueskyOAuthClientModule.kt new file mode 100644 index 0000000000..93906a3ab1 --- /dev/null +++ b/modules/expo-bluesky-oauth-client/android/src/main/java/expo/modules/blueskyoauthclient/ExpoBlueskyOAuthClientModule.kt @@ -0,0 +1,35 @@ +package expo.modules.blueskyoauthclient + +import expo.modules.kotlin.modules.Module +import expo.modules.kotlin.modules.ModuleDefinition + +class ExpoBlueskyOAuthClientModule : Module() { + override fun definition() = ModuleDefinition { + Name("ExpoBlueskyOAuthClient") + + AsyncFunction("digest") { value: ByteArray -> + return@AsyncFunction CryptoUtil().digest(value) + } + + Function("getRandomValues") { byteLength: Int -> + return@Function CryptoUtil().getRandomValues(byteLength) + } + + AsyncFunction("generateKeyPair") { keyId: String? -> + val res = CryptoUtil().generateKeyPair(keyId) + + return@AsyncFunction mapOf( + "publicKey" to res.first, + "privateKey" to res.second + ) + } + + AsyncFunction("createJwt") { jwkString: String, headerString: String, payloadString: String -> + return@AsyncFunction JWTUtil().createJwt(jwkString, headerString, payloadString) + } + + AsyncFunction("verifyJwt") { jwkString: String, tokenString: String, options: String? -> + return@AsyncFunction JWTUtil().verifyJwt(jwkString, tokenString, options) + } + } +} diff --git a/modules/expo-bluesky-oauth-client/android/src/main/java/expo/modules/blueskyoauthclient/JWTUtil.kt b/modules/expo-bluesky-oauth-client/android/src/main/java/expo/modules/blueskyoauthclient/JWTUtil.kt new file mode 100644 index 0000000000..8c099d198c --- /dev/null +++ b/modules/expo-bluesky-oauth-client/android/src/main/java/expo/modules/blueskyoauthclient/JWTUtil.kt @@ -0,0 +1,35 @@ +package expo.modules.blueskyoauthclient + +import com.nimbusds.jose.JWSHeader +import com.nimbusds.jose.crypto.ECDSASigner +import com.nimbusds.jose.crypto.ECDSAVerifier +import com.nimbusds.jose.jwk.ECKey +import com.nimbusds.jwt.JWTClaimsSet +import com.nimbusds.jwt.SignedJWT + + +class JWTUtil { + fun createJwt(jwkString: String, headerString: String, payloadString: String): String { + val key = ECKey.parse(jwkString) + val header = JWSHeader.parse(headerString) + val payload = JWTClaimsSet.parse(payloadString) + + val signer = ECDSASigner(key) + val jwt = SignedJWT(header, payload) + jwt.sign(signer) + + return jwt.serialize() + } + + fun verifyJwt(jwkString: String, tokenString: String, options: String?): Boolean { + return try { + val key = ECKey.parse(jwkString) + val jwt = SignedJWT.parse(tokenString) + val verifier = ECDSAVerifier(key) + + jwt.verify(verifier) + } catch(e: Exception) { + false + } + } +} diff --git a/modules/expo-bluesky-oauth-client/expo-module.config.json b/modules/expo-bluesky-oauth-client/expo-module.config.json new file mode 100644 index 0000000000..4e996dafe2 --- /dev/null +++ b/modules/expo-bluesky-oauth-client/expo-module.config.json @@ -0,0 +1,9 @@ +{ + "platforms": ["ios", "tvos", "android", "web"], + "ios": { + "modules": ["ExpoBlueskyOAuthClientModule"] + }, + "android": { + "modules": ["expo.modules.blueskyoauthclient.ExpoBlueskyOAuthClientModule"] + } +} diff --git a/modules/expo-bluesky-oauth-client/index.ts b/modules/expo-bluesky-oauth-client/index.ts new file mode 100644 index 0000000000..ca28960b93 --- /dev/null +++ b/modules/expo-bluesky-oauth-client/index.ts @@ -0,0 +1,6 @@ +export * from './src/crypto-subtle' +export * from './src/jose-key' +export * from './src/rn-crypto-key' +export * from './src/rn-oauth-client-factory' +export * from './src/rn-oauth-database' +export * from './src/util.web' diff --git a/modules/expo-bluesky-oauth-client/ios/CryptoUtil.swift b/modules/expo-bluesky-oauth-client/ios/CryptoUtil.swift new file mode 100644 index 0000000000..c9b95f81b9 --- /dev/null +++ b/modules/expo-bluesky-oauth-client/ios/CryptoUtil.swift @@ -0,0 +1,37 @@ +import CryptoKit +import JOSESwift + +class CryptoUtil { + // The equivalent of crypto.subtle.digest() with JS on web + public static func digest(data: Data) -> Data { + let hash = SHA256.hash(data: data) + return Data(hash) + } + + public static func getRandomValues(byteLength: Int) -> Data { + let bytes = (0.. (publicJWK: JWK, privateJWK: JWK)? { + let keyIdString = kid ?? UUID().uuidString + + let privateKey = P256.Signing.PrivateKey() + let publicKey = privateKey.publicKey + + let x = publicKey.x963Representation[1..<33].base64URLEncodedString() + let y = publicKey.x963Representation[33...].base64URLEncodedString() + let d = privateKey.rawRepresentation.base64URLEncodedString() + + let publicJWK = JWK(kty: "EC", use: "sig", crv: "P-256", kid: keyIdString, x: x, y: y, alg: "ES256") + let privateJWK = JWK(kty: "EC", use: "sig", crv: "P-256", kid: keyIdString, x: x, y: y, d: d, alg: "ES256") + + return (publicJWK, privateJWK) + } +} + +extension Data { + func base64URLEncodedString() -> String { + return self.base64EncodedString().replacingOccurrences(of: "+", with: "-").replacingOccurrences(of: "/", with: "_").replacingOccurrences(of: "=", with: "") + } +} diff --git a/modules/expo-bluesky-oauth-client/ios/ExpoBlueskyOAuthClient.podspec b/modules/expo-bluesky-oauth-client/ios/ExpoBlueskyOAuthClient.podspec new file mode 100644 index 0000000000..caeaaf4f2e --- /dev/null +++ b/modules/expo-bluesky-oauth-client/ios/ExpoBlueskyOAuthClient.podspec @@ -0,0 +1,22 @@ +Pod::Spec.new do |s| + s.name = 'ExpoBlueskyOAuthClient' + s.version = '0.0.1' + s.summary = 'A library of native functions to support Bluesky OAuth in React Native.' + s.description = 'A library of native functions to support Bluesky OAuth in React Native.' + s.author = '' + s.homepage = 'https://github.com/bluesky-social/social-app' + s.platforms = { :ios => '13.4', :tvos => '13.4' } + s.source = { git: '' } + s.static_framework = true + + s.dependency 'ExpoModulesCore' + s.dependency 'JOSESwift', '~> 2.3' + + # Swift/Objective-C compatibility + s.pod_target_xcconfig = { + 'DEFINES_MODULE' => 'YES', + 'SWIFT_COMPILATION_MODE' => 'wholemodule' + } + + s.source_files = "**/*.{h,m,mm,swift,hpp,cpp}" +end diff --git a/modules/expo-bluesky-oauth-client/ios/ExpoBlueskyOAuthClientModule.swift b/modules/expo-bluesky-oauth-client/ios/ExpoBlueskyOAuthClientModule.swift new file mode 100644 index 0000000000..729194916a --- /dev/null +++ b/modules/expo-bluesky-oauth-client/ios/ExpoBlueskyOAuthClientModule.swift @@ -0,0 +1,43 @@ +import ExpoModulesCore +import JOSESwift + +public class ExpoBlueskyOAuthClientModule: Module { + public func definition() -> ModuleDefinition { + Name("ExpoBlueskyOAuthClient") + + AsyncFunction("digest") { (data: Data, promise: Promise) in + promise.resolve(CryptoUtil.digest(data: data)) + } + + // We are going to leave this as sync to line up the APIs. It's fast, so not a big deal. + Function("getRandomValues") { (byteLength: Int) in + return CryptoUtil.getRandomValues(byteLength: byteLength) + } + + AsyncFunction ("generateKeyPair") { (kid: String?, promise: Promise) in + let keypair = try? CryptoUtil.generateKeyPair(kid: kid) + + guard let keypair = keypair else { + promise.reject("GenerateKeyError", "Error generating JWK.") + return + } + + promise.resolve([ + "publicKey": keypair.publicJWK.toJson(), + "privateKey": keypair.privateJWK.toJson() + ]) + } + + AsyncFunction("createJwt") { (jwk: String, header: String, payload: String, promise: Promise) in + guard let jwt = JWTUtil.createJwt(jwk, header: header, payload: payload) else { + promise.reject("JWTError", "Error creating JWT.") + return + } + promise.resolve(jwt) + } + + AsyncFunction("verifyJwt") { (jwk: String, token: String, options: String?, promise: Promise) in + promise.resolve(JWTUtil.verifyJwt(jwk, token: token, options: options)) + } + } +} diff --git a/modules/expo-bluesky-oauth-client/ios/JWK.swift b/modules/expo-bluesky-oauth-client/ios/JWK.swift new file mode 100644 index 0000000000..879d37a401 --- /dev/null +++ b/modules/expo-bluesky-oauth-client/ios/JWK.swift @@ -0,0 +1,29 @@ +struct JWK { + let kty: String + let use: String + let crv: String + let kid: String + let x: String + let y: String + var d: String? + let alg: String + + func toJson() -> String { + var dict: [String: Any] = [ + "kty": kty, + "use": use, + "crv": crv, + "kid": kid, + "x": x, + "y": y, + "alg": alg, + ] + + if let d = d { + dict["d"] = d + } + + let jsonData = try! JSONSerialization.data(withJSONObject: dict, options: []) + return String(data: jsonData, encoding: .utf8)! + } +} diff --git a/modules/expo-bluesky-oauth-client/ios/JWTUtil.swift b/modules/expo-bluesky-oauth-client/ios/JWTUtil.swift new file mode 100644 index 0000000000..d3dcdcee3d --- /dev/null +++ b/modules/expo-bluesky-oauth-client/ios/JWTUtil.swift @@ -0,0 +1,81 @@ +import JOSESwift + +class JWTUtil { + static func jsonToPrivateKey(_ jwkString: String) throws -> SecKey? { + guard let jsonData = jwkString.data(using: .utf8), + let jwk = try? JSONDecoder().decode(ECPrivateKey.self, from: jsonData), + let key = try? jwk.converted(to: SecKey.self) + else { + let jsonData = jwkString.data(using: .utf8)! + let jwk = try! JSONDecoder().decode(ECPrivateKey.self, from: jsonData) +// let key = try! jwk.converted(to: SecKey.self) + print("Error creating JWK from JWK string \(jwkString).") + return nil + } + + return key + } + + static func jsonToPublicKey(_ jwkString: String) throws -> SecKey? { + guard let jsonData = jwkString.data(using: .utf8), + let jwk = try? JSONDecoder().decode(ECPublicKey.self, from: jsonData), + let key = try? jwk.converted(to: SecKey.self) + else { + print("Error creating JWK from JWK string.") + return nil + } + + return key + } + + static func payloadStringToPayload(_ payloadString: String) -> Payload? { + guard let payloadData = payloadString.data(using: .utf8) else { + print("Error converting payload to data.") + return nil + } + + return Payload(payloadData) + } + + static func headerStringToPayload(_ headerString: String) -> JWSHeader? { + guard let headerData = headerString.data(using: .utf8) else { + print("Error converting header to data.") + return nil + } + + return JWSHeader(headerData) + } + + public static func createJwt(_ jwkString: String, header headerString: String, payload payloadString: String) -> String? { + guard let key = try? jsonToPrivateKey(jwkString), + let payload = payloadStringToPayload(payloadString), + let header = headerStringToPayload(headerString) + else + { + return nil + } + + let signer = Signer(signingAlgorithm: .ES256, key: key) + + guard let signer = signer, + let jws = try? JWS(header: header, payload: payload, signer: signer) + else { + print("Error creating JWS.") + return nil + } + + return jws.compactSerializedString + } + + public static func verifyJwt(_ jwkString: String, token tokenString: String, options optionsString: String?) -> Bool { + guard let key = try? jsonToPublicKey(jwkString), + let jws = try? JWS(compactSerialization: tokenString), + let verifier = Verifier(verifyingAlgorithm: .ES256, key: key), + let isVerified = try? jws.validate(using: verifier).isValid(for: verifier) + else { + return false + } + + return isVerified + } +} diff --git a/modules/expo-bluesky-oauth-client/src/crypto-subtle.ts b/modules/expo-bluesky-oauth-client/src/crypto-subtle.ts new file mode 100644 index 0000000000..c4375015a7 --- /dev/null +++ b/modules/expo-bluesky-oauth-client/src/crypto-subtle.ts @@ -0,0 +1,24 @@ +import {requireNativeModule} from 'expo-modules-core' +import {CryptoImplementation, Key} from '@atproto/oauth-client' + +import {RnCryptoKey} from './rn-crypto-key' + +// It loads the native module object from the JSI or falls back to +// the bridge module (from NativeModulesProxy) if the remote debugger is on. +const NativeModule = requireNativeModule('ExpoBlueskyOAuthClient') + +export class CryptoSubtle implements CryptoImplementation { + // We won't use the `algos` parameter here, as we will always use `ES256`. + // eslint-disable-next-line @typescript-eslint/no-unused-vars + async createKey(algos: string[] = ['ES256']): Promise { + return await RnCryptoKey.generate(undefined, ['ES256']) + } + + getRandomValues(byteLength: number): Uint8Array { + return NativeModule.getRandomValues(byteLength) + } + + async digest(bytes: Uint8Array): Promise { + return await NativeModule.digest(bytes) + } +} diff --git a/modules/expo-bluesky-oauth-client/src/crypto-subtle.web.ts b/modules/expo-bluesky-oauth-client/src/crypto-subtle.web.ts new file mode 100644 index 0000000000..14936f65df --- /dev/null +++ b/modules/expo-bluesky-oauth-client/src/crypto-subtle.web.ts @@ -0,0 +1,48 @@ +import {WebcryptoKey} from '@atproto/jwk-webcrypto' +import {CryptoImplementation, DigestAlgorithm, Key} from '@atproto/oauth-client' + +// @ts-ignore web only, this silences some warnings +const crypto = global.crypto + +export class CryptoSubtle implements CryptoImplementation { + constructor(_: any) { + if (!crypto?.subtle) { + throw new Error( + 'Crypto with CryptoSubtle is required. If running in a browser, make sure the current page is loaded over HTTPS.', + ) + } + } + + async createKey(algs: string[]): Promise { + return WebcryptoKey.generate(undefined, algs) + } + + getRandomValues(byteLength: number): Uint8Array { + const bytes = new Uint8Array(byteLength) + crypto.getRandomValues(bytes) + return bytes + } + + async digest( + bytes: Uint8Array, + algorithm: DigestAlgorithm, + ): Promise { + const buffer = await crypto.subtle.digest( + digestAlgorithmToSubtle(algorithm), + bytes, + ) + return new Uint8Array(buffer) + } +} + +// @ts-ignore web only type +function digestAlgorithmToSubtle({name}: DigestAlgorithm): AlgorithmIdentifier { + switch (name) { + case 'sha256': + case 'sha384': + case 'sha512': + return `SHA-${name.slice(-3)}` + default: + throw new Error(`Unknown hash algorithm ${name}`) + } +} diff --git a/modules/expo-bluesky-oauth-client/src/jose-key.ts b/modules/expo-bluesky-oauth-client/src/jose-key.ts new file mode 100644 index 0000000000..fcf401e1dc --- /dev/null +++ b/modules/expo-bluesky-oauth-client/src/jose-key.ts @@ -0,0 +1,106 @@ +import {requireNativeModule} from 'expo-modules-core' +import {jwkSchema} from '@atproto/jwk' +import {Key} from '@atproto/jwk' +import { + exportJWK, + importJWK, + importPKCS8, + JWK, + KeyLike, + VerifyOptions, +} from 'jose' +import {JwtHeader, JwtPayload} from 'jwt-decode' + +const NativeModule = requireNativeModule('ExpoBlueskyOAuthClient') + +export class JoseKey extends Key { + #keyObj?: KeyLike | Uint8Array + + protected async getKey() { + return (this.#keyObj ||= await importJWK(this.jwk as JWK)) + } + + async createJwt(header: JwtHeader, payload: JwtPayload): Promise { + if (header.kid && header.kid !== this.kid) { + throw new TypeError( + `Invalid "kid" (${header.kid}) used to sign with key "${this.kid}"`, + ) + } + + if (!header.alg || !this.algorithms.includes(header.alg)) { + throw new TypeError( + `Invalid "alg" (${header.alg}) used to sign with key "${this.kid}"`, + ) + } + + return await NativeModule.createJwt( + JSON.stringify(this.privateJwk), + JSON.stringify(header), + JSON.stringify(payload), + ) + } + + async verifyJwt< + P extends VerifyPayload = JwtPayload, + C extends string = string, + >(token: Jwt, options?: VerifyOptions): Promise> { + const result = await NativeModule.verifyJwt( + JSON.stringify(this.publicJwk), + token, + JSON.stringify(options), + ) + return result + // return result as VerifyResult + } + + static async fromImportable( + input: Importable, + kid?: string, + ): Promise { + if (typeof input === 'string') { + // PKCS8 + if (input.startsWith('-----')) { + return this.fromPKCS8(input, kid) + } + + // Jwk (string) + if (input.startsWith('{')) { + return this.fromJWK(input, kid) + } + + throw new TypeError('Invalid input') + } + + if (typeof input === 'object') { + // Jwk + if ('kty' in input || 'alg' in input) { + return this.fromJWK(input, kid) + } + + // KeyLike + return this.fromJWK(await exportJWK(input), kid) + } + + throw new TypeError('Invalid input') + } + + static async fromPKCS8(pem: string, kid?: string): Promise { + const keyLike = await importPKCS8(pem, '', {extractable: true}) + return this.fromJWK(await exportJWK(keyLike), kid) + } + + static async fromJWK( + input: string | Record, + inputKid?: string, + ): Promise { + const jwk = jwkSchema.parse( + typeof input === 'string' ? JSON.parse(input) : input, + ) + + const kid = either(jwk.kid, inputKid) + const alg = jwk.alg + const use = jwk.use || 'sig' + + return new JoseKey({...jwk, kid, alg, use}) + } +} diff --git a/modules/expo-bluesky-oauth-client/src/native-types.ts b/modules/expo-bluesky-oauth-client/src/native-types.ts new file mode 100644 index 0000000000..2ac1d7fa40 --- /dev/null +++ b/modules/expo-bluesky-oauth-client/src/native-types.ts @@ -0,0 +1,20 @@ +export interface CryptoKey { + algorithm: { + name: 'ECDSA' + namedCurve: 'P-256' + } + extractable: boolean + type: 'public' | 'private' + usages: ('sign' | 'verify')[] +} + +export interface NativeJWKKey { + crv: 'P-256' + ext: boolean + kty: 'EC' + x: string + y: string + use: 'sig' + alg: 'ES256' + kid: string +} diff --git a/modules/expo-bluesky-oauth-client/src/rn-crypto-key.ts b/modules/expo-bluesky-oauth-client/src/rn-crypto-key.ts new file mode 100644 index 0000000000..d5e4360889 --- /dev/null +++ b/modules/expo-bluesky-oauth-client/src/rn-crypto-key.ts @@ -0,0 +1,86 @@ +import {requireNativeModule} from 'expo-modules-core' +import {Jwk, jwkSchema} from '@atproto/jwk' + +import {JoseKey} from './jose-key' +import {CryptoKey, NativeJWKKey} from './native-types' + +const NativeModule = requireNativeModule('ExpoBlueskyOAuthClient') + +interface CryptoKeyPair { + privateKey: CryptoKey + publicKey: CryptoKey +} + +interface NativeJWKKeyPair { + privateKey: NativeJWKKey + publicKey: NativeJWKKey +} + +export class RnCryptoKey extends JoseKey { + static async generate( + kid: string | undefined, + _: string[] = ['ES256'], + __ = false, + ) { + const {privateKey, publicKey} = await NativeModule.generateKeyPair(kid) + + const nativeKeyPair = { + privateKey: JSON.parse(privateKey), + publicKey: JSON.parse(publicKey), + } + + return this.fromKeypair(nativeKeyPair.privateKey.kid, nativeKeyPair) + } + + static fromKeypair(kid: string, cryptoKeyPair: NativeJWKKeyPair) { + const use = cryptoKeyPair.privateKey.use ?? 'sig' + const alg = cryptoKeyPair.privateKey.alg ?? 'ES256' + + if (use !== 'sig') { + throw new TypeError('Unsupported JWK use') + } + + const webCryptoKeyPair: CryptoKeyPair = { + privateKey: { + algorithm: { + name: 'ECDSA', + namedCurve: 'P-256', + }, + extractable: true, + type: 'private', + usages: ['sign'], + }, + publicKey: { + algorithm: { + name: 'ECDSA', + namedCurve: 'P-256', + }, + extractable: true, + type: 'public', + usages: ['verify'], + }, + } + + return new RnCryptoKey( + jwkSchema.parse({...cryptoKeyPair.privateKey, use, kid, alg}), + webCryptoKeyPair, + ) + } + + constructor(jwk: Jwk, readonly cryptoKeyPair: CryptoKeyPair) { + super(jwk) + } + + get isPrivate() { + return true + } + + get privateJwk(): Jwk | undefined { + if (super.isPrivate) return this.jwk + throw new Error('Private key is not exportable.') + } + + protected async getKey() { + return this.cryptoKeyPair.privateKey + } +} diff --git a/modules/expo-bluesky-oauth-client/src/rn-crypto-key.web.ts b/modules/expo-bluesky-oauth-client/src/rn-crypto-key.web.ts new file mode 100644 index 0000000000..b9fbd05758 --- /dev/null +++ b/modules/expo-bluesky-oauth-client/src/rn-crypto-key.web.ts @@ -0,0 +1,78 @@ +import {Jwk, jwkSchema} from '@atproto/jwk' +import {JoseKey} from '@atproto/jwk-jose' + +import {CryptoKey} from './native-types' +import {generateKeyPair, isSignatureKeyPair} from './util.web' + +// @ts-ignore web only, stops warnings for crypto being missing +const crypto = global.crypto + +// Global has this, but this stops the warnings +interface CryptoKeyPair { + privateKey: CryptoKey + publicKey: CryptoKey +} + +export class RNCryptoKey extends JoseKey { + static async generate( + kid: string = crypto.randomUUID(), + allowedAlgos: string[] = ['ES256'], + exportable = false, + ) { + const cryptoKeyPair: CryptoKeyPair = await generateKeyPair( + allowedAlgos, + exportable, + ) + return this.fromKeypair(kid, cryptoKeyPair) + } + + static async fromKeypair( + kid: string, + cryptoKeyPair: CryptoKeyPair, + ): Promise { + if (!isSignatureKeyPair(cryptoKeyPair)) { + throw new TypeError('CryptoKeyPair must be compatible with sign/verify') + } + + // https://datatracker.ietf.org/doc/html/rfc7517 + // > The "use" and "key_ops" JWK members SHOULD NOT be used together; [...] + // > Applications should specify which of these members they use. + + // eslint-disable-next-line @typescript-eslint/no-unused-vars + const {key_ops: _, ...jwk} = await crypto.subtle.exportKey( + 'jwk', + cryptoKeyPair.privateKey.extractable + ? cryptoKeyPair.privateKey + : cryptoKeyPair.publicKey, + ) + + const use = jwk.use ?? 'sig' + const alg = jwk.alg ?? 'ES256' + + if (use !== 'sig') { + throw new TypeError('Unsupported JWK use') + } + + return new RNCryptoKey( + jwkSchema.parse({...jwk, use, kid, alg}), + cryptoKeyPair, + ) + } + + constructor(jwk: Jwk, readonly cryptoKeyPair: CryptoKeyPair) { + super(jwk) + } + + get isPrivate() { + return true + } + + get privateJwk(): Jwk | undefined { + if (super.isPrivate) return this.jwk + throw new Error('Private key is not exportable.') + } + + protected async getKey() { + return this.cryptoKeyPair.privateKey + } +} diff --git a/modules/expo-bluesky-oauth-client/src/rn-oauth-client-factory.ts b/modules/expo-bluesky-oauth-client/src/rn-oauth-client-factory.ts new file mode 100644 index 0000000000..3e6f0c37cc --- /dev/null +++ b/modules/expo-bluesky-oauth-client/src/rn-oauth-client-factory.ts @@ -0,0 +1,153 @@ +import {Fetch} from '@atproto/fetch' +import {UniversalIdentityResolver} from '@atproto/identity-resolver' +import { + OAuthAuthorizeOptions, + OAuthClientFactory, + OAuthResponseMode, + OAuthResponseType, + Session, +} from '@atproto/oauth-client' +import {OAuthClientMetadata} from '@atproto/oauth-client-metadata' +import IsomorphicOAuthServerMetadataResolver from '@atproto/oauth-server-metadata-resolver' + +import {CryptoSubtle} from './crypto-subtle' +import { + BrowserOAuthDatabase, + DatabaseStore, + PopupStateData, +} from './rn-oauth-database' + +export type BrowserOauthClientFactoryOptions = { + responseMode?: OAuthResponseMode + responseType?: OAuthResponseType + clientMetadata: OAuthClientMetadata + fetch?: Fetch + crypto?: Crypto +} + +const POPUP_KEY_PREFIX = '@@oauth-popup-callback:' + +export class BrowserOAuthClientFactory extends OAuthClientFactory { + readonly popupStore: DatabaseStore + readonly sessionStore: DatabaseStore + + constructor({ + clientMetadata, + // "fragment" is safer as it is not sent to the server + responseMode = 'fragment', + responseType, + crypto = globalThis.crypto, + fetch = globalThis.fetch, + }: BrowserOauthClientFactoryOptions) { + const database = new BrowserOAuthDatabase() + + super({ + clientMetadata, + responseMode, + responseType, + fetch, + cryptoImplementation: new CryptoSubtle(crypto), + sessionStore: database.getSessionStore(), + stateStore: database.getStateStore(), + metadataResolver: new IsomorphicOAuthServerMetadataResolver({ + fetch, + cache: database.getMetadataCache(), + }), + identityResolver: UniversalIdentityResolver.from({ + fetch, + didCache: database.getDidCache(), + handleCache: database.getHandleCache(), + }), + dpopNonceCache: database.getDpopNonceCache(), + }) + + this.sessionStore = database.getSessionStore() + this.popupStore = database.getPopupStore() + } + + async restoreAll() { + const sessionIds = await this.sessionStore.getKeys() + return Object.fromEntries( + await Promise.all( + sessionIds.map( + async sessionId => + [sessionId, await this.restore(sessionId, false)] as const, + ), + ), + ) + } + + async init(sessionId?: string, forceRefresh = false) { + const signInResult = await this.signInCallback() + if (signInResult) { + return signInResult + } else if (sessionId) { + const client = await this.restore(sessionId, forceRefresh) + return {client} + } else { + // TODO: we could restore any session from the store ? + } + } + + async signIn(input: string, options?: OAuthAuthorizeOptions) { + return await this.authorize(input, options) + } + + async signInCallback() { + const redirectUri = new URL(this.clientMetadata.redirect_uris[0]) + if (location.pathname !== redirectUri.pathname) return null + + const params = + this.responseMode === 'query' + ? new URLSearchParams(location.search) + : new URLSearchParams(location.hash.slice(1)) + + // Only if the query string contains oauth callback params + if ( + !params.has('iss') || + !params.has('state') || + !(params.has('code') || params.has('error')) + ) { + return null + } + + // Replace the current history entry without the query string (this will + // prevent this 'if' branch to run again if the user refreshes the page) + history.replaceState(null, '', location.pathname) + + return this.callback(params) + .then(async result => { + if (result.state?.startsWith(POPUP_KEY_PREFIX)) { + const stateKey = result.state.slice(POPUP_KEY_PREFIX.length) + + await this.popupStore.set(stateKey, { + status: 'fulfilled', + value: result.client.sessionId, + }) + + window.close() // continued in signInPopup + throw new Error('Login complete, please close the popup window.') + } + + return result + }) + .catch(async err => { + // TODO: Throw a proper error from parent class to actually detect + // oauth authorization errors + const state = typeof (err as any)?.state + if (typeof state === 'string' && state?.startsWith(POPUP_KEY_PREFIX)) { + const stateKey = state.slice(POPUP_KEY_PREFIX.length) + + await this.popupStore.set(stateKey, { + status: 'rejected', + reason: err, + }) + + window.close() // continued in signInPopup + throw new Error('Login complete, please close the popup window.') + } + + throw err + }) + } +} diff --git a/modules/expo-bluesky-oauth-client/src/rn-oauth-database.native.ts b/modules/expo-bluesky-oauth-client/src/rn-oauth-database.native.ts new file mode 100644 index 0000000000..8a10341910 --- /dev/null +++ b/modules/expo-bluesky-oauth-client/src/rn-oauth-database.native.ts @@ -0,0 +1,214 @@ +import {GenericStore, Value} from '@atproto/caching' +import {DidDocument} from '@atproto/did' +import {ResolvedHandle} from '@atproto/handle-resolver' +import {Key} from '@atproto/jwk' +import {WebcryptoKey} from '@atproto/jwk-webcrypto' +import {InternalStateData, Session, TokenSet} from '@atproto/oauth-client' +import {OAuthServerMetadata} from '@atproto/oauth-server-metadata' +import Storage from '@react-native-async-storage/async-storage' + +type Item = { + value: string + expiresAt: null | Date +} + +type EncodedKey = { + keyId: string + keyPair: CryptoKeyPair +} + +function encodeKey(key: Key): EncodedKey { + if (!(key instanceof WebcryptoKey) || !key.kid) { + throw new Error('Invalid key object') + } + return { + keyId: key.kid, + keyPair: key.cryptoKeyPair, + } +} + +async function decodeKey(encoded: EncodedKey): Promise { + return WebcryptoKey.fromKeypair(encoded.keyId, encoded.keyPair) +} + +export type Schema = { + state: Item<{ + dpopKey: EncodedKey + + iss: string + nonce: string + verifier?: string + appState?: string + }> + session: Item<{ + dpopKey: EncodedKey + + tokenSet: TokenSet + }> + + didCache: Item + dpopNonceCache: Item + handleCache: Item + metadataCache: Item +} + +export type DatabaseStore = GenericStore & { + getKeys: () => Promise +} + +const STORES = [ + 'state', + 'session', + + 'didCache', + 'dpopNonceCache', + 'handleCache', + 'metadataCache', +] as const + +export class BrowserOAuthDatabase { + async delete(key: string) { + await Storage.removeItem(key) + } + + protected createStore( + dbName: N, + { + encode, + decode, + maxAge, + }: { + encode: (value: V) => Schema[N]['value'] | PromiseLike + decode: (encoded: Schema[N]['value']) => V | PromiseLike + maxAge?: number + }, + ): DatabaseStore { + return { + get: async key => { + const itemJson = await Storage.getItem(`${dbName}.${key}`) + if (itemJson == null) return undefined + + const item = JSON.parse(itemJson) as Schema[N] + + // Too old, proactively delete + if (item.expiresAt != null && item.expiresAt < new Date()) { + await this.delete(`${dbName}.${key}`) + return undefined + } + + // Item found and valid. Decode + return decode(item.value) + }, + + getKeys: async () => { + const keys = await Storage.getAllKeys() + return keys.filter(key => key.startsWith(`${dbName}.`)) as string[] + }, + + set: async (key, value) => { + const item = { + value: await encode(value), + expiresAt: maxAge == null ? null : new Date(Date.now() + maxAge), + } as Schema[N] + + await Storage.setItem(`${dbName}.${key}`, JSON.stringify(item)) + }, + + del: async key => { + await this.delete(`${dbName}.${key}`) + }, + } + } + + getSessionStore(): DatabaseStore { + return this.createStore('session', { + encode: ({dpopKey, ...session}) => ({ + ...session, + dpopKey: encodeKey(dpopKey), + }), + decode: async ({dpopKey, ...encoded}) => ({ + ...encoded, + dpopKey: await decodeKey(dpopKey), + }), + }) + } + + getStateStore(): DatabaseStore { + return this.createStore('state', { + encode: ({dpopKey, ...session}) => ({ + ...session, + dpopKey: encodeKey(dpopKey), + }), + decode: async ({dpopKey, ...encoded}) => ({ + ...encoded, + dpopKey: await decodeKey(dpopKey), + }), + }) + } + + getDpopNonceCache(): undefined | DatabaseStore { + return this.createStore('dpopNonceCache', { + // No time limit. It is better to try with a potentially outdated nonce + // and potentially succeed rather than make requests without a nonce and + // 100% fail. + encode: value => value, + decode: encoded => encoded, + }) + } + + getDidCache(): undefined | DatabaseStore { + return this.createStore('didCache', { + maxAge: 60e3, + encode: value => value, + decode: encoded => encoded, + }) + } + + getHandleCache(): undefined | DatabaseStore { + return this.createStore('handleCache', { + maxAge: 60e3, + encode: value => value, + decode: encoded => encoded, + }) + } + + getMetadataCache(): undefined | DatabaseStore { + return this.createStore('metadataCache', { + maxAge: 60e3, + encode: value => value, + decode: encoded => encoded, + }) + } + + async cleanup() { + await Promise.all( + STORES.map( + async storeName => + [ + storeName, + await tx + .objectStore(storeName) + .index('expiresAt') + .getAllKeys(query), + ] as const, + ), + ) + + const storesWithInvalidKeys = res.filter(r => r[1].length > 0) + + await db.transaction( + storesWithInvalidKeys.map(r => r[0]), + 'readwrite', + tx => + Promise.all( + storesWithInvalidKeys.map(async ([name, keys]) => + tx.objectStore(name).delete(keys), + ), + ), + ) + } + + async [Symbol.asyncDispose]() { + await this.cleanup() + } +} diff --git a/modules/expo-bluesky-oauth-client/src/rn-oauth-database.ts b/modules/expo-bluesky-oauth-client/src/rn-oauth-database.ts new file mode 100644 index 0000000000..7b388dfc71 --- /dev/null +++ b/modules/expo-bluesky-oauth-client/src/rn-oauth-database.ts @@ -0,0 +1,269 @@ +import {GenericStore, Value} from '@atproto/caching' +import {DidDocument} from '@atproto/did' +import {ResolvedHandle} from '@atproto/handle-resolver' +import {DB, DBObjectStore} from '@atproto/indexed-db' +import {Key} from '@atproto/jwk' +import {WebcryptoKey} from '@atproto/jwk-webcrypto' +import {InternalStateData, Session, TokenSet} from '@atproto/oauth-client' +import {OAuthServerMetadata} from '@atproto/oauth-server-metadata' + +type Item = { + value: V + expiresAt: null | Date +} + +type EncodedKey = { + keyId: string + keyPair: CryptoKeyPair +} + +function encodeKey(key: Key): EncodedKey { + if (!(key instanceof WebcryptoKey) || !key.kid) { + throw new Error('Invalid key object') + } + return { + keyId: key.kid, + keyPair: key.cryptoKeyPair, + } +} + +async function decodeKey(encoded: EncodedKey): Promise { + return WebcryptoKey.fromKeypair(encoded.keyId, encoded.keyPair) +} + +export type PopupStateData = + | PromiseRejectedResult + | PromiseFulfilledResult + +export type Schema = { + popup: Item + state: Item<{ + dpopKey: EncodedKey + + iss: string + nonce: string + verifier?: string + appState?: string + }> + session: Item<{ + dpopKey: EncodedKey + + tokenSet: TokenSet + }> + + didCache: Item + dpopNonceCache: Item + handleCache: Item + metadataCache: Item +} + +export type DatabaseStore = GenericStore & { + getKeys: () => Promise +} + +const STORES = [ + 'popup', + 'state', + 'session', + + 'didCache', + 'dpopNonceCache', + 'handleCache', + 'metadataCache', +] as const + +export class BrowserOAuthDatabase { + #dbPromise = DB.open( + '@atproto-oauth-client', + [ + db => { + for (const name of STORES) { + const store = db.createObjectStore(name) + store.createIndex('expiresAt', 'expiresAt', {unique: false}) + } + }, + ], + {durability: 'strict'}, + ) + + protected async run( + storeName: N, + mode: 'readonly' | 'readwrite', + fn: (s: DBObjectStore) => R | Promise, + ): Promise { + const db = await this.#dbPromise + return await db.transaction([storeName], mode, tx => + fn(tx.objectStore(storeName)), + ) + } + + protected createStore( + name: N, + { + encode, + decode, + maxAge, + }: { + encode: (value: V) => Schema[N]['value'] | PromiseLike + decode: (encoded: Schema[N]['value']) => V | PromiseLike + maxAge?: number + }, + ): DatabaseStore { + return { + get: async key => { + // Find item in store + const item = await this.run(name, 'readonly', dbStore => { + return dbStore.get(key) + }) + + // Not found + if (item === undefined) return undefined + + // Too old, proactively delete + if (item.expiresAt != null && item.expiresAt < new Date()) { + await this.run(name, 'readwrite', dbStore => { + return dbStore.delete(key) + }) + return undefined + } + + // Item found and valid. Decode + return decode(item.value) + }, + + getKeys: async () => { + const keys = await this.run(name, 'readonly', dbStore => { + return dbStore.getAllKeys() + }) + return keys.filter(key => typeof key === 'string') as string[] + }, + + set: async (key, value) => { + // Create encoded item record + const item = { + value: await encode(value), + expiresAt: maxAge == null ? null : new Date(Date.now() + maxAge), + } as Schema[N] + + // Store item record + await this.run(name, 'readwrite', dbStore => { + return dbStore.put(item, key) + }) + }, + + del: async key => { + // Delete + await this.run(name, 'readwrite', dbStore => { + return dbStore.delete(key) + }) + }, + } + } + + getSessionStore(): DatabaseStore { + return this.createStore('session', { + encode: ({dpopKey, ...session}) => ({ + ...session, + dpopKey: encodeKey(dpopKey), + }), + decode: async ({dpopKey, ...encoded}) => ({ + ...encoded, + dpopKey: await decodeKey(dpopKey), + }), + }) + } + + getStateStore(): DatabaseStore { + return this.createStore('state', { + encode: ({dpopKey, ...session}) => ({ + ...session, + dpopKey: encodeKey(dpopKey), + }), + decode: async ({dpopKey, ...encoded}) => ({ + ...encoded, + dpopKey: await decodeKey(dpopKey), + }), + }) + } + + getPopupStore(): DatabaseStore { + return this.createStore('popup', { + encode: value => value, + decode: encoded => encoded, + }) + } + + getDpopNonceCache(): undefined | DatabaseStore { + return this.createStore('dpopNonceCache', { + // No time limit. It is better to try with a potentially outdated nonce + // and potentially succeed rather than make requests without a nonce and + // 100% fail. + encode: value => value, + decode: encoded => encoded, + }) + } + + getDidCache(): undefined | DatabaseStore { + return this.createStore('didCache', { + maxAge: 60e3, + encode: value => value, + decode: encoded => encoded, + }) + } + + getHandleCache(): undefined | DatabaseStore { + return this.createStore('handleCache', { + maxAge: 60e3, + encode: value => value, + decode: encoded => encoded, + }) + } + + getMetadataCache(): undefined | DatabaseStore { + return this.createStore('metadataCache', { + maxAge: 60e3, + encode: value => value, + decode: encoded => encoded, + }) + } + + async cleanup() { + const db = await this.#dbPromise + const query = IDBKeyRange.lowerBound(new Date()) + const res = await db.transaction(STORES, 'readonly', tx => + Promise.all( + STORES.map( + async storeName => + [ + storeName, + await tx + .objectStore(storeName) + .index('expiresAt') + .getAllKeys(query), + ] as const, + ), + ), + ) + + const storesWithInvalidKeys = res.filter(r => r[1].length > 0) + + await db.transaction( + storesWithInvalidKeys.map(r => r[0]), + 'readwrite', + tx => + Promise.all( + storesWithInvalidKeys.map(async ([name, keys]) => + tx.objectStore(name).delete(keys), + ), + ), + ) + } + + async [Symbol.asyncDispose]() { + // TODO: call cleanup at a constant interval ? + await this.cleanup() + + const db = await this.#dbPromise + await (db[Symbol.asyncDispose] || db[Symbol.dispose]).call(db) + } +} diff --git a/modules/expo-bluesky-oauth-client/src/store.ts b/modules/expo-bluesky-oauth-client/src/store.ts new file mode 100644 index 0000000000..e69de29bb2 diff --git a/modules/expo-bluesky-oauth-client/src/store.web.ts b/modules/expo-bluesky-oauth-client/src/store.web.ts new file mode 100644 index 0000000000..e69de29bb2 diff --git a/modules/expo-bluesky-oauth-client/src/util.web.ts b/modules/expo-bluesky-oauth-client/src/util.web.ts new file mode 100644 index 0000000000..654365aae5 --- /dev/null +++ b/modules/expo-bluesky-oauth-client/src/util.web.ts @@ -0,0 +1,41 @@ +// @ts-ignore web only, this silences errors throughout the whole file for crypto being missing +const crypto = global.crypto + +export async function generateKeyPair(algs: string[], extractable = false) { + const errors: unknown[] = [] + try { + return await crypto.subtle.generateKey( + { + name: 'ECDSA', + namedCurve: `P-256`, + }, + extractable, + ['sign', 'verify'], + ) + } catch (err) { + errors.push(err) + } + + console.log(errors) + throw new AggregateError(errors, 'Failed to generate keypair') +} + +export function isSignatureKeyPair( + v: unknown, + extractable?: boolean, +): v is CryptoKeyPair { + return ( + typeof v === 'object' && + v !== null && + 'privateKey' in v && + v.privateKey instanceof CryptoKey && + v.privateKey.type === 'private' && + (extractable == null || v.privateKey.extractable === extractable) && + v.privateKey.usages.includes('sign') && + 'publicKey' in v && + v.publicKey instanceof CryptoKey && + v.publicKey.type === 'public' && + v.publicKey.extractable === true && + v.publicKey.usages.includes('verify') + ) +} diff --git a/package.json b/package.json index 5eaba7a972..92d088c038 100644 --- a/package.json +++ b/package.json @@ -131,6 +131,7 @@ "expo-web-browser": "~12.8.2", "fast-text-encoding": "^1.0.6", "history": "^5.3.0", + "jose": "^5.2.4", "js-sha256": "^0.9.0", "jwt-decode": "^4.0.0", "lande": "^1.0.10", diff --git a/src/view/screens/Home.tsx b/src/view/screens/Home.tsx index 39bdac669c..a97f2d2986 100644 --- a/src/view/screens/Home.tsx +++ b/src/view/screens/Home.tsx @@ -19,6 +19,7 @@ import {Pager, PagerRef, RenderTabBarFnProps} from 'view/com/pager/Pager' import {CustomFeedEmptyState} from 'view/com/posts/CustomFeedEmptyState' import {FollowingEmptyState} from 'view/com/posts/FollowingEmptyState' import {FollowingEndOfFeed} from 'view/com/posts/FollowingEndOfFeed' +import {RnCryptoKey} from '../../../modules/expo-bluesky-oauth-client' import {HomeLoggedOutCTA} from '../com/auth/HomeLoggedOutCTA' import {HomeHeader} from '../com/home/HomeHeader' @@ -51,6 +52,18 @@ function HomeScreenReady({ preferences: UsePreferencesQueryResponse pinnedFeedInfos: FeedSourceInfo[] }) { + React.useEffect(() => { + ;(async () => { + const key = await RnCryptoKey.generate(undefined, ['ES256'], false) + console.log('public', key.publicJwk) + const jwt = await key.createJwt( + {alg: 'ES256', kid: key.kid}, + {sub: 'test'}, + ) + console.log(jwt) + })() + }, []) + const allFeeds = React.useMemo(() => { const feeds: FeedDescriptor[] = [] feeds.push('home') diff --git a/yarn.lock b/yarn.lock index 1a61c8b037..f5a3799da4 100644 --- a/yarn.lock +++ b/yarn.lock @@ -15031,6 +15031,11 @@ jose@^5.0.1: resolved "https://registry.yarnpkg.com/jose/-/jose-5.1.3.tgz#303959d85c51b5cb14725f930270b72be56abdca" integrity sha512-GPExOkcMsCLBTi1YetY2LmkoY559fss0+0KVa6kOfb2YFe84nAM7Nm/XzuZozah4iHgmBGrCOHL5/cy670SBRw== +jose@^5.2.4: + version "5.2.4" + resolved "https://registry.yarnpkg.com/jose/-/jose-5.2.4.tgz#c0d296caeeed0b8444a8b8c3b68403d61aa4ed72" + integrity sha512-6ScbIk2WWCeXkmzF6bRPmEuaqy1m8SbsRFMa/FLrSCkGIhj8OLVG/IH+XHVmNMx/KUo8cVWEE6oKR4dJ+S0Rkg== + js-base64@^3.7.2: version "3.7.5" resolved "https://registry.yarnpkg.com/js-base64/-/js-base64-3.7.5.tgz#21e24cf6b886f76d6f5f165bfcd69cc55b9e3fca" From 0aab04f670eb4231d5daa2fa3a1d5b2c81339b50 Mon Sep 17 00:00:00 2001 From: Hailey Date: Thu, 11 Apr 2024 22:12:09 -0700 Subject: [PATCH 26/54] fix names --- .../expo-bluesky-oauth-client/src/rn-oauth-client-factory.ts | 2 +- .../expo-bluesky-oauth-client/src/rn-oauth-database.native.ts | 2 +- modules/expo-bluesky-oauth-client/src/rn-oauth-database.ts | 2 +- 3 files changed, 3 insertions(+), 3 deletions(-) diff --git a/modules/expo-bluesky-oauth-client/src/rn-oauth-client-factory.ts b/modules/expo-bluesky-oauth-client/src/rn-oauth-client-factory.ts index 3e6f0c37cc..c15eb60308 100644 --- a/modules/expo-bluesky-oauth-client/src/rn-oauth-client-factory.ts +++ b/modules/expo-bluesky-oauth-client/src/rn-oauth-client-factory.ts @@ -27,7 +27,7 @@ export type BrowserOauthClientFactoryOptions = { const POPUP_KEY_PREFIX = '@@oauth-popup-callback:' -export class BrowserOAuthClientFactory extends OAuthClientFactory { +export class RNOAuthClientFactory extends OAuthClientFactory { readonly popupStore: DatabaseStore readonly sessionStore: DatabaseStore diff --git a/modules/expo-bluesky-oauth-client/src/rn-oauth-database.native.ts b/modules/expo-bluesky-oauth-client/src/rn-oauth-database.native.ts index 8a10341910..96a7ae74d7 100644 --- a/modules/expo-bluesky-oauth-client/src/rn-oauth-database.native.ts +++ b/modules/expo-bluesky-oauth-client/src/rn-oauth-database.native.ts @@ -66,7 +66,7 @@ const STORES = [ 'metadataCache', ] as const -export class BrowserOAuthDatabase { +export class RNOAuthDatabase { async delete(key: string) { await Storage.removeItem(key) } diff --git a/modules/expo-bluesky-oauth-client/src/rn-oauth-database.ts b/modules/expo-bluesky-oauth-client/src/rn-oauth-database.ts index 7b388dfc71..8da68b8138 100644 --- a/modules/expo-bluesky-oauth-client/src/rn-oauth-database.ts +++ b/modules/expo-bluesky-oauth-client/src/rn-oauth-database.ts @@ -72,7 +72,7 @@ const STORES = [ 'metadataCache', ] as const -export class BrowserOAuthDatabase { +export class RNOAuthDatabase { #dbPromise = DB.open( '@atproto-oauth-client', [ From 72e22a2427b7c2f9db0b66dadee22bfb6e1d4bb6 Mon Sep 17 00:00:00 2001 From: Hailey Date: Thu, 11 Apr 2024 22:15:17 -0700 Subject: [PATCH 27/54] revert babel changes --- babel.config.js | 4 +--- 1 file changed, 1 insertion(+), 3 deletions(-) diff --git a/babel.config.js b/babel.config.js index 785aee02cb..43b2c7bce3 100644 --- a/babel.config.js +++ b/babel.config.js @@ -42,9 +42,7 @@ module.exports = function (api) { platform: './src/platform', state: './src/state', view: './src/view', - crypto: 'react-native-quick-crypto', - stream: 'stream-browserify', - buffer: '@craftzdog/react-native-buffer', + crypto: './src/platform/crypto.ts', }, }, ], From 70251e5be4550248b46b4c33123b378ab477414c Mon Sep 17 00:00:00 2001 From: Hailey Date: Thu, 11 Apr 2024 22:21:16 -0700 Subject: [PATCH 28/54] few small changes --- .../src/rn-oauth-client-factory.ts | 4 ++-- src/screens/Login/hooks/useLogin.ts | 6 +++--- 2 files changed, 5 insertions(+), 5 deletions(-) diff --git a/modules/expo-bluesky-oauth-client/src/rn-oauth-client-factory.ts b/modules/expo-bluesky-oauth-client/src/rn-oauth-client-factory.ts index c15eb60308..2f854e1b26 100644 --- a/modules/expo-bluesky-oauth-client/src/rn-oauth-client-factory.ts +++ b/modules/expo-bluesky-oauth-client/src/rn-oauth-client-factory.ts @@ -12,9 +12,9 @@ import IsomorphicOAuthServerMetadataResolver from '@atproto/oauth-server-metadat import {CryptoSubtle} from './crypto-subtle' import { - BrowserOAuthDatabase, DatabaseStore, PopupStateData, + RNOAuthDatabase, } from './rn-oauth-database' export type BrowserOauthClientFactoryOptions = { @@ -39,7 +39,7 @@ export class RNOAuthClientFactory extends OAuthClientFactory { crypto = globalThis.crypto, fetch = globalThis.fetch, }: BrowserOauthClientFactoryOptions) { - const database = new BrowserOAuthDatabase() + const database = new RNOAuthDatabase() super({ clientMetadata, diff --git a/src/screens/Login/hooks/useLogin.ts b/src/screens/Login/hooks/useLogin.ts index 13840e1ac2..5de8483be0 100644 --- a/src/screens/Login/hooks/useLogin.ts +++ b/src/screens/Login/hooks/useLogin.ts @@ -1,6 +1,5 @@ import React from 'react' import * as Browser from 'expo-web-browser' -import {BrowserOAuthClientFactory} from '@atproto/oauth-client-react-native' import { buildOAuthUrl, @@ -12,11 +11,12 @@ import { OAUTH_RESPONSE_TYPES, OAUTH_SCOPE, } from 'lib/oauth' +import {RNOAuthClientFactory} from '../../../../modules/expo-bluesky-oauth-client' // Service URL here is just a placeholder, this isn't how it will actually work export function useLogin(serviceUrl: string | undefined) { const openAuthSession = React.useCallback(async () => { - const oauthFactory = new BrowserOAuthClientFactory({ + const oauthFactory = new RNOAuthClientFactory({ clientMetadata: { client_id: OAUTH_CLIENT_ID, redirect_uris: [OAUTH_REDIRECT_URI], @@ -28,7 +28,7 @@ export function useLogin(serviceUrl: string | undefined) { }, }) - await oauthFactory.signIn('afepwasfojefpaowejfpwef') + await oauthFactory.signIn('alice.test') return From d948e7369970a74c4e0ce286c60ed9770fabeefe Mon Sep 17 00:00:00 2001 From: Hailey Date: Fri, 12 Apr 2024 00:33:55 -0700 Subject: [PATCH 29/54] update dev variables --- src/lib/oauth.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/lib/oauth.ts b/src/lib/oauth.ts index d1152a6e6f..f917d12a25 100644 --- a/src/lib/oauth.ts +++ b/src/lib/oauth.ts @@ -1,7 +1,7 @@ import {isWeb} from 'platform/detection' export const OAUTH_CLIENT_ID = 'http://localhost/' -export const OAUTH_REDIRECT_URI = 'http://127.0.0.1:5173/' +export const OAUTH_REDIRECT_URI = 'http://localhost:5173/' export const OAUTH_SCOPE = 'openid profile email phone offline_access' export const OAUTH_GRANT_TYPES = [ 'authorization_code', From 73c98bc3afd1a975771c1db1895f313e6e3004b3 Mon Sep 17 00:00:00 2001 From: Hailey Date: Fri, 12 Apr 2024 00:35:30 -0700 Subject: [PATCH 30/54] native factory impl --- .../src/rn-oauth-client-factory.native.ts | 147 ++++++++++++++++++ 1 file changed, 147 insertions(+) create mode 100644 modules/expo-bluesky-oauth-client/src/rn-oauth-client-factory.native.ts diff --git a/modules/expo-bluesky-oauth-client/src/rn-oauth-client-factory.native.ts b/modules/expo-bluesky-oauth-client/src/rn-oauth-client-factory.native.ts new file mode 100644 index 0000000000..79da5cd035 --- /dev/null +++ b/modules/expo-bluesky-oauth-client/src/rn-oauth-client-factory.native.ts @@ -0,0 +1,147 @@ +import {Fetch} from '@atproto/fetch' +import {UniversalIdentityResolver} from '@atproto/identity-resolver' +import { + OAuthAuthorizeOptions, + OAuthClientFactory, + OAuthResponseMode, + OAuthResponseType, + Session, +} from '@atproto/oauth-client' +import {OAuthClientMetadata} from '@atproto/oauth-client-metadata' +import IsomorphicOAuthServerMetadataResolver from '@atproto/oauth-server-metadata-resolver' + +import {CryptoSubtle} from './crypto-subtle' +import {DatabaseStore, RNOAuthDatabase} from './rn-oauth-database' + +export type BrowserOauthClientFactoryOptions = { + responseMode?: OAuthResponseMode + responseType?: OAuthResponseType + clientMetadata: OAuthClientMetadata + fetch?: Fetch + crypto?: Crypto +} + +const POPUP_KEY_PREFIX = '@@oauth-popup-callback:' + +export class RNOAuthClientFactory extends OAuthClientFactory { + readonly sessionStore: DatabaseStore + + constructor({ + clientMetadata, + // "fragment" is safer as it is not sent to the server + responseMode = 'fragment', + responseType, + crypto = {subtle: CryptoSubtle}, + fetch = globalThis.fetch, + }: BrowserOauthClientFactoryOptions) { + const database = new RNOAuthDatabase() + + super({ + clientMetadata, + responseMode, + responseType, + fetch, + cryptoImplementation: new CryptoSubtle(crypto), + sessionStore: database.getSessionStore(), + stateStore: database.getStateStore(), + metadataResolver: new IsomorphicOAuthServerMetadataResolver({ + fetch, + cache: database.getMetadataCache(), + }), + identityResolver: UniversalIdentityResolver.from({ + fetch, + didCache: database.getDidCache(), + handleCache: database.getHandleCache(), + }), + dpopNonceCache: database.getDpopNonceCache(), + }) + + this.sessionStore = database.getSessionStore() + } + + async restoreAll() { + const sessionIds = await this.sessionStore.getKeys() + return Object.fromEntries( + await Promise.all( + sessionIds.map( + async sessionId => + [sessionId, await this.restore(sessionId, false)] as const, + ), + ), + ) + } + + async init(sessionId?: string, forceRefresh = false) { + const signInResult = await this.signInCallback() + if (signInResult) { + return signInResult + } else if (sessionId) { + const client = await this.restore(sessionId, forceRefresh) + return {client} + } else { + // TODO: we could restore any session from the store ? + } + } + + async signIn(input: string, options?: OAuthAuthorizeOptions) { + return await this.authorize(input, options) + } + + async signInCallback() { + const redirectUri = new URL(this.clientMetadata.redirect_uris[0]) + if (location.pathname !== redirectUri.pathname) return null + + const params = + this.responseMode === 'query' + ? new URLSearchParams(location.search) + : new URLSearchParams(location.hash.slice(1)) + + // Only if the query string contains oauth callback params + if ( + !params.has('iss') || + !params.has('state') || + !(params.has('code') || params.has('error')) + ) { + return null + } + + // Replace the current history entry without the query string (this will + // prevent this 'if' branch to run again if the user refreshes the page) + history.replaceState(null, '', location.pathname) + + return this.callback(params) + .then(async result => { + if (result.state?.startsWith(POPUP_KEY_PREFIX)) { + const stateKey = result.state.slice(POPUP_KEY_PREFIX.length) + + await this.popupStore.set(stateKey, { + status: 'fulfilled', + value: result.client.sessionId, + }) + + window.close() // continued in signInPopup + throw new Error('Login complete, please close the popup window.') + } + + return result + }) + .catch(async err => { + // TODO: Throw a proper error from parent class to actually detect + // oauth authorization errors + const state = typeof (err as any)?.state + if (typeof state === 'string' && state?.startsWith(POPUP_KEY_PREFIX)) { + const stateKey = state.slice(POPUP_KEY_PREFIX.length) + + await this.popupStore.set(stateKey, { + status: 'rejected', + reason: err, + }) + + window.close() // continued in signInPopup + throw new Error('Login complete, please close the popup window.') + } + + throw err + }) + } +} From 00abca970a52da971272d7926b88f1618a190173 Mon Sep 17 00:00:00 2001 From: Hailey Date: Fri, 12 Apr 2024 00:36:34 -0700 Subject: [PATCH 31/54] few more cleanups --- .../src/rn-oauth-client-factory.native.ts | 4 ++-- .../src/rn-oauth-client-factory.ts | 8 ++++---- 2 files changed, 6 insertions(+), 6 deletions(-) diff --git a/modules/expo-bluesky-oauth-client/src/rn-oauth-client-factory.native.ts b/modules/expo-bluesky-oauth-client/src/rn-oauth-client-factory.native.ts index 79da5cd035..81713e140c 100644 --- a/modules/expo-bluesky-oauth-client/src/rn-oauth-client-factory.native.ts +++ b/modules/expo-bluesky-oauth-client/src/rn-oauth-client-factory.native.ts @@ -13,7 +13,7 @@ import IsomorphicOAuthServerMetadataResolver from '@atproto/oauth-server-metadat import {CryptoSubtle} from './crypto-subtle' import {DatabaseStore, RNOAuthDatabase} from './rn-oauth-database' -export type BrowserOauthClientFactoryOptions = { +export type RNOAuthClientOptions = { responseMode?: OAuthResponseMode responseType?: OAuthResponseType clientMetadata: OAuthClientMetadata @@ -33,7 +33,7 @@ export class RNOAuthClientFactory extends OAuthClientFactory { responseType, crypto = {subtle: CryptoSubtle}, fetch = globalThis.fetch, - }: BrowserOauthClientFactoryOptions) { + }: RNOAuthClientOptions) { const database = new RNOAuthDatabase() super({ diff --git a/modules/expo-bluesky-oauth-client/src/rn-oauth-client-factory.ts b/modules/expo-bluesky-oauth-client/src/rn-oauth-client-factory.ts index c15eb60308..a8e2dc179e 100644 --- a/modules/expo-bluesky-oauth-client/src/rn-oauth-client-factory.ts +++ b/modules/expo-bluesky-oauth-client/src/rn-oauth-client-factory.ts @@ -12,12 +12,12 @@ import IsomorphicOAuthServerMetadataResolver from '@atproto/oauth-server-metadat import {CryptoSubtle} from './crypto-subtle' import { - BrowserOAuthDatabase, DatabaseStore, PopupStateData, + RNOAuthDatabase, } from './rn-oauth-database' -export type BrowserOauthClientFactoryOptions = { +export type RNOAuthClientOptions = { responseMode?: OAuthResponseMode responseType?: OAuthResponseType clientMetadata: OAuthClientMetadata @@ -38,8 +38,8 @@ export class RNOAuthClientFactory extends OAuthClientFactory { responseType, crypto = globalThis.crypto, fetch = globalThis.fetch, - }: BrowserOauthClientFactoryOptions) { - const database = new BrowserOAuthDatabase() + }: RNOAuthClientOptions) { + const database = new RNOAuthDatabase() super({ clientMetadata, From 951f5f25d6c567455f42d71d9d0955f0ef0785bf Mon Sep 17 00:00:00 2001 From: Hailey Date: Fri, 12 Apr 2024 02:24:16 -0700 Subject: [PATCH 32/54] it works! --- .../src/rn-oauth-client-factory.native.ts | 8 +++++--- .../src/rn-oauth-database.native.ts | 4 ++-- src/lib/oauth.ts | 2 +- src/screens/Login/hooks/useLogin.ts | 4 +++- 4 files changed, 11 insertions(+), 7 deletions(-) diff --git a/modules/expo-bluesky-oauth-client/src/rn-oauth-client-factory.native.ts b/modules/expo-bluesky-oauth-client/src/rn-oauth-client-factory.native.ts index 81713e140c..08efd3ca8f 100644 --- a/modules/expo-bluesky-oauth-client/src/rn-oauth-client-factory.native.ts +++ b/modules/expo-bluesky-oauth-client/src/rn-oauth-client-factory.native.ts @@ -8,7 +8,7 @@ import { Session, } from '@atproto/oauth-client' import {OAuthClientMetadata} from '@atproto/oauth-client-metadata' -import IsomorphicOAuthServerMetadataResolver from '@atproto/oauth-server-metadata-resolver' +import {IsomorphicOAuthServerMetadataResolver} from '@atproto/oauth-server-metadata-resolver' import {CryptoSubtle} from './crypto-subtle' import {DatabaseStore, RNOAuthDatabase} from './rn-oauth-database' @@ -31,7 +31,7 @@ export class RNOAuthClientFactory extends OAuthClientFactory { // "fragment" is safer as it is not sent to the server responseMode = 'fragment', responseType, - crypto = {subtle: CryptoSubtle}, + crypto, fetch = globalThis.fetch, }: RNOAuthClientOptions) { const database = new RNOAuthDatabase() @@ -41,7 +41,7 @@ export class RNOAuthClientFactory extends OAuthClientFactory { responseMode, responseType, fetch, - cryptoImplementation: new CryptoSubtle(crypto), + cryptoImplementation: new CryptoSubtle(), sessionStore: database.getSessionStore(), stateStore: database.getStateStore(), metadataResolver: new IsomorphicOAuthServerMetadataResolver({ @@ -52,6 +52,8 @@ export class RNOAuthClientFactory extends OAuthClientFactory { fetch, didCache: database.getDidCache(), handleCache: database.getHandleCache(), + plcDirectoryUrl: 'http://localhost:2582', // dev-env + atprotoLexiconUrl: 'http://localhost:2584', // dev-env (bsky appview) }), dpopNonceCache: database.getDpopNonceCache(), }) diff --git a/modules/expo-bluesky-oauth-client/src/rn-oauth-database.native.ts b/modules/expo-bluesky-oauth-client/src/rn-oauth-database.native.ts index 96a7ae74d7..0ed6e41d03 100644 --- a/modules/expo-bluesky-oauth-client/src/rn-oauth-database.native.ts +++ b/modules/expo-bluesky-oauth-client/src/rn-oauth-database.native.ts @@ -7,7 +7,7 @@ import {InternalStateData, Session, TokenSet} from '@atproto/oauth-client' import {OAuthServerMetadata} from '@atproto/oauth-server-metadata' import Storage from '@react-native-async-storage/async-storage' -type Item = { +type Item = { value: string expiresAt: null | Date } @@ -18,7 +18,7 @@ type EncodedKey = { } function encodeKey(key: Key): EncodedKey { - if (!(key instanceof WebcryptoKey) || !key.kid) { + if (!key.kid) { throw new Error('Invalid key object') } return { diff --git a/src/lib/oauth.ts b/src/lib/oauth.ts index f917d12a25..d1152a6e6f 100644 --- a/src/lib/oauth.ts +++ b/src/lib/oauth.ts @@ -1,7 +1,7 @@ import {isWeb} from 'platform/detection' export const OAUTH_CLIENT_ID = 'http://localhost/' -export const OAUTH_REDIRECT_URI = 'http://localhost:5173/' +export const OAUTH_REDIRECT_URI = 'http://127.0.0.1:5173/' export const OAUTH_SCOPE = 'openid profile email phone offline_access' export const OAUTH_GRANT_TYPES = [ 'authorization_code', diff --git a/src/screens/Login/hooks/useLogin.ts b/src/screens/Login/hooks/useLogin.ts index 5de8483be0..c596d994f8 100644 --- a/src/screens/Login/hooks/useLogin.ts +++ b/src/screens/Login/hooks/useLogin.ts @@ -26,9 +26,11 @@ export function useLogin(serviceUrl: string | undefined) { dpop_bound_access_tokens: DPOP_BOUND_ACCESS_TOKENS, application_type: OAUTH_APPLICATION_TYPE, }, + fetch: global.fetch, }) - await oauthFactory.signIn('alice.test') + const res = await oauthFactory.signIn('http://localhost:2583/') + console.log(res) return From bc67bdb5b1237756b6429505f5503d6dfee940e9 Mon Sep 17 00:00:00 2001 From: Hailey Date: Fri, 12 Apr 2024 09:20:07 -0700 Subject: [PATCH 33/54] rm some useless code --- src/lib/oauth.ts | 12 +----------- src/screens/Login/hooks/useLogin.ts | 18 ++++++------------ 2 files changed, 7 insertions(+), 23 deletions(-) diff --git a/src/lib/oauth.ts b/src/lib/oauth.ts index d1152a6e6f..391ca85059 100644 --- a/src/lib/oauth.ts +++ b/src/lib/oauth.ts @@ -1,7 +1,7 @@ import {isWeb} from 'platform/detection' export const OAUTH_CLIENT_ID = 'http://localhost/' -export const OAUTH_REDIRECT_URI = 'http://127.0.0.1:5173/' +export const OAUTH_REDIRECT_URI = 'http://127.0.0.1:2583/' export const OAUTH_SCOPE = 'openid profile email phone offline_access' export const OAUTH_GRANT_TYPES = [ 'authorization_code', @@ -10,13 +10,3 @@ export const OAUTH_GRANT_TYPES = [ export const OAUTH_RESPONSE_TYPES = ['code', 'code id_token'] as const export const DPOP_BOUND_ACCESS_TOKENS = true export const OAUTH_APPLICATION_TYPE = isWeb ? 'web' : 'native' // TODO what should we put here for native - -export const buildOAuthUrl = (serviceUrl: string, state: string) => { - const url = new URL(serviceUrl) - url.searchParams.set('client_id', OAUTH_CLIENT_ID) - url.searchParams.set('redirect_uri', OAUTH_REDIRECT_URI) - url.searchParams.set('response_type', OAUTH_RESPONSE_TYPES.join(' ')) - url.searchParams.set('scope', OAUTH_SCOPE) - url.searchParams.set('state', state) - return url.toString() -} diff --git a/src/screens/Login/hooks/useLogin.ts b/src/screens/Login/hooks/useLogin.ts index c596d994f8..89cc9528f8 100644 --- a/src/screens/Login/hooks/useLogin.ts +++ b/src/screens/Login/hooks/useLogin.ts @@ -2,7 +2,6 @@ import React from 'react' import * as Browser from 'expo-web-browser' import { - buildOAuthUrl, DPOP_BOUND_ACCESS_TOKENS, OAUTH_APPLICATION_TYPE, OAUTH_CLIENT_ID, @@ -14,7 +13,7 @@ import { import {RNOAuthClientFactory} from '../../../../modules/expo-bluesky-oauth-client' // Service URL here is just a placeholder, this isn't how it will actually work -export function useLogin(serviceUrl: string | undefined) { +export function useLogin() { const openAuthSession = React.useCallback(async () => { const oauthFactory = new RNOAuthClientFactory({ clientMetadata: { @@ -29,24 +28,19 @@ export function useLogin(serviceUrl: string | undefined) { fetch: global.fetch, }) - const res = await oauthFactory.signIn('http://localhost:2583/') - console.log(res) + const url = await oauthFactory.signIn('http://localhost:2583/') - return - - if (!serviceUrl) return - - const url = buildOAuthUrl(serviceUrl, '123') // TODO replace '123' with the appropriate state + console.log(url.href) const authSession = await Browser.openAuthSessionAsync( - url, // This isn't actually how this will work - OAUTH_REDIRECT_URI, // Replace this as well with the appropriate link + url.href, + OAUTH_REDIRECT_URI, ) if (authSession.type !== 'success') { return } - }, [serviceUrl]) + }, []) return { openAuthSession, From c97e439a7ce5af4186f920d9a6d99f04f01dd2cb Mon Sep 17 00:00:00 2001 From: Hailey Date: Sun, 14 Apr 2024 16:26:31 -0700 Subject: [PATCH 34/54] better layout --- modules/expo-bluesky-oauth-client/index.ts | 12 +- .../{src => src-old}/crypto-subtle.ts | 0 .../{src => src-old}/crypto-subtle.web.ts | 0 .../{src => src-old}/jose-key.ts | 0 .../{src => src-old}/native-types.ts | 0 .../{src => src-old}/rn-crypto-key.ts | 0 .../{src => src-old}/rn-crypto-key.web.ts | 0 .../rn-oauth-client-factory.native.ts | 0 .../rn-oauth-client-factory.ts | 0 .../rn-oauth-database.native.ts | 0 .../{src => src-old}/rn-oauth-database.ts | 0 .../{src => src-old}/store.ts | 0 .../{src => src-old}/store.web.ts | 0 .../{src => src-old}/util.web.ts | 0 .../src/oauth-client-react-native.ts | 44 +++++++ .../src/react-native-crypto-implementation.ts | 27 +++++ .../src/react-native-key.ts | 110 ++++++++++++++++++ .../src/react-native-store-with-key.ts | 51 ++++++++ .../src/react-native-store.ts | 25 ++++ 19 files changed, 263 insertions(+), 6 deletions(-) rename modules/expo-bluesky-oauth-client/{src => src-old}/crypto-subtle.ts (100%) rename modules/expo-bluesky-oauth-client/{src => src-old}/crypto-subtle.web.ts (100%) rename modules/expo-bluesky-oauth-client/{src => src-old}/jose-key.ts (100%) rename modules/expo-bluesky-oauth-client/{src => src-old}/native-types.ts (100%) rename modules/expo-bluesky-oauth-client/{src => src-old}/rn-crypto-key.ts (100%) rename modules/expo-bluesky-oauth-client/{src => src-old}/rn-crypto-key.web.ts (100%) rename modules/expo-bluesky-oauth-client/{src => src-old}/rn-oauth-client-factory.native.ts (100%) rename modules/expo-bluesky-oauth-client/{src => src-old}/rn-oauth-client-factory.ts (100%) rename modules/expo-bluesky-oauth-client/{src => src-old}/rn-oauth-database.native.ts (100%) rename modules/expo-bluesky-oauth-client/{src => src-old}/rn-oauth-database.ts (100%) rename modules/expo-bluesky-oauth-client/{src => src-old}/store.ts (100%) rename modules/expo-bluesky-oauth-client/{src => src-old}/store.web.ts (100%) rename modules/expo-bluesky-oauth-client/{src => src-old}/util.web.ts (100%) create mode 100644 modules/expo-bluesky-oauth-client/src/oauth-client-react-native.ts create mode 100644 modules/expo-bluesky-oauth-client/src/react-native-crypto-implementation.ts create mode 100644 modules/expo-bluesky-oauth-client/src/react-native-key.ts create mode 100644 modules/expo-bluesky-oauth-client/src/react-native-store-with-key.ts create mode 100644 modules/expo-bluesky-oauth-client/src/react-native-store.ts diff --git a/modules/expo-bluesky-oauth-client/index.ts b/modules/expo-bluesky-oauth-client/index.ts index ca28960b93..05f53f9803 100644 --- a/modules/expo-bluesky-oauth-client/index.ts +++ b/modules/expo-bluesky-oauth-client/index.ts @@ -1,6 +1,6 @@ -export * from './src/crypto-subtle' -export * from './src/jose-key' -export * from './src/rn-crypto-key' -export * from './src/rn-oauth-client-factory' -export * from './src/rn-oauth-database' -export * from './src/util.web' +export * from './src-old/crypto-subtle' +export * from './src-old/jose-key' +export * from './src-old/rn-crypto-key' +export * from './src-old/rn-oauth-client-factory' +export * from './src-old/rn-oauth-database' +export * from './src-old/util.web' diff --git a/modules/expo-bluesky-oauth-client/src/crypto-subtle.ts b/modules/expo-bluesky-oauth-client/src-old/crypto-subtle.ts similarity index 100% rename from modules/expo-bluesky-oauth-client/src/crypto-subtle.ts rename to modules/expo-bluesky-oauth-client/src-old/crypto-subtle.ts diff --git a/modules/expo-bluesky-oauth-client/src/crypto-subtle.web.ts b/modules/expo-bluesky-oauth-client/src-old/crypto-subtle.web.ts similarity index 100% rename from modules/expo-bluesky-oauth-client/src/crypto-subtle.web.ts rename to modules/expo-bluesky-oauth-client/src-old/crypto-subtle.web.ts diff --git a/modules/expo-bluesky-oauth-client/src/jose-key.ts b/modules/expo-bluesky-oauth-client/src-old/jose-key.ts similarity index 100% rename from modules/expo-bluesky-oauth-client/src/jose-key.ts rename to modules/expo-bluesky-oauth-client/src-old/jose-key.ts diff --git a/modules/expo-bluesky-oauth-client/src/native-types.ts b/modules/expo-bluesky-oauth-client/src-old/native-types.ts similarity index 100% rename from modules/expo-bluesky-oauth-client/src/native-types.ts rename to modules/expo-bluesky-oauth-client/src-old/native-types.ts diff --git a/modules/expo-bluesky-oauth-client/src/rn-crypto-key.ts b/modules/expo-bluesky-oauth-client/src-old/rn-crypto-key.ts similarity index 100% rename from modules/expo-bluesky-oauth-client/src/rn-crypto-key.ts rename to modules/expo-bluesky-oauth-client/src-old/rn-crypto-key.ts diff --git a/modules/expo-bluesky-oauth-client/src/rn-crypto-key.web.ts b/modules/expo-bluesky-oauth-client/src-old/rn-crypto-key.web.ts similarity index 100% rename from modules/expo-bluesky-oauth-client/src/rn-crypto-key.web.ts rename to modules/expo-bluesky-oauth-client/src-old/rn-crypto-key.web.ts diff --git a/modules/expo-bluesky-oauth-client/src/rn-oauth-client-factory.native.ts b/modules/expo-bluesky-oauth-client/src-old/rn-oauth-client-factory.native.ts similarity index 100% rename from modules/expo-bluesky-oauth-client/src/rn-oauth-client-factory.native.ts rename to modules/expo-bluesky-oauth-client/src-old/rn-oauth-client-factory.native.ts diff --git a/modules/expo-bluesky-oauth-client/src/rn-oauth-client-factory.ts b/modules/expo-bluesky-oauth-client/src-old/rn-oauth-client-factory.ts similarity index 100% rename from modules/expo-bluesky-oauth-client/src/rn-oauth-client-factory.ts rename to modules/expo-bluesky-oauth-client/src-old/rn-oauth-client-factory.ts diff --git a/modules/expo-bluesky-oauth-client/src/rn-oauth-database.native.ts b/modules/expo-bluesky-oauth-client/src-old/rn-oauth-database.native.ts similarity index 100% rename from modules/expo-bluesky-oauth-client/src/rn-oauth-database.native.ts rename to modules/expo-bluesky-oauth-client/src-old/rn-oauth-database.native.ts diff --git a/modules/expo-bluesky-oauth-client/src/rn-oauth-database.ts b/modules/expo-bluesky-oauth-client/src-old/rn-oauth-database.ts similarity index 100% rename from modules/expo-bluesky-oauth-client/src/rn-oauth-database.ts rename to modules/expo-bluesky-oauth-client/src-old/rn-oauth-database.ts diff --git a/modules/expo-bluesky-oauth-client/src/store.ts b/modules/expo-bluesky-oauth-client/src-old/store.ts similarity index 100% rename from modules/expo-bluesky-oauth-client/src/store.ts rename to modules/expo-bluesky-oauth-client/src-old/store.ts diff --git a/modules/expo-bluesky-oauth-client/src/store.web.ts b/modules/expo-bluesky-oauth-client/src-old/store.web.ts similarity index 100% rename from modules/expo-bluesky-oauth-client/src/store.web.ts rename to modules/expo-bluesky-oauth-client/src-old/store.web.ts diff --git a/modules/expo-bluesky-oauth-client/src/util.web.ts b/modules/expo-bluesky-oauth-client/src-old/util.web.ts similarity index 100% rename from modules/expo-bluesky-oauth-client/src/util.web.ts rename to modules/expo-bluesky-oauth-client/src-old/util.web.ts diff --git a/modules/expo-bluesky-oauth-client/src/oauth-client-react-native.ts b/modules/expo-bluesky-oauth-client/src/oauth-client-react-native.ts new file mode 100644 index 0000000000..4ce738202e --- /dev/null +++ b/modules/expo-bluesky-oauth-client/src/oauth-client-react-native.ts @@ -0,0 +1,44 @@ +import {requireNativeModule} from 'expo-modules-core' +import {Jwk, Jwt} from '@atproto/jwk' + +const NativeModule = requireNativeModule('ExpoBlueskyOAuthClient') + +const LINKING_ERROR = + 'The package ExpoBlueskyOAuthClient is not linked. Make sure you have run `expo install expo-bluesky-oauth-client` and rebuilt your app.' + +export const OauthClientReactNative = (NativeModule as null) || { + getRandomValues(_length: number): Uint8Array { + throw new Error(LINKING_ERROR) + }, + + /** + * @throws if the algorithm is not supported ("sha256" must be supported) + */ + digest(_bytes: Uint8Array, _algorithm: string): Uint8Array { + throw new Error(LINKING_ERROR) + }, + + /** + * Create a private JWK for the given algorithm. The JWK should have a "use" + * an does not need a "kid" property. + * + * @throws if the algorithm is not supported ("ES256" must be supported) + */ + generateJwk(_algo: string): Jwk { + throw new Error(LINKING_ERROR) + }, + + createJwt(_header: unknown, _payload: unknown, _jwk: unknown): Jwt { + throw new Error(LINKING_ERROR) + }, + + verifyJwt( + _token: Jwt, + _jwk: Jwk, + ): { + payload: Record + protectedHeader: Record + } { + throw new Error(LINKING_ERROR) + }, +} diff --git a/modules/expo-bluesky-oauth-client/src/react-native-crypto-implementation.ts b/modules/expo-bluesky-oauth-client/src/react-native-crypto-implementation.ts new file mode 100644 index 0000000000..757e5615a0 --- /dev/null +++ b/modules/expo-bluesky-oauth-client/src/react-native-crypto-implementation.ts @@ -0,0 +1,27 @@ +import {CryptoImplementaton, DigestAlgorithm, Key} from '@atproto/oauth-client' + +import {OauthClientReactNative} from './oauth-client-react-native' +import {ReactNativeKey} from './react-native-key' + +export class ReactNativeCryptoImplementation implements CryptoImplementaton { + async createKey(algs: string[]): Promise { + const bytes = await this.getRandomValues(12) + const kid = Array.from(bytes, byteToHex).join('') + return ReactNativeKey.generate(kid, algs) + } + + async getRandomValues(length: number): Promise { + return OauthClientReactNative.getRandomValues(length) + } + + async digest( + bytes: Uint8Array, + algorithm: DigestAlgorithm, + ): Promise { + return OauthClientReactNative.digest(bytes, algorithm.name) + } +} + +function byteToHex(b: number): string { + return b.toString(16).padStart(2, '0') +} diff --git a/modules/expo-bluesky-oauth-client/src/react-native-key.ts b/modules/expo-bluesky-oauth-client/src/react-native-key.ts new file mode 100644 index 0000000000..03d27db691 --- /dev/null +++ b/modules/expo-bluesky-oauth-client/src/react-native-key.ts @@ -0,0 +1,110 @@ +import { + jwkValidator, + Jwt, + JwtHeader, + jwtHeaderSchema, + JwtPayload, + jwtPayloadSchema, + Key, + VerifyOptions, + VerifyPayload, + VerifyResult, +} from '@atproto/jwk' + +import {OauthClientReactNative} from './oauth-client-react-native' + +export class ReactNativeKey extends Key { + static async generate(kid: string, allowedAlgos: string[]) { + for (const algo of allowedAlgos) { + try { + // Note: OauthClientReactNative.generatePrivateJwk should throw if it + // doesn't support the algorithm. + const jwk = await OauthClientReactNative.generateJwk(algo) + const use = jwk.use || 'sig' + return new ReactNativeKey(jwkValidator.parse({...jwk, use, kid})) + } catch { + // Ignore, try next one + } + } + + throw new Error('No supported algorithms') + } + + async createJwt(header: JwtHeader, payload: JwtPayload): Promise { + return OauthClientReactNative.createJwt(header, payload, this.jwk) + } + + async verifyJwt< + P extends VerifyPayload = JwtPayload, + C extends string = string, + >(token: Jwt, options?: VerifyOptions): Promise> { + const result = await OauthClientReactNative.verifyJwt(token, this.jwk) + + const payload = jwtPayloadSchema.parse(result.payload) + const protectedHeader = jwtHeaderSchema.parse(result.protectedHeader) + + if (options?.audience != null) { + const audience = Array.isArray(options.audience) + ? options.audience + : [options.audience] + if (!audience.includes(payload.aud)) { + throw new Error('Invalid audience') + } + } + + if (options?.issuer != null) { + const issuer = Array.isArray(options.issuer) + ? options.issuer + : [options.issuer] + if (!issuer.includes(payload.iss)) { + throw new Error('Invalid issuer') + } + } + + if (options?.subject != null && payload.sub !== options.subject) { + throw new Error('Invalid subject') + } + + if (options?.typ != null && protectedHeader.typ !== options.typ) { + throw new Error('Invalid type') + } + + if (options?.requiredClaims != null) { + for (const key of options.requiredClaims) { + if ( + !Object.hasOwn(payload, key) || + (payload as Record)[key] === undefined + ) { + throw new Error(`Missing claim: ${key}`) + } + } + } + + if (payload.iat == null) { + throw new Error('Missing issued at') + } + + const now = (options?.currentDate?.getTime() ?? Date.now()) / 1e3 + const clockTolerance = options?.clockTolerance ?? 0 + + if (options?.maxTokenAge != null) { + if (payload.iat < now - options.maxTokenAge + clockTolerance) { + throw new Error('Invalid issued at') + } + } + + if (payload.nbf != null) { + if (payload.nbf > now - clockTolerance) { + throw new Error('Invalid not before') + } + } + + if (payload.exp != null) { + if (payload.exp < now + clockTolerance) { + throw new Error('Invalid expiration') + } + } + + return {payload, protectedHeader} as VerifyResult + } +} diff --git a/modules/expo-bluesky-oauth-client/src/react-native-store-with-key.ts b/modules/expo-bluesky-oauth-client/src/react-native-store-with-key.ts new file mode 100644 index 0000000000..b65e9a46ed --- /dev/null +++ b/modules/expo-bluesky-oauth-client/src/react-native-store-with-key.ts @@ -0,0 +1,51 @@ +import {GenericStore, Value} from '@atproto/caching' +import {Jwk} from '@atproto/jwk' + +import {ReactNativeKey} from './react-native-key.js' +import {ReactNativeStore} from './react-native-store.js' + +type ExposedValue = Value & {dpopKey: ReactNativeKey} +type StoredValue = Omit & { + dpopKey: Jwk +} + +/** + * Uses a {@link ReactNativeStore} to store values that contain a + * {@link ReactNativeKey} as `dpopKey` property. This works by serializing the + * {@link Key} to a JWK before storing it, and deserializing it back to a + * {@link ReactNativeKey} when retrieving the value. + */ +export class ReactNativeStoreWithKey + implements GenericStore +{ + internalStore: ReactNativeStore> + + constructor( + protected valueExpiresAt: (value: StoredValue) => null | Date, + ) { + this.internalStore = new ReactNativeStore(valueExpiresAt) + } + + async set(key: string, value: V): Promise { + const {dpopKey, ...rest} = value + if (!dpopKey.privateJwk) throw new Error('dpopKey.privateJwk is required') + await this.internalStore.set(key, { + ...rest, + dpopKey: dpopKey.privateJwk, + }) + } + + async get(key: string): Promise { + const value = await this.internalStore.get(key) + if (!value) return undefined + + return { + ...value, + dpopKey: new ReactNativeKey(value.dpopKey), + } as V + } + + async del(key: string): Promise { + await this.internalStore.del(key) + } +} diff --git a/modules/expo-bluesky-oauth-client/src/react-native-store.ts b/modules/expo-bluesky-oauth-client/src/react-native-store.ts new file mode 100644 index 0000000000..518a72243d --- /dev/null +++ b/modules/expo-bluesky-oauth-client/src/react-native-store.ts @@ -0,0 +1,25 @@ +import {GenericStore, Value} from '@atproto/caching' +import Storage from '@react-native-async-storage/async-storage' + +export class ReactNativeStore + implements GenericStore +{ + constructor(protected valueExpiresAt: (value: V) => null | Date) { + throw new Error('Not implemented') + } + + async get(key: string): Promise { + const itemJson = await Storage.getItem(key) + if (itemJson == null) return undefined + + return JSON.parse(itemJson) as V + } + + async set(key: string, value: V): Promise { + await Storage.setItem(key, JSON.stringify(value)) + } + + async del(key: string): Promise { + await Storage.delete(key) + } +} From 71f1e44653cc44b56d92917aef8300c1d3e09545 Mon Sep 17 00:00:00 2001 From: Hailey Date: Sun, 14 Apr 2024 22:21:47 -0700 Subject: [PATCH 35/54] add jwt struct --- .../expo-bluesky-oauth-client/ios/JWK.swift | 56 ++++--- .../ios/JWTHeader.swift | 154 ++++++++++++++++++ 2 files changed, 185 insertions(+), 25 deletions(-) create mode 100644 modules/expo-bluesky-oauth-client/ios/JWTHeader.swift diff --git a/modules/expo-bluesky-oauth-client/ios/JWK.swift b/modules/expo-bluesky-oauth-client/ios/JWK.swift index 879d37a401..41d9522aa0 100644 --- a/modules/expo-bluesky-oauth-client/ios/JWK.swift +++ b/modules/expo-bluesky-oauth-client/ios/JWK.swift @@ -1,29 +1,35 @@ -struct JWK { - let kty: String - let use: String - let crv: String - let kid: String - let x: String - let y: String +import ExpoModulesCore + +struct JWK : Record { + @Field + var alg: String + @Field + var kty: String + @Field + var crv: String? + @Field + var x: String? + @Field + var y: String? + @Field + var e: String? + @Field + var n: String? + @Field var d: String? - let alg: String + @Field + var use: String? + @Field + var kid: String? - func toJson() -> String { - var dict: [String: Any] = [ - "kty": kty, - "use": use, - "crv": crv, - "kid": kid, - "x": x, - "y": y, - "alg": alg, - ] - - if let d = d { - dict["d"] = d - } - - let jsonData = try! JSONSerialization.data(withJSONObject: dict, options: []) - return String(data: jsonData, encoding: .utf8)! + func toField() -> Field { + return Field(wrappedValue: self) } } + +struct JWKPair : Record { + @Field + var privateKey: JWK + @Field + var publicKey: JWK +} diff --git a/modules/expo-bluesky-oauth-client/ios/JWTHeader.swift b/modules/expo-bluesky-oauth-client/ios/JWTHeader.swift new file mode 100644 index 0000000000..c80ce57f1e --- /dev/null +++ b/modules/expo-bluesky-oauth-client/ios/JWTHeader.swift @@ -0,0 +1,154 @@ +import ExpoModulesCore + +struct JWTHeader : Record { + @Field + var alg: String = "ES256" + @Field + var jku: String? + @Field + var jwk: JWK + @Field + var kid: String? + @Field + var x5u: String? + @Field + var x5c: String? + @Field + var x5t: String? + @Field + var typ: String? + @Field + var cty: String? + @Field + var crit: String? +} + +struct JWTPayload : Record { + @Field + var iss: String? + @Field + var aud: String? + @Field + var sub: String? + @Field + var exp: Int? + @Field + var nbr: Int? + @Field + var iat: Int? + @Field + var jti: String? + @Field + var htm: String? + @Field + var htu: String? + @Field + var ath: String? + @Field + var acr: String? + @Field + var azp: String? + @Field + var amr: String? + @Field + var cnf: JWTPayloadCNF? + @Field + var client_id: String? + @Field + var scope: String? + @Field + var nonce: String? + @Field + var at_hash: String? + @Field + var c_hash: String? + @Field + var s_hash: String? + @Field + var auth_time: Int? + @Field + var name: String? + @Field + var family_name: String? + @Field + var given_name: String? + @Field + var middle_name: String? + @Field + var nickname: String? + @Field + var preferred_username: String? + @Field + var gender: String? + @Field + var picture: String? + @Field + var profile: String? + @Field + var website: String? + @Field + var birthdate: String? + @Field + var zoneinfo: String? + @Field + var locale: String? + @Field + var updated_at: Int? + @Field + var email: String? + @Field + var email_verified: String? + @Field + var phone_number: String? + @Field + var phone_number_verified: Bool? + @Field + var address: JWTPayloadAddress? + @Field + var authorization_details: JWTPayloadAuthorizationDetails? +} + +struct JWTPayloadCNF : Record { + @Field + var kid: String? + @Field + var jwk: JWK? + @Field + var jwe: String? + @Field + var jku: String? + @Field + var jkt: String? + @Field + var osc: String? +} + +struct JWTPayloadAddress : Record { + @Field + var formatted: String? + @Field + var street_address: String? + @Field + var locality: String? + @Field + var region: String? + @Field + var postal_code: String? + @Field + var country: String? +} + +struct JWTPayloadAuthorizationDetails : Record { + @Field + var type: String + @Field + var locations: [String]? + @Field + var actions: [String]? + @Field + var datatypes: [String]? + @Field + var identifier: String? + @Field + var privileges: [String]? +} From 2ada0cbd0dd07432b7d9c76917d4672ed908738b Mon Sep 17 00:00:00 2001 From: Hailey Date: Sun, 14 Apr 2024 23:27:27 -0700 Subject: [PATCH 36/54] better implementation --- modules/expo-bluesky-oauth-client/index.ts | 10 ++--- .../ios/CryptoUtil.swift | 37 ++++++++++++++++--- .../ios/ExpoBlueskyOAuthClientModule.swift | 20 +++++----- .../expo-bluesky-oauth-client/ios/JWK.swift | 12 ++++++ .../ios/{JWTHeader.swift => JWT.swift} | 31 +++++++++++++++- .../ios/JWTUtil.swift | 27 +++----------- .../src/oauth-client-react-native.ts | 18 +++++---- .../src/react-native-crypto-implementation.ts | 6 +-- .../src/react-native-key.ts | 7 +++- .../src/react-native-store-with-key.ts | 4 +- .../src/react-native-store.ts | 2 +- src/view/screens/Home.tsx | 24 +++++++++--- 12 files changed, 136 insertions(+), 62 deletions(-) rename modules/expo-bluesky-oauth-client/ios/{JWTHeader.swift => JWT.swift} (76%) diff --git a/modules/expo-bluesky-oauth-client/index.ts b/modules/expo-bluesky-oauth-client/index.ts index 05f53f9803..79e975ebc9 100644 --- a/modules/expo-bluesky-oauth-client/index.ts +++ b/modules/expo-bluesky-oauth-client/index.ts @@ -1,6 +1,4 @@ -export * from './src-old/crypto-subtle' -export * from './src-old/jose-key' -export * from './src-old/rn-crypto-key' -export * from './src-old/rn-oauth-client-factory' -export * from './src-old/rn-oauth-database' -export * from './src-old/util.web' +export * from './src/oauth-client-react-native' +export * from './src/react-native-crypto-implementation' +export * from './src/react-native-key' +export * from './src/react-native-store-with-key' diff --git a/modules/expo-bluesky-oauth-client/ios/CryptoUtil.swift b/modules/expo-bluesky-oauth-client/ios/CryptoUtil.swift index c9b95f81b9..fd20643f9a 100644 --- a/modules/expo-bluesky-oauth-client/ios/CryptoUtil.swift +++ b/modules/expo-bluesky-oauth-client/ios/CryptoUtil.swift @@ -1,5 +1,6 @@ import CryptoKit import JOSESwift +import ExpoModulesCore class CryptoUtil { // The equivalent of crypto.subtle.digest() with JS on web @@ -13,8 +14,8 @@ class CryptoUtil { return Data(bytes) } - public static func generateKeyPair(kid: String?) throws -> (publicJWK: JWK, privateJWK: JWK)? { - let keyIdString = kid ?? UUID().uuidString + public static func generateKeyPair() throws -> JWKPair? { + let keyIdString = UUID().uuidString let privateKey = P256.Signing.PrivateKey() let publicKey = privateKey.publicKey @@ -23,10 +24,27 @@ class CryptoUtil { let y = publicKey.x963Representation[33...].base64URLEncodedString() let d = privateKey.rawRepresentation.base64URLEncodedString() - let publicJWK = JWK(kty: "EC", use: "sig", crv: "P-256", kid: keyIdString, x: x, y: y, alg: "ES256") - let privateJWK = JWK(kty: "EC", use: "sig", crv: "P-256", kid: keyIdString, x: x, y: y, d: d, alg: "ES256") + let publicJWK = JWK( + alg: "ES256".toField(), + kty: "EC".toField(), + crv: "P-256".toNullableField(), + x: x.toNullableField(), + y: y.toNullableField(), + use: "sig".toNullableField(), + kid: keyIdString.toNullableField() + ) + let privateJWK = JWK( + alg: "ES256".toField(), + kty: "EC".toField(), + crv: "P-256".toNullableField(), + x: x.toNullableField(), + y: y.toNullableField(), + d: d.toNullableField(), + use: "sig".toNullableField(), + kid: keyIdString.toNullableField() + ) - return (publicJWK, privateJWK) + return JWKPair(privateKey: privateJWK.toField(), publicKey: publicJWK.toField()) } } @@ -35,3 +53,12 @@ extension Data { return self.base64EncodedString().replacingOccurrences(of: "+", with: "-").replacingOccurrences(of: "/", with: "_").replacingOccurrences(of: "=", with: "") } } + +extension String { + func toField() -> Field { + return Field(wrappedValue: self) + } + func toNullableField() -> Field { + return Field(wrappedValue: self) + } +} diff --git a/modules/expo-bluesky-oauth-client/ios/ExpoBlueskyOAuthClientModule.swift b/modules/expo-bluesky-oauth-client/ios/ExpoBlueskyOAuthClientModule.swift index 729194916a..510d60fdc6 100644 --- a/modules/expo-bluesky-oauth-client/ios/ExpoBlueskyOAuthClientModule.swift +++ b/modules/expo-bluesky-oauth-client/ios/ExpoBlueskyOAuthClientModule.swift @@ -14,22 +14,24 @@ public class ExpoBlueskyOAuthClientModule: Module { return CryptoUtil.getRandomValues(byteLength: byteLength) } - AsyncFunction ("generateKeyPair") { (kid: String?, promise: Promise) in - let keypair = try? CryptoUtil.generateKeyPair(kid: kid) + AsyncFunction ("generateJwk") { (algo: String?, promise: Promise) in + if algo != "ES256" { + promise.reject("GenerateKeyError", "Algorithim not supported.") + return + } + + let keypair = try? CryptoUtil.generateKeyPair() - guard let keypair = keypair else { + guard keypair != nil else { promise.reject("GenerateKeyError", "Error generating JWK.") return } - promise.resolve([ - "publicKey": keypair.publicJWK.toJson(), - "privateKey": keypair.privateJWK.toJson() - ]) + promise.resolve(keypair) } - AsyncFunction("createJwt") { (jwk: String, header: String, payload: String, promise: Promise) in - guard let jwt = JWTUtil.createJwt(jwk, header: header, payload: payload) else { + AsyncFunction("createJwt") { (header: JWTHeader, payload: JWTPayload, jwk: JWK, promise: Promise) in + guard let jwt = JWTUtil.createJwt(header: header, payload: payload, jwk: jwk) else { promise.reject("JWTError", "Error creating JWT.") return } diff --git a/modules/expo-bluesky-oauth-client/ios/JWK.swift b/modules/expo-bluesky-oauth-client/ios/JWK.swift index 41d9522aa0..e9d80fe3b4 100644 --- a/modules/expo-bluesky-oauth-client/ios/JWK.swift +++ b/modules/expo-bluesky-oauth-client/ios/JWK.swift @@ -1,4 +1,5 @@ import ExpoModulesCore +import JOSESwift struct JWK : Record { @Field @@ -25,6 +26,17 @@ struct JWK : Record { func toField() -> Field { return Field(wrappedValue: self) } + + func toSecKey() throws -> SecKey? { + let jsonData = try JSONSerialization.data(withJSONObject: self.toDictionary()) + guard let jwk = try? JSONDecoder().decode(ECPrivateKey.self, from: jsonData), + let key = try? jwk.converted(to: SecKey.self) + else { + print("Error creating SecKey.") + return nil + } + return key + } } struct JWKPair : Record { diff --git a/modules/expo-bluesky-oauth-client/ios/JWTHeader.swift b/modules/expo-bluesky-oauth-client/ios/JWT.swift similarity index 76% rename from modules/expo-bluesky-oauth-client/ios/JWTHeader.swift rename to modules/expo-bluesky-oauth-client/ios/JWT.swift index c80ce57f1e..b6c27a8c66 100644 --- a/modules/expo-bluesky-oauth-client/ios/JWTHeader.swift +++ b/modules/expo-bluesky-oauth-client/ios/JWT.swift @@ -1,4 +1,5 @@ import ExpoModulesCore +import JOSESwift struct JWTHeader : Record { @Field @@ -6,8 +7,6 @@ struct JWTHeader : Record { @Field var jku: String? @Field - var jwk: JWK - @Field var kid: String? @Field var x5u: String? @@ -21,6 +20,14 @@ struct JWTHeader : Record { var cty: String? @Field var crit: String? + + func toField() -> Field { + return Field(wrappedValue: self) + } + + func toJWSHeader() throws -> JWSHeader? { + return JWSHeader(try JSONSerialization.data(withJSONObject: self.toDictionary())) + } } struct JWTPayload : Record { @@ -106,6 +113,14 @@ struct JWTPayload : Record { var address: JWTPayloadAddress? @Field var authorization_details: JWTPayloadAuthorizationDetails? + + func toField() -> Field { + return Field(wrappedValue: self) + } + + func toPayload() throws -> Payload { + return Payload(try JSONSerialization.data(withJSONObject: self.toDictionary())) + } } struct JWTPayloadCNF : Record { @@ -121,6 +136,10 @@ struct JWTPayloadCNF : Record { var jkt: String? @Field var osc: String? + + func toField() -> Field { + return Field(wrappedValue: self) + } } struct JWTPayloadAddress : Record { @@ -136,6 +155,10 @@ struct JWTPayloadAddress : Record { var postal_code: String? @Field var country: String? + + func toField() -> Field { + return Field(wrappedValue: self) + } } struct JWTPayloadAuthorizationDetails : Record { @@ -151,4 +174,8 @@ struct JWTPayloadAuthorizationDetails : Record { var identifier: String? @Field var privileges: [String]? + + func toField() -> Field { + return Field(wrappedValue: self) + } } diff --git a/modules/expo-bluesky-oauth-client/ios/JWTUtil.swift b/modules/expo-bluesky-oauth-client/ios/JWTUtil.swift index d3dcdcee3d..fc10136c86 100644 --- a/modules/expo-bluesky-oauth-client/ios/JWTUtil.swift +++ b/modules/expo-bluesky-oauth-client/ios/JWTUtil.swift @@ -1,21 +1,6 @@ import JOSESwift class JWTUtil { - static func jsonToPrivateKey(_ jwkString: String) throws -> SecKey? { - guard let jsonData = jwkString.data(using: .utf8), - let jwk = try? JSONDecoder().decode(ECPrivateKey.self, from: jsonData), - let key = try? jwk.converted(to: SecKey.self) - else { - let jsonData = jwkString.data(using: .utf8)! - let jwk = try! JSONDecoder().decode(ECPrivateKey.self, from: jsonData) -// let key = try! jwk.converted(to: SecKey.self) - print("Error creating JWK from JWK string \(jwkString).") - return nil - } - - return key - } - static func jsonToPublicKey(_ jwkString: String) throws -> SecKey? { guard let jsonData = jwkString.data(using: .utf8), let jwk = try? JSONDecoder().decode(ECPublicKey.self, from: jsonData), @@ -46,12 +31,12 @@ class JWTUtil { return JWSHeader(headerData) } - public static func createJwt(_ jwkString: String, header headerString: String, payload payloadString: String) -> String? { - guard let key = try? jsonToPrivateKey(jwkString), - let payload = payloadStringToPayload(payloadString), - let header = headerStringToPayload(headerString) - else - { + public static func createJwt(header: JWTHeader, payload: JWTPayload, jwk: JWK) -> String? { + guard let header = try? header.toJWSHeader(), + let payload = try? payload.toPayload(), + let key = try? jwk.toSecKey() + else { + print("didn't have one") return nil } diff --git a/modules/expo-bluesky-oauth-client/src/oauth-client-react-native.ts b/modules/expo-bluesky-oauth-client/src/oauth-client-react-native.ts index 4ce738202e..7fd1889d44 100644 --- a/modules/expo-bluesky-oauth-client/src/oauth-client-react-native.ts +++ b/modules/expo-bluesky-oauth-client/src/oauth-client-react-native.ts @@ -1,5 +1,5 @@ import {requireNativeModule} from 'expo-modules-core' -import {Jwk, Jwt} from '@atproto/jwk' +import {Jwk, Jwt, Key} from '@atproto/jwk' const NativeModule = requireNativeModule('ExpoBlueskyOAuthClient') @@ -14,7 +14,7 @@ export const OauthClientReactNative = (NativeModule as null) || { /** * @throws if the algorithm is not supported ("sha256" must be supported) */ - digest(_bytes: Uint8Array, _algorithm: string): Uint8Array { + async digest(_bytes: Uint8Array, _algorithm: string): Promise { throw new Error(LINKING_ERROR) }, @@ -24,21 +24,25 @@ export const OauthClientReactNative = (NativeModule as null) || { * * @throws if the algorithm is not supported ("ES256" must be supported) */ - generateJwk(_algo: string): Jwk { + async generateJwk(_algo: string): Promise<{publicKey: Key; privateKey: Key}> { throw new Error(LINKING_ERROR) }, - createJwt(_header: unknown, _payload: unknown, _jwk: unknown): Jwt { + async createJwt( + _header: unknown, + _payload: unknown, + _jwk: unknown, + ): Promise { throw new Error(LINKING_ERROR) }, - verifyJwt( + async verifyJwt( _token: Jwt, _jwk: Jwk, - ): { + ): Promise<{ payload: Record protectedHeader: Record - } { + }> { throw new Error(LINKING_ERROR) }, } diff --git a/modules/expo-bluesky-oauth-client/src/react-native-crypto-implementation.ts b/modules/expo-bluesky-oauth-client/src/react-native-crypto-implementation.ts index 757e5615a0..26f58dc7b2 100644 --- a/modules/expo-bluesky-oauth-client/src/react-native-crypto-implementation.ts +++ b/modules/expo-bluesky-oauth-client/src/react-native-crypto-implementation.ts @@ -1,13 +1,13 @@ -import {CryptoImplementaton, DigestAlgorithm, Key} from '@atproto/oauth-client' +import {CryptoImplementation, DigestAlgorithm, Key} from '@atproto/oauth-client' import {OauthClientReactNative} from './oauth-client-react-native' import {ReactNativeKey} from './react-native-key' -export class ReactNativeCryptoImplementation implements CryptoImplementaton { +export class ReactNativeCryptoImplementation implements CryptoImplementation { async createKey(algs: string[]): Promise { const bytes = await this.getRandomValues(12) const kid = Array.from(bytes, byteToHex).join('') - return ReactNativeKey.generate(kid, algs) + return await ReactNativeKey.generate(kid, algs) } async getRandomValues(length: number): Promise { diff --git a/modules/expo-bluesky-oauth-client/src/react-native-key.ts b/modules/expo-bluesky-oauth-client/src/react-native-key.ts index 03d27db691..d4b8ef355c 100644 --- a/modules/expo-bluesky-oauth-client/src/react-native-key.ts +++ b/modules/expo-bluesky-oauth-client/src/react-native-key.ts @@ -19,7 +19,12 @@ export class ReactNativeKey extends Key { try { // Note: OauthClientReactNative.generatePrivateJwk should throw if it // doesn't support the algorithm. - const jwk = await OauthClientReactNative.generateJwk(algo) + const res = await OauthClientReactNative.generateJwk(algo) + const jwk = jwkValidator.parse({ + ...res.privateKey, + key_ops: ['sign', 'verify'], + kid, + }) const use = jwk.use || 'sig' return new ReactNativeKey(jwkValidator.parse({...jwk, use, kid})) } catch { diff --git a/modules/expo-bluesky-oauth-client/src/react-native-store-with-key.ts b/modules/expo-bluesky-oauth-client/src/react-native-store-with-key.ts index b65e9a46ed..435e566cf5 100644 --- a/modules/expo-bluesky-oauth-client/src/react-native-store-with-key.ts +++ b/modules/expo-bluesky-oauth-client/src/react-native-store-with-key.ts @@ -1,8 +1,8 @@ import {GenericStore, Value} from '@atproto/caching' import {Jwk} from '@atproto/jwk' -import {ReactNativeKey} from './react-native-key.js' -import {ReactNativeStore} from './react-native-store.js' +import {ReactNativeKey} from './react-native-key' +import {ReactNativeStore} from './react-native-store' type ExposedValue = Value & {dpopKey: ReactNativeKey} type StoredValue = Omit & { diff --git a/modules/expo-bluesky-oauth-client/src/react-native-store.ts b/modules/expo-bluesky-oauth-client/src/react-native-store.ts index 518a72243d..0a3d186d07 100644 --- a/modules/expo-bluesky-oauth-client/src/react-native-store.ts +++ b/modules/expo-bluesky-oauth-client/src/react-native-store.ts @@ -20,6 +20,6 @@ export class ReactNativeStore } async del(key: string): Promise { - await Storage.delete(key) + await Storage.removeItem(key) } } diff --git a/src/view/screens/Home.tsx b/src/view/screens/Home.tsx index a97f2d2986..b201db08a9 100644 --- a/src/view/screens/Home.tsx +++ b/src/view/screens/Home.tsx @@ -19,7 +19,7 @@ import {Pager, PagerRef, RenderTabBarFnProps} from 'view/com/pager/Pager' import {CustomFeedEmptyState} from 'view/com/posts/CustomFeedEmptyState' import {FollowingEmptyState} from 'view/com/posts/FollowingEmptyState' import {FollowingEndOfFeed} from 'view/com/posts/FollowingEndOfFeed' -import {RnCryptoKey} from '../../../modules/expo-bluesky-oauth-client' +import {ReactNativeKey} from '../../../modules/expo-bluesky-oauth-client' import {HomeLoggedOutCTA} from '../com/auth/HomeLoggedOutCTA' import {HomeHeader} from '../com/home/HomeHeader' @@ -54,13 +54,27 @@ function HomeScreenReady({ }) { React.useEffect(() => { ;(async () => { - const key = await RnCryptoKey.generate(undefined, ['ES256'], false) - console.log('public', key.publicJwk) + const key = await ReactNativeKey.generate('test', ['ES256']) + console.log(key.privateJwk) + const jwt = await key.createJwt( - {alg: 'ES256', kid: key.kid}, - {sub: 'test'}, + { + alg: 'ES256', + kid: key.kid, + }, + { + sub: 'test', + }, ) + console.log(jwt) + + // console.log('public', key.publicJwk) + // const jwt = await key.createJwt( + // {alg: 'ES256', kid: key.kid}, + // {sub: 'test'}, + // ) + // console.log(jwt) })() }, []) From 2c1b3709afbf1486a8668cd4759ba18e05ff44ef Mon Sep 17 00:00:00 2001 From: Hailey Date: Mon, 15 Apr 2024 00:37:12 -0700 Subject: [PATCH 37/54] swift impl --- .../ios/ExpoBlueskyOAuthClientModule.swift | 4 +-- .../expo-bluesky-oauth-client/ios/JWK.swift | 13 ++++++- .../expo-bluesky-oauth-client/ios/JWT.swift | 15 ++++---- .../ios/JWTUtil.swift | 34 +++++++++++++++---- .../src/oauth-client-react-native.ts | 2 +- .../src/react-native-key.ts | 12 +++++-- src/view/screens/Home.tsx | 10 ++---- 7 files changed, 62 insertions(+), 28 deletions(-) diff --git a/modules/expo-bluesky-oauth-client/ios/ExpoBlueskyOAuthClientModule.swift b/modules/expo-bluesky-oauth-client/ios/ExpoBlueskyOAuthClientModule.swift index 510d60fdc6..ee0b4f5ce5 100644 --- a/modules/expo-bluesky-oauth-client/ios/ExpoBlueskyOAuthClientModule.swift +++ b/modules/expo-bluesky-oauth-client/ios/ExpoBlueskyOAuthClientModule.swift @@ -38,8 +38,8 @@ public class ExpoBlueskyOAuthClientModule: Module { promise.resolve(jwt) } - AsyncFunction("verifyJwt") { (jwk: String, token: String, options: String?, promise: Promise) in - promise.resolve(JWTUtil.verifyJwt(jwk, token: token, options: options)) + AsyncFunction("verifyJwt") { (token: String, jwk: JWK, promise: Promise) in + promise.resolve(JWTUtil.verifyJwt(token: token, jwk: jwk)) } } } diff --git a/modules/expo-bluesky-oauth-client/ios/JWK.swift b/modules/expo-bluesky-oauth-client/ios/JWK.swift index e9d80fe3b4..daa7206914 100644 --- a/modules/expo-bluesky-oauth-client/ios/JWK.swift +++ b/modules/expo-bluesky-oauth-client/ios/JWK.swift @@ -27,7 +27,7 @@ struct JWK : Record { return Field(wrappedValue: self) } - func toSecKey() throws -> SecKey? { + func toPrivateSecKey() throws -> SecKey? { let jsonData = try JSONSerialization.data(withJSONObject: self.toDictionary()) guard let jwk = try? JSONDecoder().decode(ECPrivateKey.self, from: jsonData), let key = try? jwk.converted(to: SecKey.self) @@ -37,6 +37,17 @@ struct JWK : Record { } return key } + + func toPublicSecKey() throws -> SecKey? { + let jsonData = try JSONSerialization.data(withJSONObject: self.toDictionary()) + guard let jwk = try? JSONDecoder().decode(ECPublicKey.self, from: jsonData), + let key = try? jwk.converted(to: SecKey.self) + else { + print("Error creating SecKey.") + return nil + } + return key + } } struct JWKPair : Record { diff --git a/modules/expo-bluesky-oauth-client/ios/JWT.swift b/modules/expo-bluesky-oauth-client/ios/JWT.swift index b6c27a8c66..dc3dbf9f54 100644 --- a/modules/expo-bluesky-oauth-client/ios/JWT.swift +++ b/modules/expo-bluesky-oauth-client/ios/JWT.swift @@ -7,13 +7,9 @@ struct JWTHeader : Record { @Field var jku: String? @Field - var kid: String? - @Field - var x5u: String? + var jwk: JWK @Field - var x5c: String? - @Field - var x5t: String? + var kid: String? @Field var typ: String? @Field @@ -179,3 +175,10 @@ struct JWTPayloadAuthorizationDetails : Record { return Field(wrappedValue: self) } } + +struct JWTVerifyResponse : Record { + @Field + var protectedHeader: JWTHeader + @Field + var payload: String +} diff --git a/modules/expo-bluesky-oauth-client/ios/JWTUtil.swift b/modules/expo-bluesky-oauth-client/ios/JWTUtil.swift index fc10136c86..f14d2a113d 100644 --- a/modules/expo-bluesky-oauth-client/ios/JWTUtil.swift +++ b/modules/expo-bluesky-oauth-client/ios/JWTUtil.swift @@ -1,3 +1,4 @@ +import ExpoModulesCore import JOSESwift class JWTUtil { @@ -34,7 +35,7 @@ class JWTUtil { public static func createJwt(header: JWTHeader, payload: JWTPayload, jwk: JWK) -> String? { guard let header = try? header.toJWSHeader(), let payload = try? payload.toPayload(), - let key = try? jwk.toSecKey() + let key = try? jwk.toPrivateSecKey() else { print("didn't have one") return nil @@ -52,15 +53,34 @@ class JWTUtil { return jws.compactSerializedString } - public static func verifyJwt(_ jwkString: String, token tokenString: String, options optionsString: String?) -> Bool { - guard let key = try? jsonToPublicKey(jwkString), - let jws = try? JWS(compactSerialization: tokenString), + public static func verifyJwt(token: String, jwk: JWK) -> JWTVerifyResponse? { + guard let key = try? jwk.toPublicSecKey(), + let jws = try? JWS(compactSerialization: token), let verifier = Verifier(verifyingAlgorithm: .ES256, key: key), - let isVerified = try? jws.validate(using: verifier).isValid(for: verifier) + let validation = try? jws.validate(using: verifier) else { - return false + return nil + } + + let header = validation.header + let serializedHeader = JWTHeader( + alg: "ES256", + jku: Field(wrappedValue: header.jku?.absoluteString), + kid: Field(wrappedValue:header.kid), + typ: Field(wrappedValue: header.typ), + cty: Field(wrappedValue: header.cty), + crit: Field(wrappedValue: header.cty) + ) + + let payload = String(data: validation.payload.data(), encoding: .utf8) + + guard let payload = payload else { + return nil } - return isVerified + return JWTVerifyResponse( + protectedHeader: serializedHeader.toField(), + payload: payload.toField() + ) } } diff --git a/modules/expo-bluesky-oauth-client/src/oauth-client-react-native.ts b/modules/expo-bluesky-oauth-client/src/oauth-client-react-native.ts index 7fd1889d44..2816b7055a 100644 --- a/modules/expo-bluesky-oauth-client/src/oauth-client-react-native.ts +++ b/modules/expo-bluesky-oauth-client/src/oauth-client-react-native.ts @@ -40,7 +40,7 @@ export const OauthClientReactNative = (NativeModule as null) || { _token: Jwt, _jwk: Jwk, ): Promise<{ - payload: Record + payload: string // this is a JSON response to make Swift a bit easier to work with protectedHeader: Record }> { throw new Error(LINKING_ERROR) diff --git a/modules/expo-bluesky-oauth-client/src/react-native-key.ts b/modules/expo-bluesky-oauth-client/src/react-native-key.ts index d4b8ef355c..5b769d348d 100644 --- a/modules/expo-bluesky-oauth-client/src/react-native-key.ts +++ b/modules/expo-bluesky-oauth-client/src/react-native-key.ts @@ -2,7 +2,6 @@ import { jwkValidator, Jwt, JwtHeader, - jwtHeaderSchema, JwtPayload, jwtPayloadSchema, Key, @@ -45,8 +44,13 @@ export class ReactNativeKey extends Key { >(token: Jwt, options?: VerifyOptions): Promise> { const result = await OauthClientReactNative.verifyJwt(token, this.jwk) - const payload = jwtPayloadSchema.parse(result.payload) - const protectedHeader = jwtHeaderSchema.parse(result.protectedHeader) + let payloadParsed = JSON.parse(result.payload) + payloadParsed = Object.fromEntries( + Object.entries(payloadParsed as object).filter(([_, v]) => v !== null), + ) + + const payload = jwtPayloadSchema.parse(payloadParsed) + const protectedHeader = result.protectedHeader if (options?.audience != null) { const audience = Array.isArray(options.audience) @@ -85,6 +89,8 @@ export class ReactNativeKey extends Key { } } + console.log(payload) + if (payload.iat == null) { throw new Error('Missing issued at') } diff --git a/src/view/screens/Home.tsx b/src/view/screens/Home.tsx index b201db08a9..f50d9a8fca 100644 --- a/src/view/screens/Home.tsx +++ b/src/view/screens/Home.tsx @@ -64,17 +64,11 @@ function HomeScreenReady({ }, { sub: 'test', + iat: Math.floor(Date.now() / 1000), }, ) - console.log(jwt) - - // console.log('public', key.publicJwk) - // const jwt = await key.createJwt( - // {alg: 'ES256', kid: key.kid}, - // {sub: 'test'}, - // ) - // console.log(jwt) + const verified = await key.verifyJwt(jwt) })() }, []) From 9f6db0ef679cd8fffedd0b70e08e7d975b72ada1 Mon Sep 17 00:00:00 2001 From: Hailey Date: Mon, 15 Apr 2024 00:42:22 -0700 Subject: [PATCH 38/54] few fixes --- .../expo-bluesky-oauth-client/src/react-native-key.ts | 11 +++++------ src/view/screens/Home.tsx | 2 ++ 2 files changed, 7 insertions(+), 6 deletions(-) diff --git a/modules/expo-bluesky-oauth-client/src/react-native-key.ts b/modules/expo-bluesky-oauth-client/src/react-native-key.ts index 5b769d348d..6d93b0cfdb 100644 --- a/modules/expo-bluesky-oauth-client/src/react-native-key.ts +++ b/modules/expo-bluesky-oauth-client/src/react-native-key.ts @@ -19,11 +19,7 @@ export class ReactNativeKey extends Key { // Note: OauthClientReactNative.generatePrivateJwk should throw if it // doesn't support the algorithm. const res = await OauthClientReactNative.generateJwk(algo) - const jwk = jwkValidator.parse({ - ...res.privateKey, - key_ops: ['sign', 'verify'], - kid, - }) + const jwk = res.privateKey const use = jwk.use || 'sig' return new ReactNativeKey(jwkValidator.parse({...jwk, use, kid})) } catch { @@ -44,12 +40,15 @@ export class ReactNativeKey extends Key { >(token: Jwt, options?: VerifyOptions): Promise> { const result = await OauthClientReactNative.verifyJwt(token, this.jwk) + // TODO see if we can make these `undefined` or maybe update zod to allow `nullable()` let payloadParsed = JSON.parse(result.payload) payloadParsed = Object.fromEntries( Object.entries(payloadParsed as object).filter(([_, v]) => v !== null), ) - const payload = jwtPayloadSchema.parse(payloadParsed) + + // We don't need to validate this, because the native types ensure it is correct. But this is a TODO + // for the same reason above const protectedHeader = result.protectedHeader if (options?.audience != null) { diff --git a/src/view/screens/Home.tsx b/src/view/screens/Home.tsx index f50d9a8fca..bb4e9e1aa8 100644 --- a/src/view/screens/Home.tsx +++ b/src/view/screens/Home.tsx @@ -69,6 +69,8 @@ function HomeScreenReady({ ) const verified = await key.verifyJwt(jwt) + + console.log(verified) })() }, []) From f4a236285022f8738574454ea959ce3fe41a038b Mon Sep 17 00:00:00 2001 From: Hailey Date: Mon, 15 Apr 2024 00:44:10 -0700 Subject: [PATCH 39/54] rm old files now that we have the skeleton ready --- .../src-old/crypto-subtle.ts | 24 -- .../src-old/crypto-subtle.web.ts | 48 ---- .../src-old/jose-key.ts | 106 ------- .../src-old/native-types.ts | 20 -- .../src-old/rn-crypto-key.ts | 86 ------ .../src-old/rn-crypto-key.web.ts | 78 ----- .../src-old/rn-oauth-client-factory.native.ts | 147 ---------- .../src-old/rn-oauth-client-factory.ts | 153 ---------- .../src-old/rn-oauth-database.native.ts | 214 -------------- .../src-old/rn-oauth-database.ts | 269 ------------------ .../src-old/store.ts | 0 .../src-old/store.web.ts | 0 .../src-old/util.web.ts | 41 --- 13 files changed, 1186 deletions(-) delete mode 100644 modules/expo-bluesky-oauth-client/src-old/crypto-subtle.ts delete mode 100644 modules/expo-bluesky-oauth-client/src-old/crypto-subtle.web.ts delete mode 100644 modules/expo-bluesky-oauth-client/src-old/jose-key.ts delete mode 100644 modules/expo-bluesky-oauth-client/src-old/native-types.ts delete mode 100644 modules/expo-bluesky-oauth-client/src-old/rn-crypto-key.ts delete mode 100644 modules/expo-bluesky-oauth-client/src-old/rn-crypto-key.web.ts delete mode 100644 modules/expo-bluesky-oauth-client/src-old/rn-oauth-client-factory.native.ts delete mode 100644 modules/expo-bluesky-oauth-client/src-old/rn-oauth-client-factory.ts delete mode 100644 modules/expo-bluesky-oauth-client/src-old/rn-oauth-database.native.ts delete mode 100644 modules/expo-bluesky-oauth-client/src-old/rn-oauth-database.ts delete mode 100644 modules/expo-bluesky-oauth-client/src-old/store.ts delete mode 100644 modules/expo-bluesky-oauth-client/src-old/store.web.ts delete mode 100644 modules/expo-bluesky-oauth-client/src-old/util.web.ts diff --git a/modules/expo-bluesky-oauth-client/src-old/crypto-subtle.ts b/modules/expo-bluesky-oauth-client/src-old/crypto-subtle.ts deleted file mode 100644 index c4375015a7..0000000000 --- a/modules/expo-bluesky-oauth-client/src-old/crypto-subtle.ts +++ /dev/null @@ -1,24 +0,0 @@ -import {requireNativeModule} from 'expo-modules-core' -import {CryptoImplementation, Key} from '@atproto/oauth-client' - -import {RnCryptoKey} from './rn-crypto-key' - -// It loads the native module object from the JSI or falls back to -// the bridge module (from NativeModulesProxy) if the remote debugger is on. -const NativeModule = requireNativeModule('ExpoBlueskyOAuthClient') - -export class CryptoSubtle implements CryptoImplementation { - // We won't use the `algos` parameter here, as we will always use `ES256`. - // eslint-disable-next-line @typescript-eslint/no-unused-vars - async createKey(algos: string[] = ['ES256']): Promise { - return await RnCryptoKey.generate(undefined, ['ES256']) - } - - getRandomValues(byteLength: number): Uint8Array { - return NativeModule.getRandomValues(byteLength) - } - - async digest(bytes: Uint8Array): Promise { - return await NativeModule.digest(bytes) - } -} diff --git a/modules/expo-bluesky-oauth-client/src-old/crypto-subtle.web.ts b/modules/expo-bluesky-oauth-client/src-old/crypto-subtle.web.ts deleted file mode 100644 index 14936f65df..0000000000 --- a/modules/expo-bluesky-oauth-client/src-old/crypto-subtle.web.ts +++ /dev/null @@ -1,48 +0,0 @@ -import {WebcryptoKey} from '@atproto/jwk-webcrypto' -import {CryptoImplementation, DigestAlgorithm, Key} from '@atproto/oauth-client' - -// @ts-ignore web only, this silences some warnings -const crypto = global.crypto - -export class CryptoSubtle implements CryptoImplementation { - constructor(_: any) { - if (!crypto?.subtle) { - throw new Error( - 'Crypto with CryptoSubtle is required. If running in a browser, make sure the current page is loaded over HTTPS.', - ) - } - } - - async createKey(algs: string[]): Promise { - return WebcryptoKey.generate(undefined, algs) - } - - getRandomValues(byteLength: number): Uint8Array { - const bytes = new Uint8Array(byteLength) - crypto.getRandomValues(bytes) - return bytes - } - - async digest( - bytes: Uint8Array, - algorithm: DigestAlgorithm, - ): Promise { - const buffer = await crypto.subtle.digest( - digestAlgorithmToSubtle(algorithm), - bytes, - ) - return new Uint8Array(buffer) - } -} - -// @ts-ignore web only type -function digestAlgorithmToSubtle({name}: DigestAlgorithm): AlgorithmIdentifier { - switch (name) { - case 'sha256': - case 'sha384': - case 'sha512': - return `SHA-${name.slice(-3)}` - default: - throw new Error(`Unknown hash algorithm ${name}`) - } -} diff --git a/modules/expo-bluesky-oauth-client/src-old/jose-key.ts b/modules/expo-bluesky-oauth-client/src-old/jose-key.ts deleted file mode 100644 index fcf401e1dc..0000000000 --- a/modules/expo-bluesky-oauth-client/src-old/jose-key.ts +++ /dev/null @@ -1,106 +0,0 @@ -import {requireNativeModule} from 'expo-modules-core' -import {jwkSchema} from '@atproto/jwk' -import {Key} from '@atproto/jwk' -import { - exportJWK, - importJWK, - importPKCS8, - JWK, - KeyLike, - VerifyOptions, -} from 'jose' -import {JwtHeader, JwtPayload} from 'jwt-decode' - -const NativeModule = requireNativeModule('ExpoBlueskyOAuthClient') - -export class JoseKey extends Key { - #keyObj?: KeyLike | Uint8Array - - protected async getKey() { - return (this.#keyObj ||= await importJWK(this.jwk as JWK)) - } - - async createJwt(header: JwtHeader, payload: JwtPayload): Promise { - if (header.kid && header.kid !== this.kid) { - throw new TypeError( - `Invalid "kid" (${header.kid}) used to sign with key "${this.kid}"`, - ) - } - - if (!header.alg || !this.algorithms.includes(header.alg)) { - throw new TypeError( - `Invalid "alg" (${header.alg}) used to sign with key "${this.kid}"`, - ) - } - - return await NativeModule.createJwt( - JSON.stringify(this.privateJwk), - JSON.stringify(header), - JSON.stringify(payload), - ) - } - - async verifyJwt< - P extends VerifyPayload = JwtPayload, - C extends string = string, - >(token: Jwt, options?: VerifyOptions): Promise> { - const result = await NativeModule.verifyJwt( - JSON.stringify(this.publicJwk), - token, - JSON.stringify(options), - ) - return result - // return result as VerifyResult - } - - static async fromImportable( - input: Importable, - kid?: string, - ): Promise { - if (typeof input === 'string') { - // PKCS8 - if (input.startsWith('-----')) { - return this.fromPKCS8(input, kid) - } - - // Jwk (string) - if (input.startsWith('{')) { - return this.fromJWK(input, kid) - } - - throw new TypeError('Invalid input') - } - - if (typeof input === 'object') { - // Jwk - if ('kty' in input || 'alg' in input) { - return this.fromJWK(input, kid) - } - - // KeyLike - return this.fromJWK(await exportJWK(input), kid) - } - - throw new TypeError('Invalid input') - } - - static async fromPKCS8(pem: string, kid?: string): Promise { - const keyLike = await importPKCS8(pem, '', {extractable: true}) - return this.fromJWK(await exportJWK(keyLike), kid) - } - - static async fromJWK( - input: string | Record, - inputKid?: string, - ): Promise { - const jwk = jwkSchema.parse( - typeof input === 'string' ? JSON.parse(input) : input, - ) - - const kid = either(jwk.kid, inputKid) - const alg = jwk.alg - const use = jwk.use || 'sig' - - return new JoseKey({...jwk, kid, alg, use}) - } -} diff --git a/modules/expo-bluesky-oauth-client/src-old/native-types.ts b/modules/expo-bluesky-oauth-client/src-old/native-types.ts deleted file mode 100644 index 2ac1d7fa40..0000000000 --- a/modules/expo-bluesky-oauth-client/src-old/native-types.ts +++ /dev/null @@ -1,20 +0,0 @@ -export interface CryptoKey { - algorithm: { - name: 'ECDSA' - namedCurve: 'P-256' - } - extractable: boolean - type: 'public' | 'private' - usages: ('sign' | 'verify')[] -} - -export interface NativeJWKKey { - crv: 'P-256' - ext: boolean - kty: 'EC' - x: string - y: string - use: 'sig' - alg: 'ES256' - kid: string -} diff --git a/modules/expo-bluesky-oauth-client/src-old/rn-crypto-key.ts b/modules/expo-bluesky-oauth-client/src-old/rn-crypto-key.ts deleted file mode 100644 index d5e4360889..0000000000 --- a/modules/expo-bluesky-oauth-client/src-old/rn-crypto-key.ts +++ /dev/null @@ -1,86 +0,0 @@ -import {requireNativeModule} from 'expo-modules-core' -import {Jwk, jwkSchema} from '@atproto/jwk' - -import {JoseKey} from './jose-key' -import {CryptoKey, NativeJWKKey} from './native-types' - -const NativeModule = requireNativeModule('ExpoBlueskyOAuthClient') - -interface CryptoKeyPair { - privateKey: CryptoKey - publicKey: CryptoKey -} - -interface NativeJWKKeyPair { - privateKey: NativeJWKKey - publicKey: NativeJWKKey -} - -export class RnCryptoKey extends JoseKey { - static async generate( - kid: string | undefined, - _: string[] = ['ES256'], - __ = false, - ) { - const {privateKey, publicKey} = await NativeModule.generateKeyPair(kid) - - const nativeKeyPair = { - privateKey: JSON.parse(privateKey), - publicKey: JSON.parse(publicKey), - } - - return this.fromKeypair(nativeKeyPair.privateKey.kid, nativeKeyPair) - } - - static fromKeypair(kid: string, cryptoKeyPair: NativeJWKKeyPair) { - const use = cryptoKeyPair.privateKey.use ?? 'sig' - const alg = cryptoKeyPair.privateKey.alg ?? 'ES256' - - if (use !== 'sig') { - throw new TypeError('Unsupported JWK use') - } - - const webCryptoKeyPair: CryptoKeyPair = { - privateKey: { - algorithm: { - name: 'ECDSA', - namedCurve: 'P-256', - }, - extractable: true, - type: 'private', - usages: ['sign'], - }, - publicKey: { - algorithm: { - name: 'ECDSA', - namedCurve: 'P-256', - }, - extractable: true, - type: 'public', - usages: ['verify'], - }, - } - - return new RnCryptoKey( - jwkSchema.parse({...cryptoKeyPair.privateKey, use, kid, alg}), - webCryptoKeyPair, - ) - } - - constructor(jwk: Jwk, readonly cryptoKeyPair: CryptoKeyPair) { - super(jwk) - } - - get isPrivate() { - return true - } - - get privateJwk(): Jwk | undefined { - if (super.isPrivate) return this.jwk - throw new Error('Private key is not exportable.') - } - - protected async getKey() { - return this.cryptoKeyPair.privateKey - } -} diff --git a/modules/expo-bluesky-oauth-client/src-old/rn-crypto-key.web.ts b/modules/expo-bluesky-oauth-client/src-old/rn-crypto-key.web.ts deleted file mode 100644 index b9fbd05758..0000000000 --- a/modules/expo-bluesky-oauth-client/src-old/rn-crypto-key.web.ts +++ /dev/null @@ -1,78 +0,0 @@ -import {Jwk, jwkSchema} from '@atproto/jwk' -import {JoseKey} from '@atproto/jwk-jose' - -import {CryptoKey} from './native-types' -import {generateKeyPair, isSignatureKeyPair} from './util.web' - -// @ts-ignore web only, stops warnings for crypto being missing -const crypto = global.crypto - -// Global has this, but this stops the warnings -interface CryptoKeyPair { - privateKey: CryptoKey - publicKey: CryptoKey -} - -export class RNCryptoKey extends JoseKey { - static async generate( - kid: string = crypto.randomUUID(), - allowedAlgos: string[] = ['ES256'], - exportable = false, - ) { - const cryptoKeyPair: CryptoKeyPair = await generateKeyPair( - allowedAlgos, - exportable, - ) - return this.fromKeypair(kid, cryptoKeyPair) - } - - static async fromKeypair( - kid: string, - cryptoKeyPair: CryptoKeyPair, - ): Promise { - if (!isSignatureKeyPair(cryptoKeyPair)) { - throw new TypeError('CryptoKeyPair must be compatible with sign/verify') - } - - // https://datatracker.ietf.org/doc/html/rfc7517 - // > The "use" and "key_ops" JWK members SHOULD NOT be used together; [...] - // > Applications should specify which of these members they use. - - // eslint-disable-next-line @typescript-eslint/no-unused-vars - const {key_ops: _, ...jwk} = await crypto.subtle.exportKey( - 'jwk', - cryptoKeyPair.privateKey.extractable - ? cryptoKeyPair.privateKey - : cryptoKeyPair.publicKey, - ) - - const use = jwk.use ?? 'sig' - const alg = jwk.alg ?? 'ES256' - - if (use !== 'sig') { - throw new TypeError('Unsupported JWK use') - } - - return new RNCryptoKey( - jwkSchema.parse({...jwk, use, kid, alg}), - cryptoKeyPair, - ) - } - - constructor(jwk: Jwk, readonly cryptoKeyPair: CryptoKeyPair) { - super(jwk) - } - - get isPrivate() { - return true - } - - get privateJwk(): Jwk | undefined { - if (super.isPrivate) return this.jwk - throw new Error('Private key is not exportable.') - } - - protected async getKey() { - return this.cryptoKeyPair.privateKey - } -} diff --git a/modules/expo-bluesky-oauth-client/src-old/rn-oauth-client-factory.native.ts b/modules/expo-bluesky-oauth-client/src-old/rn-oauth-client-factory.native.ts deleted file mode 100644 index 81713e140c..0000000000 --- a/modules/expo-bluesky-oauth-client/src-old/rn-oauth-client-factory.native.ts +++ /dev/null @@ -1,147 +0,0 @@ -import {Fetch} from '@atproto/fetch' -import {UniversalIdentityResolver} from '@atproto/identity-resolver' -import { - OAuthAuthorizeOptions, - OAuthClientFactory, - OAuthResponseMode, - OAuthResponseType, - Session, -} from '@atproto/oauth-client' -import {OAuthClientMetadata} from '@atproto/oauth-client-metadata' -import IsomorphicOAuthServerMetadataResolver from '@atproto/oauth-server-metadata-resolver' - -import {CryptoSubtle} from './crypto-subtle' -import {DatabaseStore, RNOAuthDatabase} from './rn-oauth-database' - -export type RNOAuthClientOptions = { - responseMode?: OAuthResponseMode - responseType?: OAuthResponseType - clientMetadata: OAuthClientMetadata - fetch?: Fetch - crypto?: Crypto -} - -const POPUP_KEY_PREFIX = '@@oauth-popup-callback:' - -export class RNOAuthClientFactory extends OAuthClientFactory { - readonly sessionStore: DatabaseStore - - constructor({ - clientMetadata, - // "fragment" is safer as it is not sent to the server - responseMode = 'fragment', - responseType, - crypto = {subtle: CryptoSubtle}, - fetch = globalThis.fetch, - }: RNOAuthClientOptions) { - const database = new RNOAuthDatabase() - - super({ - clientMetadata, - responseMode, - responseType, - fetch, - cryptoImplementation: new CryptoSubtle(crypto), - sessionStore: database.getSessionStore(), - stateStore: database.getStateStore(), - metadataResolver: new IsomorphicOAuthServerMetadataResolver({ - fetch, - cache: database.getMetadataCache(), - }), - identityResolver: UniversalIdentityResolver.from({ - fetch, - didCache: database.getDidCache(), - handleCache: database.getHandleCache(), - }), - dpopNonceCache: database.getDpopNonceCache(), - }) - - this.sessionStore = database.getSessionStore() - } - - async restoreAll() { - const sessionIds = await this.sessionStore.getKeys() - return Object.fromEntries( - await Promise.all( - sessionIds.map( - async sessionId => - [sessionId, await this.restore(sessionId, false)] as const, - ), - ), - ) - } - - async init(sessionId?: string, forceRefresh = false) { - const signInResult = await this.signInCallback() - if (signInResult) { - return signInResult - } else if (sessionId) { - const client = await this.restore(sessionId, forceRefresh) - return {client} - } else { - // TODO: we could restore any session from the store ? - } - } - - async signIn(input: string, options?: OAuthAuthorizeOptions) { - return await this.authorize(input, options) - } - - async signInCallback() { - const redirectUri = new URL(this.clientMetadata.redirect_uris[0]) - if (location.pathname !== redirectUri.pathname) return null - - const params = - this.responseMode === 'query' - ? new URLSearchParams(location.search) - : new URLSearchParams(location.hash.slice(1)) - - // Only if the query string contains oauth callback params - if ( - !params.has('iss') || - !params.has('state') || - !(params.has('code') || params.has('error')) - ) { - return null - } - - // Replace the current history entry without the query string (this will - // prevent this 'if' branch to run again if the user refreshes the page) - history.replaceState(null, '', location.pathname) - - return this.callback(params) - .then(async result => { - if (result.state?.startsWith(POPUP_KEY_PREFIX)) { - const stateKey = result.state.slice(POPUP_KEY_PREFIX.length) - - await this.popupStore.set(stateKey, { - status: 'fulfilled', - value: result.client.sessionId, - }) - - window.close() // continued in signInPopup - throw new Error('Login complete, please close the popup window.') - } - - return result - }) - .catch(async err => { - // TODO: Throw a proper error from parent class to actually detect - // oauth authorization errors - const state = typeof (err as any)?.state - if (typeof state === 'string' && state?.startsWith(POPUP_KEY_PREFIX)) { - const stateKey = state.slice(POPUP_KEY_PREFIX.length) - - await this.popupStore.set(stateKey, { - status: 'rejected', - reason: err, - }) - - window.close() // continued in signInPopup - throw new Error('Login complete, please close the popup window.') - } - - throw err - }) - } -} diff --git a/modules/expo-bluesky-oauth-client/src-old/rn-oauth-client-factory.ts b/modules/expo-bluesky-oauth-client/src-old/rn-oauth-client-factory.ts deleted file mode 100644 index a8e2dc179e..0000000000 --- a/modules/expo-bluesky-oauth-client/src-old/rn-oauth-client-factory.ts +++ /dev/null @@ -1,153 +0,0 @@ -import {Fetch} from '@atproto/fetch' -import {UniversalIdentityResolver} from '@atproto/identity-resolver' -import { - OAuthAuthorizeOptions, - OAuthClientFactory, - OAuthResponseMode, - OAuthResponseType, - Session, -} from '@atproto/oauth-client' -import {OAuthClientMetadata} from '@atproto/oauth-client-metadata' -import IsomorphicOAuthServerMetadataResolver from '@atproto/oauth-server-metadata-resolver' - -import {CryptoSubtle} from './crypto-subtle' -import { - DatabaseStore, - PopupStateData, - RNOAuthDatabase, -} from './rn-oauth-database' - -export type RNOAuthClientOptions = { - responseMode?: OAuthResponseMode - responseType?: OAuthResponseType - clientMetadata: OAuthClientMetadata - fetch?: Fetch - crypto?: Crypto -} - -const POPUP_KEY_PREFIX = '@@oauth-popup-callback:' - -export class RNOAuthClientFactory extends OAuthClientFactory { - readonly popupStore: DatabaseStore - readonly sessionStore: DatabaseStore - - constructor({ - clientMetadata, - // "fragment" is safer as it is not sent to the server - responseMode = 'fragment', - responseType, - crypto = globalThis.crypto, - fetch = globalThis.fetch, - }: RNOAuthClientOptions) { - const database = new RNOAuthDatabase() - - super({ - clientMetadata, - responseMode, - responseType, - fetch, - cryptoImplementation: new CryptoSubtle(crypto), - sessionStore: database.getSessionStore(), - stateStore: database.getStateStore(), - metadataResolver: new IsomorphicOAuthServerMetadataResolver({ - fetch, - cache: database.getMetadataCache(), - }), - identityResolver: UniversalIdentityResolver.from({ - fetch, - didCache: database.getDidCache(), - handleCache: database.getHandleCache(), - }), - dpopNonceCache: database.getDpopNonceCache(), - }) - - this.sessionStore = database.getSessionStore() - this.popupStore = database.getPopupStore() - } - - async restoreAll() { - const sessionIds = await this.sessionStore.getKeys() - return Object.fromEntries( - await Promise.all( - sessionIds.map( - async sessionId => - [sessionId, await this.restore(sessionId, false)] as const, - ), - ), - ) - } - - async init(sessionId?: string, forceRefresh = false) { - const signInResult = await this.signInCallback() - if (signInResult) { - return signInResult - } else if (sessionId) { - const client = await this.restore(sessionId, forceRefresh) - return {client} - } else { - // TODO: we could restore any session from the store ? - } - } - - async signIn(input: string, options?: OAuthAuthorizeOptions) { - return await this.authorize(input, options) - } - - async signInCallback() { - const redirectUri = new URL(this.clientMetadata.redirect_uris[0]) - if (location.pathname !== redirectUri.pathname) return null - - const params = - this.responseMode === 'query' - ? new URLSearchParams(location.search) - : new URLSearchParams(location.hash.slice(1)) - - // Only if the query string contains oauth callback params - if ( - !params.has('iss') || - !params.has('state') || - !(params.has('code') || params.has('error')) - ) { - return null - } - - // Replace the current history entry without the query string (this will - // prevent this 'if' branch to run again if the user refreshes the page) - history.replaceState(null, '', location.pathname) - - return this.callback(params) - .then(async result => { - if (result.state?.startsWith(POPUP_KEY_PREFIX)) { - const stateKey = result.state.slice(POPUP_KEY_PREFIX.length) - - await this.popupStore.set(stateKey, { - status: 'fulfilled', - value: result.client.sessionId, - }) - - window.close() // continued in signInPopup - throw new Error('Login complete, please close the popup window.') - } - - return result - }) - .catch(async err => { - // TODO: Throw a proper error from parent class to actually detect - // oauth authorization errors - const state = typeof (err as any)?.state - if (typeof state === 'string' && state?.startsWith(POPUP_KEY_PREFIX)) { - const stateKey = state.slice(POPUP_KEY_PREFIX.length) - - await this.popupStore.set(stateKey, { - status: 'rejected', - reason: err, - }) - - window.close() // continued in signInPopup - throw new Error('Login complete, please close the popup window.') - } - - throw err - }) - } -} diff --git a/modules/expo-bluesky-oauth-client/src-old/rn-oauth-database.native.ts b/modules/expo-bluesky-oauth-client/src-old/rn-oauth-database.native.ts deleted file mode 100644 index 96a7ae74d7..0000000000 --- a/modules/expo-bluesky-oauth-client/src-old/rn-oauth-database.native.ts +++ /dev/null @@ -1,214 +0,0 @@ -import {GenericStore, Value} from '@atproto/caching' -import {DidDocument} from '@atproto/did' -import {ResolvedHandle} from '@atproto/handle-resolver' -import {Key} from '@atproto/jwk' -import {WebcryptoKey} from '@atproto/jwk-webcrypto' -import {InternalStateData, Session, TokenSet} from '@atproto/oauth-client' -import {OAuthServerMetadata} from '@atproto/oauth-server-metadata' -import Storage from '@react-native-async-storage/async-storage' - -type Item = { - value: string - expiresAt: null | Date -} - -type EncodedKey = { - keyId: string - keyPair: CryptoKeyPair -} - -function encodeKey(key: Key): EncodedKey { - if (!(key instanceof WebcryptoKey) || !key.kid) { - throw new Error('Invalid key object') - } - return { - keyId: key.kid, - keyPair: key.cryptoKeyPair, - } -} - -async function decodeKey(encoded: EncodedKey): Promise { - return WebcryptoKey.fromKeypair(encoded.keyId, encoded.keyPair) -} - -export type Schema = { - state: Item<{ - dpopKey: EncodedKey - - iss: string - nonce: string - verifier?: string - appState?: string - }> - session: Item<{ - dpopKey: EncodedKey - - tokenSet: TokenSet - }> - - didCache: Item - dpopNonceCache: Item - handleCache: Item - metadataCache: Item -} - -export type DatabaseStore = GenericStore & { - getKeys: () => Promise -} - -const STORES = [ - 'state', - 'session', - - 'didCache', - 'dpopNonceCache', - 'handleCache', - 'metadataCache', -] as const - -export class RNOAuthDatabase { - async delete(key: string) { - await Storage.removeItem(key) - } - - protected createStore( - dbName: N, - { - encode, - decode, - maxAge, - }: { - encode: (value: V) => Schema[N]['value'] | PromiseLike - decode: (encoded: Schema[N]['value']) => V | PromiseLike - maxAge?: number - }, - ): DatabaseStore { - return { - get: async key => { - const itemJson = await Storage.getItem(`${dbName}.${key}`) - if (itemJson == null) return undefined - - const item = JSON.parse(itemJson) as Schema[N] - - // Too old, proactively delete - if (item.expiresAt != null && item.expiresAt < new Date()) { - await this.delete(`${dbName}.${key}`) - return undefined - } - - // Item found and valid. Decode - return decode(item.value) - }, - - getKeys: async () => { - const keys = await Storage.getAllKeys() - return keys.filter(key => key.startsWith(`${dbName}.`)) as string[] - }, - - set: async (key, value) => { - const item = { - value: await encode(value), - expiresAt: maxAge == null ? null : new Date(Date.now() + maxAge), - } as Schema[N] - - await Storage.setItem(`${dbName}.${key}`, JSON.stringify(item)) - }, - - del: async key => { - await this.delete(`${dbName}.${key}`) - }, - } - } - - getSessionStore(): DatabaseStore { - return this.createStore('session', { - encode: ({dpopKey, ...session}) => ({ - ...session, - dpopKey: encodeKey(dpopKey), - }), - decode: async ({dpopKey, ...encoded}) => ({ - ...encoded, - dpopKey: await decodeKey(dpopKey), - }), - }) - } - - getStateStore(): DatabaseStore { - return this.createStore('state', { - encode: ({dpopKey, ...session}) => ({ - ...session, - dpopKey: encodeKey(dpopKey), - }), - decode: async ({dpopKey, ...encoded}) => ({ - ...encoded, - dpopKey: await decodeKey(dpopKey), - }), - }) - } - - getDpopNonceCache(): undefined | DatabaseStore { - return this.createStore('dpopNonceCache', { - // No time limit. It is better to try with a potentially outdated nonce - // and potentially succeed rather than make requests without a nonce and - // 100% fail. - encode: value => value, - decode: encoded => encoded, - }) - } - - getDidCache(): undefined | DatabaseStore { - return this.createStore('didCache', { - maxAge: 60e3, - encode: value => value, - decode: encoded => encoded, - }) - } - - getHandleCache(): undefined | DatabaseStore { - return this.createStore('handleCache', { - maxAge: 60e3, - encode: value => value, - decode: encoded => encoded, - }) - } - - getMetadataCache(): undefined | DatabaseStore { - return this.createStore('metadataCache', { - maxAge: 60e3, - encode: value => value, - decode: encoded => encoded, - }) - } - - async cleanup() { - await Promise.all( - STORES.map( - async storeName => - [ - storeName, - await tx - .objectStore(storeName) - .index('expiresAt') - .getAllKeys(query), - ] as const, - ), - ) - - const storesWithInvalidKeys = res.filter(r => r[1].length > 0) - - await db.transaction( - storesWithInvalidKeys.map(r => r[0]), - 'readwrite', - tx => - Promise.all( - storesWithInvalidKeys.map(async ([name, keys]) => - tx.objectStore(name).delete(keys), - ), - ), - ) - } - - async [Symbol.asyncDispose]() { - await this.cleanup() - } -} diff --git a/modules/expo-bluesky-oauth-client/src-old/rn-oauth-database.ts b/modules/expo-bluesky-oauth-client/src-old/rn-oauth-database.ts deleted file mode 100644 index 8da68b8138..0000000000 --- a/modules/expo-bluesky-oauth-client/src-old/rn-oauth-database.ts +++ /dev/null @@ -1,269 +0,0 @@ -import {GenericStore, Value} from '@atproto/caching' -import {DidDocument} from '@atproto/did' -import {ResolvedHandle} from '@atproto/handle-resolver' -import {DB, DBObjectStore} from '@atproto/indexed-db' -import {Key} from '@atproto/jwk' -import {WebcryptoKey} from '@atproto/jwk-webcrypto' -import {InternalStateData, Session, TokenSet} from '@atproto/oauth-client' -import {OAuthServerMetadata} from '@atproto/oauth-server-metadata' - -type Item = { - value: V - expiresAt: null | Date -} - -type EncodedKey = { - keyId: string - keyPair: CryptoKeyPair -} - -function encodeKey(key: Key): EncodedKey { - if (!(key instanceof WebcryptoKey) || !key.kid) { - throw new Error('Invalid key object') - } - return { - keyId: key.kid, - keyPair: key.cryptoKeyPair, - } -} - -async function decodeKey(encoded: EncodedKey): Promise { - return WebcryptoKey.fromKeypair(encoded.keyId, encoded.keyPair) -} - -export type PopupStateData = - | PromiseRejectedResult - | PromiseFulfilledResult - -export type Schema = { - popup: Item - state: Item<{ - dpopKey: EncodedKey - - iss: string - nonce: string - verifier?: string - appState?: string - }> - session: Item<{ - dpopKey: EncodedKey - - tokenSet: TokenSet - }> - - didCache: Item - dpopNonceCache: Item - handleCache: Item - metadataCache: Item -} - -export type DatabaseStore = GenericStore & { - getKeys: () => Promise -} - -const STORES = [ - 'popup', - 'state', - 'session', - - 'didCache', - 'dpopNonceCache', - 'handleCache', - 'metadataCache', -] as const - -export class RNOAuthDatabase { - #dbPromise = DB.open( - '@atproto-oauth-client', - [ - db => { - for (const name of STORES) { - const store = db.createObjectStore(name) - store.createIndex('expiresAt', 'expiresAt', {unique: false}) - } - }, - ], - {durability: 'strict'}, - ) - - protected async run( - storeName: N, - mode: 'readonly' | 'readwrite', - fn: (s: DBObjectStore) => R | Promise, - ): Promise { - const db = await this.#dbPromise - return await db.transaction([storeName], mode, tx => - fn(tx.objectStore(storeName)), - ) - } - - protected createStore( - name: N, - { - encode, - decode, - maxAge, - }: { - encode: (value: V) => Schema[N]['value'] | PromiseLike - decode: (encoded: Schema[N]['value']) => V | PromiseLike - maxAge?: number - }, - ): DatabaseStore { - return { - get: async key => { - // Find item in store - const item = await this.run(name, 'readonly', dbStore => { - return dbStore.get(key) - }) - - // Not found - if (item === undefined) return undefined - - // Too old, proactively delete - if (item.expiresAt != null && item.expiresAt < new Date()) { - await this.run(name, 'readwrite', dbStore => { - return dbStore.delete(key) - }) - return undefined - } - - // Item found and valid. Decode - return decode(item.value) - }, - - getKeys: async () => { - const keys = await this.run(name, 'readonly', dbStore => { - return dbStore.getAllKeys() - }) - return keys.filter(key => typeof key === 'string') as string[] - }, - - set: async (key, value) => { - // Create encoded item record - const item = { - value: await encode(value), - expiresAt: maxAge == null ? null : new Date(Date.now() + maxAge), - } as Schema[N] - - // Store item record - await this.run(name, 'readwrite', dbStore => { - return dbStore.put(item, key) - }) - }, - - del: async key => { - // Delete - await this.run(name, 'readwrite', dbStore => { - return dbStore.delete(key) - }) - }, - } - } - - getSessionStore(): DatabaseStore { - return this.createStore('session', { - encode: ({dpopKey, ...session}) => ({ - ...session, - dpopKey: encodeKey(dpopKey), - }), - decode: async ({dpopKey, ...encoded}) => ({ - ...encoded, - dpopKey: await decodeKey(dpopKey), - }), - }) - } - - getStateStore(): DatabaseStore { - return this.createStore('state', { - encode: ({dpopKey, ...session}) => ({ - ...session, - dpopKey: encodeKey(dpopKey), - }), - decode: async ({dpopKey, ...encoded}) => ({ - ...encoded, - dpopKey: await decodeKey(dpopKey), - }), - }) - } - - getPopupStore(): DatabaseStore { - return this.createStore('popup', { - encode: value => value, - decode: encoded => encoded, - }) - } - - getDpopNonceCache(): undefined | DatabaseStore { - return this.createStore('dpopNonceCache', { - // No time limit. It is better to try with a potentially outdated nonce - // and potentially succeed rather than make requests without a nonce and - // 100% fail. - encode: value => value, - decode: encoded => encoded, - }) - } - - getDidCache(): undefined | DatabaseStore { - return this.createStore('didCache', { - maxAge: 60e3, - encode: value => value, - decode: encoded => encoded, - }) - } - - getHandleCache(): undefined | DatabaseStore { - return this.createStore('handleCache', { - maxAge: 60e3, - encode: value => value, - decode: encoded => encoded, - }) - } - - getMetadataCache(): undefined | DatabaseStore { - return this.createStore('metadataCache', { - maxAge: 60e3, - encode: value => value, - decode: encoded => encoded, - }) - } - - async cleanup() { - const db = await this.#dbPromise - const query = IDBKeyRange.lowerBound(new Date()) - const res = await db.transaction(STORES, 'readonly', tx => - Promise.all( - STORES.map( - async storeName => - [ - storeName, - await tx - .objectStore(storeName) - .index('expiresAt') - .getAllKeys(query), - ] as const, - ), - ), - ) - - const storesWithInvalidKeys = res.filter(r => r[1].length > 0) - - await db.transaction( - storesWithInvalidKeys.map(r => r[0]), - 'readwrite', - tx => - Promise.all( - storesWithInvalidKeys.map(async ([name, keys]) => - tx.objectStore(name).delete(keys), - ), - ), - ) - } - - async [Symbol.asyncDispose]() { - // TODO: call cleanup at a constant interval ? - await this.cleanup() - - const db = await this.#dbPromise - await (db[Symbol.asyncDispose] || db[Symbol.dispose]).call(db) - } -} diff --git a/modules/expo-bluesky-oauth-client/src-old/store.ts b/modules/expo-bluesky-oauth-client/src-old/store.ts deleted file mode 100644 index e69de29bb2..0000000000 diff --git a/modules/expo-bluesky-oauth-client/src-old/store.web.ts b/modules/expo-bluesky-oauth-client/src-old/store.web.ts deleted file mode 100644 index e69de29bb2..0000000000 diff --git a/modules/expo-bluesky-oauth-client/src-old/util.web.ts b/modules/expo-bluesky-oauth-client/src-old/util.web.ts deleted file mode 100644 index 654365aae5..0000000000 --- a/modules/expo-bluesky-oauth-client/src-old/util.web.ts +++ /dev/null @@ -1,41 +0,0 @@ -// @ts-ignore web only, this silences errors throughout the whole file for crypto being missing -const crypto = global.crypto - -export async function generateKeyPair(algs: string[], extractable = false) { - const errors: unknown[] = [] - try { - return await crypto.subtle.generateKey( - { - name: 'ECDSA', - namedCurve: `P-256`, - }, - extractable, - ['sign', 'verify'], - ) - } catch (err) { - errors.push(err) - } - - console.log(errors) - throw new AggregateError(errors, 'Failed to generate keypair') -} - -export function isSignatureKeyPair( - v: unknown, - extractable?: boolean, -): v is CryptoKeyPair { - return ( - typeof v === 'object' && - v !== null && - 'privateKey' in v && - v.privateKey instanceof CryptoKey && - v.privateKey.type === 'private' && - (extractable == null || v.privateKey.extractable === extractable) && - v.privateKey.usages.includes('sign') && - 'publicKey' in v && - v.publicKey instanceof CryptoKey && - v.publicKey.type === 'public' && - v.publicKey.extractable === true && - v.publicKey.usages.includes('verify') - ) -} From a07d29114364e48ac6ca0883c882aa15f62f1e06 Mon Sep 17 00:00:00 2001 From: Hailey Date: Mon, 15 Apr 2024 00:57:12 -0700 Subject: [PATCH 40/54] add structs --- .../android/src/main/java/JWK.kt | 34 ++++ .../expo/modules/blueskyoauthclient/JWT.kt | 158 ++++++++++++++++++ 2 files changed, 192 insertions(+) create mode 100644 modules/expo-bluesky-oauth-client/android/src/main/java/JWK.kt create mode 100644 modules/expo-bluesky-oauth-client/android/src/main/java/expo/modules/blueskyoauthclient/JWT.kt diff --git a/modules/expo-bluesky-oauth-client/android/src/main/java/JWK.kt b/modules/expo-bluesky-oauth-client/android/src/main/java/JWK.kt new file mode 100644 index 0000000000..5959ea8c00 --- /dev/null +++ b/modules/expo-bluesky-oauth-client/android/src/main/java/JWK.kt @@ -0,0 +1,34 @@ +package expo.modules.blueskyoauthclient + +import expo.modules.kotlin.records.Record +import expo.modules.kotlin.records.Field + +class JWK : Record { + @Field + var alg: String = "" + @Field + var kty: String = "" + @Field + var crv: String? = null + @Field + var x: String? = null + @Field + var y: String? = null + @Field + var e: String? = null + @Field + var n: String? = null + @Field + var d: String? = null + @Field + var use: String? = null + @Field + var kid: String? = null +} + +class JWKPair : Record { + @Field + val privateKey: JWK = JWK() + @Field + val publicKey: JWK = JWK() +} \ No newline at end of file diff --git a/modules/expo-bluesky-oauth-client/android/src/main/java/expo/modules/blueskyoauthclient/JWT.kt b/modules/expo-bluesky-oauth-client/android/src/main/java/expo/modules/blueskyoauthclient/JWT.kt new file mode 100644 index 0000000000..235ed07dba --- /dev/null +++ b/modules/expo-bluesky-oauth-client/android/src/main/java/expo/modules/blueskyoauthclient/JWT.kt @@ -0,0 +1,158 @@ +package expo.modules.blueskyoauthclient + +import expo.modules.kotlin.records.Record +import expo.modules.kotlin.records.Field + +class JWTHeader : Record { + @Field + val alg: String = "" + @Field + var jku: String? = null + @Field + var jwk: JWK = JWK() + @Field + var kid: String? = null + @Field + var typ: String? = null + @Field + var cty: String? = null + @Field + var crit: String? = null +} + +class JWTPayload : Record { + @Field + var iss: String? = null + @Field + var aud: String? = null + @Field + var sub: String? = null + @Field + var exp: Int? = null + @Field + var nbr: Int? = null + @Field + var iat: Int? = null + @Field + var jti: String? = null + @Field + var htm: String? = null + @Field + var htu: String? = null + @Field + var ath: String? = null + @Field + var acr: String? = null + @Field + var azp: String? = null + @Field + var amr: String? = null + @Field + var cnf: JWTPayloadCNF? = null + @Field + var client_id: String? = null + @Field + var scope: String? = null + @Field + var nonce: String? = null + @Field + var at_hash: String? = null + @Field + var c_hash: String? = null + @Field + var s_hash: String? = null + @Field + var auth_time: Int? = null + @Field + var name: String? = null + @Field + var family_name: String? = null + @Field + var given_name: String? = null + @Field + var middle_name: String? = null + @Field + var nickname: String? = null + @Field + var preferred_username: String? = null + @Field + var gender: String? = null + @Field + var picture: String? = null + @Field + var profile: String? = null + @Field + var website: String? = null + @Field + var birthdate: String? = null + @Field + var zoneinfo: String? = null + @Field + var locale: String? = null + @Field + var updated_at: Int? = null + @Field + var email: String? = null + @Field + var email_verified: String? = null + @Field + var phone_number: String? = null + @Field + var phone_number_verified: Boolean? = null + @Field + var address: JWTPayloadAddress? = null + @Field + var authorization_details: JWTPayloadAuthorizationDetails? = null +} + +class JWTPayloadCNF : Record { + @Field + var kid: String? = null + @Field + var jwk: JWK? = null + @Field + var jwe: String? = null + @Field + var jku: String? = null + @Field + var jkt: String? = null + @Field + var osc: String? = null +} + +class JWTPayloadAddress : Record { + @Field + var formatted: String? = null + @Field + var street_address: String? = null + @Field + var locality: String? = null + @Field + var region: String? = null + @Field + var postal_code: String? = null + @Field + var country: String? = null +} + +class JWTPayloadAuthorizationDetails : Record { + @Field + var type: String = "" + @Field + var locations: Array? = null + @Field + var actions: Array? = null + @Field + var datatypes: Array? = null + @Field + var identifier: String? = null + @Field + var privileges: Array? = null +} + +class JWTVerifyResponse : Record { + @Field + var protectedHeader: JWTHeader = JWTHeader() + @Field + var payload: String = "" +} From adab484fb756810f47cd07a9c4aa61222206494e Mon Sep 17 00:00:00 2001 From: Hailey Date: Mon, 15 Apr 2024 01:04:51 -0700 Subject: [PATCH 41/54] oops --- .../android/src/main/java/JWK.kt | 41 ++-- .../expo/modules/blueskyoauthclient/JWT.kt | 214 ++++++------------ 2 files changed, 79 insertions(+), 176 deletions(-) diff --git a/modules/expo-bluesky-oauth-client/android/src/main/java/JWK.kt b/modules/expo-bluesky-oauth-client/android/src/main/java/JWK.kt index 5959ea8c00..64835b370b 100644 --- a/modules/expo-bluesky-oauth-client/android/src/main/java/JWK.kt +++ b/modules/expo-bluesky-oauth-client/android/src/main/java/JWK.kt @@ -3,32 +3,17 @@ package expo.modules.blueskyoauthclient import expo.modules.kotlin.records.Record import expo.modules.kotlin.records.Field -class JWK : Record { - @Field - var alg: String = "" - @Field - var kty: String = "" - @Field - var crv: String? = null - @Field - var x: String? = null - @Field - var y: String? = null - @Field - var e: String? = null - @Field - var n: String? = null - @Field - var d: String? = null - @Field - var use: String? = null - @Field - var kid: String? = null -} +class JWK( + @Field var alg: String = "", + @Field var kty: String = "", + @Field var crv: String? = null, + @Field var x: String? = null, + @Field var y: String? = null, + @Field var e: String? = null, + @Field var n: String? = null, + @Field var d: String? = null, + @Field var use: String? = null, + @Field var kid: String? = null +) : Record -class JWKPair : Record { - @Field - val privateKey: JWK = JWK() - @Field - val publicKey: JWK = JWK() -} \ No newline at end of file +class JWKPair(@Field val privateKey: JWK, @Field val publicKey: JWK) : Record \ No newline at end of file diff --git a/modules/expo-bluesky-oauth-client/android/src/main/java/expo/modules/blueskyoauthclient/JWT.kt b/modules/expo-bluesky-oauth-client/android/src/main/java/expo/modules/blueskyoauthclient/JWT.kt index 235ed07dba..a36df26a3a 100644 --- a/modules/expo-bluesky-oauth-client/android/src/main/java/expo/modules/blueskyoauthclient/JWT.kt +++ b/modules/expo-bluesky-oauth-client/android/src/main/java/expo/modules/blueskyoauthclient/JWT.kt @@ -3,156 +3,74 @@ package expo.modules.blueskyoauthclient import expo.modules.kotlin.records.Record import expo.modules.kotlin.records.Field -class JWTHeader : Record { - @Field - val alg: String = "" - @Field - var jku: String? = null - @Field - var jwk: JWK = JWK() - @Field - var kid: String? = null - @Field - var typ: String? = null - @Field - var cty: String? = null - @Field - var crit: String? = null -} +class JWTHeader( + @Field var alg: String = "", + @Field var jku: String? = null, + @Field var jwk: JWK = JWK(), + @Field var kid: String? = null, + @Field var typ: String? = null, + @Field var cty: String? = null, + @Field var crit: String? = null +) : Record -class JWTPayload : Record { - @Field - var iss: String? = null - @Field - var aud: String? = null - @Field - var sub: String? = null - @Field - var exp: Int? = null - @Field - var nbr: Int? = null - @Field - var iat: Int? = null - @Field - var jti: String? = null - @Field - var htm: String? = null - @Field - var htu: String? = null - @Field - var ath: String? = null - @Field - var acr: String? = null - @Field - var azp: String? = null - @Field - var amr: String? = null - @Field - var cnf: JWTPayloadCNF? = null - @Field - var client_id: String? = null - @Field - var scope: String? = null - @Field - var nonce: String? = null - @Field - var at_hash: String? = null - @Field - var c_hash: String? = null - @Field - var s_hash: String? = null - @Field - var auth_time: Int? = null - @Field - var name: String? = null - @Field - var family_name: String? = null - @Field - var given_name: String? = null - @Field - var middle_name: String? = null - @Field - var nickname: String? = null - @Field - var preferred_username: String? = null - @Field - var gender: String? = null - @Field - var picture: String? = null - @Field - var profile: String? = null - @Field - var website: String? = null - @Field - var birthdate: String? = null - @Field - var zoneinfo: String? = null - @Field - var locale: String? = null - @Field - var updated_at: Int? = null - @Field - var email: String? = null - @Field - var email_verified: String? = null - @Field - var phone_number: String? = null - @Field - var phone_number_verified: Boolean? = null - @Field - var address: JWTPayloadAddress? = null - @Field - var authorization_details: JWTPayloadAuthorizationDetails? = null -} +class JWTPayload( + @Field var iss: String? = null, + @Field var aud: String? = null, + @Field var sub: String? = null, + @Field var exp: Int? = null, + @Field var nbr: Int? = null, + @Field var iat: Int? = null, + @Field var jti: String? = null, + @Field var htm: String? = null, + @Field var htu: String? = null, + @Field var ath: String? = null, + @Field var acr: String? = null, + @Field var azp: String? = null, + @Field var amr: String? = null, + @Field var cnf: JWTPayloadCNF? = null, + @Field var client_id: String? = null, + @Field var scope: String? = null, + @Field var nonce: String? = null, + @Field var at_hash: String? = null, + @Field var c_hash: String? = null, + @Field var s_hash: String? = null, + @Field var auth_time: Int? = null, + @Field var name: String? = null, + @Field var family_name: String? = null, + @Field var given_name: String? = null, + @Field var middle_name: String? = null, + @Field var nickname: String? = null, + @Field var preferred_username: String? = null, +) : Record -class JWTPayloadCNF : Record { - @Field - var kid: String? = null - @Field - var jwk: JWK? = null - @Field - var jwe: String? = null - @Field - var jku: String? = null - @Field - var jkt: String? = null - @Field - var osc: String? = null -} +class JWTPayloadCNF( + @Field var jwk: JWK? = null, + @Field var jwe: String? = null, + @Field var jku: String? = null, + @Field var jkt: String? = null, + @Field var osc: String? = null +) : Record -class JWTPayloadAddress : Record { - @Field - var formatted: String? = null - @Field - var street_address: String? = null - @Field - var locality: String? = null - @Field - var region: String? = null - @Field - var postal_code: String? = null - @Field - var country: String? = null -} +class JWTPayloadAddress( + @Field var formatted: String? = null, + @Field var street_address: String? = null, + @Field var locality: String? = null, + @Field var region: String? = null, + @Field var postal_code: String? = null, + @Field var country: String? = null +) : Record -class JWTPayloadAuthorizationDetails : Record { - @Field - var type: String = "" - @Field - var locations: Array? = null - @Field - var actions: Array? = null - @Field - var datatypes: Array? = null - @Field - var identifier: String? = null - @Field - var privileges: Array? = null -} +class JWTPayloadAuthorizationDetails( + @Field var type: String? = null, + @Field var locations: Array? = null, + @Field var actions: Array? = null, + @Field var datatypes: Array? = null, + @Field var identifier: String? = null, + @Field var privileges: Array? = null +) : Record -class JWTVerifyResponse : Record { - @Field - var protectedHeader: JWTHeader = JWTHeader() - @Field - var payload: String = "" -} +class JWTVerifyResponse( + @Field var header: JWTHeader = JWTHeader(), + @Field var payload: JWTPayload = JWTPayload(), + @Field var signature: String = "" +) : Record \ No newline at end of file From 37f584021469061e37794e5616a2e620d5bccbd6 Mon Sep 17 00:00:00 2001 From: Hailey Date: Mon, 15 Apr 2024 01:07:36 -0700 Subject: [PATCH 42/54] update genkeypair --- .../modules/blueskyoauthclient/CryptoUtil.kt | 27 +++++++++++++++---- 1 file changed, 22 insertions(+), 5 deletions(-) diff --git a/modules/expo-bluesky-oauth-client/android/src/main/java/expo/modules/blueskyoauthclient/CryptoUtil.kt b/modules/expo-bluesky-oauth-client/android/src/main/java/expo/modules/blueskyoauthclient/CryptoUtil.kt index 2abce43009..1c8ea0eaa5 100644 --- a/modules/expo-bluesky-oauth-client/android/src/main/java/expo/modules/blueskyoauthclient/CryptoUtil.kt +++ b/modules/expo-bluesky-oauth-client/android/src/main/java/expo/modules/blueskyoauthclient/CryptoUtil.kt @@ -22,8 +22,8 @@ class CryptoUtil { return random } - fun generateKeyPair(keyId: String?): Pair { - val keyIdString = keyId ?: UUID.randomUUID().toString() + fun generateKeyPair(): Any { + val keyIdString = UUID.randomUUID().toString() val keyPairGen = KeyPairGenerator.getInstance("EC") keyPairGen.initialize(Curve.P_256.toECParameterSpec()) @@ -44,9 +44,26 @@ class CryptoUtil { .algorithm(Algorithm.parse("ES256")) .build() - return Pair( - publicJwk.toString(), - privateJwk.toString() + + return JWKPair( + JWK( + alg = privateJwk.algorithm.toString(), + kty = privateJwk.keyType.toString(), + crv = privateJwk.curve.toString(), + x = privateJwk.x.toString(), + y = privateJwk.y.toString(), + use = privateJwk.keyUse.toString(), + kid = privateJwk.keyID + ), + JWK( + alg = publicJwk.algorithm.toString(), + kty = publicJwk.keyType.toString(), + crv = publicJwk.curve.toString(), + x = publicJwk.x.toString(), + y = publicJwk.y.toString(), + use = publicJwk.keyUse.toString(), + kid = publicJwk.keyID + ) ) } } From caddbeb227b35092e3bbd5550815bc29beefe0b7 Mon Sep 17 00:00:00 2001 From: Hailey Date: Mon, 15 Apr 2024 02:03:10 -0700 Subject: [PATCH 43/54] android impl --- .../android/src/main/java/JWK.kt | 19 --- .../modules/blueskyoauthclient/CryptoUtil.kt | 1 + .../ExpoBlueskyOAuthClientModule.kt | 21 ++-- .../expo/modules/blueskyoauthclient/JWK.kt | 34 +++++ .../expo/modules/blueskyoauthclient/JWT.kt | 117 ++++++++++++++++-- .../modules/blueskyoauthclient/JWTUtil.kt | 63 +++++++--- .../expo-bluesky-oauth-client/ios/JWT.swift | 2 +- src/view/screens/Home.tsx | 3 + 8 files changed, 206 insertions(+), 54 deletions(-) delete mode 100644 modules/expo-bluesky-oauth-client/android/src/main/java/JWK.kt create mode 100644 modules/expo-bluesky-oauth-client/android/src/main/java/expo/modules/blueskyoauthclient/JWK.kt diff --git a/modules/expo-bluesky-oauth-client/android/src/main/java/JWK.kt b/modules/expo-bluesky-oauth-client/android/src/main/java/JWK.kt deleted file mode 100644 index 64835b370b..0000000000 --- a/modules/expo-bluesky-oauth-client/android/src/main/java/JWK.kt +++ /dev/null @@ -1,19 +0,0 @@ -package expo.modules.blueskyoauthclient - -import expo.modules.kotlin.records.Record -import expo.modules.kotlin.records.Field - -class JWK( - @Field var alg: String = "", - @Field var kty: String = "", - @Field var crv: String? = null, - @Field var x: String? = null, - @Field var y: String? = null, - @Field var e: String? = null, - @Field var n: String? = null, - @Field var d: String? = null, - @Field var use: String? = null, - @Field var kid: String? = null -) : Record - -class JWKPair(@Field val privateKey: JWK, @Field val publicKey: JWK) : Record \ No newline at end of file diff --git a/modules/expo-bluesky-oauth-client/android/src/main/java/expo/modules/blueskyoauthclient/CryptoUtil.kt b/modules/expo-bluesky-oauth-client/android/src/main/java/expo/modules/blueskyoauthclient/CryptoUtil.kt index 1c8ea0eaa5..eab729a8b8 100644 --- a/modules/expo-bluesky-oauth-client/android/src/main/java/expo/modules/blueskyoauthclient/CryptoUtil.kt +++ b/modules/expo-bluesky-oauth-client/android/src/main/java/expo/modules/blueskyoauthclient/CryptoUtil.kt @@ -52,6 +52,7 @@ class CryptoUtil { crv = privateJwk.curve.toString(), x = privateJwk.x.toString(), y = privateJwk.y.toString(), + d = privateJwk.d.toString(), use = privateJwk.keyUse.toString(), kid = privateJwk.keyID ), diff --git a/modules/expo-bluesky-oauth-client/android/src/main/java/expo/modules/blueskyoauthclient/ExpoBlueskyOAuthClientModule.kt b/modules/expo-bluesky-oauth-client/android/src/main/java/expo/modules/blueskyoauthclient/ExpoBlueskyOAuthClientModule.kt index 93906a3ab1..15e6305b45 100644 --- a/modules/expo-bluesky-oauth-client/android/src/main/java/expo/modules/blueskyoauthclient/ExpoBlueskyOAuthClientModule.kt +++ b/modules/expo-bluesky-oauth-client/android/src/main/java/expo/modules/blueskyoauthclient/ExpoBlueskyOAuthClientModule.kt @@ -1,5 +1,6 @@ package expo.modules.blueskyoauthclient +import android.util.Log import expo.modules.kotlin.modules.Module import expo.modules.kotlin.modules.ModuleDefinition @@ -15,21 +16,19 @@ class ExpoBlueskyOAuthClientModule : Module() { return@Function CryptoUtil().getRandomValues(byteLength) } - AsyncFunction("generateKeyPair") { keyId: String? -> - val res = CryptoUtil().generateKeyPair(keyId) - - return@AsyncFunction mapOf( - "publicKey" to res.first, - "privateKey" to res.second - ) + AsyncFunction("generateJwk") { algorithim: String -> + if (algorithim != "ES256") { + throw Exception("Unsupported algorithm") + } + return@AsyncFunction CryptoUtil().generateKeyPair() } - AsyncFunction("createJwt") { jwkString: String, headerString: String, payloadString: String -> - return@AsyncFunction JWTUtil().createJwt(jwkString, headerString, payloadString) + AsyncFunction("createJwt") { header: JWTHeader, payload: JWTPayload, jwk: JWK -> + return@AsyncFunction JWTUtil().createJwt(header, payload, jwk) } - AsyncFunction("verifyJwt") { jwkString: String, tokenString: String, options: String? -> - return@AsyncFunction JWTUtil().verifyJwt(jwkString, tokenString, options) + AsyncFunction("verifyJwt") { token: String, jwk: JWK -> + return@AsyncFunction JWTUtil().verifyJwt(token, jwk) } } } diff --git a/modules/expo-bluesky-oauth-client/android/src/main/java/expo/modules/blueskyoauthclient/JWK.kt b/modules/expo-bluesky-oauth-client/android/src/main/java/expo/modules/blueskyoauthclient/JWK.kt new file mode 100644 index 0000000000..2ce71dd8fa --- /dev/null +++ b/modules/expo-bluesky-oauth-client/android/src/main/java/expo/modules/blueskyoauthclient/JWK.kt @@ -0,0 +1,34 @@ +package expo.modules.blueskyoauthclient + +import expo.modules.kotlin.records.Record +import expo.modules.kotlin.records.Field + +class JWK( + @Field var alg: String = "", + @Field var kty: String = "", + @Field var crv: String? = null, + @Field var x: String? = null, + @Field var y: String? = null, + @Field var e: String? = null, + @Field var n: String? = null, + @Field var d: String? = null, + @Field var use: String? = null, + @Field var kid: String? = null +) : Record { + fun toJson(): String { + val parts = mutableListOf() + if (alg.isNotEmpty()) parts.add("\"alg\": \"$alg\"") + if (kty.isNotEmpty()) parts.add("\"kty\": \"$kty\"") + if (crv != null) parts.add("\"crv\": \"$crv\"") + if (x != null) parts.add("\"x\": \"$x\"") + if (y != null) parts.add("\"y\": \"$y\"") + if (e != null) parts.add("\"e\": \"$e\"") + if (n != null) parts.add("\"n\": \"$n\"") + if (d != null) parts.add("\"d\": \"$d\"") + if (use != null) parts.add("\"use\": \"$use\"") + if (kid != null) parts.add("\"kid\": \"$kid\"") + return "{ ${parts.joinToString()} }" + } +} + +class JWKPair(@Field val privateKey: JWK, @Field val publicKey: JWK) : Record \ No newline at end of file diff --git a/modules/expo-bluesky-oauth-client/android/src/main/java/expo/modules/blueskyoauthclient/JWT.kt b/modules/expo-bluesky-oauth-client/android/src/main/java/expo/modules/blueskyoauthclient/JWT.kt index a36df26a3a..8b761615a0 100644 --- a/modules/expo-bluesky-oauth-client/android/src/main/java/expo/modules/blueskyoauthclient/JWT.kt +++ b/modules/expo-bluesky-oauth-client/android/src/main/java/expo/modules/blueskyoauthclient/JWT.kt @@ -6,12 +6,24 @@ import expo.modules.kotlin.records.Field class JWTHeader( @Field var alg: String = "", @Field var jku: String? = null, - @Field var jwk: JWK = JWK(), + @Field var jwk: JWK? = null, @Field var kid: String? = null, @Field var typ: String? = null, @Field var cty: String? = null, @Field var crit: String? = null -) : Record +) : Record { + fun toJson(): String { + val parts = mutableListOf() + if (alg.isNotEmpty()) parts.add("\"alg\": \"$alg\"") + if (jku != null) parts.add("\"jku\": \"$jku\"") + if (jwk != null) parts.add("\"jwk\": ${jwk?.toJson()}") + if (kid != null) parts.add("\"kid\": \"$kid\"") + if (typ != null) parts.add("\"typ\": \"$typ\"") + if (cty != null) parts.add("\"cty\": \"$cty\"") + if (crit != null) parts.add("\"crit\": \"$crit\"") + return "{ ${parts.joinToString()} }" + } +} class JWTPayload( @Field var iss: String? = null, @@ -41,7 +53,63 @@ class JWTPayload( @Field var middle_name: String? = null, @Field var nickname: String? = null, @Field var preferred_username: String? = null, -) : Record + @Field var gender: String? = null, + @Field var picture: String? = null, + @Field var profile: String? = null, + @Field var birthdate: String? = null, + @Field var zoneinfo: String? = null, + @Field var updated_at: Int? = null, + @Field var email: String? = null, + @Field var email_verified: Boolean? = null, + @Field var phone_number: String? = null, + @Field var phone_number_verified: Boolean? = null, + @Field var address: JWTPayloadAddress? = null, + @Field var authorization_details: JWTPayloadAuthorizationDetails? = null +) : Record { + fun toJson(): String { + val parts = mutableListOf() + if (iss != null) parts.add("\"iss\": \"$iss\"") + if (aud != null) parts.add("\"aud\": \"$aud\"") + if (sub != null) parts.add("\"sub\": \"$sub\"") + if (exp != null) parts.add("\"exp\": $exp") + if (nbr != null) parts.add("\"nbr\": $nbr") + if (iat != null) parts.add("\"iat\": $iat") + if (jti != null) parts.add("\"jti\": \"$jti\"") + if (htm != null) parts.add("\"htm\": \"$htm\"") + if (htu != null) parts.add("\"htu\": \"$htu\"") + if (ath != null) parts.add("\"ath\": \"$ath\"") + if (acr != null) parts.add("\"acr\": \"$acr\"") + if (azp != null) parts.add("\"azp\": \"$azp\"") + if (amr != null) parts.add("\"amr\": \"$amr\"") + if (cnf != null) parts.add("\"cnf\": ${cnf?.toJson()}") + if (client_id != null) parts.add("\"client_id\": \"$client_id\"") + if (scope != null) parts.add("\"scope\": \"$scope\"") + if (nonce != null) parts.add("\"nonce\": \"$nonce\"") + if (at_hash != null) parts.add("\"at_hash\": \"$at_hash\"") + if (c_hash != null) parts.add("\"c_hash\": \"$c_hash\"") + if (s_hash != null) parts.add("\"s_hash\": \"$s_hash\"") + if (auth_time != null) parts.add("\"auth_time\": $auth_time") + if (name != null) parts.add("\"name\": \"$name\"") + if (family_name != null) parts.add("\"family_name\": \"$family_name\"") + if (given_name != null) parts.add("\"given_name\": \"$given_name\"") + if (middle_name != null) parts.add("\"middle_name\": \"$middle_name\"") + if (nickname != null) parts.add("\"nickname\": \"$nickname\"") + if (preferred_username != null) parts.add("\"preferred_username\": \"$preferred_username\"") + if (gender != null) parts.add("\"gender\": \"$gender\"") + if (picture != null) parts.add("\"picture\": \"$picture\"") + if (profile != null) parts.add("\"profile\": \"$profile\"") + if (birthdate != null) parts.add("\"birthdate\": \"$birthdate\"") + if (zoneinfo != null) parts.add("\"zoneinfo\": \"$zoneinfo\"") + if (updated_at != null) parts.add("\"updated_at\": $updated_at") + if (email != null) parts.add("\"email\": \"$email\"") + if (email_verified != null) parts.add("\"email_verified\": $email_verified") + if (phone_number != null) parts.add("\"phone_number\": \"$phone_number\"") + if (phone_number_verified != null) parts.add("\"phone_number_verified\": $phone_number_verified") + if (address != null) parts.add("\"address\": ${address?.toJson()}") + if (authorization_details != null) parts.add("\"authorization_details\": ${authorization_details?.toJson()}") + return "{ ${parts.joinToString()} }" + } +} class JWTPayloadCNF( @Field var jwk: JWK? = null, @@ -49,7 +117,17 @@ class JWTPayloadCNF( @Field var jku: String? = null, @Field var jkt: String? = null, @Field var osc: String? = null -) : Record +) : Record { + fun toJson(): String { + val parts = mutableListOf() + if (jwk != null) parts.add("\"jwk\": ${jwk?.toJson()}") + if (jwe != null) parts.add("\"jwe\": \"$jwe\"") + if (jku != null) parts.add("\"jku\": \"$jku\"") + if (jkt != null) parts.add("\"jkt\": \"$jkt\"") + if (osc != null) parts.add("\"osc\": \"$osc\"") + return "{ ${parts.joinToString()} }" + } +} class JWTPayloadAddress( @Field var formatted: String? = null, @@ -58,7 +136,18 @@ class JWTPayloadAddress( @Field var region: String? = null, @Field var postal_code: String? = null, @Field var country: String? = null -) : Record +) : Record { + fun toJson(): String { + val parts = mutableListOf() + if (formatted != null) parts.add("\"formatted\": \"$formatted\"") + if (street_address != null) parts.add("\"street_address\": \"$street_address\"") + if (locality != null) parts.add("\"locality\": \"$locality\"") + if (region != null) parts.add("\"region\": \"$region\"") + if (postal_code != null) parts.add("\"postal_code\": \"$postal_code\"") + if (country != null) parts.add("\"country\": \"$country\"") + return "{ ${parts.joinToString()} }" + } +} class JWTPayloadAuthorizationDetails( @Field var type: String? = null, @@ -67,10 +156,20 @@ class JWTPayloadAuthorizationDetails( @Field var datatypes: Array? = null, @Field var identifier: String? = null, @Field var privileges: Array? = null -) : Record +) : Record { + fun toJson(): String { + val parts = mutableListOf() + if (type != null) parts.add("\"type\": \"$type\"") + if (locations != null) parts.add("\"locations\": [${locations?.joinToString()}]") + if (actions != null) parts.add("\"actions\": [${actions?.joinToString()}]") + if (datatypes != null) parts.add("\"datatypes\": [${datatypes?.joinToString()}]") + if (identifier != null) parts.add("\"identifier\": \"$identifier\"") + if (privileges != null) parts.add("\"privileges\": [${privileges?.joinToString()}]") + return "{ ${parts.joinToString()} }" + } +} class JWTVerifyResponse( - @Field var header: JWTHeader = JWTHeader(), - @Field var payload: JWTPayload = JWTPayload(), - @Field var signature: String = "" + @Field var protectedHeader: JWTHeader = JWTHeader(), + @Field var payload: String = "", ) : Record \ No newline at end of file diff --git a/modules/expo-bluesky-oauth-client/android/src/main/java/expo/modules/blueskyoauthclient/JWTUtil.kt b/modules/expo-bluesky-oauth-client/android/src/main/java/expo/modules/blueskyoauthclient/JWTUtil.kt index 8c099d198c..1fb9276e5d 100644 --- a/modules/expo-bluesky-oauth-client/android/src/main/java/expo/modules/blueskyoauthclient/JWTUtil.kt +++ b/modules/expo-bluesky-oauth-client/android/src/main/java/expo/modules/blueskyoauthclient/JWTUtil.kt @@ -7,29 +7,64 @@ import com.nimbusds.jose.jwk.ECKey import com.nimbusds.jwt.JWTClaimsSet import com.nimbusds.jwt.SignedJWT - class JWTUtil { - fun createJwt(jwkString: String, headerString: String, payloadString: String): String { - val key = ECKey.parse(jwkString) - val header = JWSHeader.parse(headerString) - val payload = JWTClaimsSet.parse(payloadString) + fun createJwt(header: JWTHeader, payload: JWTPayload, jwk: JWK): String { + val parsedKey = ECKey.parse(jwk.toJson()) + val parsedHeader = JWSHeader.parse(header.toJson()) + val parsedPayload = JWTClaimsSet.parse(payload.toJson()) - val signer = ECDSASigner(key) - val jwt = SignedJWT(header, payload) + val signer = ECDSASigner(parsedKey) + val jwt = SignedJWT(parsedHeader, parsedPayload) jwt.sign(signer) return jwt.serialize() } - fun verifyJwt(jwkString: String, tokenString: String, options: String?): Boolean { - return try { - val key = ECKey.parse(jwkString) - val jwt = SignedJWT.parse(tokenString) - val verifier = ECDSAVerifier(key) + fun verifyJwt(token: String, jwk: JWK): JWTVerifyResponse { + try { + val parsedKey = ECKey.parse(jwk.toJson()) + val jwt = SignedJWT.parse(token) + val verifier = ECDSAVerifier(parsedKey) + + if (!jwt.verify(verifier)) { + throw Exception("Invalid signature") + } + + val header = jwt.header + val payload = jwt.payload + val ecKey = header.jwk?.toECKey() + val serializedJwk = if (ecKey != null) { + JWK( + alg = ecKey.algorithm.toString(), + kty = ecKey.keyType.toString(), + crv = ecKey.curve.toString(), + x = ecKey.x.toString(), + y = ecKey.y.toString(), + d = ecKey.d.toString(), + use = ecKey.keyUse.toString(), + kid = ecKey.keyID + ) + } else { + null + } + + val serializedHeader = JWTHeader( + alg = header.algorithm.toString(), + jku = header.jwkurl?.toString(), + jwk = serializedJwk, + kid = header.keyID, + typ = header.type?.toString(), + cty = header.contentType, + crit = header.criticalParams?.joinToString() + ) + val serializedPayload = payload.toString() - jwt.verify(verifier) + return JWTVerifyResponse( + protectedHeader = serializedHeader, + payload = serializedPayload, + ) } catch(e: Exception) { - false + throw e } } } diff --git a/modules/expo-bluesky-oauth-client/ios/JWT.swift b/modules/expo-bluesky-oauth-client/ios/JWT.swift index dc3dbf9f54..8ce014048a 100644 --- a/modules/expo-bluesky-oauth-client/ios/JWT.swift +++ b/modules/expo-bluesky-oauth-client/ios/JWT.swift @@ -100,7 +100,7 @@ struct JWTPayload : Record { @Field var email: String? @Field - var email_verified: String? + var email_verified: Bool? @Field var phone_number: String? @Field diff --git a/src/view/screens/Home.tsx b/src/view/screens/Home.tsx index bb4e9e1aa8..cce0121328 100644 --- a/src/view/screens/Home.tsx +++ b/src/view/screens/Home.tsx @@ -68,6 +68,9 @@ function HomeScreenReady({ }, ) + console.log(jwt) + console.log(key.publicJwk) + const verified = await key.verifyJwt(jwt) console.log(verified) From e01a127166c61cd98745ffafd70409d5c3ab9a43 Mon Sep 17 00:00:00 2001 From: Hailey Date: Mon, 15 Apr 2024 02:04:17 -0700 Subject: [PATCH 44/54] rm log --- src/view/screens/Home.tsx | 26 -------------------------- 1 file changed, 26 deletions(-) diff --git a/src/view/screens/Home.tsx b/src/view/screens/Home.tsx index cce0121328..39bdac669c 100644 --- a/src/view/screens/Home.tsx +++ b/src/view/screens/Home.tsx @@ -19,7 +19,6 @@ import {Pager, PagerRef, RenderTabBarFnProps} from 'view/com/pager/Pager' import {CustomFeedEmptyState} from 'view/com/posts/CustomFeedEmptyState' import {FollowingEmptyState} from 'view/com/posts/FollowingEmptyState' import {FollowingEndOfFeed} from 'view/com/posts/FollowingEndOfFeed' -import {ReactNativeKey} from '../../../modules/expo-bluesky-oauth-client' import {HomeLoggedOutCTA} from '../com/auth/HomeLoggedOutCTA' import {HomeHeader} from '../com/home/HomeHeader' @@ -52,31 +51,6 @@ function HomeScreenReady({ preferences: UsePreferencesQueryResponse pinnedFeedInfos: FeedSourceInfo[] }) { - React.useEffect(() => { - ;(async () => { - const key = await ReactNativeKey.generate('test', ['ES256']) - console.log(key.privateJwk) - - const jwt = await key.createJwt( - { - alg: 'ES256', - kid: key.kid, - }, - { - sub: 'test', - iat: Math.floor(Date.now() / 1000), - }, - ) - - console.log(jwt) - console.log(key.publicJwk) - - const verified = await key.verifyJwt(jwt) - - console.log(verified) - })() - }, []) - const allFeeds = React.useMemo(() => { const feeds: FeedDescriptor[] = [] feeds.push('home') From 3e5a3ac2e9338cc7b05e5889ead84af98f338e42 Mon Sep 17 00:00:00 2001 From: Hailey Date: Mon, 15 Apr 2024 02:18:14 -0700 Subject: [PATCH 45/54] create factory (copy db for now) --- ...eact-native-oauth-client-factory.native.ts | 143 ++++++++++++ .../src/react-native-oauth-client-factory.ts | 1 + .../src/react-native-oauth-database.native.ts | 214 ++++++++++++++++++ .../src/react-native-oauth-database.ts | 1 + .../src/react-native-store.web.ts | 1 + 5 files changed, 360 insertions(+) create mode 100644 modules/expo-bluesky-oauth-client/src/react-native-oauth-client-factory.native.ts create mode 100644 modules/expo-bluesky-oauth-client/src/react-native-oauth-client-factory.ts create mode 100644 modules/expo-bluesky-oauth-client/src/react-native-oauth-database.native.ts create mode 100644 modules/expo-bluesky-oauth-client/src/react-native-oauth-database.ts create mode 100644 modules/expo-bluesky-oauth-client/src/react-native-store.web.ts diff --git a/modules/expo-bluesky-oauth-client/src/react-native-oauth-client-factory.native.ts b/modules/expo-bluesky-oauth-client/src/react-native-oauth-client-factory.native.ts new file mode 100644 index 0000000000..754a26f95f --- /dev/null +++ b/modules/expo-bluesky-oauth-client/src/react-native-oauth-client-factory.native.ts @@ -0,0 +1,143 @@ +import {Fetch} from '@atproto/fetch' +import {UniversalIdentityResolver} from '@atproto/identity-resolver' +import { + OAuthAuthorizeOptions, + OAuthClientFactory, + OAuthResponseMode, + OAuthResponseType, + Session, +} from '@atproto/oauth-client' +import {OAuthClientMetadata} from '@atproto/oauth-client-metadata' +import {IsomorphicOAuthServerMetadataResolver} from '@atproto/oauth-server-metadata-resolver' + +import {ReactNativeCryptoImplementation} from './react-native-crypto-implementation' +import {DatabaseStore} from './react-native-oauth-database' +import {RNOAuthDatabase} from './react-native-oauth-database.native' + +export type RNOAuthClientOptions = { + responseMode?: OAuthResponseMode + responseType?: OAuthResponseType + clientMetadata: OAuthClientMetadata + fetch?: Fetch + crypto?: any +} + +export class RNOAuthClientFactory extends OAuthClientFactory { + readonly sessionStore: DatabaseStore + + constructor({ + clientMetadata, + // "fragment" is safer as it is not sent to the server + responseMode = 'fragment', + fetch = globalThis.fetch, + }: RNOAuthClientOptions) { + const database = new RNOAuthDatabase() + + super({ + clientMetadata, + responseMode, + fetch, + cryptoImplementation: new ReactNativeCryptoImplementation(), + sessionStore: database.getSessionStore(), + stateStore: database.getStateStore(), + metadataResolver: new IsomorphicOAuthServerMetadataResolver({ + fetch, + cache: database.getMetadataCache(), + }), + identityResolver: UniversalIdentityResolver.from({ + fetch, + didCache: database.getDidCache(), + handleCache: database.getHandleCache(), + }), + dpopNonceCache: database.getDpopNonceCache(), + }) + + this.sessionStore = database.getSessionStore() + } + + async restoreAll() { + const sessionIds = await this.sessionStore.getKeys() + return Object.fromEntries( + await Promise.all( + sessionIds.map( + async sessionId => + [sessionId, await this.restore(sessionId, false)] as const, + ), + ), + ) + } + + // async init(sessionId?: string, forceRefresh = false) { + // // // const signInResult = await this.signInCallback() + // // if (signInResult) { + // // return signInResult + // // } else if (sessionId) { + // // const client = await this.restore(sessionId, forceRefresh) + // // return {client} + // // } else { + // // // TODO: we could restore any session from the store ? + // // } + // } + + async signIn(input: string, options?: OAuthAuthorizeOptions) { + return await this.authorize(input, options) + } + + // async signInCallback() { + // const redirectUri = new URL(this.clientMetadata.redirect_uris[0]) + // if (location.pathname !== redirectUri.pathname) return null + // + // const params = + // this.responseMode === 'query' + // ? new URLSearchParams(location.search) + // : new URLSearchParams(location.hash.slice(1)) + // + // // Only if the query string contains oauth callback params + // if ( + // !params.has('iss') || + // !params.has('state') || + // !(params.has('code') || params.has('error')) + // ) { + // return null + // } + // + // // Replace the current history entry without the query string (this will + // // prevent this 'if' branch to run again if the user refreshes the page) + // history.replaceState(null, '', location.pathname) + // + // return this.callback(params) + // .then(async result => { + // if (result.state?.startsWith(POPUP_KEY_PREFIX)) { + // const stateKey = result.state.slice(POPUP_KEY_PREFIX.length) + // + // await this.popupStore.set(stateKey, { + // status: 'fulfilled', + // value: result.client.sessionId, + // }) + // + // window.close() // continued in signInPopup + // throw new Error('Login complete, please close the popup window.') + // } + // + // return result + // }) + // .catch(async err => { + // // TODO: Throw a proper error from parent class to actually detect + // // oauth authorization errors + // const state = typeof (err as any)?.state + // if (typeof state === 'string' && state?.startsWith(POPUP_KEY_PREFIX)) { + // const stateKey = state.slice(POPUP_KEY_PREFIX.length) + // + // await this.popupStore.set(stateKey, { + // status: 'rejected', + // reason: err, + // }) + // + // window.close() // continued in signInPopup + // throw new Error('Login complete, please close the popup window.') + // } + // + // throw err + // }) + // } +} diff --git a/modules/expo-bluesky-oauth-client/src/react-native-oauth-client-factory.ts b/modules/expo-bluesky-oauth-client/src/react-native-oauth-client-factory.ts new file mode 100644 index 0000000000..0e6468d9ea --- /dev/null +++ b/modules/expo-bluesky-oauth-client/src/react-native-oauth-client-factory.ts @@ -0,0 +1 @@ +export * from '@atproto/oauth-client-browser/src/browser-oauth-client-factory' diff --git a/modules/expo-bluesky-oauth-client/src/react-native-oauth-database.native.ts b/modules/expo-bluesky-oauth-client/src/react-native-oauth-database.native.ts new file mode 100644 index 0000000000..96a7ae74d7 --- /dev/null +++ b/modules/expo-bluesky-oauth-client/src/react-native-oauth-database.native.ts @@ -0,0 +1,214 @@ +import {GenericStore, Value} from '@atproto/caching' +import {DidDocument} from '@atproto/did' +import {ResolvedHandle} from '@atproto/handle-resolver' +import {Key} from '@atproto/jwk' +import {WebcryptoKey} from '@atproto/jwk-webcrypto' +import {InternalStateData, Session, TokenSet} from '@atproto/oauth-client' +import {OAuthServerMetadata} from '@atproto/oauth-server-metadata' +import Storage from '@react-native-async-storage/async-storage' + +type Item = { + value: string + expiresAt: null | Date +} + +type EncodedKey = { + keyId: string + keyPair: CryptoKeyPair +} + +function encodeKey(key: Key): EncodedKey { + if (!(key instanceof WebcryptoKey) || !key.kid) { + throw new Error('Invalid key object') + } + return { + keyId: key.kid, + keyPair: key.cryptoKeyPair, + } +} + +async function decodeKey(encoded: EncodedKey): Promise { + return WebcryptoKey.fromKeypair(encoded.keyId, encoded.keyPair) +} + +export type Schema = { + state: Item<{ + dpopKey: EncodedKey + + iss: string + nonce: string + verifier?: string + appState?: string + }> + session: Item<{ + dpopKey: EncodedKey + + tokenSet: TokenSet + }> + + didCache: Item + dpopNonceCache: Item + handleCache: Item + metadataCache: Item +} + +export type DatabaseStore = GenericStore & { + getKeys: () => Promise +} + +const STORES = [ + 'state', + 'session', + + 'didCache', + 'dpopNonceCache', + 'handleCache', + 'metadataCache', +] as const + +export class RNOAuthDatabase { + async delete(key: string) { + await Storage.removeItem(key) + } + + protected createStore( + dbName: N, + { + encode, + decode, + maxAge, + }: { + encode: (value: V) => Schema[N]['value'] | PromiseLike + decode: (encoded: Schema[N]['value']) => V | PromiseLike + maxAge?: number + }, + ): DatabaseStore { + return { + get: async key => { + const itemJson = await Storage.getItem(`${dbName}.${key}`) + if (itemJson == null) return undefined + + const item = JSON.parse(itemJson) as Schema[N] + + // Too old, proactively delete + if (item.expiresAt != null && item.expiresAt < new Date()) { + await this.delete(`${dbName}.${key}`) + return undefined + } + + // Item found and valid. Decode + return decode(item.value) + }, + + getKeys: async () => { + const keys = await Storage.getAllKeys() + return keys.filter(key => key.startsWith(`${dbName}.`)) as string[] + }, + + set: async (key, value) => { + const item = { + value: await encode(value), + expiresAt: maxAge == null ? null : new Date(Date.now() + maxAge), + } as Schema[N] + + await Storage.setItem(`${dbName}.${key}`, JSON.stringify(item)) + }, + + del: async key => { + await this.delete(`${dbName}.${key}`) + }, + } + } + + getSessionStore(): DatabaseStore { + return this.createStore('session', { + encode: ({dpopKey, ...session}) => ({ + ...session, + dpopKey: encodeKey(dpopKey), + }), + decode: async ({dpopKey, ...encoded}) => ({ + ...encoded, + dpopKey: await decodeKey(dpopKey), + }), + }) + } + + getStateStore(): DatabaseStore { + return this.createStore('state', { + encode: ({dpopKey, ...session}) => ({ + ...session, + dpopKey: encodeKey(dpopKey), + }), + decode: async ({dpopKey, ...encoded}) => ({ + ...encoded, + dpopKey: await decodeKey(dpopKey), + }), + }) + } + + getDpopNonceCache(): undefined | DatabaseStore { + return this.createStore('dpopNonceCache', { + // No time limit. It is better to try with a potentially outdated nonce + // and potentially succeed rather than make requests without a nonce and + // 100% fail. + encode: value => value, + decode: encoded => encoded, + }) + } + + getDidCache(): undefined | DatabaseStore { + return this.createStore('didCache', { + maxAge: 60e3, + encode: value => value, + decode: encoded => encoded, + }) + } + + getHandleCache(): undefined | DatabaseStore { + return this.createStore('handleCache', { + maxAge: 60e3, + encode: value => value, + decode: encoded => encoded, + }) + } + + getMetadataCache(): undefined | DatabaseStore { + return this.createStore('metadataCache', { + maxAge: 60e3, + encode: value => value, + decode: encoded => encoded, + }) + } + + async cleanup() { + await Promise.all( + STORES.map( + async storeName => + [ + storeName, + await tx + .objectStore(storeName) + .index('expiresAt') + .getAllKeys(query), + ] as const, + ), + ) + + const storesWithInvalidKeys = res.filter(r => r[1].length > 0) + + await db.transaction( + storesWithInvalidKeys.map(r => r[0]), + 'readwrite', + tx => + Promise.all( + storesWithInvalidKeys.map(async ([name, keys]) => + tx.objectStore(name).delete(keys), + ), + ), + ) + } + + async [Symbol.asyncDispose]() { + await this.cleanup() + } +} diff --git a/modules/expo-bluesky-oauth-client/src/react-native-oauth-database.ts b/modules/expo-bluesky-oauth-client/src/react-native-oauth-database.ts new file mode 100644 index 0000000000..1fc298076b --- /dev/null +++ b/modules/expo-bluesky-oauth-client/src/react-native-oauth-database.ts @@ -0,0 +1 @@ +export * from '@atproto/oauth-client-browser/src/browser-oauth-database' diff --git a/modules/expo-bluesky-oauth-client/src/react-native-store.web.ts b/modules/expo-bluesky-oauth-client/src/react-native-store.web.ts new file mode 100644 index 0000000000..b0aef4a5db --- /dev/null +++ b/modules/expo-bluesky-oauth-client/src/react-native-store.web.ts @@ -0,0 +1 @@ +export * from '@atproto/oauth-client-browser/src/indexed-db-store' From db80f6e424cdfadcb3d05536b5ab9101515b802a Mon Sep 17 00:00:00 2001 From: Hailey Date: Mon, 15 Apr 2024 02:25:04 -0700 Subject: [PATCH 46/54] changes --- src/screens/Login/hooks/useLogin.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/screens/Login/hooks/useLogin.ts b/src/screens/Login/hooks/useLogin.ts index 89cc9528f8..1aa01b040d 100644 --- a/src/screens/Login/hooks/useLogin.ts +++ b/src/screens/Login/hooks/useLogin.ts @@ -10,7 +10,7 @@ import { OAUTH_RESPONSE_TYPES, OAUTH_SCOPE, } from 'lib/oauth' -import {RNOAuthClientFactory} from '../../../../modules/expo-bluesky-oauth-client' +import {RNOAuthClientFactory} from '../../../../modules/expo-bluesky-oauth-client/src/react-native-oauth-client-factory.native' // Service URL here is just a placeholder, this isn't how it will actually work export function useLogin() { From 56f2bafad7e109d716a3a5ea325b9f5a35097efc Mon Sep 17 00:00:00 2001 From: Hailey Date: Mon, 15 Apr 2024 02:31:03 -0700 Subject: [PATCH 47/54] fix --- .../main/java/expo/modules/blueskyoauthclient/CryptoUtil.kt | 3 ++- .../ios/ExpoBlueskyOAuthClientModule.swift | 6 +++++- .../src/react-native-crypto-implementation.ts | 1 + 3 files changed, 8 insertions(+), 2 deletions(-) diff --git a/modules/expo-bluesky-oauth-client/android/src/main/java/expo/modules/blueskyoauthclient/CryptoUtil.kt b/modules/expo-bluesky-oauth-client/android/src/main/java/expo/modules/blueskyoauthclient/CryptoUtil.kt index eab729a8b8..4a40453a39 100644 --- a/modules/expo-bluesky-oauth-client/android/src/main/java/expo/modules/blueskyoauthclient/CryptoUtil.kt +++ b/modules/expo-bluesky-oauth-client/android/src/main/java/expo/modules/blueskyoauthclient/CryptoUtil.kt @@ -11,7 +11,8 @@ import com.nimbusds.jose.jwk.KeyUse import java.util.UUID class CryptoUtil { - fun digest(data: ByteArray): ByteArray { + fun digest(data: ByteArray, algorithmName: String): ByteArray { + if(algorithmName != "sha256") throw Exception("Unsupported algorithm") val digest = MessageDigest.getInstance("sha256") return digest.digest(data) } diff --git a/modules/expo-bluesky-oauth-client/ios/ExpoBlueskyOAuthClientModule.swift b/modules/expo-bluesky-oauth-client/ios/ExpoBlueskyOAuthClientModule.swift index ee0b4f5ce5..702e56253a 100644 --- a/modules/expo-bluesky-oauth-client/ios/ExpoBlueskyOAuthClientModule.swift +++ b/modules/expo-bluesky-oauth-client/ios/ExpoBlueskyOAuthClientModule.swift @@ -5,7 +5,11 @@ public class ExpoBlueskyOAuthClientModule: Module { public func definition() -> ModuleDefinition { Name("ExpoBlueskyOAuthClient") - AsyncFunction("digest") { (data: Data, promise: Promise) in + AsyncFunction("digest") { (data: Data, algorithmName: String, promise: Promise) in + if algorithmName != "sha256" { + promise.reject("Error", "Algorithim not supported") + return + } promise.resolve(CryptoUtil.digest(data: data)) } diff --git a/modules/expo-bluesky-oauth-client/src/react-native-crypto-implementation.ts b/modules/expo-bluesky-oauth-client/src/react-native-crypto-implementation.ts index 26f58dc7b2..1de6d7e6b5 100644 --- a/modules/expo-bluesky-oauth-client/src/react-native-crypto-implementation.ts +++ b/modules/expo-bluesky-oauth-client/src/react-native-crypto-implementation.ts @@ -18,6 +18,7 @@ export class ReactNativeCryptoImplementation implements CryptoImplementation { bytes: Uint8Array, algorithm: DigestAlgorithm, ): Promise { + console.log(algorithm) return OauthClientReactNative.digest(bytes, algorithm.name) } } From d56116a6f468530e94dc00aac57942cc2b1acc51 Mon Sep 17 00:00:00 2001 From: Hailey Date: Mon, 15 Apr 2024 02:47:43 -0700 Subject: [PATCH 48/54] few more things --- .../expo-bluesky-oauth-client/src/react-native-key.ts | 4 +++- .../src/react-native-oauth-database.native.ts | 6 +----- src/screens/Login/LoginForm.tsx | 11 +---------- 3 files changed, 5 insertions(+), 16 deletions(-) diff --git a/modules/expo-bluesky-oauth-client/src/react-native-key.ts b/modules/expo-bluesky-oauth-client/src/react-native-key.ts index 6d93b0cfdb..6f76b5ca33 100644 --- a/modules/expo-bluesky-oauth-client/src/react-native-key.ts +++ b/modules/expo-bluesky-oauth-client/src/react-native-key.ts @@ -49,7 +49,9 @@ export class ReactNativeKey extends Key { // We don't need to validate this, because the native types ensure it is correct. But this is a TODO // for the same reason above - const protectedHeader = result.protectedHeader + const protectedHeader = Object.fromEntries( + Object.entries(result.protectedHeader).filter(([_, v]) => v !== null), + ) if (options?.audience != null) { const audience = Array.isArray(options.audience) diff --git a/modules/expo-bluesky-oauth-client/src/react-native-oauth-database.native.ts b/modules/expo-bluesky-oauth-client/src/react-native-oauth-database.native.ts index 96a7ae74d7..c85275f343 100644 --- a/modules/expo-bluesky-oauth-client/src/react-native-oauth-database.native.ts +++ b/modules/expo-bluesky-oauth-client/src/react-native-oauth-database.native.ts @@ -14,13 +14,10 @@ type Item = { type EncodedKey = { keyId: string - keyPair: CryptoKeyPair + keyPair: CryptoKey } function encodeKey(key: Key): EncodedKey { - if (!(key instanceof WebcryptoKey) || !key.kid) { - throw new Error('Invalid key object') - } return { keyId: key.kid, keyPair: key.cryptoKeyPair, @@ -42,7 +39,6 @@ export type Schema = { }> session: Item<{ dpopKey: EncodedKey - tokenSet: TokenSet }> diff --git a/src/screens/Login/LoginForm.tsx b/src/screens/Login/LoginForm.tsx index 0c541c5a90..6f20354be0 100644 --- a/src/screens/Login/LoginForm.tsx +++ b/src/screens/Login/LoginForm.tsx @@ -1,12 +1,10 @@ import React from 'react' import {Keyboard, View} from 'react-native' -import * as Browser from 'expo-web-browser' import {ComAtprotoServerDescribeServer} from '@atproto/api' import {msg, Trans} from '@lingui/macro' import {useLingui} from '@lingui/react' import {useAnalytics} from '#/lib/analytics/analytics' -import {isAndroid} from 'platform/detection' import {useLogin} from '#/screens/Login/hooks/useLogin' import {atoms as a} from '#/alf' import {Button, ButtonText} from '#/components/Button' @@ -34,14 +32,7 @@ export const LoginForm = ({ }) => { const {track} = useAnalytics() const {_} = useLingui() - const {openAuthSession} = useLogin(serviceUrl) - - // This improves speed at which the browser presents itself on Android - React.useEffect(() => { - if (isAndroid) { - Browser.warmUpAsync() - } - }, []) + const {openAuthSession} = useLogin() const onPressSelectService = React.useCallback(() => { Keyboard.dismiss() From c499a0366100b9c525cb7e7e4e2846b106367d05 Mon Sep 17 00:00:00 2001 From: Hailey Date: Mon, 15 Apr 2024 02:48:01 -0700 Subject: [PATCH 49/54] fix android --- .../main/java/expo/modules/blueskyoauthclient/CryptoUtil.kt | 3 +-- .../modules/blueskyoauthclient/ExpoBlueskyOAuthClientModule.kt | 3 ++- .../src/react-native-crypto-implementation.ts | 1 - 3 files changed, 3 insertions(+), 4 deletions(-) diff --git a/modules/expo-bluesky-oauth-client/android/src/main/java/expo/modules/blueskyoauthclient/CryptoUtil.kt b/modules/expo-bluesky-oauth-client/android/src/main/java/expo/modules/blueskyoauthclient/CryptoUtil.kt index 4a40453a39..eab729a8b8 100644 --- a/modules/expo-bluesky-oauth-client/android/src/main/java/expo/modules/blueskyoauthclient/CryptoUtil.kt +++ b/modules/expo-bluesky-oauth-client/android/src/main/java/expo/modules/blueskyoauthclient/CryptoUtil.kt @@ -11,8 +11,7 @@ import com.nimbusds.jose.jwk.KeyUse import java.util.UUID class CryptoUtil { - fun digest(data: ByteArray, algorithmName: String): ByteArray { - if(algorithmName != "sha256") throw Exception("Unsupported algorithm") + fun digest(data: ByteArray): ByteArray { val digest = MessageDigest.getInstance("sha256") return digest.digest(data) } diff --git a/modules/expo-bluesky-oauth-client/android/src/main/java/expo/modules/blueskyoauthclient/ExpoBlueskyOAuthClientModule.kt b/modules/expo-bluesky-oauth-client/android/src/main/java/expo/modules/blueskyoauthclient/ExpoBlueskyOAuthClientModule.kt index 15e6305b45..93e7f70364 100644 --- a/modules/expo-bluesky-oauth-client/android/src/main/java/expo/modules/blueskyoauthclient/ExpoBlueskyOAuthClientModule.kt +++ b/modules/expo-bluesky-oauth-client/android/src/main/java/expo/modules/blueskyoauthclient/ExpoBlueskyOAuthClientModule.kt @@ -8,7 +8,8 @@ class ExpoBlueskyOAuthClientModule : Module() { override fun definition() = ModuleDefinition { Name("ExpoBlueskyOAuthClient") - AsyncFunction("digest") { value: ByteArray -> + AsyncFunction("digest") { value: ByteArray, algorithmName: String -> + if(algorithmName != "sha256") throw Exception("Unsupported algorithm") return@AsyncFunction CryptoUtil().digest(value) } diff --git a/modules/expo-bluesky-oauth-client/src/react-native-crypto-implementation.ts b/modules/expo-bluesky-oauth-client/src/react-native-crypto-implementation.ts index 1de6d7e6b5..26f58dc7b2 100644 --- a/modules/expo-bluesky-oauth-client/src/react-native-crypto-implementation.ts +++ b/modules/expo-bluesky-oauth-client/src/react-native-crypto-implementation.ts @@ -18,7 +18,6 @@ export class ReactNativeCryptoImplementation implements CryptoImplementation { bytes: Uint8Array, algorithm: DigestAlgorithm, ): Promise { - console.log(algorithm) return OauthClientReactNative.digest(bytes, algorithm.name) } } From 7ed21e448d982a0e6a6965f3d094ea379b12dd3c Mon Sep 17 00:00:00 2001 From: Hailey Date: Mon, 15 Apr 2024 03:52:10 -0700 Subject: [PATCH 50/54] rely on zod to remove os specific restraints --- .../ios/CryptoUtil.swift | 30 +-- .../ios/ExpoBlueskyOAuthClientModule.swift | 4 +- .../expo-bluesky-oauth-client/ios/JWK.swift | 74 +++----- .../expo-bluesky-oauth-client/ios/JWT.swift | 179 +----------------- .../ios/JWTUtil.swift | 60 ++++-- .../src/oauth-client-react-native.ts | 10 +- .../src/react-native-key.ts | 17 +- 7 files changed, 90 insertions(+), 284 deletions(-) diff --git a/modules/expo-bluesky-oauth-client/ios/CryptoUtil.swift b/modules/expo-bluesky-oauth-client/ios/CryptoUtil.swift index fd20643f9a..a3c26069ae 100644 --- a/modules/expo-bluesky-oauth-client/ios/CryptoUtil.swift +++ b/modules/expo-bluesky-oauth-client/ios/CryptoUtil.swift @@ -14,7 +14,7 @@ class CryptoUtil { return Data(bytes) } - public static func generateKeyPair() throws -> JWKPair? { + public static func generateKeyPair() throws -> [String:String] { let keyIdString = UUID().uuidString let privateKey = P256.Signing.PrivateKey() @@ -23,28 +23,14 @@ class CryptoUtil { let x = publicKey.x963Representation[1..<33].base64URLEncodedString() let y = publicKey.x963Representation[33...].base64URLEncodedString() let d = privateKey.rawRepresentation.base64URLEncodedString() + + let publicJWK = JWK(kty: "EC", use: "sig", crv: "P-256", kid: keyIdString, x: x, y: y, alg: "ES256") + let privateJWK = JWK(kty: "EC", use: "sig", crv: "P-256", kid: keyIdString, x: x, y: y, d: d, alg: "ES256") - let publicJWK = JWK( - alg: "ES256".toField(), - kty: "EC".toField(), - crv: "P-256".toNullableField(), - x: x.toNullableField(), - y: y.toNullableField(), - use: "sig".toNullableField(), - kid: keyIdString.toNullableField() - ) - let privateJWK = JWK( - alg: "ES256".toField(), - kty: "EC".toField(), - crv: "P-256".toNullableField(), - x: x.toNullableField(), - y: y.toNullableField(), - d: d.toNullableField(), - use: "sig".toNullableField(), - kid: keyIdString.toNullableField() - ) - - return JWKPair(privateKey: privateJWK.toField(), publicKey: publicJWK.toField()) + return [ + "privateKey": privateJWK.toJson(), + "publicKey": publicJWK.toJson() + ] } } diff --git a/modules/expo-bluesky-oauth-client/ios/ExpoBlueskyOAuthClientModule.swift b/modules/expo-bluesky-oauth-client/ios/ExpoBlueskyOAuthClientModule.swift index 702e56253a..386b8d6430 100644 --- a/modules/expo-bluesky-oauth-client/ios/ExpoBlueskyOAuthClientModule.swift +++ b/modules/expo-bluesky-oauth-client/ios/ExpoBlueskyOAuthClientModule.swift @@ -34,7 +34,7 @@ public class ExpoBlueskyOAuthClientModule: Module { promise.resolve(keypair) } - AsyncFunction("createJwt") { (header: JWTHeader, payload: JWTPayload, jwk: JWK, promise: Promise) in + AsyncFunction("createJwt") { (header: String, payload: String, jwk: String, promise: Promise) in guard let jwt = JWTUtil.createJwt(header: header, payload: payload, jwk: jwk) else { promise.reject("JWTError", "Error creating JWT.") return @@ -42,7 +42,7 @@ public class ExpoBlueskyOAuthClientModule: Module { promise.resolve(jwt) } - AsyncFunction("verifyJwt") { (token: String, jwk: JWK, promise: Promise) in + AsyncFunction("verifyJwt") { (token: String, jwk: String, promise: Promise) in promise.resolve(JWTUtil.verifyJwt(token: token, jwk: jwk)) } } diff --git a/modules/expo-bluesky-oauth-client/ios/JWK.swift b/modules/expo-bluesky-oauth-client/ios/JWK.swift index daa7206914..72148a9cbf 100644 --- a/modules/expo-bluesky-oauth-client/ios/JWK.swift +++ b/modules/expo-bluesky-oauth-client/ios/JWK.swift @@ -1,58 +1,32 @@ import ExpoModulesCore import JOSESwift -struct JWK : Record { - @Field - var alg: String - @Field - var kty: String - @Field - var crv: String? - @Field - var x: String? - @Field - var y: String? - @Field - var e: String? - @Field - var n: String? - @Field +struct JWK { + let kty: String + let use: String + let crv: String + let kid: String + let x: String + let y: String var d: String? - @Field - var use: String? - @Field - var kid: String? + let alg: String - func toField() -> Field { - return Field(wrappedValue: self) - } - - func toPrivateSecKey() throws -> SecKey? { - let jsonData = try JSONSerialization.data(withJSONObject: self.toDictionary()) - guard let jwk = try? JSONDecoder().decode(ECPrivateKey.self, from: jsonData), - let key = try? jwk.converted(to: SecKey.self) - else { - print("Error creating SecKey.") - return nil - } - return key - } - - func toPublicSecKey() throws -> SecKey? { - let jsonData = try JSONSerialization.data(withJSONObject: self.toDictionary()) - guard let jwk = try? JSONDecoder().decode(ECPublicKey.self, from: jsonData), - let key = try? jwk.converted(to: SecKey.self) - else { - print("Error creating SecKey.") - return nil + func toJson() -> String { + var dict: [String: Any] = [ + "kty": kty, + "use": use, + "crv": crv, + "kid": kid, + "x": x, + "y": y, + "alg": alg, + ] + + if let d = d { + dict["d"] = d } - return key - } -} -struct JWKPair : Record { - @Field - var privateKey: JWK - @Field - var publicKey: JWK + let jsonData = try! JSONSerialization.data(withJSONObject: dict, options: []) + return String(data: jsonData, encoding: .utf8)! + } } diff --git a/modules/expo-bluesky-oauth-client/ios/JWT.swift b/modules/expo-bluesky-oauth-client/ios/JWT.swift index 8ce014048a..18c68cbf04 100644 --- a/modules/expo-bluesky-oauth-client/ios/JWT.swift +++ b/modules/expo-bluesky-oauth-client/ios/JWT.swift @@ -1,184 +1,9 @@ import ExpoModulesCore import JOSESwift -struct JWTHeader : Record { - @Field - var alg: String = "ES256" - @Field - var jku: String? - @Field - var jwk: JWK - @Field - var kid: String? - @Field - var typ: String? - @Field - var cty: String? - @Field - var crit: String? - - func toField() -> Field { - return Field(wrappedValue: self) - } - - func toJWSHeader() throws -> JWSHeader? { - return JWSHeader(try JSONSerialization.data(withJSONObject: self.toDictionary())) - } -} - -struct JWTPayload : Record { - @Field - var iss: String? - @Field - var aud: String? - @Field - var sub: String? - @Field - var exp: Int? - @Field - var nbr: Int? - @Field - var iat: Int? - @Field - var jti: String? - @Field - var htm: String? - @Field - var htu: String? - @Field - var ath: String? - @Field - var acr: String? - @Field - var azp: String? - @Field - var amr: String? - @Field - var cnf: JWTPayloadCNF? - @Field - var client_id: String? - @Field - var scope: String? - @Field - var nonce: String? - @Field - var at_hash: String? - @Field - var c_hash: String? - @Field - var s_hash: String? - @Field - var auth_time: Int? - @Field - var name: String? - @Field - var family_name: String? - @Field - var given_name: String? - @Field - var middle_name: String? - @Field - var nickname: String? - @Field - var preferred_username: String? - @Field - var gender: String? - @Field - var picture: String? - @Field - var profile: String? - @Field - var website: String? - @Field - var birthdate: String? - @Field - var zoneinfo: String? - @Field - var locale: String? - @Field - var updated_at: Int? - @Field - var email: String? - @Field - var email_verified: Bool? - @Field - var phone_number: String? - @Field - var phone_number_verified: Bool? - @Field - var address: JWTPayloadAddress? - @Field - var authorization_details: JWTPayloadAuthorizationDetails? - - func toField() -> Field { - return Field(wrappedValue: self) - } - - func toPayload() throws -> Payload { - return Payload(try JSONSerialization.data(withJSONObject: self.toDictionary())) - } -} - -struct JWTPayloadCNF : Record { - @Field - var kid: String? - @Field - var jwk: JWK? - @Field - var jwe: String? - @Field - var jku: String? - @Field - var jkt: String? - @Field - var osc: String? - - func toField() -> Field { - return Field(wrappedValue: self) - } -} - -struct JWTPayloadAddress : Record { - @Field - var formatted: String? - @Field - var street_address: String? - @Field - var locality: String? - @Field - var region: String? - @Field - var postal_code: String? - @Field - var country: String? - - func toField() -> Field { - return Field(wrappedValue: self) - } -} - -struct JWTPayloadAuthorizationDetails : Record { - @Field - var type: String - @Field - var locations: [String]? - @Field - var actions: [String]? - @Field - var datatypes: [String]? - @Field - var identifier: String? - @Field - var privileges: [String]? - - func toField() -> Field { - return Field(wrappedValue: self) - } -} - struct JWTVerifyResponse : Record { @Field - var protectedHeader: JWTHeader + var payload: String? @Field - var payload: String + var protectedHeader: String? } diff --git a/modules/expo-bluesky-oauth-client/ios/JWTUtil.swift b/modules/expo-bluesky-oauth-client/ios/JWTUtil.swift index f14d2a113d..5997f587f1 100644 --- a/modules/expo-bluesky-oauth-client/ios/JWTUtil.swift +++ b/modules/expo-bluesky-oauth-client/ios/JWTUtil.swift @@ -2,6 +2,18 @@ import ExpoModulesCore import JOSESwift class JWTUtil { + static func jsonToPrivateKey(_ jwkString: String) throws -> SecKey? { + guard let jsonData = jwkString.data(using: .utf8), + let jwk = try? JSONDecoder().decode(ECPrivateKey.self, from: jsonData), + let key = try? jwk.converted(to: SecKey.self) + else { + print("Error creating JWK from JWK string.") + return nil + } + + return key + } + static func jsonToPublicKey(_ jwkString: String) throws -> SecKey? { guard let jsonData = jwkString.data(using: .utf8), let jwk = try? JSONDecoder().decode(ECPublicKey.self, from: jsonData), @@ -32,12 +44,11 @@ class JWTUtil { return JWSHeader(headerData) } - public static func createJwt(header: JWTHeader, payload: JWTPayload, jwk: JWK) -> String? { - guard let header = try? header.toJWSHeader(), - let payload = try? payload.toPayload(), - let key = try? jwk.toPrivateSecKey() + public static func createJwt(header: String, payload: String, jwk: String) -> String? { + guard let header = headerStringToPayload(header), + let payload = payloadStringToPayload(payload), + let key = try? jsonToPrivateKey(jwk) else { - print("didn't have one") return nil } @@ -53,8 +64,8 @@ class JWTUtil { return jws.compactSerializedString } - public static func verifyJwt(token: String, jwk: JWK) -> JWTVerifyResponse? { - guard let key = try? jwk.toPublicSecKey(), + public static func verifyJwt(token: String, jwk: String) -> [String: Any]? { + guard let key = try? jsonToPublicKey(jwk), let jws = try? JWS(compactSerialization: token), let verifier = Verifier(verifyingAlgorithm: .ES256, key: key), let validation = try? jws.validate(using: verifier) @@ -63,24 +74,33 @@ class JWTUtil { } let header = validation.header - let serializedHeader = JWTHeader( - alg: "ES256", - jku: Field(wrappedValue: header.jku?.absoluteString), - kid: Field(wrappedValue:header.kid), - typ: Field(wrappedValue: header.typ), - cty: Field(wrappedValue: header.cty), - crit: Field(wrappedValue: header.cty) - ) - let payload = String(data: validation.payload.data(), encoding: .utf8) guard let payload = payload else { return nil } - return JWTVerifyResponse( - protectedHeader: serializedHeader.toField(), - payload: payload.toField() - ) + var protectedHeader: [String:Any] = [:] + protectedHeader["alg"] = "ES256" + if header.jku != nil { + protectedHeader["jku"] = header.jku?.absoluteString + } + if header.kid != nil { + protectedHeader["kid"] = header.kid + } + if header.typ != nil { + protectedHeader["typ"] = header.typ + } + if header.cty != nil { + protectedHeader["cty"] = header.cty + } + if header.crit != nil { + protectedHeader["crit"] = header.crit + } + + return [ + "payload": payload, + "protectedHeader": protectedHeader + ] } } diff --git a/modules/expo-bluesky-oauth-client/src/oauth-client-react-native.ts b/modules/expo-bluesky-oauth-client/src/oauth-client-react-native.ts index 2816b7055a..cf29ac193e 100644 --- a/modules/expo-bluesky-oauth-client/src/oauth-client-react-native.ts +++ b/modules/expo-bluesky-oauth-client/src/oauth-client-react-native.ts @@ -1,5 +1,5 @@ import {requireNativeModule} from 'expo-modules-core' -import {Jwk, Jwt, Key} from '@atproto/jwk' +import {Jwk, Jwt} from '@atproto/jwk' const NativeModule = requireNativeModule('ExpoBlueskyOAuthClient') @@ -24,7 +24,9 @@ export const OauthClientReactNative = (NativeModule as null) || { * * @throws if the algorithm is not supported ("ES256" must be supported) */ - async generateJwk(_algo: string): Promise<{publicKey: Key; privateKey: Key}> { + async generateJwk( + _algo: string, + ): Promise<{publicKey: string; privateKey: string}> { throw new Error(LINKING_ERROR) }, @@ -40,8 +42,8 @@ export const OauthClientReactNative = (NativeModule as null) || { _token: Jwt, _jwk: Jwk, ): Promise<{ - payload: string // this is a JSON response to make Swift a bit easier to work with - protectedHeader: Record + payload: string + protectedHeader: string }> { throw new Error(LINKING_ERROR) }, diff --git a/modules/expo-bluesky-oauth-client/src/react-native-key.ts b/modules/expo-bluesky-oauth-client/src/react-native-key.ts index 6d93b0cfdb..d13754ddce 100644 --- a/modules/expo-bluesky-oauth-client/src/react-native-key.ts +++ b/modules/expo-bluesky-oauth-client/src/react-native-key.ts @@ -19,7 +19,7 @@ export class ReactNativeKey extends Key { // Note: OauthClientReactNative.generatePrivateJwk should throw if it // doesn't support the algorithm. const res = await OauthClientReactNative.generateJwk(algo) - const jwk = res.privateKey + const jwk = JSON.parse(res.privateKey) as Record const use = jwk.use || 'sig' return new ReactNativeKey(jwkValidator.parse({...jwk, use, kid})) } catch { @@ -31,7 +31,11 @@ export class ReactNativeKey extends Key { } async createJwt(header: JwtHeader, payload: JwtPayload): Promise { - return OauthClientReactNative.createJwt(header, payload, this.jwk) + return await OauthClientReactNative.createJwt( + JSON.stringify(header), + JSON.stringify(payload), + JSON.stringify(this.jwk), + ) } async verifyJwt< @@ -40,16 +44,11 @@ export class ReactNativeKey extends Key { >(token: Jwt, options?: VerifyOptions): Promise> { const result = await OauthClientReactNative.verifyJwt(token, this.jwk) - // TODO see if we can make these `undefined` or maybe update zod to allow `nullable()` let payloadParsed = JSON.parse(result.payload) - payloadParsed = Object.fromEntries( - Object.entries(payloadParsed as object).filter(([_, v]) => v !== null), - ) const payload = jwtPayloadSchema.parse(payloadParsed) - // We don't need to validate this, because the native types ensure it is correct. But this is a TODO - // for the same reason above - const protectedHeader = result.protectedHeader + let protectedHeaderParsed = JSON.parse(result.protectedHeader) + const protectedHeader = jwtPayloadSchema.parse(protectedHeaderParsed) if (options?.audience != null) { const audience = Array.isArray(options.audience) From f464875db0fc9ef3e8e06d2e3379687f29b04db5 Mon Sep 17 00:00:00 2001 From: Hailey Date: Mon, 15 Apr 2024 09:25:43 -0700 Subject: [PATCH 51/54] simplify kt --- .../modules/blueskyoauthclient/CryptoUtil.kt | 26 +-- .../ExpoBlueskyOAuthClientModule.kt | 4 +- .../expo/modules/blueskyoauthclient/JWK.kt | 33 +--- .../expo/modules/blueskyoauthclient/JWT.kt | 170 +----------------- .../modules/blueskyoauthclient/JWTUtil.kt | 44 +---- 5 files changed, 21 insertions(+), 256 deletions(-) diff --git a/modules/expo-bluesky-oauth-client/android/src/main/java/expo/modules/blueskyoauthclient/CryptoUtil.kt b/modules/expo-bluesky-oauth-client/android/src/main/java/expo/modules/blueskyoauthclient/CryptoUtil.kt index eab729a8b8..e47060aa95 100644 --- a/modules/expo-bluesky-oauth-client/android/src/main/java/expo/modules/blueskyoauthclient/CryptoUtil.kt +++ b/modules/expo-bluesky-oauth-client/android/src/main/java/expo/modules/blueskyoauthclient/CryptoUtil.kt @@ -22,7 +22,7 @@ class CryptoUtil { return random } - fun generateKeyPair(): Any { + fun generateKeyPair(): Map { val keyIdString = UUID.randomUUID().toString() val keyPairGen = KeyPairGenerator.getInstance("EC") @@ -44,27 +44,9 @@ class CryptoUtil { .algorithm(Algorithm.parse("ES256")) .build() - - return JWKPair( - JWK( - alg = privateJwk.algorithm.toString(), - kty = privateJwk.keyType.toString(), - crv = privateJwk.curve.toString(), - x = privateJwk.x.toString(), - y = privateJwk.y.toString(), - d = privateJwk.d.toString(), - use = privateJwk.keyUse.toString(), - kid = privateJwk.keyID - ), - JWK( - alg = publicJwk.algorithm.toString(), - kty = publicJwk.keyType.toString(), - crv = publicJwk.curve.toString(), - x = publicJwk.x.toString(), - y = publicJwk.y.toString(), - use = publicJwk.keyUse.toString(), - kid = publicJwk.keyID - ) + return mapOf( + "privateKey" to privateJwk.toJSONString(), + "publicKey" to publicJwk.toJSONString() ) } } diff --git a/modules/expo-bluesky-oauth-client/android/src/main/java/expo/modules/blueskyoauthclient/ExpoBlueskyOAuthClientModule.kt b/modules/expo-bluesky-oauth-client/android/src/main/java/expo/modules/blueskyoauthclient/ExpoBlueskyOAuthClientModule.kt index 93e7f70364..8ebdf91e50 100644 --- a/modules/expo-bluesky-oauth-client/android/src/main/java/expo/modules/blueskyoauthclient/ExpoBlueskyOAuthClientModule.kt +++ b/modules/expo-bluesky-oauth-client/android/src/main/java/expo/modules/blueskyoauthclient/ExpoBlueskyOAuthClientModule.kt @@ -17,14 +17,14 @@ class ExpoBlueskyOAuthClientModule : Module() { return@Function CryptoUtil().getRandomValues(byteLength) } - AsyncFunction("generateJwk") { algorithim: String -> + AsyncFunction("generateJwk") { algorithim: String? -> if (algorithim != "ES256") { throw Exception("Unsupported algorithm") } return@AsyncFunction CryptoUtil().generateKeyPair() } - AsyncFunction("createJwt") { header: JWTHeader, payload: JWTPayload, jwk: JWK -> + AsyncFunction("createJwt") { header: String, payload: String, jwk: String -> return@AsyncFunction JWTUtil().createJwt(header, payload, jwk) } diff --git a/modules/expo-bluesky-oauth-client/android/src/main/java/expo/modules/blueskyoauthclient/JWK.kt b/modules/expo-bluesky-oauth-client/android/src/main/java/expo/modules/blueskyoauthclient/JWK.kt index 2ce71dd8fa..9f9f1dd629 100644 --- a/modules/expo-bluesky-oauth-client/android/src/main/java/expo/modules/blueskyoauthclient/JWK.kt +++ b/modules/expo-bluesky-oauth-client/android/src/main/java/expo/modules/blueskyoauthclient/JWK.kt @@ -3,32 +3,7 @@ package expo.modules.blueskyoauthclient import expo.modules.kotlin.records.Record import expo.modules.kotlin.records.Field -class JWK( - @Field var alg: String = "", - @Field var kty: String = "", - @Field var crv: String? = null, - @Field var x: String? = null, - @Field var y: String? = null, - @Field var e: String? = null, - @Field var n: String? = null, - @Field var d: String? = null, - @Field var use: String? = null, - @Field var kid: String? = null -) : Record { - fun toJson(): String { - val parts = mutableListOf() - if (alg.isNotEmpty()) parts.add("\"alg\": \"$alg\"") - if (kty.isNotEmpty()) parts.add("\"kty\": \"$kty\"") - if (crv != null) parts.add("\"crv\": \"$crv\"") - if (x != null) parts.add("\"x\": \"$x\"") - if (y != null) parts.add("\"y\": \"$y\"") - if (e != null) parts.add("\"e\": \"$e\"") - if (n != null) parts.add("\"n\": \"$n\"") - if (d != null) parts.add("\"d\": \"$d\"") - if (use != null) parts.add("\"use\": \"$use\"") - if (kid != null) parts.add("\"kid\": \"$kid\"") - return "{ ${parts.joinToString()} }" - } -} - -class JWKPair(@Field val privateKey: JWK, @Field val publicKey: JWK) : Record \ No newline at end of file +class JWKPair( + @Field val privateKey: String, + @Field val publicKey: String +) : Record \ No newline at end of file diff --git a/modules/expo-bluesky-oauth-client/android/src/main/java/expo/modules/blueskyoauthclient/JWT.kt b/modules/expo-bluesky-oauth-client/android/src/main/java/expo/modules/blueskyoauthclient/JWT.kt index 8b761615a0..4990fa66a2 100644 --- a/modules/expo-bluesky-oauth-client/android/src/main/java/expo/modules/blueskyoauthclient/JWT.kt +++ b/modules/expo-bluesky-oauth-client/android/src/main/java/expo/modules/blueskyoauthclient/JWT.kt @@ -3,173 +3,7 @@ package expo.modules.blueskyoauthclient import expo.modules.kotlin.records.Record import expo.modules.kotlin.records.Field -class JWTHeader( - @Field var alg: String = "", - @Field var jku: String? = null, - @Field var jwk: JWK? = null, - @Field var kid: String? = null, - @Field var typ: String? = null, - @Field var cty: String? = null, - @Field var crit: String? = null -) : Record { - fun toJson(): String { - val parts = mutableListOf() - if (alg.isNotEmpty()) parts.add("\"alg\": \"$alg\"") - if (jku != null) parts.add("\"jku\": \"$jku\"") - if (jwk != null) parts.add("\"jwk\": ${jwk?.toJson()}") - if (kid != null) parts.add("\"kid\": \"$kid\"") - if (typ != null) parts.add("\"typ\": \"$typ\"") - if (cty != null) parts.add("\"cty\": \"$cty\"") - if (crit != null) parts.add("\"crit\": \"$crit\"") - return "{ ${parts.joinToString()} }" - } -} - -class JWTPayload( - @Field var iss: String? = null, - @Field var aud: String? = null, - @Field var sub: String? = null, - @Field var exp: Int? = null, - @Field var nbr: Int? = null, - @Field var iat: Int? = null, - @Field var jti: String? = null, - @Field var htm: String? = null, - @Field var htu: String? = null, - @Field var ath: String? = null, - @Field var acr: String? = null, - @Field var azp: String? = null, - @Field var amr: String? = null, - @Field var cnf: JWTPayloadCNF? = null, - @Field var client_id: String? = null, - @Field var scope: String? = null, - @Field var nonce: String? = null, - @Field var at_hash: String? = null, - @Field var c_hash: String? = null, - @Field var s_hash: String? = null, - @Field var auth_time: Int? = null, - @Field var name: String? = null, - @Field var family_name: String? = null, - @Field var given_name: String? = null, - @Field var middle_name: String? = null, - @Field var nickname: String? = null, - @Field var preferred_username: String? = null, - @Field var gender: String? = null, - @Field var picture: String? = null, - @Field var profile: String? = null, - @Field var birthdate: String? = null, - @Field var zoneinfo: String? = null, - @Field var updated_at: Int? = null, - @Field var email: String? = null, - @Field var email_verified: Boolean? = null, - @Field var phone_number: String? = null, - @Field var phone_number_verified: Boolean? = null, - @Field var address: JWTPayloadAddress? = null, - @Field var authorization_details: JWTPayloadAuthorizationDetails? = null -) : Record { - fun toJson(): String { - val parts = mutableListOf() - if (iss != null) parts.add("\"iss\": \"$iss\"") - if (aud != null) parts.add("\"aud\": \"$aud\"") - if (sub != null) parts.add("\"sub\": \"$sub\"") - if (exp != null) parts.add("\"exp\": $exp") - if (nbr != null) parts.add("\"nbr\": $nbr") - if (iat != null) parts.add("\"iat\": $iat") - if (jti != null) parts.add("\"jti\": \"$jti\"") - if (htm != null) parts.add("\"htm\": \"$htm\"") - if (htu != null) parts.add("\"htu\": \"$htu\"") - if (ath != null) parts.add("\"ath\": \"$ath\"") - if (acr != null) parts.add("\"acr\": \"$acr\"") - if (azp != null) parts.add("\"azp\": \"$azp\"") - if (amr != null) parts.add("\"amr\": \"$amr\"") - if (cnf != null) parts.add("\"cnf\": ${cnf?.toJson()}") - if (client_id != null) parts.add("\"client_id\": \"$client_id\"") - if (scope != null) parts.add("\"scope\": \"$scope\"") - if (nonce != null) parts.add("\"nonce\": \"$nonce\"") - if (at_hash != null) parts.add("\"at_hash\": \"$at_hash\"") - if (c_hash != null) parts.add("\"c_hash\": \"$c_hash\"") - if (s_hash != null) parts.add("\"s_hash\": \"$s_hash\"") - if (auth_time != null) parts.add("\"auth_time\": $auth_time") - if (name != null) parts.add("\"name\": \"$name\"") - if (family_name != null) parts.add("\"family_name\": \"$family_name\"") - if (given_name != null) parts.add("\"given_name\": \"$given_name\"") - if (middle_name != null) parts.add("\"middle_name\": \"$middle_name\"") - if (nickname != null) parts.add("\"nickname\": \"$nickname\"") - if (preferred_username != null) parts.add("\"preferred_username\": \"$preferred_username\"") - if (gender != null) parts.add("\"gender\": \"$gender\"") - if (picture != null) parts.add("\"picture\": \"$picture\"") - if (profile != null) parts.add("\"profile\": \"$profile\"") - if (birthdate != null) parts.add("\"birthdate\": \"$birthdate\"") - if (zoneinfo != null) parts.add("\"zoneinfo\": \"$zoneinfo\"") - if (updated_at != null) parts.add("\"updated_at\": $updated_at") - if (email != null) parts.add("\"email\": \"$email\"") - if (email_verified != null) parts.add("\"email_verified\": $email_verified") - if (phone_number != null) parts.add("\"phone_number\": \"$phone_number\"") - if (phone_number_verified != null) parts.add("\"phone_number_verified\": $phone_number_verified") - if (address != null) parts.add("\"address\": ${address?.toJson()}") - if (authorization_details != null) parts.add("\"authorization_details\": ${authorization_details?.toJson()}") - return "{ ${parts.joinToString()} }" - } -} - -class JWTPayloadCNF( - @Field var jwk: JWK? = null, - @Field var jwe: String? = null, - @Field var jku: String? = null, - @Field var jkt: String? = null, - @Field var osc: String? = null -) : Record { - fun toJson(): String { - val parts = mutableListOf() - if (jwk != null) parts.add("\"jwk\": ${jwk?.toJson()}") - if (jwe != null) parts.add("\"jwe\": \"$jwe\"") - if (jku != null) parts.add("\"jku\": \"$jku\"") - if (jkt != null) parts.add("\"jkt\": \"$jkt\"") - if (osc != null) parts.add("\"osc\": \"$osc\"") - return "{ ${parts.joinToString()} }" - } -} - -class JWTPayloadAddress( - @Field var formatted: String? = null, - @Field var street_address: String? = null, - @Field var locality: String? = null, - @Field var region: String? = null, - @Field var postal_code: String? = null, - @Field var country: String? = null -) : Record { - fun toJson(): String { - val parts = mutableListOf() - if (formatted != null) parts.add("\"formatted\": \"$formatted\"") - if (street_address != null) parts.add("\"street_address\": \"$street_address\"") - if (locality != null) parts.add("\"locality\": \"$locality\"") - if (region != null) parts.add("\"region\": \"$region\"") - if (postal_code != null) parts.add("\"postal_code\": \"$postal_code\"") - if (country != null) parts.add("\"country\": \"$country\"") - return "{ ${parts.joinToString()} }" - } -} - -class JWTPayloadAuthorizationDetails( - @Field var type: String? = null, - @Field var locations: Array? = null, - @Field var actions: Array? = null, - @Field var datatypes: Array? = null, - @Field var identifier: String? = null, - @Field var privileges: Array? = null -) : Record { - fun toJson(): String { - val parts = mutableListOf() - if (type != null) parts.add("\"type\": \"$type\"") - if (locations != null) parts.add("\"locations\": [${locations?.joinToString()}]") - if (actions != null) parts.add("\"actions\": [${actions?.joinToString()}]") - if (datatypes != null) parts.add("\"datatypes\": [${datatypes?.joinToString()}]") - if (identifier != null) parts.add("\"identifier\": \"$identifier\"") - if (privileges != null) parts.add("\"privileges\": [${privileges?.joinToString()}]") - return "{ ${parts.joinToString()} }" - } -} - class JWTVerifyResponse( - @Field var protectedHeader: JWTHeader = JWTHeader(), - @Field var payload: String = "", + @Field var protectedHeader: String, + @Field var payload: String, ) : Record \ No newline at end of file diff --git a/modules/expo-bluesky-oauth-client/android/src/main/java/expo/modules/blueskyoauthclient/JWTUtil.kt b/modules/expo-bluesky-oauth-client/android/src/main/java/expo/modules/blueskyoauthclient/JWTUtil.kt index 1fb9276e5d..f01d4a79bc 100644 --- a/modules/expo-bluesky-oauth-client/android/src/main/java/expo/modules/blueskyoauthclient/JWTUtil.kt +++ b/modules/expo-bluesky-oauth-client/android/src/main/java/expo/modules/blueskyoauthclient/JWTUtil.kt @@ -8,10 +8,10 @@ import com.nimbusds.jwt.JWTClaimsSet import com.nimbusds.jwt.SignedJWT class JWTUtil { - fun createJwt(header: JWTHeader, payload: JWTPayload, jwk: JWK): String { - val parsedKey = ECKey.parse(jwk.toJson()) - val parsedHeader = JWSHeader.parse(header.toJson()) - val parsedPayload = JWTClaimsSet.parse(payload.toJson()) + fun createJwt(header: String, payload: String, jwk: String): String { + val parsedKey = ECKey.parse(jwk) + val parsedHeader = JWSHeader.parse(header) + val parsedPayload = JWTClaimsSet.parse(payload) val signer = ECDSASigner(parsedKey) val jwt = SignedJWT(parsedHeader, parsedPayload) @@ -20,9 +20,9 @@ class JWTUtil { return jwt.serialize() } - fun verifyJwt(token: String, jwk: JWK): JWTVerifyResponse { + fun verifyJwt(token: String, jwk: String): Map { try { - val parsedKey = ECKey.parse(jwk.toJson()) + val parsedKey = ECKey.parse(jwk) val jwt = SignedJWT.parse(token) val verifier = ECDSAVerifier(parsedKey) @@ -32,36 +32,10 @@ class JWTUtil { val header = jwt.header val payload = jwt.payload - val ecKey = header.jwk?.toECKey() - val serializedJwk = if (ecKey != null) { - JWK( - alg = ecKey.algorithm.toString(), - kty = ecKey.keyType.toString(), - crv = ecKey.curve.toString(), - x = ecKey.x.toString(), - y = ecKey.y.toString(), - d = ecKey.d.toString(), - use = ecKey.keyUse.toString(), - kid = ecKey.keyID - ) - } else { - null - } - - val serializedHeader = JWTHeader( - alg = header.algorithm.toString(), - jku = header.jwkurl?.toString(), - jwk = serializedJwk, - kid = header.keyID, - typ = header.type?.toString(), - cty = header.contentType, - crit = header.criticalParams?.joinToString() - ) - val serializedPayload = payload.toString() - return JWTVerifyResponse( - protectedHeader = serializedHeader, - payload = serializedPayload, + return mapOf( + "payload" to payload.toString(), + "protectedHeader" to header.toString() ) } catch(e: Exception) { throw e From 20f298853139fda58a599eace62bb9e4177559ea Mon Sep 17 00:00:00 2001 From: Hailey Date: Wed, 17 Apr 2024 02:16:24 -0700 Subject: [PATCH 52/54] more progress working through this --- .../ExpoBlueskyOAuthClientModule.kt | 2 +- ...eact-native-oauth-client-factory.native.ts | 120 +++++++++--------- 2 files changed, 64 insertions(+), 58 deletions(-) diff --git a/modules/expo-bluesky-oauth-client/android/src/main/java/expo/modules/blueskyoauthclient/ExpoBlueskyOAuthClientModule.kt b/modules/expo-bluesky-oauth-client/android/src/main/java/expo/modules/blueskyoauthclient/ExpoBlueskyOAuthClientModule.kt index 8ebdf91e50..d97c5752f8 100644 --- a/modules/expo-bluesky-oauth-client/android/src/main/java/expo/modules/blueskyoauthclient/ExpoBlueskyOAuthClientModule.kt +++ b/modules/expo-bluesky-oauth-client/android/src/main/java/expo/modules/blueskyoauthclient/ExpoBlueskyOAuthClientModule.kt @@ -28,7 +28,7 @@ class ExpoBlueskyOAuthClientModule : Module() { return@AsyncFunction JWTUtil().createJwt(header, payload, jwk) } - AsyncFunction("verifyJwt") { token: String, jwk: JWK -> + AsyncFunction("verifyJwt") { token: String, jwk: String -> return@AsyncFunction JWTUtil().verifyJwt(token, jwk) } } diff --git a/modules/expo-bluesky-oauth-client/src/react-native-oauth-client-factory.native.ts b/modules/expo-bluesky-oauth-client/src/react-native-oauth-client-factory.native.ts index 754a26f95f..ed6a292c79 100644 --- a/modules/expo-bluesky-oauth-client/src/react-native-oauth-client-factory.native.ts +++ b/modules/expo-bluesky-oauth-client/src/react-native-oauth-client-factory.native.ts @@ -20,6 +20,8 @@ export type RNOAuthClientOptions = { clientMetadata: OAuthClientMetadata fetch?: Fetch crypto?: any + plcDirectoryUrl?: string + atprotoLexiconUrl?: string } export class RNOAuthClientFactory extends OAuthClientFactory { @@ -30,6 +32,8 @@ export class RNOAuthClientFactory extends OAuthClientFactory { // "fragment" is safer as it is not sent to the server responseMode = 'fragment', fetch = globalThis.fetch, + plcDirectoryUrl, + atprotoLexiconUrl, }: RNOAuthClientOptions) { const database = new RNOAuthDatabase() @@ -46,6 +50,8 @@ export class RNOAuthClientFactory extends OAuthClientFactory { }), identityResolver: UniversalIdentityResolver.from({ fetch, + plcDirectoryUrl, + atprotoLexiconUrl, didCache: database.getDidCache(), handleCache: database.getHandleCache(), }), @@ -80,64 +86,64 @@ export class RNOAuthClientFactory extends OAuthClientFactory { // } async signIn(input: string, options?: OAuthAuthorizeOptions) { + console.log(options) return await this.authorize(input, options) } - // async signInCallback() { - // const redirectUri = new URL(this.clientMetadata.redirect_uris[0]) - // if (location.pathname !== redirectUri.pathname) return null - // - // const params = - // this.responseMode === 'query' - // ? new URLSearchParams(location.search) - // : new URLSearchParams(location.hash.slice(1)) - // - // // Only if the query string contains oauth callback params - // if ( - // !params.has('iss') || - // !params.has('state') || - // !(params.has('code') || params.has('error')) - // ) { - // return null - // } - // - // // Replace the current history entry without the query string (this will - // // prevent this 'if' branch to run again if the user refreshes the page) - // history.replaceState(null, '', location.pathname) - // - // return this.callback(params) - // .then(async result => { - // if (result.state?.startsWith(POPUP_KEY_PREFIX)) { - // const stateKey = result.state.slice(POPUP_KEY_PREFIX.length) - // - // await this.popupStore.set(stateKey, { - // status: 'fulfilled', - // value: result.client.sessionId, - // }) - // - // window.close() // continued in signInPopup - // throw new Error('Login complete, please close the popup window.') - // } - // - // return result - // }) - // .catch(async err => { - // // TODO: Throw a proper error from parent class to actually detect - // // oauth authorization errors - // const state = typeof (err as any)?.state - // if (typeof state === 'string' && state?.startsWith(POPUP_KEY_PREFIX)) { - // const stateKey = state.slice(POPUP_KEY_PREFIX.length) - // - // await this.popupStore.set(stateKey, { - // status: 'rejected', - // reason: err, - // }) - // - // window.close() // continued in signInPopup - // throw new Error('Login complete, please close the popup window.') - // } - // - // throw err - // }) - // } + async signInCallback(callback: string) { + // const redirectUri = new URL(this.clientMetadata.redirect_uris[0]) + // if (location.pathname !== redirectUri.pathname) return null + // + const params = new URL(callback).searchParams + // Only if the query string contains oauth callback params + if ( + !params.has('iss') || + !params.has('state') || + !(params.has('code') || params.has('error')) + ) { + console.log('no') + return null + } else { + console.log('has!') + } + // + // // Replace the current history entry without the query string (this will + // // prevent this 'if' branch to run again if the user refreshes the page) + // history.replaceState(null, '', location.pathname) + // + // return this.callback(params) + // .then(async result => { + // if (result.state?.startsWith(POPUP_KEY_PREFIX)) { + // const stateKey = result.state.slice(POPUP_KEY_PREFIX.length) + // + // await this.popupStore.set(stateKey, { + // status: 'fulfilled', + // value: result.client.sessionId, + // }) + // + // window.close() // continued in signInPopup + // throw new Error('Login complete, please close the popup window.') + // } + // + // return result + // }) + // .catch(async err => { + // // TODO: Throw a proper error from parent class to actually detect + // // oauth authorization errors + // const state = typeof (err as any)?.state + // if (typeof state === 'string' && state?.startsWith(POPUP_KEY_PREFIX)) { + // const stateKey = state.slice(POPUP_KEY_PREFIX.length) + // + // await this.popupStore.set(stateKey, { + // status: 'rejected', + // reason: err, + // }) + // + // window.close() // continued in signInPopup + // throw new Error('Login complete, please close the popup window.') + // } + // + // throw err + // }) + } } From c7e29091c9f33f43421aeca9743a16bb1ad9601f Mon Sep 17 00:00:00 2001 From: Hailey Date: Thu, 25 Apr 2024 12:21:55 -0700 Subject: [PATCH 53/54] metro config temp --- metro.config.js | 58 ++++++++++++++++++++++++++++++++++++++++++------- 1 file changed, 50 insertions(+), 8 deletions(-) diff --git a/metro.config.js b/metro.config.js index c3cd6e9649..170c84ce1a 100644 --- a/metro.config.js +++ b/metro.config.js @@ -1,22 +1,64 @@ // Learn more https://docs.expo.io/guides/customizing-metro +const fs = require('fs') +const path = require('path') const {getDefaultConfig} = require('expo/metro-config') const cfg = getDefaultConfig(__dirname) -cfg.resolver.sourceExts = process.env.RN_SRC_EXT - ? process.env.RN_SRC_EXT.split(',').concat(cfg.resolver.sourceExts) - : cfg.resolver.sourceExts +if (process.env.ATPROTO_ROOT) { + const atprotoPackages = path.resolve(process.env.ATPROTO_ROOT, 'packages') + + cfg.watchFolders ||= [] + cfg.watchFolders.push( + ...fs + .readdirSync(atprotoPackages) + .map(dir => path.join(atprotoPackages, dir)) + .filter(dir => fs.statSync(dir).isDirectory()), + ) + + const resolveRequest = cfg.resolver.resolveRequest + cfg.resolver.resolveRequest = (context, moduleName, platform) => { + // When resolving a module from the atproto packages, try finding it there + // first. If it's not found, try resolving it from the project root (here). + if (context.originModulePath.startsWith(atprotoPackages)) { + try { + return context.resolveRequest(context, moduleName, platform) + } catch (err) { + // If a module is not found in the atproto packages, try and resolve it + // from here (e.g. @babel polyfills) + return { + type: 'sourceFile', + filePath: require.resolve(moduleName), + } + } + } -cfg.resolver.resolveRequest = (context, moduleName, platform) => { - if (moduleName === 'crypto' && platform !== 'web') { - return context.resolveRequest( + // When resolving an @atproto/* module, replace the path prefix with + // . + if (moduleName.startsWith('@atproto/')) { + const [prefix, suffix] = moduleName.split('/', 2) + const resolution = context.resolveRequest(context, moduleName, platform) + const relativePathIdx = resolution.filePath.lastIndexOf(moduleName) + const relativePath = resolution.filePath.slice( + relativePathIdx + moduleName.length + 1, + ) + return { + type: 'sourceFile', + filePath: path.join(atprotoPackages, suffix, relativePath), + } + } + + return (resolveRequest || context.resolveRequest)( context, - 'react-native-quick-crypto', + moduleName, platform, ) } - return context.resolveRequest(context, moduleName, platform) } +cfg.resolver.sourceExts = process.env.RN_SRC_EXT + ? process.env.RN_SRC_EXT.split(',').concat(cfg.resolver.sourceExts) + : cfg.resolver.sourceExts + cfg.transformer.getTransformOptions = async () => ({ transform: { experimentalImportSupport: true, From 0d850133fe1fba1c24566a3059b5a3ddb9821d45 Mon Sep 17 00:00:00 2001 From: Matthieu Sieben Date: Tue, 23 Apr 2024 16:07:27 +0200 Subject: [PATCH 54/54] feat(dev): allow using @atproto packages from another directory --- metro.config.js | 40 ++++++++++++++++++++++++++++++++++++++++ webpack.config.js | 21 +++++++++++++++++++++ 2 files changed, 61 insertions(+) diff --git a/metro.config.js b/metro.config.js index a49d95f9aa..80d2e34baf 100644 --- a/metro.config.js +++ b/metro.config.js @@ -1,7 +1,47 @@ // Learn more https://docs.expo.io/guides/customizing-metro +const path = require('path') const {getDefaultConfig} = require('expo/metro-config') const cfg = getDefaultConfig(__dirname) +if (process.env.ATPROTO_ROOT) { + const atprotoRoot = path.resolve(process.cwd(), process.env.ATPROTO_ROOT) + + // Watch folders are used as roots for the virtual file system. Any file that + // needs to be resolved by the metro bundler must be within one of the watch + // folders. Since we will be resolving dependencies from the atproto packages, + // we need to add the atproto root to the watch folders so that the + cfg.watchFolders ||= [] + cfg.watchFolders.push(atprotoRoot) + + const resolveRequest = cfg.resolver.resolveRequest + cfg.resolver.resolveRequest = (context, moduleName, platform) => { + // Alias @atproto/* modules to the corresponding package in the atproto root + if (moduleName.startsWith('@atproto/')) { + const [, packageName] = moduleName.split('/', 2) + const packagePath = path.join(atprotoRoot, 'packages', packageName) + return context.resolveRequest(context, packagePath, platform) + } + + // Polyfills are added by the build process and are not actual dependencies + // of the @atproto/* packages. Resolve those from here. + if ( + moduleName.startsWith('@babel/') && + context.originModulePath.startsWith(atprotoRoot) + ) { + return { + type: 'sourceFile', + filePath: require.resolve(moduleName), + } + } + + return (resolveRequest || context.resolveRequest)( + context, + moduleName, + platform, + ) + } +} + cfg.resolver.sourceExts = process.env.RN_SRC_EXT ? process.env.RN_SRC_EXT.split(',').concat(cfg.resolver.sourceExts) : cfg.resolver.sourceExts diff --git a/webpack.config.js b/webpack.config.js index 6f1de3b8b7..5aa2d47f5b 100644 --- a/webpack.config.js +++ b/webpack.config.js @@ -1,3 +1,5 @@ +const fs = require('fs') +const path = require('path') const createExpoWebpackConfigAsync = require('@expo/webpack-config') const {withAlias} = require('@expo/webpack-config/addons') const ReactRefreshWebpackPlugin = require('@pmmmwh/react-refresh-webpack-plugin') @@ -22,6 +24,25 @@ module.exports = async function (env, argv) { 'react-native$': 'react-native-web', 'react-native-webview': 'react-native-web-webview', }) + + if (process.env.ATPROTO_ROOT) { + const atprotoRoot = path.resolve(process.cwd(), process.env.ATPROTO_ROOT) + const atprotoPackages = path.join(atprotoRoot, 'packages') + + config = withAlias( + config, + Object.fromEntries( + fs + .readdirSync(atprotoPackages) + .map(pkgName => [pkgName, path.join(atprotoPackages, pkgName)]) + .filter(([_, pkgPath]) => + fs.existsSync(path.join(pkgPath, 'package.json')), + ) + .map(([pkgName, pkgPath]) => [`@atproto/${pkgName}`, pkgPath]), + ), + ) + } + config.module.rules = [ ...(config.module.rules || []), reactNativeWebWebviewConfiguration,