Merge remote-tracking branch 'origin/main' into font-scaling

* origin/main: (24 commits)
  Fix border radius on avatars (#5392)
  Make like animation on web same speed as mobile (#5391)
  Pre-fill alt text with 10-million card post (#5389)
  Tweak `pt-BR` string (#5372)
  Bump 1.91.2 (#5387)
  [Video] Additional android fixes (#5373)
  Milly tweaks (#5365)
  [Video] Fix safari showing spinner (#5364)
  [Video] Volume controls on web (#5363)
  Updates to Irish translation, back to 100% (#5345)
  Update catalan messages.po (#5223)
  Update Japanese translation (#5217)
  Update Korean localization (#5254)
  Update Chinese localization (#5291)
  Update French localization (#5227)
  Update Portuguese localization (#5256)
  [Video] Fix scrubber tap target (#5360)
  loosen checks on vtt file (#5359)
  Remove overflow hidden from external link outer el (#5356)
  Disallow some font scaling
  ...
This commit is contained in:
Eric Bailey
2024-09-17 14:13:37 -05:00
44 changed files with 4103 additions and 4694 deletions
+1
View File
@@ -75,6 +75,7 @@ export function LikedByList({uri}: {uri: string}) {
isLoading={isUriLoading || isLikedByLoading}
isError={isError}
emptyType="results"
emptyTitle={_(msg`No likes yet`)}
emptyMessage={_(
msg`Nobody has liked this yet. Maybe you should be the first!`,
)}
+3 -1
View File
@@ -59,7 +59,9 @@ export function Outer({
export function TitleText({children}: React.PropsWithChildren<{}>) {
const {titleId} = React.useContext(Context)
return (
<Text nativeID={titleId} style={[a.text_2xl, a.font_bold, a.pb_sm]}>
<Text
nativeID={titleId}
style={[a.text_2xl, a.font_bold, a.pb_sm, a.leading_snug]}>
{children}
</Text>
)
@@ -0,0 +1,129 @@
import React from 'react'
import {View} from 'react-native'
import Svg, {Circle, Path} from 'react-native-svg'
import {msg, Trans} from '@lingui/macro'
import {useLingui} from '@lingui/react'
import {Nux, useUpsertNuxMutation} from '#/state/queries/nuxs'
import {atoms as a, ViewStyleProp} from '#/alf'
import {Button, ButtonProps} from '#/components/Button'
import * as Dialog from '#/components/Dialog'
import {InlineLinkText} from '#/components/Link'
import * as Prompt from '#/components/Prompt'
import {TenMillion} from './'
export function Trigger({children}: {children: ButtonProps['children']}) {
const {_} = useLingui()
const {mutate: upsertNux} = useUpsertNuxMutation()
const [show, setShow] = React.useState(false)
const [fallback, setFallback] = React.useState(false)
const control = Prompt.usePromptControl()
const handleOnPress = () => {
if (!fallback) {
setShow(true)
upsertNux({
id: Nux.TenMillionDialog,
completed: true,
data: undefined,
})
} else {
control.open()
}
}
const onHandleFallback = () => {
setFallback(true)
control.open()
}
return (
<>
<Button
label={_(msg`Bluesky is celebrating 10 million users!`)}
onPress={handleOnPress}>
{children}
</Button>
{show && !fallback && (
<TenMillion
showTimeout={0}
onClose={() => setShow(false)}
onFallback={onHandleFallback}
/>
)}
<Prompt.Outer control={control}>
<View style={{maxWidth: 300}}>
<Prompt.TitleText>
<Trans>Bluesky is celebrating 10 million users!</Trans>
</Prompt.TitleText>
</View>
<Prompt.DescriptionText>
<Trans>
Together, we're rebuilding the social internet. We're glad you're
here.
</Trans>
</Prompt.DescriptionText>
<Prompt.DescriptionText>
<Trans>
To learn more,{' '}
<InlineLinkText
label={_(msg`View our post`)}
to="/profile/bsky.app/post/3l47prg3wgy23"
onPress={() => {
control.close()
}}
style={[a.text_md, a.leading_snug]}>
<Trans>check out our post.</Trans>
</InlineLinkText>
</Trans>
</Prompt.DescriptionText>
<Dialog.Close />
</Prompt.Outer>
</>
)
}
export function Icon({width, style}: {width: number} & ViewStyleProp) {
return (
<Svg width={width} height={width} viewBox="0 0 36 36" style={style}>
<Path
fill="#dd2e44"
d="M11.626 7.488a1.4 1.4 0 0 0-.268.395l-.008-.008L.134 33.141l.011.011c-.208.403.14 1.223.853 1.937c.713.713 1.533 1.061 1.936.853l.01.01L28.21 24.735l-.008-.009c.147-.07.282-.155.395-.269c1.562-1.562-.971-6.627-5.656-11.313c-4.687-4.686-9.752-7.218-11.315-5.656"
/>
<Path
fill="#ea596e"
d="M13 12L.416 32.506l-.282.635l.011.011c-.208.403.14 1.223.853 1.937c.232.232.473.408.709.557L17 17z"
/>
<Path
fill="#a0041e"
d="M23.012 13.066c4.67 4.672 7.263 9.652 5.789 11.124c-1.473 1.474-6.453-1.118-11.126-5.788c-4.671-4.672-7.263-9.654-5.79-11.127c1.474-1.473 6.454 1.119 11.127 5.791"
/>
<Path
fill="#aa8dd8"
d="M18.59 13.609a1 1 0 0 1-.734.215c-.868-.094-1.598-.396-2.109-.873c-.541-.505-.808-1.183-.735-1.862c.128-1.192 1.324-2.286 3.363-2.066c.793.085 1.147-.17 1.159-.292c.014-.121-.277-.446-1.07-.532c-.868-.094-1.598-.396-2.11-.873c-.541-.505-.809-1.183-.735-1.862c.13-1.192 1.325-2.286 3.362-2.065c.578.062.883-.057 1.012-.134c.103-.063.144-.123.148-.158c.012-.121-.275-.446-1.07-.532a1 1 0 0 1-.886-1.102a.997.997 0 0 1 1.101-.886c2.037.219 2.973 1.542 2.844 2.735c-.13 1.194-1.325 2.286-3.364 2.067c-.578-.063-.88.057-1.01.134c-.103.062-.145.123-.149.157c-.013.122.276.446 1.071.532c2.037.22 2.973 1.542 2.844 2.735s-1.324 2.286-3.362 2.065c-.578-.062-.882.058-1.012.134c-.104.064-.144.124-.148.158c-.013.121.276.446 1.07.532a1 1 0 0 1 .52 1.773"
/>
<Path
fill="#77b255"
d="M30.661 22.857c1.973-.557 3.334.323 3.658 1.478c.324 1.154-.378 2.615-2.35 3.17c-.77.216-1.001.584-.97.701c.034.118.425.312 1.193.095c1.972-.555 3.333.325 3.657 1.479c.326 1.155-.378 2.614-2.351 3.17c-.769.216-1.001.585-.967.702s.423.311 1.192.095a1 1 0 1 1 .54 1.925c-1.971.555-3.333-.323-3.659-1.479c-.324-1.154.379-2.613 2.353-3.169c.77-.217 1.001-.584.967-.702c-.032-.117-.422-.312-1.19-.096c-1.974.556-3.334-.322-3.659-1.479c-.325-1.154.378-2.613 2.351-3.17c.768-.215.999-.585.967-.701c-.034-.118-.423-.312-1.192-.096a1 1 0 1 1-.54-1.923"
/>
<Path
fill="#aa8dd8"
d="M23.001 20.16a1.001 1.001 0 0 1-.626-1.781c.218-.175 5.418-4.259 12.767-3.208a1 1 0 1 1-.283 1.979c-6.493-.922-11.187 2.754-11.233 2.791a1 1 0 0 1-.625.219"
/>
<Path
fill="#77b255"
d="M5.754 16a1 1 0 0 1-.958-1.287c1.133-3.773 2.16-9.794.898-11.364c-.141-.178-.354-.353-.842-.316c-.938.072-.849 2.051-.848 2.071a1 1 0 1 1-1.994.149c-.103-1.379.326-4.035 2.692-4.214c1.056-.08 1.933.287 2.552 1.057c2.371 2.951-.036 11.506-.542 13.192a1 1 0 0 1-.958.712"
/>
<Circle cx="25.5" cy="9.5" r="1.5" fill="#5c913b" />
<Circle cx="2" cy="18" r="2" fill="#9266cc" />
<Circle cx="32.5" cy="19.5" r="1.5" fill="#5c913b" />
<Circle cx="23.5" cy="31.5" r="1.5" fill="#5c913b" />
<Circle cx="28" cy="4" r="2" fill="#ffcc4d" />
<Circle cx="32.5" cy="8.5" r="1.5" fill="#ffcc4d" />
<Circle cx="29.5" cy="12.5" r="1.5" fill="#ffcc4d" />
<Circle cx="7.5" cy="23.5" r="1.5" fill="#ffcc4d" />
</Svg>
)
}
@@ -87,7 +87,15 @@ function Frame({children}: {children: React.ReactNode}) {
)
}
export function TenMillion() {
export function TenMillion({
showTimeout,
onClose,
onFallback,
}: {
showTimeout?: number
onClose?: () => void
onFallback?: () => void
}) {
const agent = useAgent()
const nuxDialogs = useNuxDialogContext()
const [userNumber, setUserNumber] = React.useState<number>(0)
@@ -115,12 +123,16 @@ export function TenMillion() {
const data = await res.json()
if (data.number) {
if (data.number && data.number <= 10_000_000) {
setUserNumber(data.number)
} else {
// should be rare
nuxDialogs.dismissActiveNux()
onFallback?.()
}
} else {
nuxDialogs.dismissActiveNux()
onFallback?.()
}
}
@@ -128,6 +140,7 @@ export function TenMillion() {
fetching.current = true
networkRetry(3, fetchUserNumber).catch(() => {
nuxDialogs.dismissActiveNux()
onFallback?.()
})
}
}, [
@@ -136,12 +149,27 @@ export function TenMillion() {
setUserNumber,
nuxDialogs.dismissActiveNux,
nuxDialogs,
onFallback,
])
return userNumber ? <TenMillionInner userNumber={userNumber} /> : null
return userNumber ? (
<TenMillionInner
userNumber={userNumber}
showTimeout={showTimeout ?? 3e3}
onClose={onClose}
/>
) : null
}
export function TenMillionInner({userNumber}: {userNumber: number}) {
export function TenMillionInner({
userNumber,
showTimeout,
onClose: onCloseOuter,
}: {
userNumber: number
showTimeout: number
onClose?: () => void
}) {
const t = useTheme()
const lightTheme = useTheme('light')
const {_, i18n} = useLingui()
@@ -169,6 +197,27 @@ export function TenMillionInner({userNumber}: {userNumber: number}) {
const isLoadingData = isProfileLoading || !moderation || !profile
const isLoadingImage = !uri
const displayName = React.useMemo(() => {
if (!profile || !moderation) return ''
return sanitizeDisplayName(
profile.displayName || sanitizeHandle(profile.handle),
moderation.ui('displayName'),
)
}, [profile, moderation])
const handle = React.useMemo(() => {
if (!profile) return ''
return sanitizeHandle(profile.handle, '@')
}, [profile])
const joinedDate = React.useMemo(() => {
if (!profile || !profile.createdAt) return ''
const date = i18n.date(profile.createdAt, {
month: 'short',
day: 'numeric',
year: 'numeric',
})
return date
}, [i18n, profile])
const error: string = React.useMemo(() => {
if (profileError) {
return _(
@@ -184,14 +233,15 @@ export function TenMillionInner({userNumber}: {userNumber: number}) {
React.useEffect(() => {
const timeout = setTimeout(() => {
control.open()
}, 3e3)
}, showTimeout)
return () => {
clearTimeout(timeout)
}
}, [control])
}, [control, showTimeout])
const onClose = React.useCallback(() => {
nuxDialogs.dismissActiveNux()
}, [nuxDialogs])
onCloseOuter?.()
}, [nuxDialogs, onCloseOuter])
/*
* Actions
@@ -206,19 +256,34 @@ export function TenMillionInner({userNumber}: {userNumber: number}) {
msg`Bluesky now has over 10 million users, and I was #${i18n.number(
userNumber,
)}!`,
), // TODO
),
imageUris: [
{
uri,
width: WIDTH,
height: HEIGHT,
altText: _(
msg`A virtual certificate with text "Celebrating 10M users on Bluesky, #${i18n.number(
userNumber,
)}, ${displayName} ${handle}, joined on ${joinedDate}"`,
),
},
],
})
}, 1e3)
})
}
}, [_, i18n, control, openComposer, uri, userNumber])
}, [
_,
i18n,
control,
openComposer,
uri,
userNumber,
displayName,
handle,
joinedDate,
])
const onNativeShare = React.useCallback(() => {
if (uri) {
control.close(() => {
@@ -358,6 +423,7 @@ export function TenMillionInner({userNumber}: {userNumber: number}) {
},
]}>
<Text
allowFontScaling={false}
style={[
a.text_md,
a.font_bold,
@@ -372,6 +438,7 @@ export function TenMillionInner({userNumber}: {userNumber: number}) {
</Text>
<View style={[a.flex_row, a.align_start]}>
<Text
allowFontScaling={false}
style={[
a.absolute,
{
@@ -391,6 +458,7 @@ export function TenMillionInner({userNumber}: {userNumber: number}) {
#
</Text>
<Text
allowFontScaling={false}
style={[
a.relative,
a.text_center,
@@ -450,6 +518,7 @@ export function TenMillionInner({userNumber}: {userNumber: number}) {
*/}
<View style={[a.gap_2xs, a.flex_1]}>
<Text
allowFontScaling={false}
style={[
a.flex_1,
a.text_sm,
@@ -457,15 +526,12 @@ export function TenMillionInner({userNumber}: {userNumber: number}) {
a.leading_tight,
{maxWidth: '60%'},
]}>
{sanitizeDisplayName(
profile.displayName ||
sanitizeHandle(profile.handle),
moderation.ui('displayName'),
)}
{displayName}
</Text>
<View
style={[a.flex_row, a.justify_between, a.gap_4xl]}>
<Text
allowFontScaling={false}
numberOfLines={1}
style={[
a.flex_1,
@@ -474,11 +540,12 @@ export function TenMillionInner({userNumber}: {userNumber: number}) {
a.leading_snug,
lightTheme.atoms.text_contrast_medium,
]}>
{sanitizeHandle(profile.handle, '@')}
{handle}
</Text>
{profile.createdAt && (
<Text
allowFontScaling={false}
numberOfLines={1}
ellipsizeMode="head"
style={[
@@ -489,14 +556,7 @@ export function TenMillionInner({userNumber}: {userNumber: number}) {
a.text_right,
lightTheme.atoms.text_contrast_low,
]}>
<Trans>
Joined{' '}
{i18n.date(profile.createdAt, {
month: 'short',
day: 'numeric',
year: 'numeric',
})}
</Trans>
<Trans>Joined on {joinedDate}</Trans>
</Text>
)}
</View>
@@ -582,6 +642,7 @@ export function TenMillionInner({userNumber}: {userNumber: number}) {
<View style={[gtMobile ? a.p_2xl : a.p_xl]}>
<Text
allowFontScaling={false}
style={[
a.text_5xl,
a.leading_tight,
@@ -610,9 +671,12 @@ export function TenMillionInner({userNumber}: {userNumber: number}) {
a.gap_md,
a.pt_xl,
]}>
<Text style={[a.text_md, a.italic, t.atoms.text_contrast_medium]}>
<Trans>Brag a little!</Trans>
</Text>
{gtMobile && (
<Text
style={[a.text_md, a.italic, t.atoms.text_contrast_medium]}>
<Trans>Brag a little!</Trans>
</Text>
)}
<Button
disabled={isLoadingImage}
+8 -38
View File
@@ -9,6 +9,7 @@ import {
useUpsertNuxMutation,
} from '#/state/queries/nuxs'
import {useSession} from '#/state/session'
import {useOnboardingState} from '#/state/shell'
import {isSnoozed, snooze, unsnooze} from '#/components/dialogs/nuxs/snoozing'
import {TenMillion} from '#/components/dialogs/nuxs/TenMillion'
import {IS_DEV} from '#/env'
@@ -18,31 +19,12 @@ type Context = {
dismissActiveNux: () => void
}
/**
* If we fail to complete a NUX here, it may show again on next reload,
* or if prefs state updates. If `true`, this fallback ensures that the last
* shown NUX won't show again, at least for this session.
*
* This is temporary, and only needed for the 10Milly dialog rn, since we
* aren't snoozing that one in device storage.
*/
let __isSnoozedFallback = false
const queuedNuxs: {
id: Nux
enabled(props: {gate: ReturnType<typeof useGate>}): boolean
/**
* TEMP only intended for use with the 10Milly dialog rn, since there are no
* other NUX dialogs configured
*/
unsafe_disableSnooze: boolean
enabled?: (props: {gate: ReturnType<typeof useGate>}) => boolean
}[] = [
{
id: Nux.TenMillionDialog,
enabled({gate}) {
return gate('ten_million_dialog')
},
unsafe_disableSnooze: true,
},
]
@@ -57,7 +39,8 @@ export function useNuxDialogContext() {
export function NuxDialogs() {
const {hasSession} = useSession()
return hasSession ? <Inner /> : null
const onboardingState = useOnboardingState()
return hasSession && !onboardingState.isActive ? <Inner /> : null
}
function Inner() {
@@ -90,30 +73,23 @@ function Inner() {
}
React.useEffect(() => {
if (__isSnoozedFallback) return
if (snoozed) return
if (!nuxs) return
for (const {id, enabled, unsafe_disableSnooze} of queuedNuxs) {
for (const {id, enabled} of queuedNuxs) {
const nux = nuxs.find(nux => nux.id === id)
// check if completed first
if (nux && nux.completed) continue
// then check gate (track exposure)
if (!enabled({gate})) continue
if (enabled && !enabled({gate})) continue
// we have a winner
setActiveNux(id)
/**
* TEMP only intended for use with the 10Milly dialog rn, since there are no
* other NUX dialogs configured
*/
if (!unsafe_disableSnooze) {
// immediately snooze for a day
snoozeNuxDialog()
}
// immediately snooze for a day
snoozeNuxDialog()
// immediately update remote data (affects next reload)
upsertNux({
@@ -124,12 +100,6 @@ function Inner() {
logger.error(`NUX dialogs: failed to upsert '${id}' NUX`, {
safeMessage: e.message,
})
/*
* TEMP only intended for use with the 10Milly dialog rn
*/
if (unsafe_disableSnooze) {
__isSnoozedFallback = true
}
})
break
+1 -1
View File
@@ -10,7 +10,7 @@ import {
} from '#/components/icons/Heart2'
const animationConfig = {
duration: 400,
duration: 600,
easing: 'cubic-bezier(0.4, 0, 0.2, 1)',
fill: 'forwards' as FillMode,
}
-4
View File
@@ -97,10 +97,6 @@ export function useComposeIntent() {
if (part.includes('https://') || part.includes('http://')) {
return false
}
console.log({
part,
text: VALID_IMAGE_REGEX.test(part),
})
// We also should just filter out cases that don't have all the info we need
return VALID_IMAGE_REGEX.test(part)
})
+1 -4
View File
@@ -1,6 +1,3 @@
export type Gate =
// Keep this alphabetic please.
| 'debug_show_feedcontext'
| 'typography_neue'
| 'suggested_feeds_interstitial'
| 'ten_million_dialog'
'debug_show_feedcontext' | 'typography_neue' | 'suggested_feeds_interstitial'
+61 -61
View File
@@ -31,11 +31,11 @@ msgstr "{0, plural, one {{formattedCount} other} other {{formattedCount} others}
#: src/lib/hooks/useTimeAgo.ts:156
msgid "{0, plural, one {# day} other {# days}}"
msgstr ""
msgstr "{0, plural, one {# dia} other {# dies}}"
#: src/lib/hooks/useTimeAgo.ts:146
msgid "{0, plural, one {# hour} other {# hours}}"
msgstr ""
msgstr "{0, plural, one {# hora} other {# hores}}"
#: src/view/shell/desktop/RightNav.tsx:168
#~ msgid "{0, plural, one {# invite code available} other {# invite codes available}}"
@@ -59,11 +59,11 @@ msgstr "{0, plural, one {# etiqueta s'ha aplicat a aquest contingut} other {# et
#: src/lib/hooks/useTimeAgo.ts:136
msgid "{0, plural, one {# minute} other {# minutes}}"
msgstr ""
msgstr "{0, plural, one {# minut} other {# minuts}}"
#: src/lib/hooks/useTimeAgo.ts:167
msgid "{0, plural, one {# month} other {# months}}"
msgstr ""
msgstr "{0, plural, one {# mes} other {# mesos}}"
#: src/view/com/util/post-ctrls/RepostButton.tsx:71
msgid "{0, plural, one {# repost} other {# reposts}}"
@@ -71,11 +71,11 @@ msgstr "{0, plural, one {# republicació} other {# republicacions}}"
#: src/lib/hooks/useTimeAgo.ts:126
msgid "{0, plural, one {# second} other {# seconds}}"
msgstr ""
msgstr "{0, plural, one {# segon} other {# segons}}"
#: src/components/KnownFollowers.tsx:179
#~ msgid "{0, plural, one {and # other} other {and # others}}"
#~ msgstr ""
#~ msgstr "{0, plural, one {i # altre} other {i # altres}}"
#: src/components/ProfileHoverCard/index.web.tsx:398
#: src/screens/Profile/Header/Metrics.tsx:23
@@ -144,7 +144,7 @@ msgstr "{0} s'han unit aquesta setmana"
#: src/view/com/util/post-embeds/VideoEmbedInner/VideoWebControls.tsx:637
msgid "{0} of {1}"
msgstr ""
msgstr "{0} de {1}"
#: src/screens/StarterPack/StarterPackScreen.tsx:467
msgid "{0} people have used this starter pack!"
@@ -169,27 +169,27 @@ msgstr "Starter pack de {0}"
#. How many days have passed, displayed in a narrow form
#: src/lib/hooks/useTimeAgo.ts:158
msgid "{0}d"
msgstr ""
msgstr "{0}d"
#. How many hours have passed, displayed in a narrow form
#: src/lib/hooks/useTimeAgo.ts:148
msgid "{0}h"
msgstr ""
msgstr "{0}h"
#. How many minutes have passed, displayed in a narrow form
#: src/lib/hooks/useTimeAgo.ts:138
msgid "{0}m"
msgstr ""
msgstr "{0}m"
#. How many months have passed, displayed in a narrow form
#: src/lib/hooks/useTimeAgo.ts:169
msgid "{0}mo"
msgstr ""
msgstr "{0}me"
#. How many seconds have passed, displayed in a narrow form
#: src/lib/hooks/useTimeAgo.ts:128
msgid "{0}s"
msgstr ""
msgstr "{0}s"
#: src/components/LabelingServiceCard/index.tsx:71
msgid "{count, plural, one {Liked by # user} other {Liked by # users}}"
@@ -504,7 +504,7 @@ msgstr "Afegeix text alternatiu"
#: src/view/com/composer/videos/SubtitleDialog.tsx:109
msgid "Add alt text (optional)"
msgstr ""
msgstr "Afegeix text alternatiu (opcional)"
#: src/view/screens/AppPasswords.tsx:105
#: src/view/screens/AppPasswords.tsx:147
@@ -700,7 +700,7 @@ msgstr "Hi ha hagut un error"
#: src/state/queries/video/video.ts:232
msgid "An error occurred while compressing the video."
msgstr ""
msgstr "Hi ha hagut un error mentre es comprimia el vídeo."
#: src/components/StarterPack/ProfileStarterPacks.tsx:315
msgid "An error occurred while generating your starter pack. Want to try again?"
@@ -712,7 +712,7 @@ msgstr "Hi ha hagut un error mentre es carregava el vídeo. Prova-ho més tard."
#: src/view/com/util/post-embeds/VideoEmbed.web.tsx:174
msgid "An error occurred while loading the video. Please try again."
msgstr ""
msgstr "Hi ha hagut un error mentre es carregava el vídeo. Prova-ho més tard."
#: src/components/dialogs/nuxs/TenMillion/index.tsx:250
msgid "An error occurred while saving the image!"
@@ -729,7 +729,7 @@ msgstr "S'ha produït un error en desar el codi QR!"
#: src/view/com/composer/videos/SelectVideoBtn.tsx:75
msgid "An error occurred while selecting the video"
msgstr ""
msgstr "Hi ha hagut un error mentre es seleccionava el vídeo"
#: src/components/dms/MessageMenu.tsx:134
#~ msgid "An error occurred while trying to delete the message. Please try again."
@@ -1286,15 +1286,15 @@ msgstr "Cancel·la obrir la web enllaçada"
#: src/view/com/util/post-ctrls/PostCtrls.tsx:155
#: src/view/com/util/post-ctrls/PostCtrls.tsx:191
msgid "Cannot interact with a blocked user"
msgstr ""
msgstr "No pots interactuar amb un usuari bloquejat"
#: src/view/com/composer/videos/SubtitleDialog.tsx:135
msgid "Captions (.vtt)"
msgstr ""
msgstr "Subtítols (.vtt)"
#: src/view/com/composer/videos/SubtitleDialog.tsx:56
msgid "Captions & alt text"
msgstr ""
msgstr "Subtítols i text alternatiu"
#: src/components/dialogs/nuxs/TenMillion/index.tsx:368
msgid "Celebrating {0} users"
@@ -1905,7 +1905,7 @@ msgstr "No s'ha pogut silenciar el xat"
#: src/view/com/composer/videos/VideoPreview.web.tsx:56
msgid "Could not process your video"
msgstr ""
msgstr "No s'ha pogut processar el teu vídeo"
#: src/components/dms/ConvoMenu.tsx:68
#~ msgid "Could not unmute chat"
@@ -2213,7 +2213,7 @@ msgstr "Els missatges directes són aquí!"
#: src/view/screens/AccessibilitySettings.tsx:111
msgid "Disable autoplay for videos and GIFs"
msgstr ""
msgstr "Desactiva la reproducció automàtica per GIFs i vídeos"
#: src/view/screens/Settings/DisableEmail2FADialog.tsx:90
msgid "Disable Email 2FA"
@@ -2558,7 +2558,7 @@ msgstr "Adreça de correu"
#: src/components/intents/VerifyEmailIntentDialog.tsx:95
msgid "Email Resent"
msgstr ""
msgstr "S'ha tornat a enviar el correu"
#: src/view/com/modals/ChangeEmail.tsx:54
#: src/view/com/modals/ChangeEmail.tsx:83
@@ -2575,7 +2575,7 @@ msgstr "Correu verificat"
#: src/components/intents/VerifyEmailIntentDialog.tsx:71
msgid "Email Verified"
msgstr ""
msgstr "Correu verificat"
#: src/view/screens/Settings/index.tsx:319
msgid "Email:"
@@ -2662,7 +2662,7 @@ msgstr "Fi del canal"
#: src/view/com/composer/videos/SubtitleDialog.tsx:161
msgid "Ensure you have selected a language for each subtitle file."
msgstr ""
msgstr "Assegura't que has seleccionat un idioma per a cada fitxer de subtítols."
#: src/view/com/modals/AddAppPasswords.tsx:161
msgid "Enter a name for this App Password"
@@ -2776,7 +2776,7 @@ msgstr "Exclou els usuaris que segueixes"
#: src/view/com/util/post-embeds/VideoEmbedInner/VideoWebControls.tsx:397
msgid "Exit fullscreen"
msgstr ""
msgstr "Surt de la pantalla completa"
#: src/view/com/modals/DeleteAccount.tsx:293
msgid "Exits account deletion process"
@@ -2961,7 +2961,7 @@ msgstr "No s'ha pogut actualitzar la configuració"
#: src/state/queries/video/video-upload.web.ts:68
#: src/state/queries/video/video-upload.web.ts:78
msgid "Failed to upload video"
msgstr ""
msgstr "No s'ha pogut pujar el vídeo"
#: src/Navigation.tsx:226
msgid "Feed"
@@ -3314,7 +3314,7 @@ msgstr "De <0/>"
#: src/view/com/util/post-embeds/VideoEmbedInner/VideoWebControls.tsx:398
msgid "Fullscreen"
msgstr ""
msgstr "Pantalla completa"
#: src/view/com/composer/photos/SelectPhotoBtn.tsx:39
msgid "Gallery"
@@ -3579,7 +3579,7 @@ msgstr "No podem carregar el servei de moderació."
#: src/state/queries/video/video.ts:165
msgid "Hold up! Were gradually giving access to video, and youre still waiting in line. Check back soon!"
msgstr ""
msgstr "Espera! A poc a poc estem donant accés al vídeo i encara estàs a la cua. Torna més tard!"
#: src/Navigation.tsx:550
#: src/Navigation.tsx:570
@@ -3692,7 +3692,7 @@ msgstr "Suplantació d'identitat o afirmacions falses sobre identitat o afiliaci
#: src/lib/moderation/useReportOptions.ts:68
msgid "Impersonation, misinformation, or false claims"
msgstr ""
msgstr "Suplantació d'identitat, desinformació o afirmacions falses"
#: src/lib/moderation/useReportOptions.ts:91
msgid "Inappropriate messages or explicit links"
@@ -3786,7 +3786,7 @@ msgstr "Nom d'usuari o contrasenya incorrectes"
#: src/components/intents/VerifyEmailIntentDialog.tsx:82
msgid "Invalid Verification Code"
msgstr ""
msgstr "Codi de verificació invàlid"
#: src/view/screens/Settings.tsx:411
#~ msgid "Invite"
@@ -3838,11 +3838,11 @@ msgstr "Ara només ets tu! Afegeix més persones al teu starter pack cercant a d
#: src/view/com/composer/Composer.tsx:1126
msgid "Job ID: {0}"
msgstr ""
msgstr "Identificador de la tasca: {0}"
#: src/view/com/auth/SplashScreen.web.tsx:164
msgid "Jobs"
msgstr "Feines"
msgstr "Tasques"
#: src/screens/StarterPack/StarterPackLandingScreen.tsx:206
#: src/screens/StarterPack/StarterPackLandingScreen.tsx:212
@@ -4322,7 +4322,7 @@ msgstr "Compte enganyós"
#: src/lib/moderation/useReportOptions.ts:67
msgid "Misleading Post"
msgstr ""
msgstr "Publicació enganyosa"
#: src/screens/Settings/AppearanceSettings.tsx:78
msgid "Mode"
@@ -4983,7 +4983,7 @@ msgstr "Respostes més antigues primer"
#: src/components/StarterPack/QrCode.tsx:70
msgid "on<0><1/><2><3/></2></0>"
msgstr ""
msgstr "en<0><1/><2><3/></2></0>"
#: src/view/screens/Settings/index.tsx:226
msgid "Onboarding reset"
@@ -5015,7 +5015,7 @@ msgstr "Només pot tenir lletres, nombres i guionets"
#: src/view/com/composer/videos/SubtitleFilePicker.tsx:39
msgid "Only WebVTT (.vtt) files are supported"
msgstr ""
msgstr "Només s'admeten arxius WebVTT (.vtt)"
#: src/components/Lists.tsx:88
msgid "Oops, something went wrong!"
@@ -5902,7 +5902,7 @@ msgstr "Elimina el compte"
#: src/view/com/composer/ExternalEmbedRemoveBtn.tsx:28
msgid "Remove attachment"
msgstr ""
msgstr "Elimina l'adjunt"
#: src/view/com/util/UserAvatar.tsx:393
msgid "Remove Avatar"
@@ -5978,7 +5978,7 @@ msgstr "Elimina la republicació"
#: src/view/com/composer/videos/SubtitleDialog.tsx:264
msgid "Remove subtitle file"
msgstr ""
msgstr "Eliminar arxiu de subtítols"
#: src/view/com/feeds/FeedSourceCard.tsx:175
#~ msgid "Remove this feed from my feeds?"
@@ -6030,7 +6030,7 @@ msgstr "Elimina la publicació amb la citació"
#: src/view/com/composer/ExternalEmbedRemoveBtn.tsx:29
msgid "Removes the attachment"
msgstr ""
msgstr "Elimina l'adjunt"
#: src/view/com/composer/ExternalEmbedRemoveBtn.tsx:29
#~ msgid "Removes the image preview"
@@ -6297,11 +6297,11 @@ msgstr "Torna a enviar el correu"
#: src/components/intents/VerifyEmailIntentDialog.tsx:130
msgid "Resend Email"
msgstr ""
msgstr "Torna a enviar el correu"
#: src/components/intents/VerifyEmailIntentDialog.tsx:123
msgid "Resend Verification Email"
msgstr ""
msgstr "Torna a enviar el correu de verificació"
#: src/view/com/modals/ChangePassword.tsx:186
msgid "Reset code"
@@ -6601,7 +6601,7 @@ msgstr "Consulta aquesta guia"
#: src/view/com/util/post-embeds/VideoEmbedInner/VideoWebControls.tsx:631
msgid "Seek slider"
msgstr ""
msgstr "Cerca el control lliscant"
#: src/view/com/util/Selector.tsx:106
msgid "Select {item}"
@@ -6645,7 +6645,7 @@ msgstr "Tria per quant temps s'ha de silenciar aquesta paraula."
#: src/view/com/composer/videos/SubtitleDialog.tsx:249
msgid "Select language..."
msgstr ""
msgstr "Selecciona l'idioma..."
#: src/view/screens/LanguageSettings.tsx:303
msgid "Select languages"
@@ -6670,7 +6670,7 @@ msgstr "Selecciona l'opció {i} de {numItems}"
#: src/view/com/composer/videos/SubtitleFilePicker.tsx:65
msgid "Select subtitle file (.vtt)"
msgstr ""
msgstr "Tria el arxiu de subtítols (.vtt)"
#: src/screens/Onboarding/StepProfile/AvatarCreatorItems.tsx:83
msgid "Select the {emojiName} emoji as your avatar"
@@ -7566,7 +7566,7 @@ msgstr "Toca per canviar el so"
#: src/view/com/util/images/AutoSizedImage.tsx:219
#: src/view/com/util/images/AutoSizedImage.tsx:239
msgid "Tap to view full image"
msgstr ""
msgstr "Toca per a veure la imatge completa"
#: src/view/com/util/images/AutoSizedImage.tsx:70
#~ msgid "Tap to view fully"
@@ -7640,7 +7640,7 @@ msgstr ""
#: src/components/intents/VerifyEmailIntentDialog.tsx:74
msgid "Thanks, you have successfully verified your email address."
msgstr ""
msgstr "Gràcies, has verificat correctament el teu correu."
#: src/view/com/modals/ChangeHandle.tsx:459
msgid "That contains the following:"
@@ -7749,7 +7749,7 @@ msgstr "Les condicions del servei han estat traslladades a"
#: src/components/intents/VerifyEmailIntentDialog.tsx:85
msgid "The verification code you have provided is invalid. Please make sure that you have used the correct verification link or request a new one."
msgstr ""
msgstr "El codi de verificació que has proporcionat no és vàlid. Assegura't que has utilitzat l'enllaç de verificació correcte o sol·licita'n un de nou."
#: src/screens/Onboarding/StepAlgoFeeds/index.tsx:141
#~ msgid "There are many feeds to try:"
@@ -8155,7 +8155,7 @@ msgstr "Per informar d'una conversa, informa d'un dels seus missatges a través
#: src/view/com/composer/videos/SelectVideoBtn.tsx:120
msgid "To upload videos to Bluesky, you must first verify your email."
msgstr ""
msgstr "Primer has de verificar el teu correu per poder pujar vídeos a Bluesky."
#: src/components/ReportDialog/SelectLabelerView.tsx:33
msgid "To whom would you like to send this report?"
@@ -8397,7 +8397,7 @@ msgstr "T'has dona't de baixa de la llista"
#: src/state/queries/video/video.ts:245
msgid "Unsupported video type: {mimeType}"
msgstr ""
msgstr "Tipus de vídeo no compatible: {mimeType}"
#: src/lib/moderation/useReportOptions.ts:85
#~ msgid "Unwanted sexual content"
@@ -8600,7 +8600,7 @@ msgstr "Valor:"
#: src/view/com/composer/videos/SelectVideoBtn.tsx:118
msgid "Verified email required"
msgstr ""
msgstr "Es requereix un correu verificat"
#: src/view/com/modals/ChangeHandle.tsx:510
#~ msgid "Verify {0}"
@@ -8616,7 +8616,7 @@ msgstr "Verifica el correu"
#: src/components/intents/VerifyEmailIntentDialog.tsx:61
msgid "Verify email dialog"
msgstr ""
msgstr "Diàleg de verificació del correu"
#: src/view/screens/Settings/index.tsx:961
msgid "Verify my email"
@@ -8633,7 +8633,7 @@ msgstr "Verifica el correu nou"
#: src/view/com/composer/videos/SelectVideoBtn.tsx:122
msgid "Verify now"
msgstr ""
msgstr "Verifica-ho ara"
#: src/view/com/modals/ChangeHandle.tsx:505
msgid "Verify Text File"
@@ -8658,7 +8658,7 @@ msgstr "Vídeo"
#: src/state/queries/video/video.ts:138
msgid "Video failed to process"
msgstr ""
msgstr "El vídeo no s'ha pogut processar"
#: src/screens/Onboarding/index.tsx:39
#: src/screens/Onboarding/state.ts:88
@@ -8667,15 +8667,15 @@ msgstr "Videojocs"
#: src/view/com/util/post-embeds/VideoEmbed.web.tsx:167
msgid "Video not found."
msgstr ""
msgstr "No s'ha trobat el vídeo."
#: src/view/com/composer/videos/SubtitleDialog.tsx:101
msgid "Video settings"
msgstr ""
msgstr "Configuració de vídeo"
#: src/view/com/util/post-embeds/VideoEmbedInner/VideoEmbedInnerNative.tsx:93
msgid "Video: {0}"
msgstr ""
msgstr "Vídeo: {0}"
#: src/view/com/composer/videos/state.ts:27
#~ msgid "Videos cannot be larger than 50MB"
@@ -8801,7 +8801,7 @@ msgstr "Calculem {estimatedTime} fins que el teu compte estigui llest."
#: src/components/intents/VerifyEmailIntentDialog.tsx:98
msgid "We have sent another verification email to <0>{0}</0>."
msgstr ""
msgstr "Hem enviat un altre correu de verificació a <0>{0}</0>."
#: src/screens/Onboarding/StepFinished.tsx:238
msgid "We hope you have a wonderful time. Remember, Bluesky is:"
@@ -8821,7 +8821,7 @@ msgstr "Ja no hi ha més publicacions dels usuaris que segueixes. Aquí n'hi ha
#: src/state/queries/video/video.ts:170
msgid "We were unable to determine if you are allowed to upload videos. Please try again."
msgstr ""
msgstr "No hem pogut determinar si tens permís per pujar vídeos. Torna-ho a provar."
#: src/components/dialogs/BirthDateSettings.tsx:52
msgid "We were unable to load your birth date preferences. Please try again."
@@ -9056,7 +9056,7 @@ msgstr "Estàs a la cua."
#: src/state/queries/video/video.ts:161
msgid "You are not allowed to upload videos."
msgstr ""
msgstr "No t'és permés pujar vídeos."
#: src/view/com/profile/ProfileFollows.tsx:95
msgid "You are not following anyone."
@@ -9205,7 +9205,7 @@ msgstr "Has arribat al final"
#: src/state/queries/video/video-upload.shared.ts:67
msgid "You have temporarily reached the limit for video uploads. Please try again later."
msgstr ""
msgstr "Has arribat temporalment al límit de pujades de vídeos. Torna-ho a provar més tard."
#: src/components/StarterPack/ProfileStarterPacks.tsx:235
msgid "You haven't created a starter pack yet!"
@@ -9351,11 +9351,11 @@ msgstr "Has arribat al final del vostre cabal! Cerca alguns comptes més per a s
#: src/state/queries/video/video.ts:175
msgid "You've reached your daily limit for video uploads (too many bytes)"
msgstr ""
msgstr "Has assolit el teu límit diari de pujades de vídeos (massa bytes)"
#: src/state/queries/video/video.ts:180
msgid "You've reached your daily limit for video uploads (too many videos)"
msgstr ""
msgstr "Has assolit el teu límit diari de pujades de vídeos (massa vídeos)"
#: src/screens/Signup/index.tsx:146
msgid "Your account"
File diff suppressed because it is too large Load Diff
File diff suppressed because it is too large Load Diff
+44 -68
View File
@@ -8,7 +8,7 @@ msgstr ""
"Language: ja\n"
"Project-Id-Version: \n"
"Report-Msgid-Bugs-To: \n"
"PO-Revision-Date: 2024-09-07 20:28+0900\n"
"PO-Revision-Date: 2024-09-14 17:17+0900\n"
"Last-Translator: tkusano\n"
"Language-Team: Hima-Zinn, tkusano, dolciss, oboenikui, noritada, middlingphys, hibiki, reindex-ot, haoyayoi, vyv03354\n"
"Plural-Forms: \n"
@@ -532,7 +532,7 @@ msgstr "ビデオの圧縮中にエラーが発生しました。"
#: src/components/StarterPack/ProfileStarterPacks.tsx:315
msgid "An error occurred while generating your starter pack. Want to try again?"
msgstr "スターターパックの生成中にエラーが発生しました。度試しますか?"
msgstr "スターターパックの生成中にエラーが発生しました。もう一度試しますか?"
#: src/view/com/util/post-embeds/VideoEmbed.tsx:135
msgid "An error occurred while loading the video. Please try again later."
@@ -544,7 +544,7 @@ msgstr "ビデオの読み込み時にエラーが発生しました。もう一
#: src/components/dialogs/nuxs/TenMillion/index.tsx:250
msgid "An error occurred while saving the image!"
msgstr ""
msgstr "画像の保存中にエラーが発生しました!"
#: src/components/StarterPack/QrCodeDialog.tsx:71
#: src/components/StarterPack/ShareDialog.tsx:79
@@ -850,7 +850,7 @@ msgstr "Blueskyは友達と一緒のほうが楽しい!"
#: src/components/dialogs/nuxs/TenMillion/index.tsx:206
msgid "Bluesky now has over 10 million users, and I was #{0}!"
msgstr "Bluesky のユーザー数は現在 1,000 万人を超えており、私は #{0} でした。"
msgstr "Bluesky のユーザー数は現在 1,000 万人を超えており、私は #{0} 番目でした。"
#: src/components/StarterPack/ProfileStarterPacks.tsx:282
msgid "Bluesky will choose a set of recommended accounts from people in your network."
@@ -1099,14 +1099,6 @@ msgstr "確認コードが記載されたメールを確認し、ここに入力
msgid "Check your inbox for an email with the confirmation code to enter below:"
msgstr "入力したメールアドレスの受信トレイを確認して、以下に入力するための確認コードが記載されたメールが届いていないか確認してください:"
#: src/screens/Onboarding/StepInterests/index.tsx:191
#~ msgid "Choose 3 or more:"
#~ msgstr "3つ以上選んでください:"
#: src/screens/Onboarding/StepInterests/index.tsx:326
#~ msgid "Choose at least {0} more"
#~ msgstr "少なくともさらに{0}つ選んでください"
#: src/screens/StarterPack/Wizard/index.tsx:190
msgid "Choose Feeds"
msgstr "フィードの選択"
@@ -1818,10 +1810,6 @@ msgstr "新しいフィードを探す"
msgid "Discover New Feeds"
msgstr "新しいフィードを探す"
#: src/view/com/profile/ProfileHeaderSuggestedFollows.tsx:108
#~ msgid "Dismiss"
#~ msgstr "消す"
#: src/view/com/composer/Composer.tsx:1107
msgid "Dismiss error"
msgstr "エラーを消す"
@@ -2073,7 +2061,7 @@ msgstr "メールアドレス"
#: src/components/intents/VerifyEmailIntentDialog.tsx:95
msgid "Email Resent"
msgstr ""
msgstr "メール再送済"
#: src/view/com/modals/ChangeEmail.tsx:54
#: src/view/com/modals/ChangeEmail.tsx:83
@@ -2090,7 +2078,7 @@ msgstr "メールアドレスは認証されました"
#: src/components/intents/VerifyEmailIntentDialog.tsx:71
msgid "Email Verified"
msgstr ""
msgstr "メールアドレス確認完了"
#: src/view/screens/Settings/index.tsx:319
msgid "Email:"
@@ -2210,7 +2198,7 @@ msgstr "ファイルの保存中にエラーが発生しました"
#: src/screens/Signup/StepCaptcha/index.tsx:56
msgid "Error receiving captcha response."
msgstr "Captchaレスポンスの受信中にエラーが発生しました。"
msgstr "CAPTCHAレスポンスの受信中にエラーが発生しました。"
#: src/screens/Onboarding/StepInterests/index.tsx:197
#: src/view/screens/Search/Search.tsx:116
@@ -2350,7 +2338,7 @@ msgstr "スターターパックの作成に失敗しました"
#: src/view/com/modals/CreateOrEditList.tsx:194
msgid "Failed to create the list. Check your internet connection and try again."
msgstr "リストの作成に失敗しました。インターネットへの接続を確認の上、もう一度試しください。"
msgstr "リストの作成に失敗しました。インターネットへの接続を確認の上、もう一度試しください。"
#: src/components/dms/MessageMenu.tsx:73
msgid "Failed to delete message"
@@ -2393,7 +2381,7 @@ msgstr "画像の保存に失敗しました:{0}"
#: src/state/queries/notifications/settings.ts:39
msgid "Failed to save notification preferences, please try again"
msgstr "通知の設定の保存に失敗しました。再度試しください"
msgstr "通知の設定の保存に失敗しました。もう一度お試しください"
#: src/components/dms/MessageItem.tsx:225
msgid "Failed to send"
@@ -2402,11 +2390,11 @@ msgstr "送信に失敗"
#: src/components/moderation/LabelsOnMeDialog.tsx:225
#: src/screens/Messages/Conversation/ChatDisabled.tsx:87
msgid "Failed to submit appeal, please try again."
msgstr "異議申し立ての送信に失敗しました。再度試しください。"
msgstr "異議申し立ての送信に失敗しました。もう一度お試しください。"
#: src/view/com/util/forms/PostDropdownBtn.tsx:223
msgid "Failed to toggle thread mute, please try again"
msgstr "スレッドのミュートの切り替えに失敗しました。再度試しください"
msgstr "スレッドのミュートの切り替えに失敗しました。もう一度お試しください"
#: src/components/FeedCard.tsx:273
msgid "Failed to update feeds"
@@ -2554,16 +2542,16 @@ msgstr "アカウントをフォロー"
msgid "Follow all"
msgstr "すべてフォロー"
#: src/view/com/profile/FollowButton.tsx:79
msgctxt "action"
msgid "Follow Back"
msgstr ""
#: src/screens/Profile/Header/ProfileHeaderStandard.tsx:223
#: src/view/com/post-thread/PostThreadFollowBtn.tsx:142
msgid "Follow Back"
msgstr "フォローバック"
#: src/view/com/profile/FollowButton.tsx:79
msgctxt "action"
msgid "Follow Back"
msgstr "フォローバック"
#: src/view/screens/Search/Explore.tsx:334
msgid "Follow more accounts to get connected to your interests and build your network."
msgstr "もっとたくさんのアカウントをフォローして、興味あることにつながり、ネットワークを広げましょう。"
@@ -2922,7 +2910,7 @@ msgstr "そのモデレーションサービスを読み込めませんでした
#: src/state/queries/video/video.ts:165
msgid "Hold up! Were gradually giving access to video, and youre still waiting in line. Check back soon!"
msgstr ""
msgstr "待って!徐々にビデオへのアクセスを許可していますが、あなたの順番はまだです。しばらくしてもう一度確認を!"
#: src/Navigation.tsx:550
#: src/Navigation.tsx:570
@@ -3088,7 +3076,7 @@ msgstr "無効なユーザー名またはパスワード"
#: src/components/intents/VerifyEmailIntentDialog.tsx:82
msgid "Invalid Verification Code"
msgstr ""
msgstr "無効な確認コード"
#: src/view/com/modals/InviteCodes.tsx:94
msgid "Invite a Friend"
@@ -3100,7 +3088,7 @@ msgstr "招待コード"
#: src/screens/Signup/state.ts:258
msgid "Invite code not accepted. Check that you input it correctly and try again."
msgstr "招待コードが確認できません。正しく入力されていることを確認し、もう一度実行してください。"
msgstr "招待コードが確認できません。正しく入力されていることを確認し、もう一度してください。"
#: src/view/com/modals/InviteCodes.tsx:171
msgid "Invite codes: {0} available"
@@ -3147,7 +3135,7 @@ msgstr "会話に参加"
#: src/components/dialogs/nuxs/TenMillion/index.tsx:492
msgid "Joined {0}"
msgstr "{0} 参加"
msgstr "{0} 参加"
#: src/screens/Onboarding/index.tsx:21
#: src/screens/Onboarding/state.ts:89
@@ -3620,7 +3608,7 @@ msgstr "ミュート"
#: src/view/com/util/post-embeds/VideoEmbedInner/VideoWebControls.tsx:389
msgctxt "video"
msgid "Mute"
msgstr ""
msgstr "ミュート"
#: src/components/TagMenu/index.web.tsx:116
msgid "Mute {truncatedTag}"
@@ -3690,10 +3678,6 @@ msgstr "スレッドをミュート"
msgid "Mute words & tags"
msgstr "ワードとタグをミュート"
#: src/view/com/util/post-embeds/VideoEmbedInner/VideoEmbedInnerNative.tsx:168
#~ msgid "Muted"
#~ msgstr "ミュートされています"
#: src/screens/Moderation/index.tsx:264
msgid "Muted accounts"
msgstr "ミュート中のアカウント"
@@ -4509,7 +4493,7 @@ msgstr "パスワードを選択してください。"
#: src/screens/Signup/state.ts:231
msgid "Please complete the verification captcha."
msgstr "Captcha認証を完了してください。"
msgstr "CAPTCHA認証を完了してください。"
#: src/view/com/modals/ChangeEmail.tsx:65
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."
@@ -5205,11 +5189,11 @@ msgstr "メールを再送"
#: src/components/intents/VerifyEmailIntentDialog.tsx:130
msgid "Resend Email"
msgstr ""
msgstr "メール再送済"
#: src/components/intents/VerifyEmailIntentDialog.tsx:123
msgid "Resend Verification Email"
msgstr ""
msgstr "確認メールを再送済"
#: src/view/com/modals/ChangePassword.tsx:186
msgid "Reset code"
@@ -5797,10 +5781,6 @@ msgstr "バッジを表示"
msgid "Show badge and filter from feeds"
msgstr "バッジの表示とフィードからのフィルタリング"
#: src/screens/Profile/Header/ProfileHeaderStandard.tsx:215
#~ msgid "Show follows similar to {0}"
#~ msgstr "{0}に似たおすすめのフォロー候補を表示"
#: src/view/com/post-thread/PostThreadShowHiddenReplies.tsx:23
msgid "Show hidden replies"
msgstr "非表示の返信を表示"
@@ -5958,7 +5938,7 @@ msgstr "スターターパックを使わずにサインアップ"
#: src/components/FeedInterstitials.tsx:316
msgid "Similar accounts"
msgstr "類似のアカウント"
msgstr "似ているアカウント"
#: src/screens/Onboarding/StepInterests/index.tsx:245
#: src/screens/StarterPack/Wizard/index.tsx:191
@@ -6177,7 +6157,7 @@ msgstr "タップしてフルスクリーンに"
#: src/view/com/util/post-embeds/VideoEmbedInner/VideoEmbedInnerNative.tsx:151
msgid "Tap to play or pause"
msgstr ""
msgstr "タップして再生または一時停止"
#: src/view/com/util/post-embeds/VideoEmbedInner/VideoEmbedInnerNative.tsx:164
msgid "Tap to toggle sound"
@@ -6211,7 +6191,7 @@ msgstr "もう少し教えて"
#: src/components/dialogs/nuxs/TenMillion/index.tsx:518
msgid "Ten Million"
msgstr "1000万"
msgstr "1,000万"
#: src/view/shell/desktop/RightNav.tsx:90
msgid "Terms"
@@ -6252,7 +6232,7 @@ msgstr "最初の 1,000 万人のユーザーの 1 人になっていただき
#: src/components/intents/VerifyEmailIntentDialog.tsx:74
msgid "Thanks, you have successfully verified your email address."
msgstr ""
msgstr "ありがとう、メールアドレスの確認に成功しました。"
#: src/view/com/modals/ChangeHandle.tsx:459
msgid "That contains the following:"
@@ -6353,7 +6333,7 @@ msgstr "サービス規約は移動しました"
#: src/components/intents/VerifyEmailIntentDialog.tsx:85
msgid "The verification code you have provided is invalid. Please make sure that you have used the correct verification link or request a new one."
msgstr ""
msgstr "入力された確認コードが正しくありません。正しいリンクを使用したかを確認するか、新しい確認コードを要求してください。"
#: src/screens/Settings/components/DeactivateAccountDialog.tsx:86
msgid "There is no time limit for account deactivation, come back any time."
@@ -6362,17 +6342,17 @@ msgstr "アカウントの無効化に期限はありません。いつでも戻
#: src/screens/Profile/Header/ProfileHeaderLabeler.tsx:117
#: src/view/screens/ProfileFeed.tsx:545
msgid "There was an an issue contacting the server, please check your internet connection and try again."
msgstr "サーバーへの問い合わせ中に問題が発生しました。インターネットへの接続を確認の上、もう一度試しください。"
msgstr "サーバーへの問い合わせ中に問題が発生しました。インターネットへの接続を確認の上、もう一度試しください。"
#: src/view/com/posts/FeedErrorMessage.tsx:145
msgid "There was an an issue removing this feed. Please check your internet connection and try again."
msgstr "フィードの削除中に問題が発生しました。インターネットへの接続を確認の上、もう一度試しください。"
msgstr "フィードの削除中に問題が発生しました。インターネットへの接続を確認の上、もう一度試しください。"
#: src/view/com/posts/FeedShutdownMsg.tsx:52
#: src/view/com/posts/FeedShutdownMsg.tsx:71
#: src/view/screens/ProfileFeed.tsx:206
msgid "There was an an issue updating your feeds, please check your internet connection and try again."
msgstr "フィードの更新中に問題が発生しました。インターネットへの接続を確認の上、もう一度試しください。"
msgstr "フィードの更新中に問題が発生しました。インターネットへの接続を確認の上、もう一度試しください。"
#: src/components/dialogs/GifSelect.ios.tsx:197
#: src/components/dialogs/GifSelect.tsx:213
@@ -6816,7 +6796,7 @@ msgstr "ミュートを解除"
#: src/view/com/util/post-embeds/VideoEmbedInner/VideoWebControls.tsx:388
msgctxt "video"
msgid "Unmute"
msgstr ""
msgstr "ミュートを解除"
#: src/components/TagMenu/index.web.tsx:115
msgid "Unmute {truncatedTag}"
@@ -6844,10 +6824,6 @@ msgstr "スレッドのミュートを解除"
msgid "Unmute video"
msgstr "ビデオのミュートを解除"
#: src/view/com/util/post-embeds/VideoEmbedInner/VideoEmbedInnerNative.tsx:168
#~ msgid "Unmuted"
#~ msgstr "ミュート解除中"
#: src/view/screens/ProfileFeed.tsx:292
#: src/view/screens/ProfileList.tsx:673
msgid "Unpin"
@@ -7075,7 +7051,7 @@ msgstr "メールアドレスを確認"
#: src/components/intents/VerifyEmailIntentDialog.tsx:61
msgid "Verify email dialog"
msgstr ""
msgstr "メールアドレス確認ダイアログ"
#: src/view/screens/Settings/index.tsx:961
msgid "Verify my email"
@@ -7135,7 +7111,7 @@ msgstr "ビデオ:{0}"
#: src/view/com/composer/videos/SelectVideoBtn.tsx:65
#: src/view/com/composer/videos/VideoPreview.web.tsx:44
msgid "Videos must be less than 60 seconds long"
msgstr ""
msgstr "ビデオは60秒より短くなくてはなりません"
#: src/screens/Profile/Header/Shell.tsx:113
msgid "View {0}'s avatar"
@@ -7248,7 +7224,7 @@ msgstr "あなたのアカウントが準備できるまで{estimatedTime}ほど
#: src/components/intents/VerifyEmailIntentDialog.tsx:98
msgid "We have sent another verification email to <0>{0}</0>."
msgstr ""
msgstr "別の確認コードを<0>{0}</0>へ送りました。"
#: src/screens/Onboarding/StepFinished.tsx:238
msgid "We hope you have a wonderful time. Remember, Bluesky is:"
@@ -7260,7 +7236,7 @@ msgstr "あなたのフォロー中のユーザーの投稿を読み終わりま
#: src/state/queries/video/video.ts:170
msgid "We were unable to determine if you are allowed to upload videos. Please try again."
msgstr ""
msgstr "あなたがビデオのアップロードを許可されているかどうか判断できません。もう一度お試しください。"
#: src/components/dialogs/BirthDateSettings.tsx:52
msgid "We were unable to load your birth date preferences. Please try again."
@@ -7284,7 +7260,7 @@ msgstr "これはあなたの体験をカスタマイズするために使用さ
#: src/components/dms/dialogs/SearchablePeopleList.tsx:90
msgid "We're having network issues, try again"
msgstr "ネットワークで問題が発生しています。度試してください"
msgstr "ネットワークで問題が発生しています。もう一度試してください"
#: src/screens/Signup/index.tsx:100
msgid "We're so excited to have you join us!"
@@ -7300,7 +7276,7 @@ msgstr "大変申し訳ありませんが、現在ミュートされたワード
#: src/view/screens/Search/Search.tsx:206
msgid "We're sorry, but your search could not be completed. Please try again in a few minutes."
msgstr "大変申し訳ありませんが、検索を完了できませんでした。数分後に再試行してください。"
msgstr "大変申し訳ありませんが、検索を完了できませんでした。数分後にもう一度お試しください。"
#: src/view/com/composer/Composer.tsx:421
msgid "We're sorry! The post you are replying to has been deleted."
@@ -7464,7 +7440,7 @@ msgstr "あなたは並んでいます。"
#: src/state/queries/video/video.ts:161
msgid "You are not allowed to upload videos."
msgstr ""
msgstr "あなたはビデオのアップロードを許可されていません。"
#: src/view/com/profile/ProfileFollows.tsx:95
msgid "You are not following anyone."
@@ -7585,7 +7561,7 @@ msgstr "最後まで到達しました"
#: src/state/queries/video/video-upload.shared.ts:67
msgid "You have temporarily reached the limit for video uploads. Please try again later."
msgstr ""
msgstr "ビデオのアップロードの制限に一時的に到達しました。時間をおいてもう一度お試しください。"
#: src/components/StarterPack/ProfileStarterPacks.tsx:235
msgid "You haven't created a starter pack yet!"
@@ -7711,11 +7687,11 @@ msgstr "フィードはここまでです!もっとフォローするアカウ
#: src/state/queries/video/video.ts:175
msgid "You've reached your daily limit for video uploads (too many bytes)"
msgstr ""
msgstr "ビデオのアップロードの一日の上限に到達しました(容量が大きすぎます)"
#: src/state/queries/video/video.ts:180
msgid "You've reached your daily limit for video uploads (too many videos)"
msgstr ""
msgstr "ビデオのアップロードの一日の上限に到達しました(ビデオの数が多すぎます)"
#: src/screens/Signup/index.tsx:146
msgid "Your account"
@@ -7727,7 +7703,7 @@ msgstr "あなたのアカウントは削除されました"
#: src/state/queries/video/video.ts:185
msgid "Your account is not yet old enough to upload videos. Please try again later."
msgstr ""
msgstr "あなたのアカウントはまだ新しいのでビデオをアップロードできません。もう一度お試しください。"
#: src/view/screens/Settings/ExportCarDialog.tsx:65
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."
+89 -91
View File
@@ -8,7 +8,7 @@ msgstr ""
"Language: ko\n"
"Project-Id-Version: \n"
"Report-Msgid-Bugs-To: \n"
"PO-Revision-Date: 2024-09-06 14:25+0900\n"
"PO-Revision-Date: 2024-09-17 00:08+0900\n"
"Last-Translator: quiple\n"
"Language-Team: quiple, lens0021, HaruChanHeart, hazzzi, heartade\n"
"Plural-Forms: \n"
@@ -544,7 +544,7 @@ msgstr "동영상을 불러오는 동안 오류가 발생했습니다. 다시
#: src/components/dialogs/nuxs/TenMillion/index.tsx:250
msgid "An error occurred while saving the image!"
msgstr ""
msgstr "이미지를 저장하는 동안 오류가 발생했습니다"
#: src/components/StarterPack/QrCodeDialog.tsx:71
#: src/components/StarterPack/ShareDialog.tsx:79
@@ -850,7 +850,7 @@ msgstr "Bluesky는 친구와 함께하면 더 좋답니다!"
#: src/components/dialogs/nuxs/TenMillion/index.tsx:206
msgid "Bluesky now has over 10 million users, and I was #{0}!"
msgstr ""
msgstr "Bluesky는 현재 천만 명이 넘는 사용자를 보유하고 있으며, 나는 {0}번째였습니다!"
#: src/components/StarterPack/ProfileStarterPacks.tsx:282
msgid "Bluesky will choose a set of recommended accounts from people in your network."
@@ -873,9 +873,9 @@ msgstr "이미지 흐리게 및 피드에서 필터링"
msgid "Books"
msgstr "책"
#: src/components/dialogs/nuxs/TenMillion/index.tsx:614
#: src/components/dialogs/nuxs/TenMillion/index.tsx:621
msgid "Brag a little!"
msgstr ""
msgstr "자랑해 보세요!"
#: src/components/FeedInterstitials.tsx:350
msgid "Browse more accounts on the Explore page"
@@ -1016,9 +1016,9 @@ msgstr "자막(.vtt)"
msgid "Captions & alt text"
msgstr "자막 및 대체 텍스트"
#: src/components/dialogs/nuxs/TenMillion/index.tsx:368
#: src/components/dialogs/nuxs/TenMillion/index.tsx:369
msgid "Celebrating {0} users"
msgstr ""
msgstr "사용자 {0}명 축하하기"
#: src/view/com/modals/VerifyEmail.tsx:160
msgid "Change"
@@ -1099,14 +1099,6 @@ msgstr "이메일에서 로그인 코드를 확인한 후 여기에 입력하세
msgid "Check your inbox for an email with the confirmation code to enter below:"
msgstr "받은 편지함에서 아래에 입력할 인증 코드가 포함된 이메일이 있는지 확인하세요."
#: src/screens/Onboarding/StepInterests/index.tsx:191
#~ msgid "Choose 3 or more:"
#~ msgstr "3개 이상 선택하세요."
#: src/screens/Onboarding/StepInterests/index.tsx:326
#~ msgid "Choose at least {0} more"
#~ msgstr "최소 {0}개 이상 선택하세요"
#: src/screens/StarterPack/Wizard/index.tsx:190
msgid "Choose Feeds"
msgstr "피드 선택"
@@ -1818,10 +1810,6 @@ msgstr "새 피드 발견하기"
msgid "Discover New Feeds"
msgstr "새 피드 발견하기"
#: src/view/com/profile/ProfileHeaderSuggestedFollows.tsx:108
#~ msgid "Dismiss"
#~ msgstr "닫기"
#: src/view/com/composer/Composer.tsx:1107
msgid "Dismiss error"
msgstr "오류 무시"
@@ -1907,9 +1895,9 @@ msgstr "Bluesky 다운로드"
msgid "Download CAR file"
msgstr "CAR 파일 다운로드"
#: src/components/dialogs/nuxs/TenMillion/index.tsx:622
#: src/components/dialogs/nuxs/TenMillion/index.tsx:629
msgid "Download image"
msgstr ""
msgstr "이미지 다운로드"
#: src/view/com/composer/text-input/TextInput.web.tsx:269
msgid "Drop to add images"
@@ -2073,7 +2061,7 @@ msgstr "이메일 주소"
#: src/components/intents/VerifyEmailIntentDialog.tsx:95
msgid "Email Resent"
msgstr ""
msgstr "이메일 다시 전송됨"
#: src/view/com/modals/ChangeEmail.tsx:54
#: src/view/com/modals/ChangeEmail.tsx:83
@@ -2086,11 +2074,11 @@ msgstr "이메일 변경됨"
#: src/view/com/modals/VerifyEmail.tsx:85
msgid "Email verified"
msgstr "이메일 인됨"
msgstr "이메일 인됨"
#: src/components/intents/VerifyEmailIntentDialog.tsx:71
msgid "Email Verified"
msgstr ""
msgstr "이메일 인증됨"
#: src/view/screens/Settings/index.tsx:319
msgid "Email:"
@@ -2152,7 +2140,7 @@ msgstr "피드 끝"
#: src/view/com/composer/videos/SubtitleDialog.tsx:161
msgid "Ensure you have selected a language for each subtitle file."
msgstr ""
msgstr "각 자막 파일에 대한 언어를 선택했는지 확인하세요."
#: src/view/com/modals/AddAppPasswords.tsx:161
msgid "Enter a name for this App Password"
@@ -2554,16 +2542,16 @@ msgstr "계정 팔로우"
msgid "Follow all"
msgstr "모두 팔로우"
#: src/view/com/profile/FollowButton.tsx:79
msgctxt "action"
msgid "Follow Back"
msgstr ""
#: src/screens/Profile/Header/ProfileHeaderStandard.tsx:223
#: src/view/com/post-thread/PostThreadFollowBtn.tsx:142
msgid "Follow Back"
msgstr "맞팔로우"
#: src/view/com/profile/FollowButton.tsx:79
msgctxt "action"
msgid "Follow Back"
msgstr "맞팔로우"
#: src/view/screens/Search/Explore.tsx:334
msgid "Follow more accounts to get connected to your interests and build your network."
msgstr "더 많은 계정을 팔로우하고 관심 분야를 연결하여 네트워크를 구축하세요."
@@ -2922,7 +2910,7 @@ msgstr "검토 서비스를 불러올 수 없습니다."
#: src/state/queries/video/video.ts:165
msgid "Hold up! Were gradually giving access to video, and youre still waiting in line. Check back soon!"
msgstr ""
msgstr "잠깐! 동영상에 대한 접근 권한이 점차적으로 제공되고 있지만 아직은 기다려야 합니다. 나중에 다시 확인해 주세요!"
#: src/Navigation.tsx:550
#: src/Navigation.tsx:570
@@ -2977,7 +2965,7 @@ msgstr "아무것도 선택하지 않으면 모든 연령대에 적합하다는
#: src/screens/Signup/StepInfo/Policies.tsx:89
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:720
msgid "If you delete this list, you won't be able to recover it."
@@ -3088,7 +3076,7 @@ msgstr "잘못된 사용자 이름 또는 비밀번호"
#: src/components/intents/VerifyEmailIntentDialog.tsx:82
msgid "Invalid Verification Code"
msgstr ""
msgstr "잘못된 인증 코드"
#: src/view/com/modals/InviteCodes.tsx:94
msgid "Invite a Friend"
@@ -3145,9 +3133,9 @@ msgstr "Bluesky 가입하기"
msgid "Join the conversation"
msgstr "대화에 참여하기"
#: src/components/dialogs/nuxs/TenMillion/index.tsx:492
#: src/components/dialogs/nuxs/TenMillion/index.tsx:498
msgid "Joined {0}"
msgstr ""
msgstr "{0}에 가입함"
#: src/screens/Onboarding/index.tsx:21
#: src/screens/Onboarding/state.ts:89
@@ -3612,15 +3600,15 @@ msgstr "영화"
msgid "Music"
msgstr "음악"
#: src/components/TagMenu/index.tsx:263
msgid "Mute"
msgstr "뮤트"
#: src/view/com/util/post-embeds/VideoEmbedInner/VideoEmbedInnerNative.tsx:166
#: src/view/com/util/post-embeds/VideoEmbedInner/VideoWebControls.tsx:389
msgctxt "video"
msgid "Mute"
msgstr ""
#: src/components/TagMenu/index.tsx:263
#: src/view/com/util/post-embeds/VideoEmbedInner/VideoEmbedInnerNative.tsx:163
msgid "Mute"
msgstr "뮤트"
msgstr "음소거"
#: src/components/TagMenu/index.web.tsx:116
msgid "Mute {truncatedTag}"
@@ -3690,10 +3678,6 @@ msgstr "스레드 뮤트"
msgid "Mute words & tags"
msgstr "단어 및 태그 뮤트"
#: src/view/com/util/post-embeds/VideoEmbedInner/VideoEmbedInnerNative.tsx:168
#~ msgid "Muted"
#~ msgstr "뮤트됨"
#: src/screens/Moderation/index.tsx:264
msgid "Muted accounts"
msgstr "뮤트한 계정"
@@ -3906,6 +3890,11 @@ msgstr "인기 GIF를 찾을 수 없습니다. Tenor에 문제가 있을 수 있
msgid "No feeds found. Try searching for something else."
msgstr "피드를 찾을 수 없습니다. 다른 피드를 검색해 보세요."
#: src/components/LikedByList.tsx:78
#: src/view/com/post-thread/PostLikedBy.tsx:85
msgid "No likes yet"
msgstr "아직 좋아요 없음"
#: src/components/ProfileCard.tsx:336
#: src/screens/Profile/Header/ProfileHeaderStandard.tsx:116
msgid "No longer following {0}"
@@ -3925,7 +3914,7 @@ msgstr "더 이상 표시할 대화가 없습니다"
#: src/view/com/notifications/Feed.tsx:121
msgid "No notifications yet!"
msgstr "아직 알림이 없습니다."
msgstr "아직 알림이 없습니다"
#: src/components/dms/MessagesNUX.tsx:149
#: src/components/dms/MessagesNUX.tsx:152
@@ -3942,6 +3931,14 @@ msgstr "이 게시물은 작성자 외에는 누구도 인용할 수 없습니
msgid "No posts yet."
msgstr "아직 게시물이 없습니다."
#: src/view/com/post-thread/PostQuotes.tsx:106
msgid "No quotes yet"
msgstr "아직 인용 없음"
#: src/view/com/post-thread/PostRepostedBy.tsx:78
msgid "No reposts 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"
@@ -3980,11 +3977,20 @@ msgstr "사용하지 않음"
msgid "Nobody"
msgstr "없음"
#: src/components/LikedByList.tsx:79
#: src/components/LikedByList.tsx:80
#: src/components/LikesDialog.tsx:99
#: src/view/com/post-thread/PostLikedBy.tsx:87
msgid "Nobody has liked this yet. Maybe you should be the first!"
msgstr "아직 아무도 좋아요를 누르지 않았습니다. 첫 번째가 되어 보세요!"
#: src/view/com/post-thread/PostQuotes.tsx:108
msgid "Nobody has quoted this yet. Maybe you should be the first!"
msgstr "아직 아무도 인용하지 않았습니다. 첫 번째가 되어 보세요!"
#: src/view/com/post-thread/PostRepostedBy.tsx:80
msgid "Nobody has reposted this yet. Maybe you should be the first!"
msgstr "아직 아무도 재게시하지 않았습니다. 첫 번째가 되어 보세요!"
#: src/screens/StarterPack/Wizard/StepProfiles.tsx:103
msgid "Nobody was found. Try searching for someone else."
msgstr "아무도 찾을 수 없습니다. 다른 사용자를 검색해 보세요."
@@ -4081,7 +4087,7 @@ msgstr "이런! 뭔가 잘못되었습니다."
#: src/components/dialogs/nuxs/TenMillion/index.tsx:175
msgid "Oh no! We weren't able to generate an image for you to share. Rest assured, we're glad you're here 🦋"
msgstr ""
msgstr "이런! 공유할 이미지를 생성할 수 없습니다. 안심하고 방문해 주셔서 감사합니다."
#: src/screens/Profile/Header/ProfileHeaderLabeler.tsx:339
msgid "OK"
@@ -5201,15 +5207,15 @@ msgstr "이 제공자에서 필수"
#: src/view/screens/Settings/DisableEmail2FADialog.tsx:168
#: src/view/screens/Settings/DisableEmail2FADialog.tsx:171
msgid "Resend email"
msgstr "이메일 다시 보내기"
msgstr "이메일 다시 전송"
#: src/components/intents/VerifyEmailIntentDialog.tsx:130
msgid "Resend Email"
msgstr ""
msgstr "이메일 다시 전송"
#: src/components/intents/VerifyEmailIntentDialog.tsx:123
msgid "Resend Verification Email"
msgstr ""
msgstr "인증 이메일 다시 전송하기"
#: src/view/com/modals/ChangePassword.tsx:186
msgid "Reset code"
@@ -5686,7 +5692,7 @@ msgstr "성행위 또는 선정적인 노출."
msgid "Sexually Suggestive"
msgstr "외설적"
#: src/components/dialogs/nuxs/TenMillion/index.tsx:644
#: src/components/dialogs/nuxs/TenMillion/index.tsx:651
#: src/components/StarterPack/QrCodeDialog.tsx:177
#: src/screens/StarterPack/StarterPackScreen.tsx:411
#: src/screens/StarterPack/StarterPackScreen.tsx:582
@@ -5723,13 +5729,13 @@ msgstr "무시하고 공유"
msgid "Share feed"
msgstr "피드 공유"
#: src/components/dialogs/nuxs/TenMillion/index.tsx:621
#: src/components/dialogs/nuxs/TenMillion/index.tsx:628
msgid "Share image externally"
msgstr ""
msgstr "외부로 이미지 공유"
#: src/components/dialogs/nuxs/TenMillion/index.tsx:639
#: src/components/dialogs/nuxs/TenMillion/index.tsx:646
msgid "Share image in post"
msgstr ""
msgstr "게시물로 이미지 공유"
#: src/components/StarterPack/ShareDialog.tsx:124
#: src/components/StarterPack/ShareDialog.tsx:131
@@ -5797,10 +5803,6 @@ msgstr "배지 표시"
msgid "Show badge and filter from feeds"
msgstr "배지 표시 및 피드에서 필터링"
#: src/screens/Profile/Header/ProfileHeaderStandard.tsx:218
#~ msgid "Show follows similar to {0}"
#~ msgstr "{0} 님과 비슷한 팔로우 표시"
#: src/view/com/post-thread/PostThreadShowHiddenReplies.tsx:23
msgid "Show hidden replies"
msgstr "숨겨진 답글 표시"
@@ -6177,9 +6179,9 @@ msgstr "탭하여 전체화면으로 보기"
#: src/view/com/util/post-embeds/VideoEmbedInner/VideoEmbedInnerNative.tsx:151
msgid "Tap to play or pause"
msgstr ""
msgstr "탭하여 재생/일시 정지"
#: src/view/com/util/post-embeds/VideoEmbedInner/VideoEmbedInnerNative.tsx:164
#: src/view/com/util/post-embeds/VideoEmbedInner/VideoEmbedInnerNative.tsx:168
msgid "Tap to toggle sound"
msgstr "탭하여 소리 켜기/끄기"
@@ -6209,9 +6211,9 @@ msgstr "농담해 보세요!"
msgid "Tell us a little more"
msgstr "좀 더 자세히 알려주세요"
#: src/components/dialogs/nuxs/TenMillion/index.tsx:518
#: src/components/dialogs/nuxs/TenMillion/index.tsx:524
msgid "Ten Million"
msgstr ""
msgstr "천만 명"
#: src/view/shell/desktop/RightNav.tsx:90
msgid "Terms"
@@ -6246,13 +6248,13 @@ msgstr "텍스트 입력 필드"
msgid "Thank you. Your report has been sent."
msgstr "감사합니다. 신고를 전송했습니다."
#: src/components/dialogs/nuxs/TenMillion/index.tsx:593
#: src/components/dialogs/nuxs/TenMillion/index.tsx:600
msgid "Thanks for being one of our first 10 million users."
msgstr ""
msgstr "사용자 천만 명 중 한 명이 되어 주셔서 감사합니다."
#: src/components/intents/VerifyEmailIntentDialog.tsx:74
msgid "Thanks, you have successfully verified your email address."
msgstr ""
msgstr "이메일 주소를 성공적으로 인증했습니다."
#: src/view/com/modals/ChangeHandle.tsx:459
msgid "That contains the following:"
@@ -6271,7 +6273,7 @@ msgstr "이 핸들은 이미 사용 중입니다."
msgid "That starter pack could not be found."
msgstr "스타터 팩을 찾을 수 없습니다."
#: src/view/com/post-thread/PostQuotes.tsx:127
#: src/view/com/post-thread/PostQuotes.tsx:133
msgid "That's all, folks!"
msgstr "이상입니다, 여러분!"
@@ -6353,7 +6355,7 @@ msgstr "서비스 이용약관을 다음으로 이동했습니다:"
#: src/components/intents/VerifyEmailIntentDialog.tsx:85
msgid "The verification code you have provided is invalid. Please make sure that you have used the correct verification link or request a new one."
msgstr ""
msgstr "입력한 인증 코드가 올바르지 않습니다. 올바른 인증 링크를 사용했는지 확인하거나 새 인증 링크를 요청하세요."
#: src/screens/Settings/components/DeactivateAccountDialog.tsx:86
msgid "There is no time limit for account deactivation, come back any time."
@@ -6680,9 +6682,9 @@ msgstr "Bluesky에 동영상을 업로드하려면 먼저 이메일을 인증해
msgid "To whom would you like to send this report?"
msgstr "이 신고를 누구에게 보내시겠습니까?"
#: src/components/dialogs/nuxs/TenMillion/index.tsx:597
#: src/components/dialogs/nuxs/TenMillion/index.tsx:604
msgid "Together, we're rebuilding the social internet. We're glad you're here."
msgstr ""
msgstr "우리는 함께 소셜 인터넷을 재건하고 있습니다. 함께해 주셔서 감사합니다."
#: src/view/com/util/forms/DropdownButton.tsx:255
msgid "Toggle dropdown"
@@ -6807,17 +6809,17 @@ msgstr "계정 언팔로우"
msgid "Unlike this feed"
msgstr "이 피드 좋아요 취소"
#: src/view/com/util/post-embeds/VideoEmbedInner/VideoWebControls.tsx:388
msgctxt "video"
msgid "Unmute"
msgstr ""
#: src/components/TagMenu/index.tsx:263
#: src/view/com/util/post-embeds/VideoEmbedInner/VideoEmbedInnerNative.tsx:163
#: src/view/screens/ProfileList.tsx:689
msgid "Unmute"
msgstr "언뮤트"
#: src/view/com/util/post-embeds/VideoEmbedInner/VideoEmbedInnerNative.tsx:165
#: src/view/com/util/post-embeds/VideoEmbedInner/VideoWebControls.tsx:388
msgctxt "video"
msgid "Unmute"
msgstr "음소거 해제"
#: src/components/TagMenu/index.web.tsx:115
msgid "Unmute {truncatedTag}"
msgstr "{truncatedTag} 언뮤트"
@@ -6844,10 +6846,6 @@ msgstr "스레드 언뮤트"
msgid "Unmute video"
msgstr "동영상 음소거 해제"
#: src/view/com/util/post-embeds/VideoEmbedInner/VideoEmbedInnerNative.tsx:168
#~ msgid "Unmuted"
#~ msgstr "음소거 해제됨"
#: src/view/screens/ProfileFeed.tsx:292
#: src/view/screens/ProfileList.tsx:673
msgid "Unpin"
@@ -7075,7 +7073,7 @@ msgstr "이메일 인증"
#: src/components/intents/VerifyEmailIntentDialog.tsx:61
msgid "Verify email dialog"
msgstr ""
msgstr "이메일 인증 대화 상자"
#: src/view/screens/Settings/index.tsx:961
msgid "Verify my email"
@@ -7135,11 +7133,11 @@ msgstr "동영상: {0}"
#: src/view/com/composer/videos/SelectVideoBtn.tsx:65
#: src/view/com/composer/videos/VideoPreview.web.tsx:44
msgid "Videos must be less than 60 seconds long"
msgstr ""
msgstr "동영상 길이는 60초 미만이어야 합니다."
#: src/screens/Profile/Header/Shell.tsx:113
msgid "View {0}'s avatar"
msgstr "{0} 님의 아바타를 봅니다"
msgstr "{0} 님의 아바타 보기"
#: src/components/ProfileCard.tsx:110
#: src/view/com/notifications/FeedItem.tsx:273
@@ -7248,7 +7246,7 @@ msgstr "계정이 준비될 때까지 {estimatedTime}이(가) 걸릴 것으로
#: src/components/intents/VerifyEmailIntentDialog.tsx:98
msgid "We have sent another verification email to <0>{0}</0>."
msgstr ""
msgstr "<0>{0}</0>(으)로 또 다른 인증 이메일을 보냈습니다."
#: src/screens/Onboarding/StepFinished.tsx:238
msgid "We hope you have a wonderful time. Remember, Bluesky is:"
@@ -7260,7 +7258,7 @@ msgstr "팔로우한 사용자의 게시물이 부족합니다. 대신 <0/>의
#: src/state/queries/video/video.ts:170
msgid "We were unable to determine if you are allowed to upload videos. Please try again."
msgstr ""
msgstr "동영상을 업로드할 수 있는지 확인할 수 없습니다. 다시 시도해 주세요."
#: src/components/dialogs/BirthDateSettings.tsx:52
msgid "We were unable to load your birth date preferences. Please try again."
@@ -7464,7 +7462,7 @@ msgstr "대기 중입니다."
#: src/state/queries/video/video.ts:161
msgid "You are not allowed to upload videos."
msgstr ""
msgstr "동영상을 업로드할 수 없습니다."
#: src/view/com/profile/ProfileFollows.tsx:95
msgid "You are not following anyone."
@@ -7585,7 +7583,7 @@ msgstr "끝에 도달했습니다"
#: src/state/queries/video/video-upload.shared.ts:67
msgid "You have temporarily reached the limit for video uploads. Please try again later."
msgstr ""
msgstr "일시적으로 동영상 업로드 한도에 도달했습니다. 나중에 다시 시도해 주세요."
#: src/components/StarterPack/ProfileStarterPacks.tsx:235
msgid "You haven't created a starter pack yet!"
@@ -7711,11 +7709,11 @@ msgstr "피드 끝에 도달했습니다! 팔로우할 계정을 더 찾아보
#: src/state/queries/video/video.ts:175
msgid "You've reached your daily limit for video uploads (too many bytes)"
msgstr ""
msgstr "동영상 업로드 일일 한도에 도달했습니다 (용량이 너무 큼)"
#: src/state/queries/video/video.ts:180
msgid "You've reached your daily limit for video uploads (too many videos)"
msgstr ""
msgstr "동영상 업로드 일일 한도에 도달했습니다 (동영상 수가 너무 많음)"
#: src/screens/Signup/index.tsx:146
msgid "Your account"
@@ -7727,7 +7725,7 @@ msgstr "계정을 삭제했습니다"
#: src/state/queries/video/video.ts:185
msgid "Your account is not yet old enough to upload videos. Please try again later."
msgstr ""
msgstr "내 계정은 아직 동영상을 업로드할 수 없습니다. 나중에 다시 시도해 주세요."
#: src/view/screens/Settings/ExportCarDialog.tsx:65
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."
+141 -129
View File
@@ -8,7 +8,7 @@ msgstr ""
"Language: pt-BR\n"
"Project-Id-Version: \n"
"Report-Msgid-Bugs-To: \n"
"PO-Revision-Date: 2024-05-13 11:41\n"
"PO-Revision-Date: 2024-09-16 17:30+01:00\n"
"Last-Translator: fabiohcnobre\n"
"Language-Team: maisondasilva, MightyLoggor, gildaswise, gleydson, faeriarum, fabiohcnobre, garccez\n"
"Plural-Forms: nplurals=2; plural=(n != 1);\n"
@@ -28,11 +28,11 @@ msgstr "{0, plural, one {{formattedCount} outro} other {{formattedCount} outros}
#: src/lib/hooks/useTimeAgo.ts:156
msgid "{0, plural, one {# day} other {# days}}"
msgstr ""
msgstr "{0, plural, one {# dia} other {# dias}}"
#: src/lib/hooks/useTimeAgo.ts:146
msgid "{0, plural, one {# hour} other {# hours}}"
msgstr ""
msgstr "{0, plural, one {# hora} other {# horas}}"
#: src/components/moderation/LabelsOnMe.tsx:55
#~ msgid "{0, plural, one {# label has been placed on this account} other {# labels has been placed on this account}}"
@@ -52,11 +52,11 @@ msgstr "{0, plural, one {# rótulo foi colocado neste conteúdo} other {# rótul
#: src/lib/hooks/useTimeAgo.ts:136
msgid "{0, plural, one {# minute} other {# minutes}}"
msgstr ""
msgstr "{0, plural, one {# minuto} other {# minutos}}"
#: src/lib/hooks/useTimeAgo.ts:167
msgid "{0, plural, one {# month} other {# months}}"
msgstr ""
msgstr "{0, plural, one {# mês} other {# meses}}"
#: src/view/com/util/post-ctrls/RepostButton.tsx:71
msgid "{0, plural, one {# repost} other {# reposts}}"
@@ -64,7 +64,7 @@ msgstr "{0, plural, one {# repostagem} other {# repostagens}}"
#: src/lib/hooks/useTimeAgo.ts:126
msgid "{0, plural, one {# second} other {# seconds}}"
msgstr ""
msgstr "{0, plural, one {# segundo} other {# segundos}}"
#: src/components/KnownFollowers.tsx:179
#~ msgid "{0, plural, one {and # other} other {and # others}}"
@@ -116,20 +116,20 @@ msgstr "{0, plural, one {Descurtir (# curtida)} other {Descurtir (# curtidas)}}"
#. Pattern: {wordValue} in tags
#: src/components/dialogs/MutedWords.tsx:475
msgid "{0} <0>in <1>tags</1></0>"
msgstr ""
msgstr "{0} <0>em <1>tags</1></0>"
#. Pattern: {wordValue} in text, tags
#: src/components/dialogs/MutedWords.tsx:465
msgid "{0} <0>in <1>text & tags</1></0>"
msgstr ""
msgstr "{0} <0>em <1>texto e tags</1></0>"
#: src/screens/StarterPack/StarterPackLandingScreen.tsx:228
msgid "{0} joined this week"
msgstr "{0} entrou esta semana"
#: src/view/com/util/post-embeds/VideoEmbedInner/VideoWebControls.tsx:637
#: src/view/com/util/post-embeds/VideoEmbedInner/web-controls/Scrubber.tsx:195
msgid "{0} of {1}"
msgstr ""
msgstr "{0} de {1}"
#: src/screens/StarterPack/StarterPackScreen.tsx:467
msgid "{0} people have used this starter pack!"
@@ -154,27 +154,27 @@ msgstr "O pacote inicial de {0}"
#. How many days have passed, displayed in a narrow form
#: src/lib/hooks/useTimeAgo.ts:158
msgid "{0}d"
msgstr ""
msgstr "{0}d"
#. How many hours have passed, displayed in a narrow form
#: src/lib/hooks/useTimeAgo.ts:148
msgid "{0}h"
msgstr ""
msgstr "{0}h"
#. How many minutes have passed, displayed in a narrow form
#: src/lib/hooks/useTimeAgo.ts:138
msgid "{0}m"
msgstr ""
msgstr "{0}m"
#. How many months have passed, displayed in a narrow form
#: src/lib/hooks/useTimeAgo.ts:169
msgid "{0}mo"
msgstr ""
msgstr "{0}me"
#. How many seconds have passed, displayed in a narrow form
#: src/lib/hooks/useTimeAgo.ts:128
msgid "{0}s"
msgstr ""
msgstr "{0}s"
#: src/components/LabelingServiceCard/index.tsx:71
msgid "{count, plural, one {Liked by # user} other {Liked by # users}}"
@@ -343,12 +343,16 @@ msgstr "7 dias"
#~ msgid "A help tooltip"
#~ msgstr "Uma sugestão de ajuda"
#: src/components/dialogs/nuxs/TenMillion/index.tsx:266
msgid "A virtual certificate with text \"Celebrating 10M users on Bluesky, #{0}, {displayName} {handle}, joined on {joinedDate}\""
msgstr "Um certificado virtual com o texto \"Comemorando 10 milhões de usuários no Bluesky, #{0}, {displayName} {handle}, ingressou em {joinedDate}\""
#: src/view/com/util/ViewHeader.tsx:92
#: src/view/screens/Search/Search.tsx:684
msgid "Access navigation links and settings"
msgstr "Acessar links de navegação e configurações"
#: src/view/com/home/HomeHeaderLayoutMobile.tsx:56
#: src/view/com/home/HomeHeaderLayoutMobile.tsx:103
msgid "Access profile and other navigation links"
msgstr "Acessar perfil e outros links de navegação"
@@ -640,7 +644,7 @@ msgstr "Ocorreu um erro"
#~ msgid "An error occured"
#~ msgstr "Tivemos um problema"
#: src/view/com/util/post-embeds/VideoEmbedInner/VideoWebControls.tsx:413
#: src/view/com/util/post-embeds/VideoEmbedInner/web-controls/VideoControls.tsx:416
msgid "An error occurred"
msgstr "Ocorreu um erro"
@@ -660,13 +664,9 @@ msgstr "Ocorreu um erro ao carregar o vídeo. Tente novamente mais tarde."
msgid "An error occurred while loading the video. Please try again."
msgstr "Ocorreu um erro ao carregar o vídeo. Tente novamente."
#: src/components/dialogs/nuxs/TenMillion/index.tsx:250
#: src/components/dialogs/nuxs/TenMillion/index.tsx:315
msgid "An error occurred while saving the image!"
msgstr ""
#: src/components/StarterPack/ShareDialog.tsx:79
#~ msgid "An error occurred while saving the image."
#~ msgstr "Ocorreu um erro ao salvar a imagem."
msgstr "Ocorreu um erro ao salvar a imagem!"
#: src/components/StarterPack/QrCodeDialog.tsx:71
#: src/components/StarterPack/ShareDialog.tsx:79
@@ -994,6 +994,11 @@ msgstr "Bluesky é uma rede aberta que permite a escolha do seu provedor de hosp
msgid "Bluesky is better with friends!"
msgstr "Bluesky é melhor com amigos!"
#: src/components/dialogs/nuxs/TenMillion/Trigger.tsx:43
#: src/components/dialogs/nuxs/TenMillion/Trigger.tsx:59
msgid "Bluesky is celebrating 10 million users!"
msgstr ""
#: src/view/com/auth/onboarding/WelcomeDesktop.tsx:80
#: src/view/com/auth/onboarding/WelcomeMobile.tsx:82
#~ msgid "Bluesky is flexible."
@@ -1009,9 +1014,9 @@ msgstr "Bluesky é melhor com amigos!"
#~ msgid "Bluesky is public."
#~ msgstr "Bluesky é público."
#: src/components/dialogs/nuxs/TenMillion/index.tsx:206
#: src/components/dialogs/nuxs/TenMillion/index.tsx:256
msgid "Bluesky now has over 10 million users, and I was #{0}!"
msgstr "O Bluesky agora tem mais de 10 milhões de usuários, e eu tinha #{0}!"
msgstr "O Bluesky agora tem mais de 10 milhões de usuários, e eu fui o #{0}!"
#: src/components/StarterPack/ProfileStarterPacks.tsx:282
msgid "Bluesky will choose a set of recommended accounts from people in your network."
@@ -1034,7 +1039,7 @@ msgstr "Desfocar imagens e filtrar dos feeds"
msgid "Books"
msgstr "Livros"
#: src/components/dialogs/nuxs/TenMillion/index.tsx:614
#: src/components/dialogs/nuxs/TenMillion/index.tsx:677
msgid "Brag a little!"
msgstr "Gabe-se um pouco!"
@@ -1104,8 +1109,8 @@ msgid "Can only contain letters, numbers, spaces, dashes, and underscores. Must
msgstr "Só pode conter letras, números, espaços, riscas e subtraços. Deve ter pelo menos 4 caracteres, mas não mais de 32 caracteres."
#: src/components/Menu/index.tsx:235
#: src/components/Prompt.tsx:122
#: src/components/Prompt.tsx:124
#: src/components/Prompt.tsx:126
#: src/components/TagMenu/index.tsx:282
#: src/screens/Deactivated.tsx:161
#: src/view/com/composer/Composer.tsx:594
@@ -1175,7 +1180,7 @@ msgstr "Cancela a abertura do link"
#: src/view/com/util/post-ctrls/PostCtrls.tsx:155
#: src/view/com/util/post-ctrls/PostCtrls.tsx:191
msgid "Cannot interact with a blocked user"
msgstr ""
msgstr "Não é possível interagir com um usuário bloqueado"
#: src/view/com/composer/videos/SubtitleDialog.tsx:135
msgid "Captions (.vtt)"
@@ -1185,7 +1190,7 @@ msgstr "Legendas (.vtt)"
msgid "Captions & alt text"
msgstr "Legendas e texto alt"
#: src/components/dialogs/nuxs/TenMillion/index.tsx:368
#: src/components/dialogs/nuxs/TenMillion/index.tsx:434
msgid "Celebrating {0} users"
msgstr "Comemorando {0} usuários"
@@ -1536,8 +1541,8 @@ msgstr "Configure o filtro de conteúdo por categoria: {name}"
msgid "Configured in <0>moderation settings</0>."
msgstr "Configure no <0>painel de moderação</0>."
#: src/components/Prompt.tsx:165
#: src/components/Prompt.tsx:168
#: src/components/Prompt.tsx:167
#: src/components/Prompt.tsx:170
#: src/view/com/modals/SelfLabel.tsx:155
#: src/view/com/modals/VerifyEmail.tsx:239
#: src/view/com/modals/VerifyEmail.tsx:241
@@ -2054,7 +2059,7 @@ msgstr "Desabilitar feedback tátil"
#~ msgid "Disable haptics"
#~ msgstr "Desabilitar feedback tátil"
#: src/view/com/util/post-embeds/VideoEmbedInner/VideoWebControls.tsx:379
#: src/view/com/util/post-embeds/VideoEmbedInner/web-controls/VideoControls.tsx:382
msgid "Disable subtitles"
msgstr "Desativar legendas"
@@ -2190,7 +2195,7 @@ msgstr "Baixe o Bluesky"
msgid "Download CAR file"
msgstr "Baixar arquivo CAR"
#: src/components/dialogs/nuxs/TenMillion/index.tsx:622
#: src/components/dialogs/nuxs/TenMillion/index.tsx:686
msgid "Download image"
msgstr "Baixar imagem"
@@ -2369,7 +2374,7 @@ msgstr "Endereço de e-mail"
#: src/components/intents/VerifyEmailIntentDialog.tsx:95
msgid "Email Resent"
msgstr ""
msgstr "E-mail Reenviado"
#: src/view/com/modals/ChangeEmail.tsx:54
#: src/view/com/modals/ChangeEmail.tsx:83
@@ -2386,7 +2391,7 @@ msgstr "E-mail verificado"
#: src/components/intents/VerifyEmailIntentDialog.tsx:71
msgid "Email Verified"
msgstr ""
msgstr "E-mail Verificado"
#: src/view/screens/Settings/index.tsx:319
msgid "Email:"
@@ -2437,7 +2442,7 @@ msgstr "Habilitar mídia para"
msgid "Enable priority notifications"
msgstr "Habilitar notificações prioritárias"
#: src/view/com/util/post-embeds/VideoEmbedInner/VideoWebControls.tsx:380
#: src/view/com/util/post-embeds/VideoEmbedInner/web-controls/VideoControls.tsx:383
msgid "Enable subtitles"
msgstr "Habilitar legendas"
@@ -2569,7 +2574,7 @@ msgstr "Excluir usuário que você segue"
msgid "Excludes users you follow"
msgstr "Excluir usuário que você segue"
#: src/view/com/util/post-embeds/VideoEmbedInner/VideoWebControls.tsx:397
#: src/view/com/util/post-embeds/VideoEmbedInner/web-controls/VideoControls.tsx:400
msgid "Exit fullscreen"
msgstr "Sair da tela cheia"
@@ -2612,7 +2617,7 @@ msgstr "Experimental: Quando essa preferência estiver habilitada, você receber
#: src/components/dialogs/MutedWords.tsx:500
msgid "Expired"
msgstr "Expirado "
msgstr "Expirado"
#: src/components/dialogs/MutedWords.tsx:502
msgid "Expires {0}"
@@ -2740,7 +2745,7 @@ msgstr "Falha ao alternar o silenciamento do tópico, tente novamente"
#: src/components/FeedCard.tsx:273
msgid "Failed to update feeds"
msgstr "Falha ao atualizar os feeds\""
msgstr "Falha ao atualizar os feeds"
#: src/components/dms/MessagesNUX.tsx:60
#: src/screens/Messages/Settings.tsx:35
@@ -2916,18 +2921,14 @@ msgstr "Seguir Conta"
msgid "Follow all"
msgstr "Siga todos"
#: src/screens/Onboarding/StepSuggestedAccounts/index.tsx:187
#~ msgid "Follow All"
#~ msgstr "Seguir Todas"
#: src/screens/Profile/Header/ProfileHeaderStandard.tsx:223
#: src/view/com/post-thread/PostThreadFollowBtn.tsx:142
msgid "Follow Back"
msgstr "Seguir De Volta"
#: src/view/com/profile/FollowButton.tsx:79
msgctxt "action"
msgid "Follow Back"
msgstr ""
#: src/screens/Profile/Header/ProfileHeaderStandard.tsx:223
#: src/view/com/post-thread/PostThreadFollowBtn.tsx:142
msgid "Follow Back"
msgstr "Seguir De Volta"
#: src/view/screens/Search/Explore.tsx:334
@@ -3083,7 +3084,7 @@ msgctxt "from-feed"
msgid "From <0/>"
msgstr "Por <0/>"
#: src/view/com/util/post-embeds/VideoEmbedInner/VideoWebControls.tsx:398
#: src/view/com/util/post-embeds/VideoEmbedInner/web-controls/VideoControls.tsx:401
msgid "Fullscreen"
msgstr "Tela cheia"
@@ -3114,7 +3115,7 @@ msgstr "Começando"
#: src/components/MediaPreview.tsx:122
msgid "GIF"
msgstr ""
msgstr "GIF"
#: src/screens/Onboarding/StepProfile/index.tsx:225
msgid "Give your profile a face"
@@ -3427,7 +3428,7 @@ msgstr "Imagem"
msgid "Image alt text"
msgstr "Texto alternativo da imagem"
#: src/components/dialogs/nuxs/TenMillion/index.tsx:247
#: src/components/dialogs/nuxs/TenMillion/index.tsx:312
#: src/components/StarterPack/ShareDialog.tsx:76
msgid "Image saved to your camera roll!"
msgstr "Imagem salva no rolo da câmera!"
@@ -3438,7 +3439,7 @@ msgstr "Falsificação de identidade ou alegações falsas sobre identidade ou f
#: src/lib/moderation/useReportOptions.ts:68
msgid "Impersonation, misinformation, or false claims"
msgstr ""
msgstr "Falsificação de identidade, desinformação ou alegações falsas"
#: src/lib/moderation/useReportOptions.ts:91
msgid "Inappropriate messages or explicit links"
@@ -3512,7 +3513,7 @@ msgstr "Credenciais inválidas"
#: src/components/intents/VerifyEmailIntentDialog.tsx:82
msgid "Invalid Verification Code"
msgstr ""
msgstr "Código de Verificação Inválido"
#: src/view/com/modals/InviteCodes.tsx:94
msgid "Invite a Friend"
@@ -3556,7 +3557,7 @@ msgstr "É só você por enquanto! Adicione mais pessoas ao seu pacote inicial p
#: src/view/com/composer/Composer.tsx:1126
msgid "Job ID: {0}"
msgstr ""
msgstr "Job ID: {0}"
#: src/view/com/auth/SplashScreen.web.tsx:164
msgid "Jobs"
@@ -3574,8 +3575,12 @@ msgid "Join the conversation"
msgstr "Participe da conversa"
#: src/components/dialogs/nuxs/TenMillion/index.tsx:492
msgid "Joined {0}"
msgstr "Juntou-se {0}"
#~ msgid "Joined {0}"
#~ msgstr "Juntou-se {0}"
#: src/components/dialogs/nuxs/TenMillion/index.tsx:559
msgid "Joined on {joinedDate}"
msgstr ""
#: src/screens/Onboarding/index.tsx:21
#: src/screens/Onboarding/state.ts:89
@@ -3986,7 +3991,7 @@ msgstr "Conta Enganosa"
#: src/lib/moderation/useReportOptions.ts:67
msgid "Misleading Post"
msgstr ""
msgstr "Postagem Enganosa"
#: src/screens/Settings/AppearanceSettings.tsx:78
msgid "Mode"
@@ -4079,14 +4084,14 @@ msgid "Music"
msgstr "Música"
#: src/components/TagMenu/index.tsx:263
#: src/view/com/util/post-embeds/VideoEmbedInner/VideoEmbedInnerNative.tsx:163
msgid "Mute"
msgstr "Silenciar"
#: src/view/com/util/post-embeds/VideoEmbedInner/VideoWebControls.tsx:389
#: src/view/com/util/post-embeds/VideoEmbedInner/VideoEmbedInnerNative.tsx:157
#: src/view/com/util/post-embeds/VideoEmbedInner/web-controls/VolumeControl.tsx:94
msgctxt "video"
msgid "Mute"
msgstr ""
msgstr "Silenciar"
#: src/components/TagMenu/index.web.tsx:116
msgid "Mute {truncatedTag}"
@@ -4395,6 +4400,11 @@ msgstr "Nenhum GIF em destaque encontrado."
msgid "No feeds found. Try searching for something else."
msgstr "Nenhum feed encontrado. Tente pesquisar por outra coisa."
#: src/components/LikedByList.tsx:78
#: src/view/com/post-thread/PostLikedBy.tsx:85
msgid "No likes yet"
msgstr ""
#: src/components/ProfileCard.tsx:336
#: src/screens/Profile/Header/ProfileHeaderStandard.tsx:116
msgid "No longer following {0}"
@@ -4431,6 +4441,14 @@ msgstr "Ninguém além do autor pode citar esta postagem."
msgid "No posts yet."
msgstr "Nenhuma postagem ainda."
#: src/view/com/post-thread/PostQuotes.tsx:106
msgid "No quotes yet"
msgstr ""
#: src/view/com/post-thread/PostRepostedBy.tsx:78
msgid "No reposts 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"
@@ -4477,11 +4495,20 @@ msgstr "Ninguém"
#~ msgid "Nobody can reply"
#~ msgstr "Ninguém pode responder"
#: src/components/LikedByList.tsx:79
#: src/components/LikedByList.tsx:80
#: src/components/LikesDialog.tsx:99
#: src/view/com/post-thread/PostLikedBy.tsx:87
msgid "Nobody has liked this yet. Maybe you should be the first!"
msgstr "Ninguém curtiu isso ainda. Você pode ser o primeiro!"
#: src/view/com/post-thread/PostQuotes.tsx:108
msgid "Nobody has quoted this yet. Maybe you should be the first!"
msgstr ""
#: src/view/com/post-thread/PostRepostedBy.tsx:80
msgid "Nobody has reposted this yet. Maybe you should be the first!"
msgstr ""
#: src/screens/StarterPack/Wizard/StepProfiles.tsx:103
msgid "Nobody was found. Try searching for someone else."
msgstr "Ninguém foi encontrado. Tente procurar por outra pessoa."
@@ -4584,7 +4611,7 @@ msgstr "Opa!"
msgid "Oh no! Something went wrong."
msgstr "Opa! Algo deu errado."
#: src/components/dialogs/nuxs/TenMillion/index.tsx:175
#: src/components/dialogs/nuxs/TenMillion/index.tsx:224
msgid "Oh no! We weren't able to generate an image for you to share. Rest assured, we're glad you're here 🦋"
msgstr "Ah, não! Não conseguimos gerar uma imagem para você compartilhar. Fique tranquilo, estamos felizes que você esteja aqui 🦋"
@@ -4600,17 +4627,9 @@ msgstr "Ok"
msgid "Oldest replies first"
msgstr "Respostas mais antigas primeiro"
#: src/components/StarterPack/QrCode.tsx:69
#~ msgid "on"
#~ msgstr ""
#: src/lib/hooks/useTimeAgo.ts:81
#~ msgid "on {str}"
#~ msgstr ""
#: src/components/StarterPack/QrCode.tsx:70
msgid "on<0><1/><2><3/></2></0>"
msgstr ""
msgstr "no<0><1/><2><3/></2></0>"
#: src/view/screens/Settings/index.tsx:226
msgid "Onboarding reset"
@@ -4640,7 +4659,7 @@ msgstr "Apenas {0} pode responder."
msgid "Only contains letters, numbers, and hyphens"
msgstr "Contém apenas letras, números e hífens"
#: src/view/com/composer/videos/SubtitleFilePicker.tsx:39
#: src/view/com/composer/videos/SubtitleFilePicker.tsx:40
msgid "Only WebVTT (.vtt) files are supported"
msgstr "Somente arquivos WebVTT (.vtt) são suportados"
@@ -4696,7 +4715,7 @@ msgstr "Abrir opções de mensagem"
msgid "Open muted words and tags settings"
msgstr "Abrir opções de palavras/tags silenciadas"
#: src/view/com/home/HomeHeaderLayoutMobile.tsx:54
#: src/view/com/home/HomeHeaderLayoutMobile.tsx:101
msgid "Open navigation"
msgstr "Abrir navegação"
@@ -4940,12 +4959,12 @@ msgid "Password updated!"
msgstr "Senha atualizada!"
#: src/view/com/util/post-embeds/GifEmbed.tsx:46
#: src/view/com/util/post-embeds/VideoEmbedInner/VideoEmbedInnerNative.tsx:150
#: src/view/com/util/post-embeds/VideoEmbedInner/VideoWebControls.tsx:366
#: src/view/com/util/post-embeds/VideoEmbedInner/VideoEmbedInnerNative.tsx:141
#: src/view/com/util/post-embeds/VideoEmbedInner/web-controls/VideoControls.tsx:369
msgid "Pause"
msgstr "Pausar"
#: src/view/com/util/post-embeds/VideoEmbedInner/VideoWebControls.tsx:319
#: src/view/com/util/post-embeds/VideoEmbedInner/web-controls/VideoControls.tsx:320
msgid "Pause video"
msgstr "Pausar vídeo"
@@ -5005,8 +5024,8 @@ msgid "Pinned to your feeds"
msgstr "Fixado em seus feeds"
#: src/view/com/util/post-embeds/GifEmbed.tsx:46
#: src/view/com/util/post-embeds/VideoEmbedInner/VideoEmbedInnerNative.tsx:150
#: src/view/com/util/post-embeds/VideoEmbedInner/VideoWebControls.tsx:367
#: src/view/com/util/post-embeds/VideoEmbedInner/VideoEmbedInnerNative.tsx:141
#: src/view/com/util/post-embeds/VideoEmbedInner/web-controls/VideoControls.tsx:370
msgid "Play"
msgstr "Tocar"
@@ -5024,7 +5043,7 @@ msgid "Play or pause the GIF"
msgstr "Tocar ou pausar o GIF"
#: src/view/com/util/post-embeds/VideoEmbed.tsx:110
#: src/view/com/util/post-embeds/VideoEmbedInner/VideoWebControls.tsx:320
#: src/view/com/util/post-embeds/VideoEmbedInner/web-controls/VideoControls.tsx:321
msgid "Play video"
msgstr "Reproduzir vídeo"
@@ -5447,7 +5466,7 @@ msgstr "Remover conta"
#: src/view/com/composer/ExternalEmbedRemoveBtn.tsx:28
msgid "Remove attachment"
msgstr ""
msgstr "Remover anexo"
#: src/view/com/util/UserAvatar.tsx:393
msgid "Remove Avatar"
@@ -5567,7 +5586,7 @@ msgstr "Remove o post citado"
#: src/view/com/composer/ExternalEmbedRemoveBtn.tsx:29
msgid "Removes the attachment"
msgstr ""
msgstr "Remove o anexo"
#: src/view/com/composer/ExternalEmbedRemoveBtn.tsx:29
#~ msgid "Removes the image preview"
@@ -5818,11 +5837,11 @@ msgstr "Reenviar e-mail"
#: src/components/intents/VerifyEmailIntentDialog.tsx:130
msgid "Resend Email"
msgstr ""
msgstr "Reenviar E-mail"
#: src/components/intents/VerifyEmailIntentDialog.tsx:123
msgid "Resend Verification Email"
msgstr ""
msgstr "Reenviar E-mail de Verificação"
#: src/view/com/modals/ChangePassword.tsx:186
msgid "Reset code"
@@ -6092,7 +6111,7 @@ msgstr "Veja o guia"
#~ msgid "See what's next"
#~ msgstr "Veja o que vem por aí"
#: src/view/com/util/post-embeds/VideoEmbedInner/VideoWebControls.tsx:631
#: src/view/com/util/post-embeds/VideoEmbedInner/web-controls/Scrubber.tsx:189
msgid "Seek slider"
msgstr "Controle deslizante de busca"
@@ -6152,7 +6171,7 @@ msgstr "Seleciona opção {i} de {numItems}"
#~ msgid "Select some accounts below to follow"
#~ msgstr "Selecione algumas contas para seguir"
#: src/view/com/composer/videos/SubtitleFilePicker.tsx:65
#: src/view/com/composer/videos/SubtitleFilePicker.tsx:66
msgid "Select subtitle file (.vtt)"
msgstr "Selecione o arquivo de legenda (.vtt)"
@@ -6360,7 +6379,7 @@ msgstr "Atividade sexual ou nudez erótica."
msgid "Sexually Suggestive"
msgstr "Sexualmente Sugestivo"
#: src/components/dialogs/nuxs/TenMillion/index.tsx:644
#: src/components/dialogs/nuxs/TenMillion/index.tsx:708
#: src/components/StarterPack/QrCodeDialog.tsx:177
#: src/screens/StarterPack/StarterPackScreen.tsx:411
#: src/screens/StarterPack/StarterPackScreen.tsx:582
@@ -6397,11 +6416,11 @@ msgstr "Compartilhar assim"
msgid "Share feed"
msgstr "Compartilhar feed"
#: src/components/dialogs/nuxs/TenMillion/index.tsx:621
#: src/components/dialogs/nuxs/TenMillion/index.tsx:685
msgid "Share image externally"
msgstr "Compartilhar imagem externamente"
#: src/components/dialogs/nuxs/TenMillion/index.tsx:639
#: src/components/dialogs/nuxs/TenMillion/index.tsx:703
msgid "Share image in post"
msgstr "Compartilhe a imagem na postagem"
@@ -6692,10 +6711,6 @@ msgstr "Alguns outros feeds que você pode gostar"
msgid "Some people can reply"
msgstr "Algumas pessoas podem responder"
#: src/screens/StarterPack/Wizard/index.tsx:203
#~ msgid "Some subtitle"
#~ msgstr ""
#: src/screens/Messages/Conversation/index.tsx:106
msgid "Something went wrong"
msgstr "Algo deu errado"
@@ -6729,14 +6744,6 @@ msgstr "Classificar Respostas"
msgid "Sort replies to the same post by:"
msgstr "Classificar respostas de um post por:"
#: src/components/moderation/LabelsOnMeDialog.tsx:168
#~ msgid "Source:"
#~ msgstr "Fonte:"
#: src/components/moderation/LabelsOnMeDialog.tsx:169
#~ msgid "Source: <0>{0}</0>"
#~ msgstr ""
#: src/components/moderation/LabelsOnMeDialog.tsx:163
msgid "Source: <0>{sourceName}</0>"
msgstr "Fonte: <0>{sourceName}</0>"
@@ -6922,22 +6929,22 @@ msgstr "Alto"
msgid "Tap to dismiss"
msgstr "Toque para dispensar"
#: src/view/com/util/post-embeds/VideoEmbedInner/VideoEmbedInnerNative.tsx:145
#: src/view/com/util/post-embeds/VideoEmbedInner/VideoEmbedInnerNative.tsx:136
msgid "Tap to enter full screen"
msgstr "Toque para entrar em tela cheia"
#: src/view/com/util/post-embeds/VideoEmbedInner/VideoEmbedInnerNative.tsx:151
#: src/view/com/util/post-embeds/VideoEmbedInner/VideoEmbedInnerNative.tsx:142
msgid "Tap to play or pause"
msgstr ""
msgstr "Toque para reproduzir ou pausar"
#: src/view/com/util/post-embeds/VideoEmbedInner/VideoEmbedInnerNative.tsx:164
#: src/view/com/util/post-embeds/VideoEmbedInner/VideoEmbedInnerNative.tsx:159
msgid "Tap to toggle sound"
msgstr "Toque para alternar o som"
#: src/view/com/util/images/AutoSizedImage.tsx:219
#: src/view/com/util/images/AutoSizedImage.tsx:239
msgid "Tap to view full image"
msgstr ""
msgstr "Toque para ver a imagem completa"
#: src/view/com/util/images/AutoSizedImage.tsx:70
#~ msgid "Tap to view fully"
@@ -6964,7 +6971,7 @@ msgstr "Conte uma piada!"
msgid "Tell us a little more"
msgstr "Conte-nos um pouco mais"
#: src/components/dialogs/nuxs/TenMillion/index.tsx:518
#: src/components/dialogs/nuxs/TenMillion/index.tsx:578
msgid "Ten Million"
msgstr "Dez milhões"
@@ -7005,13 +7012,13 @@ msgstr "Campo de entrada de texto"
msgid "Thank you. Your report has been sent."
msgstr "Obrigado. Sua denúncia foi enviada."
#: src/components/dialogs/nuxs/TenMillion/index.tsx:593
#: src/components/dialogs/nuxs/TenMillion/index.tsx:654
msgid "Thanks for being one of our first 10 million users."
msgstr "Obrigado por ser um dos nossos primeiros 10 milhões de usuários."
#: src/components/intents/VerifyEmailIntentDialog.tsx:74
msgid "Thanks, you have successfully verified your email address."
msgstr ""
msgstr "Obrigado, seu endereço de e-mail foi verificado com sucesso."
#: src/view/com/modals/ChangeHandle.tsx:459
msgid "That contains the following:"
@@ -7030,7 +7037,7 @@ msgstr "Este identificador de usuário já está sendo usado."
msgid "That starter pack could not be found."
msgstr "Esse pacote inicial não pôde ser encontrado."
#: src/view/com/post-thread/PostQuotes.tsx:127
#: src/view/com/post-thread/PostQuotes.tsx:133
msgid "That's all, folks!"
msgstr "É isso, pessoal!"
@@ -7116,7 +7123,7 @@ msgstr "Os Termos de Serviço foram movidos para"
#: src/components/intents/VerifyEmailIntentDialog.tsx:85
msgid "The verification code you have provided is invalid. Please make sure that you have used the correct verification link or request a new one."
msgstr ""
msgstr "O código de verificação que você forneceu é inválido. Certifique-se de que você usou o link de verificação correto ou solicite novamente."
#: src/screens/Onboarding/StepAlgoFeeds/index.tsx:141
#~ msgid "There are many feeds to try:"
@@ -7485,6 +7492,10 @@ msgstr "Preferências das Threads"
msgid "To disable the email 2FA method, please verify your access to the email address."
msgstr "Para desabilitar o 2FA via e-mail, por favor verifique seu acesso a este endereço de e-mail."
#: src/components/dialogs/nuxs/TenMillion/Trigger.tsx:69
msgid "To learn more, <0>check out our post.</0>"
msgstr ""
#: src/components/dms/ReportConversationPrompt.tsx:20
msgid "To report a conversation, please report one of its messages via the conversation screen. This lets our moderators understand the context of your issue."
msgstr "Para denunciar uma conversa, por favor, denuncie uma das mensagens individualmente. Isso vai permitir a análise da situação pelos nossos moderadores."
@@ -7497,7 +7508,8 @@ msgstr "Para enviar vídeos para o Bluesky, você deve primeiro verificar seu e-
msgid "To whom would you like to send this report?"
msgstr "Para quem você gostaria de enviar esta denúncia?"
#: src/components/dialogs/nuxs/TenMillion/index.tsx:597
#: src/components/dialogs/nuxs/TenMillion/index.tsx:658
#: src/components/dialogs/nuxs/TenMillion/Trigger.tsx:63
msgid "Together, we're rebuilding the social internet. We're glad you're here."
msgstr "Juntos, estamos reconstruindo a internet social. Estamos felizes que você esteja aqui."
@@ -7637,15 +7649,15 @@ msgid "Unlike this feed"
msgstr "Descurtir este feed"
#: src/components/TagMenu/index.tsx:263
#: src/view/com/util/post-embeds/VideoEmbedInner/VideoEmbedInnerNative.tsx:163
#: src/view/screens/ProfileList.tsx:689
msgid "Unmute"
msgstr "Dessilenciar"
#: src/view/com/util/post-embeds/VideoEmbedInner/VideoWebControls.tsx:388
#: src/view/com/util/post-embeds/VideoEmbedInner/VideoEmbedInnerNative.tsx:156
#: src/view/com/util/post-embeds/VideoEmbedInner/web-controls/VolumeControl.tsx:93
msgctxt "video"
msgid "Unmute"
msgstr ""
msgstr "Ativar o áudio"
#: src/components/TagMenu/index.web.tsx:115
msgid "Unmute {truncatedTag}"
@@ -7673,13 +7685,13 @@ msgstr "Desmutar conversa"
msgid "Unmute thread"
msgstr "Dessilenciar thread"
#: src/view/com/util/post-embeds/VideoEmbedInner/VideoWebControls.tsx:317
#: src/view/com/util/post-embeds/VideoEmbedInner/web-controls/VideoControls.tsx:318
msgid "Unmute video"
msgstr "Desmutar vídeo"
msgstr "Ativar o áudio do vídeo"
#: src/view/com/util/post-embeds/VideoEmbedInner/VideoEmbedInnerNative.tsx:168
#~ msgid "Unmuted"
#~ msgstr "Desmutar"
#~ msgstr "Áudio ativado"
#: src/view/screens/ProfileFeed.tsx:292
#: src/view/screens/ProfileList.tsx:673
@@ -7920,7 +7932,7 @@ msgstr "Verificar e-mail"
#: src/components/intents/VerifyEmailIntentDialog.tsx:61
msgid "Verify email dialog"
msgstr ""
msgstr "Caixa de diálogo da verificação de e-mail"
#: src/view/screens/Settings/index.tsx:961
msgid "Verify my email"
@@ -7955,8 +7967,8 @@ msgstr "Verificar Seu E-mail"
msgid "Version {appVersion} {bundleInfo}"
msgstr "Versão {appVersion} {bundleInfo}"
#: src/view/com/util/post-embeds/VideoEmbedInner/VideoEmbedInnerNative.tsx:93
#: src/view/com/util/post-embeds/VideoEmbedInner/VideoEmbedInnerNative.tsx:144
#: src/view/com/util/post-embeds/VideoEmbedInner/VideoEmbedInnerNative.tsx:84
#: src/view/com/util/post-embeds/VideoEmbedInner/VideoEmbedInnerNative.tsx:135
msgid "Video"
msgstr "Vídeo"
@@ -7977,13 +7989,9 @@ msgstr "Vídeo não encontrado."
msgid "Video settings"
msgstr "Configurações de vídeo"
#: src/view/com/util/post-embeds/VideoEmbedInner/VideoEmbedInnerNative.tsx:93
#: src/view/com/util/post-embeds/VideoEmbedInner/VideoEmbedInnerNative.tsx:84
msgid "Video: {0}"
msgstr ""
#: src/view/com/composer/videos/state.ts:27
#~ msgid "Videos cannot be larger than 50MB"
#~ msgstr "Vídeos não podem ter mais de 100 MB"
msgstr "Vídeo: {0}"
#: src/view/com/composer/videos/SelectVideoBtn.tsx:65
#: src/view/com/composer/videos/VideoPreview.web.tsx:44
@@ -8031,6 +8039,10 @@ msgstr "Ver thread completa"
msgid "View information about these labels"
msgstr "Ver informações sobre estes rótulos"
#: src/components/dialogs/nuxs/TenMillion/Trigger.tsx:72
msgid "View our post"
msgstr ""
#: src/components/ProfileHoverCard/index.web.tsx:418
#: src/components/ProfileHoverCard/index.web.tsx:436
#: src/components/ProfileHoverCard/index.web.tsx:463
@@ -8055,8 +8067,8 @@ msgstr "Ver usuários que curtiram este feed"
msgid "View your blocked accounts"
msgstr "Veja suas contas bloqueadas"
#: src/view/com/home/HomeHeaderLayout.web.tsx:79
#: src/view/com/home/HomeHeaderLayoutMobile.tsx:86
#: src/view/com/home/HomeHeaderLayout.web.tsx:132
#: src/view/com/home/HomeHeaderLayoutMobile.tsx:143
msgid "View your feeds and explore more"
msgstr "Veja seus feeds e explore mais"
@@ -8101,7 +8113,7 @@ msgstr "Estimamos que sua conta estará pronta em mais ou menos {estimatedTime}.
#: src/components/intents/VerifyEmailIntentDialog.tsx:98
msgid "We have sent another verification email to <0>{0}</0>."
msgstr ""
msgstr "Enviamos outro e-mail de verificação para <0>{0}</0>."
#: src/screens/Onboarding/StepFinished.tsx:238
msgid "We hope you have a wonderful time. Remember, Bluesky is:"
@@ -8529,7 +8541,7 @@ msgstr "Você deve estar seguindo pelo menos sete outras pessoas para gerar um p
msgid "You must grant access to your photo library to save a QR code"
msgstr "Você deve conceder acesso à sua biblioteca de fotos para salvar o QR code."
#: src/components/dialogs/nuxs/TenMillion/index.tsx:237
#: src/components/dialogs/nuxs/TenMillion/index.tsx:302
#: src/components/StarterPack/ShareDialog.tsx:68
msgid "You must grant access to your photo library to save the image."
msgstr "Você deve conceder acesso à sua biblioteca de fotos para salvar a imagem."
+52 -76
View File
@@ -8,7 +8,7 @@ msgstr ""
"Language: zh_CN\n"
"Project-Id-Version: zh-CN for bluesky-social-app\n"
"Report-Msgid-Bugs-To: Frudrax Cheng <i@cynosura.one>\n"
"PO-Revision-Date: 2024-09-07 22:51+0800\n"
"PO-Revision-Date: 2024-09-14 10:09+0800\n"
"Last-Translator: Frudrax Cheng <i@cynosura.one>\n"
"Language-Team: Frudrax Cheng (auroursa), Simon Chan (RitsukiP), U2FsdGVkX1, Mikan Harada (mitian233), IceCodeNew\n"
"Plural-Forms: \n"
@@ -544,7 +544,7 @@ msgstr "播放视频时出现问题,请再试一次。"
#: src/components/dialogs/nuxs/TenMillion/index.tsx:250
msgid "An error occurred while saving the image!"
msgstr ""
msgstr "保存图片时发生错误!"
#: src/components/StarterPack/QrCodeDialog.tsx:71
#: src/components/StarterPack/ShareDialog.tsx:79
@@ -850,7 +850,7 @@ msgstr "Bluesky 因朋友而更好!"
#: src/components/dialogs/nuxs/TenMillion/index.tsx:206
msgid "Bluesky now has over 10 million users, and I was #{0}!"
msgstr ""
msgstr "Bluesky 现在拥有一千万名用户,我是其中的第 #{0} 位!"
#: src/components/StarterPack/ProfileStarterPacks.tsx:282
msgid "Bluesky will choose a set of recommended accounts from people in your network."
@@ -875,7 +875,7 @@ msgstr "书籍"
#: src/components/dialogs/nuxs/TenMillion/index.tsx:614
msgid "Brag a little!"
msgstr ""
msgstr "吹个牛!"
#: src/components/FeedInterstitials.tsx:350
msgid "Browse more accounts on the Explore page"
@@ -1018,7 +1018,7 @@ msgstr "字幕及替代文本"
#: src/components/dialogs/nuxs/TenMillion/index.tsx:368
msgid "Celebrating {0} users"
msgstr ""
msgstr "庆祝第 {0} 位用户"
#: src/view/com/modals/VerifyEmail.tsx:160
msgid "Change"
@@ -1099,14 +1099,6 @@ msgstr "在这里输入刚才发送到你电子邮箱里的验证码。"
msgid "Check your inbox for an email with the confirmation code to enter below:"
msgstr "查看发送至你电子邮箱的确认邮件,并在下方输入收到的验证码:"
#: src/screens/Onboarding/StepInterests/index.tsx:191
#~ msgid "Choose 3 or more:"
#~ msgstr "选择至少 3 个或更多:"
#: src/screens/Onboarding/StepInterests/index.tsx:326
#~ msgid "Choose at least {0} more"
#~ msgstr "还需选择至少 {0} 个"
#: src/screens/StarterPack/Wizard/index.tsx:190
msgid "Choose Feeds"
msgstr "选择资讯源"
@@ -1769,7 +1761,7 @@ msgstr "隆重介绍私信功能!"
#: src/view/screens/AccessibilitySettings.tsx:111
msgid "Disable autoplay for videos and GIFs"
msgstr "关闭自动播放 GIF 及视频"
msgstr "关闭自动播放视频及 GIF"
#: src/view/screens/Settings/DisableEmail2FADialog.tsx:90
msgid "Disable Email 2FA"
@@ -1818,10 +1810,6 @@ msgstr "探索新的资讯源"
msgid "Discover New Feeds"
msgstr "探索新的资讯源"
#: src/view/com/profile/ProfileHeaderSuggestedFollows.tsx:108
#~ msgid "Dismiss"
#~ msgstr "关闭"
#: src/view/com/composer/Composer.tsx:1107
msgid "Dismiss error"
msgstr "关闭错误"
@@ -1909,7 +1897,7 @@ msgstr "下载 CAR 文件"
#: src/components/dialogs/nuxs/TenMillion/index.tsx:622
msgid "Download image"
msgstr ""
msgstr "下载图片"
#: src/view/com/composer/text-input/TextInput.web.tsx:269
msgid "Drop to add images"
@@ -2073,7 +2061,7 @@ msgstr "邮箱地址"
#: src/components/intents/VerifyEmailIntentDialog.tsx:95
msgid "Email Resent"
msgstr ""
msgstr "重新发送电子邮件"
#: src/view/com/modals/ChangeEmail.tsx:54
#: src/view/com/modals/ChangeEmail.tsx:83
@@ -2090,7 +2078,7 @@ msgstr "电子邮箱已验证"
#: src/components/intents/VerifyEmailIntentDialog.tsx:71
msgid "Email Verified"
msgstr ""
msgstr "电子邮箱已验证"
#: src/view/screens/Settings/index.tsx:319
msgid "Email:"
@@ -2554,16 +2542,16 @@ msgstr "关注账户"
msgid "Follow all"
msgstr "关注所有人"
#: src/view/com/profile/FollowButton.tsx:79
msgctxt "action"
msgid "Follow Back"
msgstr ""
#: src/screens/Profile/Header/ProfileHeaderStandard.tsx:223
#: src/view/com/post-thread/PostThreadFollowBtn.tsx:142
msgid "Follow Back"
msgstr "回关"
#: src/view/com/profile/FollowButton.tsx:79
msgctxt "action"
msgid "Follow Back"
msgstr "回关"
#: src/view/screens/Search/Explore.tsx:334
msgid "Follow more accounts to get connected to your interests and build your network."
msgstr "关注更多账户以了解你的兴趣,并逐步建立你的社交网络。"
@@ -2922,7 +2910,7 @@ msgstr "无法加载此内容审核提供服务。"
#: src/state/queries/video/video.ts:165
msgid "Hold up! Were gradually giving access to video, and youre still waiting in line. Check back soon!"
msgstr ""
msgstr "请稍等!我们正在逐步开放视频功能权限,你仍在等待队伍中。请稍后再回来查看!"
#: src/Navigation.tsx:550
#: src/Navigation.tsx:570
@@ -3088,7 +3076,7 @@ msgstr "用户名或密码无效"
#: src/components/intents/VerifyEmailIntentDialog.tsx:82
msgid "Invalid Verification Code"
msgstr ""
msgstr "验证码无效"
#: src/view/com/modals/InviteCodes.tsx:94
msgid "Invite a Friend"
@@ -3147,7 +3135,7 @@ msgstr "加入对话"
#: src/components/dialogs/nuxs/TenMillion/index.tsx:492
msgid "Joined {0}"
msgstr ""
msgstr "{0} 已加入"
#: src/screens/Onboarding/index.tsx:21
#: src/screens/Onboarding/state.ts:89
@@ -3612,16 +3600,16 @@ msgstr "电影"
msgid "Music"
msgstr "音乐"
#: src/view/com/util/post-embeds/VideoEmbedInner/VideoWebControls.tsx:389
msgctxt "video"
msgid "Mute"
msgstr ""
#: src/components/TagMenu/index.tsx:263
#: src/view/com/util/post-embeds/VideoEmbedInner/VideoEmbedInnerNative.tsx:163
msgid "Mute"
msgstr "隐藏"
#: src/view/com/util/post-embeds/VideoEmbedInner/VideoWebControls.tsx:389
msgctxt "video"
msgid "Mute"
msgstr "隐藏"
#: src/components/TagMenu/index.web.tsx:116
msgid "Mute {truncatedTag}"
msgstr "隐藏 {truncatedTag}"
@@ -3690,10 +3678,6 @@ msgstr "隐藏讨论串"
msgid "Mute words & tags"
msgstr "隐藏词和标签"
#: src/view/com/util/post-embeds/VideoEmbedInner/VideoEmbedInnerNative.tsx:168
#~ msgid "Muted"
#~ msgstr "已隐藏"
#: src/screens/Moderation/index.tsx:264
msgid "Muted accounts"
msgstr "已隐藏账户"
@@ -4081,7 +4065,7 @@ msgstr "糟糕!发生了一些错误。"
#: src/components/dialogs/nuxs/TenMillion/index.tsx:175
msgid "Oh no! We weren't able to generate an image for you to share. Rest assured, we're glad you're here 🦋"
msgstr ""
msgstr "糟糕!我们无法生成图片供你分享。请放心,我们很高兴你还在这里 🦋"
#: src/screens/Profile/Header/ProfileHeaderLabeler.tsx:339
msgid "OK"
@@ -4669,7 +4653,7 @@ msgstr "点按重试"
#: src/components/KnownFollowers.tsx:124
msgid "Press to view followers of this account that you also follow"
msgstr "点按以查看同样关注此账的共同关注者"
msgstr "点按以查看同样关注此账的共同关注者"
#: src/view/com/lightbox/Lightbox.web.tsx:150
msgid "Previous image"
@@ -5205,11 +5189,11 @@ msgstr "重新发送电子邮件"
#: src/components/intents/VerifyEmailIntentDialog.tsx:130
msgid "Resend Email"
msgstr ""
msgstr "重新发送电子邮件"
#: src/components/intents/VerifyEmailIntentDialog.tsx:123
msgid "Resend Verification Email"
msgstr ""
msgstr "重新发送验证码电子邮件"
#: src/view/com/modals/ChangePassword.tsx:186
msgid "Reset code"
@@ -5725,11 +5709,11 @@ msgstr "分享资讯源"
#: src/components/dialogs/nuxs/TenMillion/index.tsx:621
msgid "Share image externally"
msgstr ""
msgstr "向外部分享图片"
#: src/components/dialogs/nuxs/TenMillion/index.tsx:639
msgid "Share image in post"
msgstr ""
msgstr "在帖文里分享图片"
#: src/components/StarterPack/ShareDialog.tsx:124
#: src/components/StarterPack/ShareDialog.tsx:131
@@ -5797,10 +5781,6 @@ msgstr "显示徽章"
msgid "Show badge and filter from feeds"
msgstr "显示徽章并从资讯源中过滤"
#: src/screens/Profile/Header/ProfileHeaderStandard.tsx:218
#~ msgid "Show follows similar to {0}"
#~ msgstr "显示类似于 {0} 的关注者"
#: src/view/com/post-thread/PostThreadShowHiddenReplies.tsx:23
msgid "Show hidden replies"
msgstr "显示已隐藏的回复"
@@ -6117,7 +6097,7 @@ msgstr "订阅这个列表"
#: src/view/screens/Search/Explore.tsx:332
msgid "Suggested accounts"
msgstr "建议的账"
msgstr "建议的账"
#: src/components/FeedInterstitials.tsx:318
msgid "Suggested for you"
@@ -6177,7 +6157,7 @@ msgstr "点击进入全屏模式"
#: src/view/com/util/post-embeds/VideoEmbedInner/VideoEmbedInnerNative.tsx:151
msgid "Tap to play or pause"
msgstr ""
msgstr "点击以播放或静音"
#: src/view/com/util/post-embeds/VideoEmbedInner/VideoEmbedInnerNative.tsx:164
msgid "Tap to toggle sound"
@@ -6211,7 +6191,7 @@ msgstr "告诉我们更多"
#: src/components/dialogs/nuxs/TenMillion/index.tsx:518
msgid "Ten Million"
msgstr ""
msgstr "一千万"
#: src/view/shell/desktop/RightNav.tsx:90
msgid "Terms"
@@ -6248,11 +6228,11 @@ msgstr "谢谢,你的举报已提交。"
#: src/components/dialogs/nuxs/TenMillion/index.tsx:593
msgid "Thanks for being one of our first 10 million users."
msgstr ""
msgstr "感谢你成为我们的首批一千万用户的其中一份子。"
#: src/components/intents/VerifyEmailIntentDialog.tsx:74
msgid "Thanks, you have successfully verified your email address."
msgstr ""
msgstr "谢谢,你已成功验证邮箱地址。"
#: src/view/com/modals/ChangeHandle.tsx:459
msgid "That contains the following:"
@@ -6353,7 +6333,7 @@ msgstr "服务条款已迁移至"
#: src/components/intents/VerifyEmailIntentDialog.tsx:85
msgid "The verification code you have provided is invalid. Please make sure that you have used the correct verification link or request a new one."
msgstr ""
msgstr "你提供的验证码无效,请检查你所使用的验证链接,或是申请新的验证链接。"
#: src/screens/Settings/components/DeactivateAccountDialog.tsx:86
msgid "There is no time limit for account deactivation, come back any time."
@@ -6501,7 +6481,7 @@ msgstr "没有 Bluesky 账户,无法查看此内容。"
#: src/screens/Messages/List/ChatListItem.tsx:213
msgid "This conversation is with a deleted or a deactivated account. Press for options."
msgstr "此对话的参与者已停用或删除账,点击以获取更多详情。"
msgstr "此对话的参与者已停用或删除账,点击以获取更多详情。"
#: src/view/screens/Settings/ExportCarDialog.tsx:93
msgid "This feature is in beta. You can read more about repository exports in <0>this blogpost</0>."
@@ -6682,7 +6662,7 @@ msgstr "你想将举报提交给谁?"
#: src/components/dialogs/nuxs/TenMillion/index.tsx:597
msgid "Together, we're rebuilding the social internet. We're glad you're here."
msgstr ""
msgstr "很高兴你能来到这里,让我们一起重新打造社交网络。"
#: src/view/com/util/forms/DropdownButton.tsx:255
msgid "Toggle dropdown"
@@ -6807,17 +6787,17 @@ msgstr "取消关注账户"
msgid "Unlike this feed"
msgstr "取消喜欢这个资讯源"
#: src/view/com/util/post-embeds/VideoEmbedInner/VideoWebControls.tsx:388
msgctxt "video"
msgid "Unmute"
msgstr ""
#: src/components/TagMenu/index.tsx:263
#: src/view/com/util/post-embeds/VideoEmbedInner/VideoEmbedInnerNative.tsx:163
#: src/view/screens/ProfileList.tsx:689
msgid "Unmute"
msgstr "取消隐藏"
#: src/view/com/util/post-embeds/VideoEmbedInner/VideoWebControls.tsx:388
msgctxt "video"
msgid "Unmute"
msgstr "取消隐藏"
#: src/components/TagMenu/index.web.tsx:115
msgid "Unmute {truncatedTag}"
msgstr "取消隐藏 {truncatedTag}"
@@ -6844,10 +6824,6 @@ msgstr "取消隐藏讨论串"
msgid "Unmute video"
msgstr "取消隐藏视频"
#: src/view/com/util/post-embeds/VideoEmbedInner/VideoEmbedInnerNative.tsx:168
#~ msgid "Unmuted"
#~ msgstr "已取消隐藏"
#: src/view/screens/ProfileFeed.tsx:292
#: src/view/screens/ProfileList.tsx:673
msgid "Unpin"
@@ -7075,7 +7051,7 @@ msgstr "验证邮箱"
#: src/components/intents/VerifyEmailIntentDialog.tsx:61
msgid "Verify email dialog"
msgstr ""
msgstr "验证邮箱对话框"
#: src/view/screens/Settings/index.tsx:961
msgid "Verify my email"
@@ -7135,7 +7111,7 @@ msgstr "视频:{0}"
#: src/view/com/composer/videos/SelectVideoBtn.tsx:65
#: src/view/com/composer/videos/VideoPreview.web.tsx:44
msgid "Videos must be less than 60 seconds long"
msgstr ""
msgstr "视频必须短于60秒"
#: src/screens/Profile/Header/Shell.tsx:113
msgid "View {0}'s avatar"
@@ -7200,7 +7176,7 @@ msgstr "查看这个资讯源被谁喜欢"
#: src/screens/Moderation/index.tsx:274
msgid "View your blocked accounts"
msgstr "查看你屏蔽的账"
msgstr "查看你屏蔽的账"
#: src/view/com/home/HomeHeaderLayout.web.tsx:79
#: src/view/com/home/HomeHeaderLayoutMobile.tsx:86
@@ -7213,7 +7189,7 @@ msgstr "查看你的内容审核列表"
#: src/screens/Moderation/index.tsx:259
msgid "View your muted accounts"
msgstr "查看你隐藏的账"
msgstr "查看你隐藏的账"
#: src/view/com/modals/LinkWarning.tsx:89
#: src/view/com/modals/LinkWarning.tsx:95
@@ -7248,7 +7224,7 @@ msgstr "我们估计还需要 {estimatedTime} 才能完成你的账户准备。"
#: src/components/intents/VerifyEmailIntentDialog.tsx:98
msgid "We have sent another verification email to <0>{0}</0>."
msgstr ""
msgstr "我们将发送另一封验证邮件至 <0>{0}</0>。"
#: src/screens/Onboarding/StepFinished.tsx:238
msgid "We hope you have a wonderful time. Remember, Bluesky is:"
@@ -7260,7 +7236,7 @@ msgstr "我们已经看完了你关注的帖文。这是来自 <0/> 的最新消
#: src/state/queries/video/video.ts:170
msgid "We were unable to determine if you are allowed to upload videos. Please try again."
msgstr ""
msgstr "我们无法确定你是否有权上传视频,请重试。"
#: src/components/dialogs/BirthDateSettings.tsx:52
msgid "We were unable to load your birth date preferences. Please try again."
@@ -7464,7 +7440,7 @@ msgstr "轮到你了。"
#: src/state/queries/video/video.ts:161
msgid "You are not allowed to upload videos."
msgstr ""
msgstr "你无法上传视频。"
#: src/view/com/profile/ProfileFollows.tsx:95
msgid "You are not following anyone."
@@ -7585,7 +7561,7 @@ msgstr "你已经到末尾了"
#: src/state/queries/video/video-upload.shared.ts:67
msgid "You have temporarily reached the limit for video uploads. Please try again later."
msgstr ""
msgstr "您已暂时达到视频上传的限制,请稍后重试。"
#: src/components/StarterPack/ProfileStarterPacks.tsx:235
msgid "You haven't created a starter pack yet!"
@@ -7711,11 +7687,11 @@ msgstr "你已经浏览完你的订阅资讯源啦!寻找一些更多的账户
#: src/state/queries/video/video.ts:175
msgid "You've reached your daily limit for video uploads (too many bytes)"
msgstr ""
msgstr "你已达到每日上传视频上限(文件太大)"
#: src/state/queries/video/video.ts:180
msgid "You've reached your daily limit for video uploads (too many videos)"
msgstr ""
msgstr "你已达到每日上传视频上限(数量太多)"
#: src/screens/Signup/index.tsx:146
msgid "Your account"
@@ -7727,7 +7703,7 @@ msgstr "你的账户已删除"
#: src/state/queries/video/video.ts:185
msgid "Your account is not yet old enough to upload videos. Please try again later."
msgstr ""
msgstr "你的账户注册时间过短,暂时无法上传视频。请过段时间再试。"
#: src/view/screens/Settings/ExportCarDialog.tsx:65
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."
+26 -33
View File
@@ -3,7 +3,7 @@ msgstr ""
"Project-Id-Version: zh-TW for bluesky-social-app\n"
"POT-Creation-Date: \n"
"Report-Msgid-Bugs-To: Kuwa Lee <kuwalee1069@gmail.com>, Frudrax Cheng <i@cynosura.one>\n"
"PO-Revision-Date: 2024-09-06 10:08+0800\n"
"PO-Revision-Date: 2024-09-13 09:16+0800\n"
"Last-Translator: \n"
"Language-Team: Frudrax Cheng <i@cynosura.one>, Kuwa Lee <kuwalee1069@gmail.com>, noeFly, snowleo208, Kisaragi Hiu, Yi-Jyun Pan, toto6038, cirx1e\n"
"Language: zh_TW\n"
@@ -1099,14 +1099,6 @@ msgstr "在此輸入寄送至您電子郵件地址的驗證碼。"
msgid "Check your inbox for an email with the confirmation code to enter below:"
msgstr "在下方輸入寄送至您電子郵件地址的驗證碼:"
#: src/screens/Onboarding/StepInterests/index.tsx:191
#~ msgid "Choose 3 or more:"
#~ msgstr "選擇至少 3 個:"
#: src/screens/Onboarding/StepInterests/index.tsx:326
#~ msgid "Choose at least {0} more"
#~ msgstr "選擇至少 {0} 個"
#: src/screens/StarterPack/Wizard/index.tsx:190
msgid "Choose Feeds"
msgstr "選擇動態源"
@@ -2073,7 +2065,7 @@ msgstr "電子郵件地址"
#: src/components/intents/VerifyEmailIntentDialog.tsx:95
msgid "Email Resent"
msgstr ""
msgstr "重新發送電子郵件"
#: src/view/com/modals/ChangeEmail.tsx:54
#: src/view/com/modals/ChangeEmail.tsx:83
@@ -2090,7 +2082,7 @@ msgstr "電子郵件已驗證"
#: src/components/intents/VerifyEmailIntentDialog.tsx:71
msgid "Email Verified"
msgstr ""
msgstr "電子郵件地址已驗證"
#: src/view/screens/Settings/index.tsx:319
msgid "Email:"
@@ -2152,7 +2144,7 @@ msgstr "已經到底部啦!"
#: src/view/com/composer/videos/SubtitleDialog.tsx:161
msgid "Ensure you have selected a language for each subtitle file."
msgstr ""
msgstr "請確認您已為每個字幕檔案選擇一種語言。"
#: src/view/com/modals/AddAppPasswords.tsx:161
msgid "Enter a name for this App Password"
@@ -2557,7 +2549,7 @@ msgstr "全部跟隨"
#: src/view/com/profile/FollowButton.tsx:79
msgctxt "action"
msgid "Follow Back"
msgstr ""
msgstr "回跟"
#: src/screens/Profile/Header/ProfileHeaderStandard.tsx:223
#: src/view/com/post-thread/PostThreadFollowBtn.tsx:142
@@ -2922,7 +2914,7 @@ msgstr "抱歉,我們無法載入該內容管理服務。"
#: src/state/queries/video/video.ts:165
msgid "Hold up! Were gradually giving access to video, and youre still waiting in line. Check back soon!"
msgstr ""
msgstr "別急!我們將逐步開放影片功能,您還在隊列中。請稍後再回來!"
#: src/Navigation.tsx:550
#: src/Navigation.tsx:570
@@ -3088,7 +3080,7 @@ msgstr "用戶名稱或密碼無效"
#: src/components/intents/VerifyEmailIntentDialog.tsx:82
msgid "Invalid Verification Code"
msgstr ""
msgstr "無效的驗證碼"
#: src/view/com/modals/InviteCodes.tsx:94
msgid "Invite a Friend"
@@ -3128,7 +3120,7 @@ msgstr "現在只有您一個人!使用上面的搜尋功能,將更多人加
#: src/view/com/composer/Composer.tsx:1126
msgid "Job ID: {0}"
msgstr ""
msgstr "Job ID: {0}"
#: src/view/com/auth/SplashScreen.web.tsx:164
msgid "Jobs"
@@ -5205,11 +5197,11 @@ msgstr "重新傳送郵件"
#: src/components/intents/VerifyEmailIntentDialog.tsx:130
msgid "Resend Email"
msgstr ""
msgstr "重新傳送電子郵件"
#: src/components/intents/VerifyEmailIntentDialog.tsx:123
msgid "Resend Verification Email"
msgstr ""
msgstr "重新傳送驗證電子郵件"
#: src/view/com/modals/ChangePassword.tsx:186
msgid "Reset code"
@@ -5797,10 +5789,6 @@ msgstr "顯示標記"
msgid "Show badge and filter from feeds"
msgstr "顯示標記並從動態源中篩選"
#: src/screens/Profile/Header/ProfileHeaderStandard.tsx:218
#~ msgid "Show follows similar to {0}"
#~ msgstr "顯示類似於 {0} 的跟隨者"
#: src/view/com/post-thread/PostThreadShowHiddenReplies.tsx:23
msgid "Show hidden replies"
msgstr "顯示隱藏回覆"
@@ -6252,7 +6240,7 @@ msgstr ""
#: src/components/intents/VerifyEmailIntentDialog.tsx:74
msgid "Thanks, you have successfully verified your email address."
msgstr ""
msgstr "謝謝,您已成功驗證您的電子郵件地址。"
#: src/view/com/modals/ChangeHandle.tsx:459
msgid "That contains the following:"
@@ -6353,7 +6341,7 @@ msgstr "服務條款已遷移到"
#: src/components/intents/VerifyEmailIntentDialog.tsx:85
msgid "The verification code you have provided is invalid. Please make sure that you have used the correct verification link or request a new one."
msgstr ""
msgstr "您提供的驗證碼無效。請確認您使用了正確的驗證連結,或申請新的驗證連結。"
#: src/screens/Settings/components/DeactivateAccountDialog.tsx:86
msgid "There is no time limit for account deactivation, come back any time."
@@ -6818,6 +6806,11 @@ msgstr ""
msgid "Unmute"
msgstr "取消靜音"
#: src/view/com/util/post-embeds/VideoEmbedInner/VideoWebControls.tsx:388
msgctxt "video"
msgid "Unmute"
msgstr "取消靜音"
#: src/components/TagMenu/index.web.tsx:115
msgid "Unmute {truncatedTag}"
msgstr "取消靜音 {truncatedTag}"
@@ -7075,7 +7068,7 @@ msgstr "驗證電子郵件"
#: src/components/intents/VerifyEmailIntentDialog.tsx:61
msgid "Verify email dialog"
msgstr ""
msgstr "驗證電子郵件對話窗"
#: src/view/screens/Settings/index.tsx:961
msgid "Verify my email"
@@ -7135,7 +7128,7 @@ msgstr "影片:{0}"
#: src/view/com/composer/videos/SelectVideoBtn.tsx:65
#: src/view/com/composer/videos/VideoPreview.web.tsx:44
msgid "Videos must be less than 60 seconds long"
msgstr ""
msgstr "影片長度必須少於 60 秒"
#: src/screens/Profile/Header/Shell.tsx:113
msgid "View {0}'s avatar"
@@ -7248,7 +7241,7 @@ msgstr "我們估計還需要 {estimatedTime} 才能準備好您的帳號。"
#: src/components/intents/VerifyEmailIntentDialog.tsx:98
msgid "We have sent another verification email to <0>{0}</0>."
msgstr ""
msgstr "我們已發送另一封驗證電子郵件至 <0>{0}</0>。"
#: src/screens/Onboarding/StepFinished.tsx:238
msgid "We hope you have a wonderful time. Remember, Bluesky is:"
@@ -7260,7 +7253,7 @@ msgstr "您已看完了您跟隨的貼文。這是來自 <0/> 的最新貼文。
#: src/state/queries/video/video.ts:170
msgid "We were unable to determine if you are allowed to upload videos. Please try again."
msgstr ""
msgstr "我們無法確定您是否有上傳影片的權限。請再試一次。"
#: src/components/dialogs/BirthDateSettings.tsx:52
msgid "We were unable to load your birth date preferences. Please try again."
@@ -7464,7 +7457,7 @@ msgstr "您正處於隊列之中。"
#: src/state/queries/video/video.ts:161
msgid "You are not allowed to upload videos."
msgstr ""
msgstr "您沒有上傳影片的權限。"
#: src/view/com/profile/ProfileFollows.tsx:95
msgid "You are not following anyone."
@@ -7585,7 +7578,7 @@ msgstr "已經到底部啦!"
#: src/state/queries/video/video-upload.shared.ts:67
msgid "You have temporarily reached the limit for video uploads. Please try again later."
msgstr ""
msgstr "您已暫時達到影片上傳的限制。請稍後再試。"
#: src/components/StarterPack/ProfileStarterPacks.tsx:235
msgid "You haven't created a starter pack yet!"
@@ -7711,11 +7704,11 @@ msgstr "您已經瀏覽完貼文啦!跟隨其他帳號吧。"
#: src/state/queries/video/video.ts:175
msgid "You've reached your daily limit for video uploads (too many bytes)"
msgstr ""
msgstr "您已達到每日影片上傳限制(位元組過多)"
#: src/state/queries/video/video.ts:180
msgid "You've reached your daily limit for video uploads (too many videos)"
msgstr ""
msgstr "您已達到每日影片上傳限制(影片數過多)"
#: src/screens/Signup/index.tsx:146
msgid "Your account"
@@ -7727,7 +7720,7 @@ msgstr "您的帳號已刪除"
#: src/state/queries/video/video.ts:185
msgid "Your account is not yet old enough to upload videos. Please try again later."
msgstr ""
msgstr "您的帳號太新以至於無法上傳影片。請稍後重試。"
#: src/view/screens/Settings/ExportCarDialog.tsx:65
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."
+7 -7
View File
@@ -1,19 +1,20 @@
import {makeAutoObservable, runInAction} from 'mobx'
import {ImageModel} from './image'
import {Image as RNImage} from 'react-native-image-crop-picker'
import {openPicker} from 'lib/media/picker'
import {getImageDim} from 'lib/media/manip'
import {openPicker} from 'lib/media/picker'
import {ImageInitOptions, ImageModel} from './image'
interface InitialImageUri {
uri: string
width: number
height: number
altText?: string
}
export class GalleryModel {
images: ImageModel[] = []
constructor(uris?: {uri: string; width: number; height: number}[]) {
constructor(uris?: InitialImageUri[]) {
makeAutoObservable(this)
if (uris) {
@@ -33,7 +34,7 @@ export class GalleryModel {
return this.images.some(image => image.altText.trim() === '')
}
*add(image_: Omit<RNImage, 'size'>) {
*add(image_: ImageInitOptions) {
if (this.size >= 4) {
return
}
@@ -59,7 +60,6 @@ export class GalleryModel {
path: uri,
height,
width,
mime: 'image/jpeg',
}
runInAction(() => {
@@ -100,10 +100,10 @@ export class GalleryModel {
async addFromUris(uris: InitialImageUri[]) {
for (const uriObj of uris) {
this.add({
mime: 'image/jpeg',
height: uriObj.height,
width: uriObj.width,
path: uriObj.uri,
altText: uriObj.altText,
})
}
}
+18 -7
View File
@@ -1,14 +1,15 @@
import {Image as RNImage} from 'react-native-image-crop-picker'
import {makeAutoObservable, runInAction} from 'mobx'
import {POST_IMG_MAX} from 'lib/constants'
import * as ImageManipulator from 'expo-image-manipulator'
import {getDataUriSize} from 'lib/media/util'
import {openCropper} from 'lib/media/picker'
import {ActionCrop, FlipType, SaveFormat} from 'expo-image-manipulator'
import {makeAutoObservable, runInAction} from 'mobx'
import {Position} from 'react-avatar-editor'
import {Dimensions} from 'lib/media/types'
import {isIOS} from 'platform/detection'
import {logger} from '#/logger'
import {POST_IMG_MAX} from 'lib/constants'
import {openCropper} from 'lib/media/picker'
import {Dimensions} from 'lib/media/types'
import {getDataUriSize} from 'lib/media/util'
import {isIOS} from 'platform/detection'
export interface ImageManipulationAttributes {
aspectRatio?: '4:3' | '1:1' | '3:4' | 'None'
@@ -19,6 +20,13 @@ export interface ImageManipulationAttributes {
flipVertical?: boolean
}
export interface ImageInitOptions {
path: string
width: number
height: number
altText?: string
}
const MAX_IMAGE_SIZE_IN_BYTES = 976560
export class ImageModel implements Omit<RNImage, 'size'> {
@@ -41,12 +49,15 @@ export class ImageModel implements Omit<RNImage, 'size'> {
}
prevAttributes: ImageManipulationAttributes = {}
constructor(image: Omit<RNImage, 'size'>) {
constructor(image: ImageInitOptions) {
makeAutoObservable(this)
this.path = image.path
this.width = image.width
this.height = image.height
if (image.altText !== undefined) {
this.setAltText(image.altText)
}
}
setRatio(aspectRatio: ImageManipulationAttributes['aspectRatio']) {
+1 -1
View File
@@ -36,7 +36,7 @@ export interface ComposerOpts {
mention?: string // handle of user to mention
openEmojiPicker?: (pos: DOMRect | undefined) => void
text?: string
imageUris?: {uri: string; width: number; height: number}[]
imageUris?: {uri: string; width: number; height: number; altText?: string}[]
}
type StateContext = ComposerOpts | undefined
@@ -28,8 +28,9 @@ export function SubtitleFilePicker({
if (selectedFile) {
if (
selectedFile.type === 'text/vtt' ||
(selectedFile.type === 'text/plain' &&
selectedFile.name.endsWith('.vtt'))
// HACK: sometimes the mime type is just straight-up missing
// best we can do is check the file extension and hope for the best
selectedFile.name.endsWith('.vtt')
) {
onSelectFile(selectedFile)
} else {
@@ -44,7 +44,7 @@ export function VideoPreview({
]}>
<BlueskyVideoView
url={video.uri}
autoplay={autoplayDisabled}
autoplay={!autoplayDisabled}
beginMuted={true}
forceTakeover={true}
ref={playerRef}
+63 -10
View File
@@ -1,6 +1,15 @@
import React from 'react'
import {StyleSheet, View} from 'react-native'
import Animated from 'react-native-reanimated'
import Animated, {
useAnimatedStyle,
useReducedMotion,
useSharedValue,
withDelay,
withRepeat,
withSequence,
withSpring,
withTiming,
} from 'react-native-reanimated'
import {msg} from '@lingui/macro'
import {useLingui} from '@lingui/react'
@@ -8,11 +17,11 @@ import {useSession} from '#/state/session'
import {useShellLayout} from '#/state/shell/shell-layout'
import {useMinimalShellHeaderTransform} from 'lib/hooks/useMinimalShellTransform'
import {useWebMediaQueries} from 'lib/hooks/useWebMediaQueries'
import {Logo} from '#/view/icons/Logo'
// import {Logo} from '#/view/icons/Logo'
import {atoms as a, useTheme} from '#/alf'
import {Icon, Trigger} from '#/components/dialogs/nuxs/TenMillion/Trigger'
import {Hashtag_Stroke2_Corner0_Rounded as FeedsIcon} from '#/components/icons/Hashtag'
import {Link} from '#/components/Link'
import {useKawaiiMode} from '../../../state/preferences/kawaii'
import {HomeHeaderLayoutMobile} from './HomeHeaderLayoutMobile'
export function HomeHeaderLayout(props: {
@@ -40,7 +49,42 @@ function HomeHeaderLayoutDesktopAndTablet({
const {hasSession} = useSession()
const {_} = useLingui()
const kawaii = useKawaiiMode()
// TEMPORARY - REMOVE AFTER MILLY
// This will just cause the icon to shake a bit when the user first opens the app, drawing attention to the celebration
// 🎉
const rotate = useSharedValue(0)
const reducedMotion = useReducedMotion()
// Run this a single time on app mount.
React.useEffect(() => {
if (reducedMotion) return
// Waits 1500ms, then rotates 10 degrees with a spring animation. Repeats once.
rotate.value = withDelay(
1000,
withRepeat(
withSequence(
withTiming(10, {duration: 100}),
withSpring(0, {
mass: 1,
damping: 1,
stiffness: 200,
overshootClamping: false,
}),
),
2,
false,
),
)
}, [rotate, reducedMotion])
const animatedStyle = useAnimatedStyle(() => ({
transform: [
{
rotateZ: `${rotate.value}deg`,
},
],
}))
return (
<>
@@ -57,21 +101,30 @@ function HomeHeaderLayoutDesktopAndTablet({
t.atoms.bg,
t.atoms.border_contrast_low,
styles.bar,
kawaii && {paddingTop: 22, paddingBottom: 16},
]}>
<View
<Animated.View
style={[
a.absolute,
a.inset_0,
a.pt_lg,
a.m_auto,
kawaii && {paddingTop: 4, paddingBottom: 0},
{
width: kawaii ? 84 : 28,
width: 28,
},
animatedStyle,
]}>
<Logo width={kawaii ? 60 : 28} />
</View>
<Trigger>
{ctx => (
<Icon
width={28}
style={{
opacity: ctx.hovered || ctx.pressed ? 0.8 : 1,
}}
/>
)}
</Trigger>
{/* <Logo width={28} /> */}
</Animated.View>
<Link
to="/feeds"
+62 -5
View File
@@ -1,6 +1,15 @@
import React from 'react'
import {StyleSheet, TouchableOpacity, View} from 'react-native'
import Animated from 'react-native-reanimated'
import Animated, {
useAnimatedStyle,
useReducedMotion,
useSharedValue,
withDelay,
withRepeat,
withSequence,
withSpring,
withTiming,
} from 'react-native-reanimated'
import {msg} from '@lingui/macro'
import {useLingui} from '@lingui/react'
@@ -11,10 +20,11 @@ import {HITSLOP_10} from 'lib/constants'
import {useMinimalShellHeaderTransform} from 'lib/hooks/useMinimalShellTransform'
import {usePalette} from 'lib/hooks/usePalette'
import {isWeb} from 'platform/detection'
import {Logo} from '#/view/icons/Logo'
// import {Logo} from '#/view/icons/Logo'
import {atoms} from '#/alf'
import {useTheme} from '#/alf'
import {atoms as a} from '#/alf'
import {Icon, Trigger} from '#/components/dialogs/nuxs/TenMillion/Trigger'
import {ColorPalette_Stroke2_Corner0_Rounded as ColorPalette} from '#/components/icons/ColorPalette'
import {Hashtag_Stroke2_Corner0_Rounded as FeedsIcon} from '#/components/icons/Hashtag'
import {Menu_Stroke2_Corner0_Rounded as Menu} from '#/components/icons/Menu'
@@ -39,6 +49,43 @@ export function HomeHeaderLayoutMobile({
setDrawerOpen(true)
}, [setDrawerOpen])
// TEMPORARY - REMOVE AFTER MILLY
// This will just cause the icon to shake a bit when the user first opens the app, drawing attention to the celebration
// 🎉
const rotate = useSharedValue(0)
const reducedMotion = useReducedMotion()
// Run this a single time on app mount.
React.useEffect(() => {
if (reducedMotion) return
// Waits 1500ms, then rotates 10 degrees with a spring animation. Repeats once.
rotate.value = withDelay(
1000,
withRepeat(
withSequence(
withTiming(10, {duration: 100}),
withSpring(0, {
mass: 1,
damping: 1,
stiffness: 200,
overshootClamping: false,
}),
),
2,
false,
),
)
}, [rotate, reducedMotion])
const animatedStyle = useAnimatedStyle(() => ({
transform: [
{
rotateZ: `${rotate.value}deg`,
},
],
}))
return (
<Animated.View
style={[pal.view, pal.border, styles.tabBar, headerMinimalShellTransform]}
@@ -59,9 +106,19 @@ export function HomeHeaderLayoutMobile({
<Menu size="lg" fill={t.atoms.text_contrast_medium.color} />
</TouchableOpacity>
</View>
<View>
<Logo width={30} />
</View>
<Animated.View style={animatedStyle}>
<Trigger>
{ctx => (
<Icon
width={28}
style={{
opacity: ctx.pressed ? 0.8 : 1,
}}
/>
)}
</Trigger>
{/* <Logo width={30} /> */}
</Animated.View>
<View
style={[
atoms.flex_row,
+9
View File
@@ -1,5 +1,7 @@
import React, {useCallback, useMemo, useState} from 'react'
import {AppBskyFeedGetLikes as GetLikes} from '@atproto/api'
import {msg} from '@lingui/macro'
import {useLingui} from '@lingui/react'
import {cleanError} from '#/lib/strings/errors'
import {logger} from '#/logger'
@@ -26,6 +28,7 @@ function keyExtractor(item: GetLikes.Like) {
}
export function PostLikedBy({uri}: {uri: string}) {
const {_} = useLingui()
const initialNumToRender = useInitialNumToRender()
const [isPTRing, setIsPTRing] = useState(false)
@@ -78,6 +81,12 @@ export function PostLikedBy({uri}: {uri: string}) {
<ListMaybePlaceholder
isLoading={isLoadingUri || isLoadingLikes}
isError={isError}
emptyType="results"
emptyTitle={_(msg`No likes yet`)}
emptyMessage={_(
msg`Nobody has liked this yet. Maybe you should be the first!`,
)}
errorMessage={cleanError(resolveError || error)}
sideBorders={false}
/>
)
+7 -1
View File
@@ -97,11 +97,17 @@ export function PostQuotes({uri}: {uri: string}) {
}
}, [isFetchingNextPage, hasNextPage, isError, fetchNextPage])
if (isLoadingUri || isLoadingQuotes || isError) {
if (quotes.length < 1) {
return (
<ListMaybePlaceholder
isLoading={isLoadingUri || isLoadingQuotes}
isError={isError}
emptyType="results"
emptyTitle={_(msg`No quotes yet`)}
emptyMessage={_(
msg`Nobody has quoted this yet. Maybe you should be the first!`,
)}
errorMessage={cleanError(resolveError || error)}
sideBorders={false}
/>
)
@@ -1,5 +1,7 @@
import React, {useCallback, useMemo, useState} from 'react'
import {AppBskyActorDefs as ActorDefs} from '@atproto/api'
import {msg} from '@lingui/macro'
import {useLingui} from '@lingui/react'
import {cleanError} from '#/lib/strings/errors'
import {logger} from '#/logger'
@@ -19,6 +21,7 @@ function keyExtractor(item: ActorDefs.ProfileViewBasic) {
}
export function PostRepostedBy({uri}: {uri: string}) {
const {_} = useLingui()
const initialNumToRender = useInitialNumToRender()
const [isPTRing, setIsPTRing] = useState(false)
@@ -71,6 +74,12 @@ export function PostRepostedBy({uri}: {uri: string}) {
<ListMaybePlaceholder
isLoading={isLoadingUri || isLoadingRepostedBy}
isError={isError}
emptyType="results"
emptyTitle={_(msg`No reposts yet`)}
emptyMessage={_(
msg`Nobody has reposted this yet. Maybe you should be the first!`,
)}
errorMessage={cleanError(resolveError || error)}
sideBorders={false}
/>
)
+8 -2
View File
@@ -19,7 +19,7 @@ import {colors} from 'lib/styles'
import {isAndroid, isNative, isWeb} from 'platform/detection'
import {precacheProfile} from 'state/queries/profile'
import {HighPriorityImage} from 'view/com/util/images/Image'
import {atoms as a, tokens, useTheme} from '#/alf'
import {tokens, useTheme} from '#/alf'
import {
Camera_Filled_Stroke2_Corner0_Rounded as CameraFilled,
Camera_Stroke2_Corner0_Rounded as Camera,
@@ -241,7 +241,13 @@ let UserAvatar = ({
onLoad={onLoad}
/>
)}
<MediaInsetBorder style={[a.rounded_full]} />
<MediaInsetBorder
style={[
{
borderRadius: aviStyle.borderRadius,
},
]}
/>
{alert}
</View>
) : (
@@ -18,7 +18,7 @@ const Context = React.createContext<{
export function Provider({children}: {children: React.ReactNode}) {
if (!isWeb) {
throw new Error('ActiveVideoWebContext may onl be used on web.')
throw new Error('ActiveVideoWebContext may only be used on web.')
}
const [activeViewId, setActiveViewId] = useState<string | null>(null)
@@ -59,7 +59,7 @@ export const ExternalLinkEmbed = ({
}
return (
<View style={[a.flex_col, a.rounded_sm, a.overflow_hidden]}>
<View style={[a.flex_col, a.rounded_sm]}>
<LinkWrapper link={link} onOpen={onOpen} style={style}>
{imageUri && !embedPlayerParams ? (
<View>
@@ -227,8 +227,16 @@ export function ExternalPlayer({
<Animated.View
ref={viewRef}
collapsable={false}
style={[aspect, a.rounded_sm]}>
{link.thumb && (!isPlayerActive || isLoading) && (
style={[
aspect,
a.rounded_sm,
a.overflow_hidden,
{
borderBottomLeftRadius: 0,
borderBottomRightRadius: 0,
},
]}>
{link.thumb && (!isPlayerActive || isLoading) ? (
<>
<Image
style={[a.flex_1, styles.topRadius]}
@@ -256,7 +264,29 @@ export function ExternalPlayer({
]}
/>
</>
) : (
<Fill
style={[
a.rounded_sm,
{
backgroundColor:
t.name === 'light' ? t.palette.contrast_975 : 'black',
borderBottomLeftRadius: 0,
borderBottomRightRadius: 0,
opacity: 0.3,
},
]}
/>
)}
<MediaInsetBorder
opaque
style={[
{
borderBottomLeftRadius: 0,
borderBottomRightRadius: 0,
},
]}
/>
<PlaceholderOverlay
isLoading={isLoading}
isPlayerActive={isPlayerActive}
@@ -7,9 +7,8 @@ import {msg} from '@lingui/macro'
import {useLingui} from '@lingui/react'
import {HITSLOP_30} from '#/lib/constants'
import {clamp} from '#/lib/numbers'
import {useAutoplayDisabled} from '#/state/preferences'
import {useVideoVolumeState} from 'view/com/util/post-embeds/VideoVolumeContext'
import {useVideoMuteState} from 'view/com/util/post-embeds/VideoVolumeContext'
import {atoms as a, useTheme} from '#/alf'
import {useIsWithinMessage} from '#/components/dms/MessageContext'
import {Mute_Stroke2_Corner0_Rounded as MuteIcon} from '#/components/icons/Mute'
@@ -38,7 +37,7 @@ export const VideoEmbedInnerNative = React.forwardRef(
const videoRef = useRef<BlueskyVideoView>(null)
const autoplayDisabled = useAutoplayDisabled()
const isWithinMessage = useIsWithinMessage()
const {muted, setMuted} = useVideoVolumeState()
const [muted, setMuted] = useVideoMuteState()
const [isPlaying, setIsPlaying] = React.useState(false)
const [timeRemaining, setTimeRemaining] = React.useState(0)
@@ -54,16 +53,8 @@ export const VideoEmbedInnerNative = React.forwardRef(
throw new Error(error)
}
let aspectRatio = 16 / 9
if (embed.aspectRatio) {
const {width, height} = embed.aspectRatio
aspectRatio = width / height
aspectRatio = clamp(aspectRatio, 1 / 1, 3 / 1)
}
return (
<View style={[a.flex_1, a.relative, {aspectRatio}]}>
<View style={[a.flex_1, a.relative]}>
<BlueskyVideoView
url={embed.playlist}
autoplay={!autoplayDisabled && !isWithinMessage}
@@ -128,7 +119,7 @@ function VideoControls({
}) {
const {_} = useLingui()
const t = useTheme()
const {muted} = useVideoVolumeState()
const [muted] = useVideoMuteState()
// show countdown when:
// 1. timeRemaining is a number - was seeing NaNs
@@ -160,7 +151,11 @@ function VideoControls({
<ControlButton
onPress={toggleMuted}
label={muted ? _(msg`Unmute`) : _(msg`Mute`)}
label={
muted
? _(msg({message: `Unmute`, context: 'video'}))
: _(msg({message: `Mute`, context: 'video'}))
}
accessibilityHint={_(msg`Tap to toggle sound`)}
style={{right: 6}}>
{muted ? (
@@ -5,7 +5,7 @@ import Hls from 'hls.js'
import {atoms as a} from '#/alf'
import {MediaInsetBorder} from '#/components/MediaInsetBorder'
import {Controls} from './VideoWebControls'
import {Controls} from './web-controls/VideoControls'
export function VideoEmbedInnerWeb({
embed,
@@ -1,896 +0,0 @@
import React, {useCallback, useEffect, useRef, useState} from 'react'
import {Pressable, View} from 'react-native'
import {SvgProps} from 'react-native-svg'
import {msg, Trans} from '@lingui/macro'
import {useLingui} from '@lingui/react'
import type Hls from 'hls.js'
import {isFirefox} from '#/lib/browser'
import {clamp} from '#/lib/numbers'
import {isIPhoneWeb} from '#/platform/detection'
import {
useAutoplayDisabled,
useSetSubtitlesEnabled,
useSubtitlesEnabled,
} from '#/state/preferences'
import {atoms as a, useTheme, web} from '#/alf'
import {Button} from '#/components/Button'
import {useIsWithinMessage} from '#/components/dms/MessageContext'
import {useFullscreen} from '#/components/hooks/useFullscreen'
import {useInteractionState} from '#/components/hooks/useInteractionState'
import {
ArrowsDiagonalIn_Stroke2_Corner0_Rounded as ArrowsInIcon,
ArrowsDiagonalOut_Stroke2_Corner0_Rounded as ArrowsOutIcon,
} from '#/components/icons/ArrowsDiagonal'
import {
CC_Filled_Corner0_Rounded as CCActiveIcon,
CC_Stroke2_Corner0_Rounded as CCInactiveIcon,
} from '#/components/icons/CC'
import {Mute_Stroke2_Corner0_Rounded as MuteIcon} from '#/components/icons/Mute'
import {Pause_Filled_Corner0_Rounded as PauseIcon} from '#/components/icons/Pause'
import {Play_Filled_Corner0_Rounded as PlayIcon} from '#/components/icons/Play'
import {SpeakerVolumeFull_Stroke2_Corner0_Rounded as UnmuteIcon} from '#/components/icons/Speaker'
import {Loader} from '#/components/Loader'
import {Text} from '#/components/Typography'
import {TimeIndicator} from './TimeIndicator'
export function Controls({
videoRef,
hlsRef,
active,
setActive,
focused,
setFocused,
onScreen,
fullscreenRef,
hasSubtitleTrack,
}: {
videoRef: React.RefObject<HTMLVideoElement>
hlsRef: React.RefObject<Hls | undefined>
active: boolean
setActive: () => void
focused: boolean
setFocused: (focused: boolean) => void
onScreen: boolean
fullscreenRef: React.RefObject<HTMLDivElement>
hasSubtitleTrack: boolean
}) {
const {
play,
pause,
playing,
muted,
toggleMute,
togglePlayPause,
currentTime,
duration,
buffering,
error,
canPlay,
} = useVideoUtils(videoRef)
const t = useTheme()
const {_} = useLingui()
const subtitlesEnabled = useSubtitlesEnabled()
const setSubtitlesEnabled = useSetSubtitlesEnabled()
const {
state: hovered,
onIn: onHover,
onOut: onEndHover,
} = useInteractionState()
const [isFullscreen, toggleFullscreen] = useFullscreen(fullscreenRef)
const {state: hasFocus, onIn: onFocus, onOut: onBlur} = useInteractionState()
const [interactingViaKeypress, setInteractingViaKeypress] = useState(false)
const onKeyDown = useCallback(() => {
setInteractingViaKeypress(true)
}, [])
useEffect(() => {
if (interactingViaKeypress) {
document.addEventListener('click', () => setInteractingViaKeypress(false))
return () => {
document.removeEventListener('click', () =>
setInteractingViaKeypress(false),
)
}
}
}, [interactingViaKeypress])
useEffect(() => {
if (isFullscreen) {
document.documentElement.style.scrollbarGutter = 'unset'
return () => {
document.documentElement.style.removeProperty('scrollbar-gutter')
}
}
}, [isFullscreen])
// pause + unfocus when another video is active
useEffect(() => {
if (!active) {
pause()
setFocused(false)
}
}, [active, pause, setFocused])
// autoplay/pause based on visibility
const isWithinMessage = useIsWithinMessage()
const autoplayDisabled = useAutoplayDisabled() || isWithinMessage
useEffect(() => {
if (active) {
if (onScreen) {
if (!autoplayDisabled) play()
} else {
pause()
}
}
}, [onScreen, pause, active, play, autoplayDisabled])
// use minimal quality when not focused
useEffect(() => {
if (!hlsRef.current) return
if (focused) {
// auto decide quality based on network conditions
hlsRef.current.autoLevelCapping = -1
// allow 30s of buffering
hlsRef.current.config.maxMaxBufferLength = 30
} else {
// back to what we initially set
hlsRef.current.autoLevelCapping = 0
hlsRef.current.config.maxMaxBufferLength = 10
}
}, [hlsRef, focused])
useEffect(() => {
if (!hlsRef.current) return
if (hasSubtitleTrack && subtitlesEnabled && canPlay) {
hlsRef.current.subtitleTrack = 0
} else {
hlsRef.current.subtitleTrack = -1
}
}, [hasSubtitleTrack, subtitlesEnabled, hlsRef, canPlay])
// clicking on any button should focus the player, if it's not already focused
const drawFocus = useCallback(() => {
if (!active) {
setActive()
}
setFocused(true)
}, [active, setActive, setFocused])
const onPressEmptySpace = useCallback(() => {
if (!focused) {
drawFocus()
if (autoplayDisabled) play()
} else {
togglePlayPause()
}
}, [togglePlayPause, drawFocus, focused, autoplayDisabled, play])
const onPressPlayPause = useCallback(() => {
drawFocus()
togglePlayPause()
}, [drawFocus, togglePlayPause])
const onPressSubtitles = useCallback(() => {
drawFocus()
setSubtitlesEnabled(!subtitlesEnabled)
}, [drawFocus, setSubtitlesEnabled, subtitlesEnabled])
const onPressMute = useCallback(() => {
drawFocus()
toggleMute()
}, [drawFocus, toggleMute])
const onPressFullscreen = useCallback(() => {
drawFocus()
toggleFullscreen()
}, [drawFocus, toggleFullscreen])
const onSeek = useCallback(
(time: number) => {
if (!videoRef.current) return
if (videoRef.current.fastSeek) {
videoRef.current.fastSeek(time)
} else {
videoRef.current.currentTime = time
}
},
[videoRef],
)
const playStateBeforeSeekRef = useRef(false)
const onSeekStart = useCallback(() => {
drawFocus()
playStateBeforeSeekRef.current = playing
pause()
}, [playing, pause, drawFocus])
const onSeekEnd = useCallback(() => {
if (playStateBeforeSeekRef.current) {
play()
}
}, [play])
const seekLeft = useCallback(() => {
if (!videoRef.current) return
// eslint-disable-next-line @typescript-eslint/no-shadow
const currentTime = videoRef.current.currentTime
// eslint-disable-next-line @typescript-eslint/no-shadow
const duration = videoRef.current.duration || 0
onSeek(clamp(currentTime - 5, 0, duration))
}, [onSeek, videoRef])
const seekRight = useCallback(() => {
if (!videoRef.current) return
// eslint-disable-next-line @typescript-eslint/no-shadow
const currentTime = videoRef.current.currentTime
// eslint-disable-next-line @typescript-eslint/no-shadow
const duration = videoRef.current.duration || 0
onSeek(clamp(currentTime + 5, 0, duration))
}, [onSeek, videoRef])
const [showCursor, setShowCursor] = useState(true)
const cursorTimeoutRef = useRef<ReturnType<typeof setTimeout>>()
const onPointerMoveEmptySpace = useCallback(() => {
setShowCursor(true)
if (cursorTimeoutRef.current) {
clearTimeout(cursorTimeoutRef.current)
}
cursorTimeoutRef.current = setTimeout(() => {
setShowCursor(false)
onEndHover()
}, 2000)
}, [onEndHover])
const onPointerLeaveEmptySpace = useCallback(() => {
setShowCursor(false)
if (cursorTimeoutRef.current) {
clearTimeout(cursorTimeoutRef.current)
}
}, [])
// these are used to trigger the hover state. on mobile, the hover state
// should stick around for a bit after they tap, and if the controls aren't
// present this initial tab should *only* show the controls and not activate anything
const onPointerDown = useCallback(
(evt: React.PointerEvent<HTMLDivElement>) => {
if (evt.pointerType !== 'mouse' && !hovered) {
evt.preventDefault()
}
},
[hovered],
)
const timeoutRef = useRef<ReturnType<typeof setTimeout>>()
const onHoverWithTimeout = useCallback(() => {
onHover()
clearTimeout(timeoutRef.current)
}, [onHover])
const onEndHoverWithTimeout = useCallback(
(evt: React.PointerEvent<HTMLDivElement>) => {
// if touch, end after 3s
// if mouse, end immediately
if (evt.pointerType !== 'mouse') {
setTimeout(onEndHover, 3000)
} else {
onEndHover()
}
},
[onEndHover],
)
const showControls =
((focused || autoplayDisabled) && !playing) ||
(interactingViaKeypress ? hasFocus : hovered)
return (
<div
style={{
position: 'absolute',
inset: 0,
overflow: 'hidden',
display: 'flex',
flexDirection: 'column',
}}
onClick={evt => {
evt.stopPropagation()
setInteractingViaKeypress(false)
}}
onPointerEnter={onHoverWithTimeout}
onPointerMove={onHoverWithTimeout}
onPointerLeave={onEndHoverWithTimeout}
onPointerDown={onPointerDown}
onFocus={onFocus}
onBlur={onBlur}
onKeyDown={onKeyDown}>
<Pressable
accessibilityRole="button"
onPointerEnter={onPointerMoveEmptySpace}
onPointerMove={onPointerMoveEmptySpace}
onPointerLeave={onPointerLeaveEmptySpace}
accessibilityHint={_(
!focused
? msg`Unmute video`
: playing
? msg`Pause video`
: msg`Play video`,
)}
style={[
a.flex_1,
web({cursor: showCursor || !playing ? 'pointer' : 'none'}),
]}
onPress={onPressEmptySpace}
/>
{!showControls && !focused && duration > 0 && (
<TimeIndicator time={Math.floor(duration - currentTime)} />
)}
<View
style={[
a.flex_shrink_0,
a.w_full,
a.px_xs,
web({
background:
'linear-gradient(rgba(0, 0, 0, 0), rgba(0, 0, 0, 0.4), rgba(0, 0, 0, 0.7))',
}),
{opacity: showControls ? 1 : 0},
{transition: 'opacity 0.2s ease-in-out'},
]}>
<Scrubber
duration={duration}
currentTime={currentTime}
onSeek={onSeek}
onSeekStart={onSeekStart}
onSeekEnd={onSeekEnd}
seekLeft={seekLeft}
seekRight={seekRight}
togglePlayPause={togglePlayPause}
drawFocus={drawFocus}
/>
<View
style={[
a.flex_1,
a.px_xs,
a.pt_sm,
a.pb_md,
a.gap_md,
a.flex_row,
a.align_center,
]}>
<ControlButton
active={playing}
activeLabel={_(msg`Pause`)}
inactiveLabel={_(msg`Play`)}
activeIcon={PauseIcon}
inactiveIcon={PlayIcon}
onPress={onPressPlayPause}
/>
<View style={a.flex_1} />
<Text style={{color: t.palette.white, fontVariant: ['tabular-nums']}}>
{formatTime(currentTime)} / {formatTime(duration)}
</Text>
{hasSubtitleTrack && (
<ControlButton
active={subtitlesEnabled}
activeLabel={_(msg`Disable subtitles`)}
inactiveLabel={_(msg`Enable subtitles`)}
activeIcon={CCActiveIcon}
inactiveIcon={CCInactiveIcon}
onPress={onPressSubtitles}
/>
)}
<ControlButton
active={muted}
activeLabel={_(msg({message: `Unmute`, context: 'video'}))}
inactiveLabel={_(msg({message: `Mute`, context: 'video'}))}
activeIcon={MuteIcon}
inactiveIcon={UnmuteIcon}
onPress={onPressMute}
/>
{!isIPhoneWeb && (
<ControlButton
active={isFullscreen}
activeLabel={_(msg`Exit fullscreen`)}
inactiveLabel={_(msg`Fullscreen`)}
activeIcon={ArrowsInIcon}
inactiveIcon={ArrowsOutIcon}
onPress={onPressFullscreen}
/>
)}
</View>
</View>
{(buffering || error) && (
<View
pointerEvents="none"
style={[a.absolute, a.inset_0, a.justify_center, a.align_center]}>
{buffering && <Loader fill={t.palette.white} size="lg" />}
{error && (
<Text style={{color: t.palette.white}}>
<Trans>An error occurred</Trans>
</Text>
)}
</View>
)}
</div>
)
}
function ControlButton({
active,
activeLabel,
inactiveLabel,
activeIcon: ActiveIcon,
inactiveIcon: InactiveIcon,
onPress,
}: {
active: boolean
activeLabel: string
inactiveLabel: string
activeIcon: React.ComponentType<Pick<SvgProps, 'fill' | 'width'>>
inactiveIcon: React.ComponentType<Pick<SvgProps, 'fill' | 'width'>>
onPress: () => void
}) {
const t = useTheme()
return (
<Button
label={active ? activeLabel : inactiveLabel}
onPress={onPress}
variant="ghost"
shape="round"
size="medium"
style={a.p_2xs}
hoverStyle={{backgroundColor: 'rgba(255, 255, 255, 0.1)'}}>
{active ? (
<ActiveIcon fill={t.palette.white} width={20} />
) : (
<InactiveIcon fill={t.palette.white} width={20} />
)}
</Button>
)
}
function Scrubber({
duration,
currentTime,
onSeek,
onSeekEnd,
onSeekStart,
seekLeft,
seekRight,
togglePlayPause,
drawFocus,
}: {
duration: number
currentTime: number
onSeek: (time: number) => void
onSeekEnd: () => void
onSeekStart: () => void
seekLeft: () => void
seekRight: () => void
togglePlayPause: () => void
drawFocus: () => void
}) {
const {_} = useLingui()
const t = useTheme()
const [scrubberActive, setScrubberActive] = useState(false)
const {
state: hovered,
onIn: onStartHover,
onOut: onEndHover,
} = useInteractionState()
const {state: focused, onIn: onFocus, onOut: onBlur} = useInteractionState()
const [seekPosition, setSeekPosition] = useState(0)
const isSeekingRef = useRef(false)
const barRef = useRef<HTMLDivElement>(null)
const circleRef = useRef<HTMLDivElement>(null)
const seek = useCallback(
(evt: React.PointerEvent<HTMLDivElement>) => {
if (!barRef.current) return
const {left, width} = barRef.current.getBoundingClientRect()
const x = evt.clientX
const percent = clamp((x - left) / width, 0, 1) * duration
onSeek(percent)
setSeekPosition(percent)
},
[duration, onSeek],
)
const onPointerDown = useCallback(
(evt: React.PointerEvent<HTMLDivElement>) => {
const target = evt.target
if (target instanceof Element) {
evt.preventDefault()
target.setPointerCapture(evt.pointerId)
isSeekingRef.current = true
seek(evt)
setScrubberActive(true)
onSeekStart()
}
},
[seek, onSeekStart],
)
const onPointerMove = useCallback(
(evt: React.PointerEvent<HTMLDivElement>) => {
if (isSeekingRef.current) {
evt.preventDefault()
seek(evt)
}
},
[seek],
)
const onPointerUp = useCallback(
(evt: React.PointerEvent<HTMLDivElement>) => {
const target = evt.target
if (isSeekingRef.current && target instanceof Element) {
evt.preventDefault()
target.releasePointerCapture(evt.pointerId)
isSeekingRef.current = false
onSeekEnd()
setScrubberActive(false)
}
},
[onSeekEnd],
)
useEffect(() => {
// HACK: there's divergent browser behaviour about what to do when
// a pointerUp event is fired outside the element that captured the
// pointer. Firefox clicks on the element the mouse is over, so we have
// to make everything unclickable while seeking -sfn
if (isFirefox && scrubberActive) {
document.body.classList.add('force-no-clicks')
return () => {
document.body.classList.remove('force-no-clicks')
}
}
}, [scrubberActive, onSeekEnd])
useEffect(() => {
if (!circleRef.current) return
if (focused) {
const abortController = new AbortController()
const {signal} = abortController
circleRef.current.addEventListener(
'keydown',
evt => {
// space: play/pause
// arrow left: seek backward
// arrow right: seek forward
if (evt.key === ' ') {
evt.preventDefault()
drawFocus()
togglePlayPause()
} else if (evt.key === 'ArrowLeft') {
evt.preventDefault()
drawFocus()
seekLeft()
} else if (evt.key === 'ArrowRight') {
evt.preventDefault()
drawFocus()
seekRight()
}
},
{signal},
)
return () => abortController.abort()
}
}, [focused, seekLeft, seekRight, togglePlayPause, drawFocus])
const progress = scrubberActive ? seekPosition : currentTime
const progressPercent = (progress / duration) * 100
return (
<View
testID="scrubber"
style={[{height: 18, width: '100%'}, a.flex_shrink_0, a.px_xs, a.py_xs]}
onPointerEnter={onStartHover}
onPointerLeave={onEndHover}>
<div
ref={barRef}
style={{
flex: 1,
display: 'flex',
alignItems: 'center',
position: 'relative',
cursor: scrubberActive ? 'grabbing' : 'grab',
}}
onPointerDown={onPointerDown}
onPointerMove={onPointerMove}
onPointerUp={onPointerUp}
onPointerCancel={onPointerUp}>
<View
style={[
a.w_full,
a.rounded_full,
a.overflow_hidden,
{backgroundColor: 'rgba(255, 255, 255, 0.4)'},
{height: hovered || scrubberActive ? 6 : 3},
]}>
{duration > 0 && (
<View
style={[
a.h_full,
{backgroundColor: t.palette.white},
{width: `${progressPercent}%`},
]}
/>
)}
</View>
<div
ref={circleRef}
aria-label={_(msg`Seek slider`)}
role="slider"
aria-valuemax={duration}
aria-valuemin={0}
aria-valuenow={currentTime}
aria-valuetext={_(
msg`${formatTime(currentTime)} of ${formatTime(duration)}`,
)}
tabIndex={0}
onFocus={onFocus}
onBlur={onBlur}
style={{
position: 'absolute',
height: 16,
width: 16,
left: `calc(${progressPercent}% - 8px)`,
borderRadius: 8,
pointerEvents: 'none',
}}>
<View
style={[
a.w_full,
a.h_full,
a.rounded_full,
{backgroundColor: t.palette.white},
{
transform: [
{
scale:
hovered || scrubberActive || focused
? scrubberActive
? 1
: 0.6
: 0,
},
],
},
]}
/>
</div>
</div>
</View>
)
}
function formatTime(time: number) {
if (isNaN(time)) {
return '--'
}
time = Math.round(time)
const minutes = Math.floor(time / 60)
const seconds = String(time % 60).padStart(2, '0')
return `${minutes}:${seconds}`
}
function useVideoUtils(ref: React.RefObject<HTMLVideoElement>) {
const [playing, setPlaying] = useState(false)
const [muted, setMuted] = useState(true)
const [currentTime, setCurrentTime] = useState(0)
const [duration, setDuration] = useState(0)
const [buffering, setBuffering] = useState(false)
const [error, setError] = useState(false)
const [canPlay, setCanPlay] = useState(false)
const playWhenReadyRef = useRef(false)
useEffect(() => {
if (!ref.current) return
let bufferingTimeout: ReturnType<typeof setTimeout> | undefined
function round(num: number) {
return Math.round(num * 100) / 100
}
// Initial values
setCurrentTime(round(ref.current.currentTime) || 0)
setDuration(round(ref.current.duration) || 0)
setMuted(ref.current.muted)
setPlaying(!ref.current.paused)
const handleTimeUpdate = () => {
if (!ref.current) return
setCurrentTime(round(ref.current.currentTime) || 0)
}
const handleDurationChange = () => {
if (!ref.current) return
setDuration(round(ref.current.duration) || 0)
}
const handlePlay = () => {
setPlaying(true)
}
const handlePause = () => {
setPlaying(false)
}
const handleVolumeChange = () => {
if (!ref.current) return
setMuted(ref.current.muted)
}
const handleError = () => {
setError(true)
}
const handleCanPlay = () => {
setBuffering(false)
setCanPlay(true)
if (!ref.current) return
if (playWhenReadyRef.current) {
ref.current.play()
playWhenReadyRef.current = false
}
}
const handleCanPlayThrough = () => {
setBuffering(false)
}
const handleWaiting = () => {
if (bufferingTimeout) clearTimeout(bufferingTimeout)
bufferingTimeout = setTimeout(() => {
setBuffering(true)
}, 200) // Delay to avoid frequent buffering state changes
}
const handlePlaying = () => {
if (bufferingTimeout) clearTimeout(bufferingTimeout)
setBuffering(false)
setError(false)
}
const handleStalled = () => {
if (bufferingTimeout) clearTimeout(bufferingTimeout)
bufferingTimeout = setTimeout(() => {
setBuffering(true)
}, 200) // Delay to avoid frequent buffering state changes
}
const handleEnded = () => {
setPlaying(false)
setBuffering(false)
setError(false)
}
const abortController = new AbortController()
ref.current.addEventListener('timeupdate', handleTimeUpdate, {
signal: abortController.signal,
})
ref.current.addEventListener('durationchange', handleDurationChange, {
signal: abortController.signal,
})
ref.current.addEventListener('play', handlePlay, {
signal: abortController.signal,
})
ref.current.addEventListener('pause', handlePause, {
signal: abortController.signal,
})
ref.current.addEventListener('volumechange', handleVolumeChange, {
signal: abortController.signal,
})
ref.current.addEventListener('error', handleError, {
signal: abortController.signal,
})
ref.current.addEventListener('canplay', handleCanPlay, {
signal: abortController.signal,
})
ref.current.addEventListener('canplaythrough', handleCanPlayThrough, {
signal: abortController.signal,
})
ref.current.addEventListener('waiting', handleWaiting, {
signal: abortController.signal,
})
ref.current.addEventListener('playing', handlePlaying, {
signal: abortController.signal,
})
ref.current.addEventListener('stalled', handleStalled, {
signal: abortController.signal,
})
ref.current.addEventListener('ended', handleEnded, {
signal: abortController.signal,
})
return () => {
abortController.abort()
clearTimeout(bufferingTimeout)
}
}, [ref])
const play = useCallback(() => {
if (!ref.current) return
if (ref.current.ended) {
ref.current.currentTime = 0
}
if (ref.current.readyState < HTMLMediaElement.HAVE_FUTURE_DATA) {
playWhenReadyRef.current = true
} else {
const promise = ref.current.play()
if (promise !== undefined) {
promise.catch(err => {
console.error('Error playing video:', err)
})
}
}
}, [ref])
const pause = useCallback(() => {
if (!ref.current) return
ref.current.pause()
playWhenReadyRef.current = false
}, [ref])
const togglePlayPause = useCallback(() => {
if (!ref.current) return
if (ref.current.paused) {
play()
} else {
pause()
}
}, [ref, play, pause])
const mute = useCallback(() => {
if (!ref.current) return
ref.current.muted = true
}, [ref])
const unmute = useCallback(() => {
if (!ref.current) return
ref.current.muted = false
}, [ref])
const toggleMute = useCallback(() => {
if (!ref.current) return
ref.current.muted = !ref.current.muted
}, [ref])
return {
play,
pause,
togglePlayPause,
duration,
currentTime,
playing,
muted,
mute,
unmute,
toggleMute,
buffering,
error,
canPlay,
}
}
@@ -0,0 +1,39 @@
import React from 'react'
import {SvgProps} from 'react-native-svg'
import {atoms as a, useTheme} from '#/alf'
import {Button} from '#/components/Button'
export function ControlButton({
active,
activeLabel,
inactiveLabel,
activeIcon: ActiveIcon,
inactiveIcon: InactiveIcon,
onPress,
}: {
active: boolean
activeLabel: string
inactiveLabel: string
activeIcon: React.ComponentType<Pick<SvgProps, 'fill' | 'width'>>
inactiveIcon: React.ComponentType<Pick<SvgProps, 'fill' | 'width'>>
onPress: () => void
}) {
const t = useTheme()
return (
<Button
label={active ? activeLabel : inactiveLabel}
onPress={onPress}
variant="ghost"
shape="round"
size="medium"
style={a.p_2xs}
hoverStyle={{backgroundColor: 'rgba(255, 255, 255, 0.1)'}}>
{active ? (
<ActiveIcon fill={t.palette.white} width={20} />
) : (
<InactiveIcon fill={t.palette.white} width={20} />
)}
</Button>
)
}
@@ -0,0 +1,232 @@
import React, {useCallback, useEffect, useRef, useState} from 'react'
import {View} from 'react-native'
import {msg} from '@lingui/macro'
import {useLingui} from '@lingui/react'
import {isFirefox} from '#/lib/browser'
import {clamp} from '#/lib/numbers'
import {atoms as a, useTheme, web} from '#/alf'
import {useInteractionState} from '#/components/hooks/useInteractionState'
import {formatTime} from './utils'
export function Scrubber({
duration,
currentTime,
onSeek,
onSeekEnd,
onSeekStart,
seekLeft,
seekRight,
togglePlayPause,
drawFocus,
}: {
duration: number
currentTime: number
onSeek: (time: number) => void
onSeekEnd: () => void
onSeekStart: () => void
seekLeft: () => void
seekRight: () => void
togglePlayPause: () => void
drawFocus: () => void
}) {
const {_} = useLingui()
const t = useTheme()
const [scrubberActive, setScrubberActive] = useState(false)
const {
state: hovered,
onIn: onStartHover,
onOut: onEndHover,
} = useInteractionState()
const {state: focused, onIn: onFocus, onOut: onBlur} = useInteractionState()
const [seekPosition, setSeekPosition] = useState(0)
const isSeekingRef = useRef(false)
const barRef = useRef<HTMLDivElement>(null)
const circleRef = useRef<HTMLDivElement>(null)
const seek = useCallback(
(evt: React.PointerEvent<HTMLDivElement>) => {
if (!barRef.current) return
const {left, width} = barRef.current.getBoundingClientRect()
const x = evt.clientX
const percent = clamp((x - left) / width, 0, 1) * duration
onSeek(percent)
setSeekPosition(percent)
},
[duration, onSeek],
)
const onPointerDown = useCallback(
(evt: React.PointerEvent<HTMLDivElement>) => {
const target = evt.target
if (target instanceof Element) {
evt.preventDefault()
target.setPointerCapture(evt.pointerId)
isSeekingRef.current = true
seek(evt)
setScrubberActive(true)
onSeekStart()
}
},
[seek, onSeekStart],
)
const onPointerMove = useCallback(
(evt: React.PointerEvent<HTMLDivElement>) => {
if (isSeekingRef.current) {
evt.preventDefault()
seek(evt)
}
},
[seek],
)
const onPointerUp = useCallback(
(evt: React.PointerEvent<HTMLDivElement>) => {
const target = evt.target
if (isSeekingRef.current && target instanceof Element) {
evt.preventDefault()
target.releasePointerCapture(evt.pointerId)
isSeekingRef.current = false
onSeekEnd()
setScrubberActive(false)
}
},
[onSeekEnd],
)
useEffect(() => {
// HACK: there's divergent browser behaviour about what to do when
// a pointerUp event is fired outside the element that captured the
// pointer. Firefox clicks on the element the mouse is over, so we have
// to make everything unclickable while seeking -sfn
if (isFirefox && scrubberActive) {
document.body.classList.add('force-no-clicks')
return () => {
document.body.classList.remove('force-no-clicks')
}
}
}, [scrubberActive, onSeekEnd])
useEffect(() => {
if (!circleRef.current) return
if (focused) {
const abortController = new AbortController()
const {signal} = abortController
circleRef.current.addEventListener(
'keydown',
evt => {
// space: play/pause
// arrow left: seek backward
// arrow right: seek forward
if (evt.key === ' ') {
evt.preventDefault()
drawFocus()
togglePlayPause()
} else if (evt.key === 'ArrowLeft') {
evt.preventDefault()
drawFocus()
seekLeft()
} else if (evt.key === 'ArrowRight') {
evt.preventDefault()
drawFocus()
seekRight()
}
},
{signal},
)
return () => abortController.abort()
}
}, [focused, seekLeft, seekRight, togglePlayPause, drawFocus])
const progress = scrubberActive ? seekPosition : currentTime
const progressPercent = (progress / duration) * 100
return (
<View
testID="scrubber"
style={[{height: 18, width: '100%'}, a.flex_shrink_0, a.px_xs]}
onPointerEnter={onStartHover}
onPointerLeave={onEndHover}>
<div
ref={barRef}
style={{
flex: 1,
display: 'flex',
alignItems: 'center',
position: 'relative',
cursor: scrubberActive ? 'grabbing' : 'grab',
padding: '4px 0',
}}
onPointerDown={onPointerDown}
onPointerMove={onPointerMove}
onPointerUp={onPointerUp}
onPointerCancel={onPointerUp}>
<View
style={[
a.w_full,
a.rounded_full,
a.overflow_hidden,
{backgroundColor: 'rgba(255, 255, 255, 0.4)'},
{height: hovered || scrubberActive ? 6 : 3},
web({transition: 'height 0.1s ease'}),
]}>
{duration > 0 && (
<View
style={[
a.h_full,
{backgroundColor: t.palette.white},
{width: `${progressPercent}%`},
]}
/>
)}
</View>
<div
ref={circleRef}
aria-label={_(msg`Seek slider`)}
role="slider"
aria-valuemax={duration}
aria-valuemin={0}
aria-valuenow={currentTime}
aria-valuetext={_(
msg`${formatTime(currentTime)} of ${formatTime(duration)}`,
)}
tabIndex={0}
onFocus={onFocus}
onBlur={onBlur}
style={{
position: 'absolute',
height: 16,
width: 16,
left: `calc(${progressPercent}% - 8px)`,
borderRadius: 8,
pointerEvents: 'none',
}}>
<View
style={[
a.w_full,
a.h_full,
a.rounded_full,
{backgroundColor: t.palette.white},
{
transform: [
{
scale:
hovered || scrubberActive || focused
? scrubberActive
? 1
: 0.6
: 0,
},
],
},
]}
/>
</div>
</div>
</View>
)
}
@@ -0,0 +1,423 @@
import React, {useCallback, useEffect, useRef, useState} from 'react'
import {Pressable, View} from 'react-native'
import {msg, Trans} from '@lingui/macro'
import {useLingui} from '@lingui/react'
import type Hls from 'hls.js'
import {isTouchDevice} from '#/lib/browser'
import {clamp} from '#/lib/numbers'
import {isIPhoneWeb} from '#/platform/detection'
import {
useAutoplayDisabled,
useSetSubtitlesEnabled,
useSubtitlesEnabled,
} from '#/state/preferences'
import {atoms as a, useTheme, web} from '#/alf'
import {useIsWithinMessage} from '#/components/dms/MessageContext'
import {useFullscreen} from '#/components/hooks/useFullscreen'
import {useInteractionState} from '#/components/hooks/useInteractionState'
import {
ArrowsDiagonalIn_Stroke2_Corner0_Rounded as ArrowsInIcon,
ArrowsDiagonalOut_Stroke2_Corner0_Rounded as ArrowsOutIcon,
} from '#/components/icons/ArrowsDiagonal'
import {
CC_Filled_Corner0_Rounded as CCActiveIcon,
CC_Stroke2_Corner0_Rounded as CCInactiveIcon,
} from '#/components/icons/CC'
import {Pause_Filled_Corner0_Rounded as PauseIcon} from '#/components/icons/Pause'
import {Play_Filled_Corner0_Rounded as PlayIcon} from '#/components/icons/Play'
import {Loader} from '#/components/Loader'
import {Text} from '#/components/Typography'
import {TimeIndicator} from '../TimeIndicator'
import {ControlButton} from './ControlButton'
import {Scrubber} from './Scrubber'
import {formatTime, useVideoElement} from './utils'
import {VolumeControl} from './VolumeControl'
export function Controls({
videoRef,
hlsRef,
active,
setActive,
focused,
setFocused,
onScreen,
fullscreenRef,
hasSubtitleTrack,
}: {
videoRef: React.RefObject<HTMLVideoElement>
hlsRef: React.RefObject<Hls | undefined>
active: boolean
setActive: () => void
focused: boolean
setFocused: (focused: boolean) => void
onScreen: boolean
fullscreenRef: React.RefObject<HTMLDivElement>
hasSubtitleTrack: boolean
}) {
const {
play,
pause,
playing,
muted,
changeMuted,
togglePlayPause,
currentTime,
duration,
buffering,
error,
canPlay,
} = useVideoElement(videoRef)
const t = useTheme()
const {_} = useLingui()
const subtitlesEnabled = useSubtitlesEnabled()
const setSubtitlesEnabled = useSetSubtitlesEnabled()
const {
state: hovered,
onIn: onHover,
onOut: onEndHover,
} = useInteractionState()
const [isFullscreen, toggleFullscreen] = useFullscreen(fullscreenRef)
const {state: hasFocus, onIn: onFocus, onOut: onBlur} = useInteractionState()
const [interactingViaKeypress, setInteractingViaKeypress] = useState(false)
const {
state: volumeHovered,
onIn: onVolumeHover,
onOut: onVolumeEndHover,
} = useInteractionState()
const onKeyDown = useCallback(() => {
setInteractingViaKeypress(true)
}, [])
useEffect(() => {
if (interactingViaKeypress) {
document.addEventListener('click', () => setInteractingViaKeypress(false))
return () => {
document.removeEventListener('click', () =>
setInteractingViaKeypress(false),
)
}
}
}, [interactingViaKeypress])
useEffect(() => {
if (isFullscreen) {
document.documentElement.style.scrollbarGutter = 'unset'
return () => {
document.documentElement.style.removeProperty('scrollbar-gutter')
}
}
}, [isFullscreen])
// pause + unfocus when another video is active
useEffect(() => {
if (!active) {
pause()
setFocused(false)
}
}, [active, pause, setFocused])
// autoplay/pause based on visibility
const isWithinMessage = useIsWithinMessage()
const autoplayDisabled = useAutoplayDisabled() || isWithinMessage
useEffect(() => {
if (active) {
if (onScreen) {
if (!autoplayDisabled) play()
} else {
pause()
}
}
}, [onScreen, pause, active, play, autoplayDisabled])
// use minimal quality when not focused
useEffect(() => {
if (!hlsRef.current) return
if (focused) {
// auto decide quality based on network conditions
hlsRef.current.autoLevelCapping = -1
// allow 30s of buffering
hlsRef.current.config.maxMaxBufferLength = 30
} else {
// back to what we initially set
hlsRef.current.autoLevelCapping = 0
hlsRef.current.config.maxMaxBufferLength = 10
}
}, [hlsRef, focused])
useEffect(() => {
if (!hlsRef.current) return
if (hasSubtitleTrack && subtitlesEnabled && canPlay) {
hlsRef.current.subtitleTrack = 0
} else {
hlsRef.current.subtitleTrack = -1
}
}, [hasSubtitleTrack, subtitlesEnabled, hlsRef, canPlay])
// clicking on any button should focus the player, if it's not already focused
const drawFocus = useCallback(() => {
if (!active) {
setActive()
}
setFocused(true)
}, [active, setActive, setFocused])
const onPressEmptySpace = useCallback(() => {
if (!focused) {
drawFocus()
if (autoplayDisabled) play()
} else {
togglePlayPause()
}
}, [togglePlayPause, drawFocus, focused, autoplayDisabled, play])
const onPressPlayPause = useCallback(() => {
drawFocus()
togglePlayPause()
}, [drawFocus, togglePlayPause])
const onPressSubtitles = useCallback(() => {
drawFocus()
setSubtitlesEnabled(!subtitlesEnabled)
}, [drawFocus, setSubtitlesEnabled, subtitlesEnabled])
const onPressFullscreen = useCallback(() => {
drawFocus()
toggleFullscreen()
}, [drawFocus, toggleFullscreen])
const onSeek = useCallback(
(time: number) => {
if (!videoRef.current) return
if (videoRef.current.fastSeek) {
videoRef.current.fastSeek(time)
} else {
videoRef.current.currentTime = time
}
},
[videoRef],
)
const playStateBeforeSeekRef = useRef(false)
const onSeekStart = useCallback(() => {
drawFocus()
playStateBeforeSeekRef.current = playing
pause()
}, [playing, pause, drawFocus])
const onSeekEnd = useCallback(() => {
if (playStateBeforeSeekRef.current) {
play()
}
}, [play])
const seekLeft = useCallback(() => {
if (!videoRef.current) return
// eslint-disable-next-line @typescript-eslint/no-shadow
const currentTime = videoRef.current.currentTime
// eslint-disable-next-line @typescript-eslint/no-shadow
const duration = videoRef.current.duration || 0
onSeek(clamp(currentTime - 5, 0, duration))
}, [onSeek, videoRef])
const seekRight = useCallback(() => {
if (!videoRef.current) return
// eslint-disable-next-line @typescript-eslint/no-shadow
const currentTime = videoRef.current.currentTime
// eslint-disable-next-line @typescript-eslint/no-shadow
const duration = videoRef.current.duration || 0
onSeek(clamp(currentTime + 5, 0, duration))
}, [onSeek, videoRef])
const [showCursor, setShowCursor] = useState(true)
const cursorTimeoutRef = useRef<ReturnType<typeof setTimeout>>()
const onPointerMoveEmptySpace = useCallback(() => {
setShowCursor(true)
if (cursorTimeoutRef.current) {
clearTimeout(cursorTimeoutRef.current)
}
cursorTimeoutRef.current = setTimeout(() => {
setShowCursor(false)
onEndHover()
}, 2000)
}, [onEndHover])
const onPointerLeaveEmptySpace = useCallback(() => {
setShowCursor(false)
if (cursorTimeoutRef.current) {
clearTimeout(cursorTimeoutRef.current)
}
}, [])
// these are used to trigger the hover state. on mobile, the hover state
// should stick around for a bit after they tap, and if the controls aren't
// present this initial tab should *only* show the controls and not activate anything
const onPointerDown = useCallback(
(evt: React.PointerEvent<HTMLDivElement>) => {
if (evt.pointerType !== 'mouse' && !hovered) {
evt.preventDefault()
}
clearTimeout(timeoutRef.current)
},
[hovered],
)
const timeoutRef = useRef<ReturnType<typeof setTimeout>>()
const onHoverWithTimeout = useCallback(() => {
onHover()
clearTimeout(timeoutRef.current)
}, [onHover])
const onEndHoverWithTimeout = useCallback(
(evt: React.PointerEvent<HTMLDivElement>) => {
// if touch, end after 3s
// if mouse, end immediately
if (evt.pointerType !== 'mouse') {
setTimeout(onEndHover, 3000)
} else {
onEndHover()
}
},
[onEndHover],
)
const showControls =
((focused || autoplayDisabled) && !playing) ||
(interactingViaKeypress ? hasFocus : hovered)
return (
<div
style={{
position: 'absolute',
inset: 0,
overflow: 'hidden',
display: 'flex',
flexDirection: 'column',
}}
onClick={evt => {
evt.stopPropagation()
setInteractingViaKeypress(false)
}}
onPointerEnter={onHoverWithTimeout}
onPointerMove={onHoverWithTimeout}
onPointerLeave={onEndHoverWithTimeout}
onPointerDown={onPointerDown}
onFocus={onFocus}
onBlur={onBlur}
onKeyDown={onKeyDown}>
<Pressable
accessibilityRole="button"
onPointerEnter={onPointerMoveEmptySpace}
onPointerMove={onPointerMoveEmptySpace}
onPointerLeave={onPointerLeaveEmptySpace}
accessibilityHint={_(
!focused
? msg`Unmute video`
: playing
? msg`Pause video`
: msg`Play video`,
)}
style={[
a.flex_1,
web({cursor: showCursor || !playing ? 'pointer' : 'none'}),
]}
onPress={onPressEmptySpace}
/>
{!showControls && !focused && duration > 0 && (
<TimeIndicator time={Math.floor(duration - currentTime)} />
)}
<View
style={[
a.flex_shrink_0,
a.w_full,
a.px_xs,
web({
background:
'linear-gradient(rgba(0, 0, 0, 0), rgba(0, 0, 0, 0.4), rgba(0, 0, 0, 0.7))',
}),
{opacity: showControls ? 1 : 0},
{transition: 'opacity 0.2s ease-in-out'},
]}>
{(!volumeHovered || isTouchDevice) && (
<Scrubber
duration={duration}
currentTime={currentTime}
onSeek={onSeek}
onSeekStart={onSeekStart}
onSeekEnd={onSeekEnd}
seekLeft={seekLeft}
seekRight={seekRight}
togglePlayPause={togglePlayPause}
drawFocus={drawFocus}
/>
)}
<View
style={[
a.flex_1,
a.px_xs,
a.pt_2xs,
a.pb_md,
a.gap_md,
a.flex_row,
a.align_center,
]}>
<ControlButton
active={playing}
activeLabel={_(msg`Pause`)}
inactiveLabel={_(msg`Play`)}
activeIcon={PauseIcon}
inactiveIcon={PlayIcon}
onPress={onPressPlayPause}
/>
<View style={a.flex_1} />
<Text style={{color: t.palette.white, fontVariant: ['tabular-nums']}}>
{formatTime(currentTime)} / {formatTime(duration)}
</Text>
{hasSubtitleTrack && (
<ControlButton
active={subtitlesEnabled}
activeLabel={_(msg`Disable subtitles`)}
inactiveLabel={_(msg`Enable subtitles`)}
activeIcon={CCActiveIcon}
inactiveIcon={CCInactiveIcon}
onPress={onPressSubtitles}
/>
)}
<VolumeControl
muted={muted}
changeMuted={changeMuted}
hovered={volumeHovered}
onHover={onVolumeHover}
onEndHover={onVolumeEndHover}
drawFocus={drawFocus}
/>
{!isIPhoneWeb && (
<ControlButton
active={isFullscreen}
activeLabel={_(msg`Exit fullscreen`)}
inactiveLabel={_(msg`Fullscreen`)}
activeIcon={ArrowsInIcon}
inactiveIcon={ArrowsOutIcon}
onPress={onPressFullscreen}
/>
)}
</View>
</View>
{(buffering || error) && (
<View
pointerEvents="none"
style={[a.absolute, a.inset_0, a.justify_center, a.align_center]}>
{buffering && <Loader fill={t.palette.white} size="lg" />}
{error && (
<Text style={{color: t.palette.white}}>
<Trans>An error occurred</Trans>
</Text>
)}
</View>
)}
</div>
)
}
@@ -0,0 +1,109 @@
import React, {useCallback} from 'react'
import {View} from 'react-native'
import Animated, {FadeIn, FadeOut} from 'react-native-reanimated'
import {msg} from '@lingui/macro'
import {useLingui} from '@lingui/react'
import {isSafari, isTouchDevice} from '#/lib/browser'
import {atoms as a} from '#/alf'
import {Mute_Stroke2_Corner0_Rounded as MuteIcon} from '#/components/icons/Mute'
import {SpeakerVolumeFull_Stroke2_Corner0_Rounded as UnmuteIcon} from '#/components/icons/Speaker'
import {useVideoVolumeState} from '../../VideoVolumeContext'
import {ControlButton} from './ControlButton'
export function VolumeControl({
muted,
changeMuted,
hovered,
onHover,
onEndHover,
drawFocus,
}: {
muted: boolean
changeMuted: (muted: boolean | ((prev: boolean) => boolean)) => void
hovered: boolean
onHover: () => void
onEndHover: () => void
drawFocus: () => void
}) {
const {_} = useLingui()
const [volume, setVolume] = useVideoVolumeState()
const onVolumeChange = useCallback(
(evt: React.ChangeEvent<HTMLInputElement>) => {
drawFocus()
const vol = sliderVolumeToVideoVolume(Number(evt.target.value))
setVolume(vol)
changeMuted(vol === 0)
},
[setVolume, drawFocus, changeMuted],
)
const sliderVolume = muted ? 0 : videoVolumeToSliderVolume(volume)
const isZeroVolume = volume === 0
const onPressMute = useCallback(() => {
drawFocus()
if (isZeroVolume) {
setVolume(1)
changeMuted(false)
} else {
changeMuted(prevMuted => !prevMuted)
}
}, [drawFocus, setVolume, isZeroVolume, changeMuted])
return (
<View
onPointerEnter={onHover}
onPointerLeave={onEndHover}
style={[a.relative]}>
{hovered && !isTouchDevice && (
<Animated.View
entering={FadeIn.duration(100)}
exiting={FadeOut.duration(100)}
style={[a.absolute, a.w_full, {height: 100, bottom: '100%'}]}>
<View
style={[
a.flex_1,
a.mb_xs,
a.px_2xs,
a.py_xs,
{backgroundColor: 'rgba(0, 0, 0, 0.6)'},
a.rounded_xs,
a.align_center,
]}>
<input
type="range"
min={0}
max={100}
value={sliderVolume}
style={
// Ridiculous safari hack for old version of safari. Fixed in sonoma beta -h
isSafari ? {height: 92, minHeight: '100%'} : {height: '100%'}
}
onChange={onVolumeChange}
// @ts-expect-error for old versions of firefox, and then re-using it for targeting the CSS -sfn
orient="vertical"
/>
</View>
</Animated.View>
)}
<ControlButton
active={muted || volume === 0}
activeLabel={_(msg({message: `Unmute`, context: 'video'}))}
inactiveLabel={_(msg({message: `Mute`, context: 'video'}))}
activeIcon={MuteIcon}
inactiveIcon={UnmuteIcon}
onPress={onPressMute}
/>
</View>
)
}
function sliderVolumeToVideoVolume(value: number) {
return Math.pow(value / 100, 4)
}
function videoVolumeToSliderVolume(value: number) {
return Math.round(Math.pow(value, 1 / 4) * 100)
}
@@ -0,0 +1,235 @@
import React, {useCallback, useEffect, useRef, useState} from 'react'
import {isSafari} from '#/lib/browser'
import {useVideoVolumeState} from '../../VideoVolumeContext'
export function useVideoElement(ref: React.RefObject<HTMLVideoElement>) {
const [playing, setPlaying] = useState(false)
const [muted, setMuted] = useState(true)
const [currentTime, setCurrentTime] = useState(0)
const [volume, setVolume] = useVideoVolumeState()
const [duration, setDuration] = useState(0)
const [buffering, setBuffering] = useState(false)
const [error, setError] = useState(false)
const [canPlay, setCanPlay] = useState(false)
const playWhenReadyRef = useRef(false)
useEffect(() => {
if (!ref.current) return
ref.current.volume = volume
}, [ref, volume])
useEffect(() => {
if (!ref.current) return
let bufferingTimeout: ReturnType<typeof setTimeout> | undefined
function round(num: number) {
return Math.round(num * 100) / 100
}
// Initial values
setCurrentTime(round(ref.current.currentTime) || 0)
setDuration(round(ref.current.duration) || 0)
setMuted(ref.current.muted)
setPlaying(!ref.current.paused)
setVolume(ref.current.volume)
const handleTimeUpdate = () => {
if (!ref.current) return
setCurrentTime(round(ref.current.currentTime) || 0)
// HACK: Safari randomly fires `stalled` events when changing between segments
// let's just clear the buffering state if the video is still progressing -sfn
if (isSafari) {
if (bufferingTimeout) clearTimeout(bufferingTimeout)
setBuffering(false)
}
}
const handleDurationChange = () => {
if (!ref.current) return
setDuration(round(ref.current.duration) || 0)
}
const handlePlay = () => {
setPlaying(true)
}
const handlePause = () => {
setPlaying(false)
}
const handleVolumeChange = () => {
if (!ref.current) return
setMuted(ref.current.muted)
}
const handleError = () => {
setError(true)
}
const handleCanPlay = () => {
if (bufferingTimeout) clearTimeout(bufferingTimeout)
setBuffering(false)
setCanPlay(true)
if (!ref.current) return
if (playWhenReadyRef.current) {
ref.current.play()
playWhenReadyRef.current = false
}
}
const handleCanPlayThrough = () => {
if (bufferingTimeout) clearTimeout(bufferingTimeout)
setBuffering(false)
}
const handleWaiting = () => {
if (bufferingTimeout) clearTimeout(bufferingTimeout)
bufferingTimeout = setTimeout(() => {
setBuffering(true)
}, 500) // Delay to avoid frequent buffering state changes
}
const handlePlaying = () => {
if (bufferingTimeout) clearTimeout(bufferingTimeout)
setBuffering(false)
setError(false)
}
const handleStalled = () => {
if (bufferingTimeout) clearTimeout(bufferingTimeout)
bufferingTimeout = setTimeout(() => {
setBuffering(true)
}, 500) // Delay to avoid frequent buffering state changes
}
const handleEnded = () => {
setPlaying(false)
setBuffering(false)
setError(false)
}
const abortController = new AbortController()
ref.current.addEventListener('timeupdate', handleTimeUpdate, {
signal: abortController.signal,
})
ref.current.addEventListener('durationchange', handleDurationChange, {
signal: abortController.signal,
})
ref.current.addEventListener('play', handlePlay, {
signal: abortController.signal,
})
ref.current.addEventListener('pause', handlePause, {
signal: abortController.signal,
})
ref.current.addEventListener('volumechange', handleVolumeChange, {
signal: abortController.signal,
})
ref.current.addEventListener('error', handleError, {
signal: abortController.signal,
})
ref.current.addEventListener('canplay', handleCanPlay, {
signal: abortController.signal,
})
ref.current.addEventListener('canplaythrough', handleCanPlayThrough, {
signal: abortController.signal,
})
ref.current.addEventListener('waiting', handleWaiting, {
signal: abortController.signal,
})
ref.current.addEventListener('playing', handlePlaying, {
signal: abortController.signal,
})
ref.current.addEventListener('stalled', handleStalled, {
signal: abortController.signal,
})
ref.current.addEventListener('ended', handleEnded, {
signal: abortController.signal,
})
ref.current.addEventListener('volumechange', handleVolumeChange, {
signal: abortController.signal,
})
return () => {
abortController.abort()
clearTimeout(bufferingTimeout)
}
}, [ref, setVolume])
const play = useCallback(() => {
if (!ref.current) return
if (ref.current.ended) {
ref.current.currentTime = 0
}
if (ref.current.readyState < HTMLMediaElement.HAVE_FUTURE_DATA) {
playWhenReadyRef.current = true
} else {
const promise = ref.current.play()
if (promise !== undefined) {
promise.catch(err => {
console.error('Error playing video:', err)
})
}
}
}, [ref])
const pause = useCallback(() => {
if (!ref.current) return
ref.current.pause()
playWhenReadyRef.current = false
}, [ref])
const togglePlayPause = useCallback(() => {
if (!ref.current) return
if (ref.current.paused) {
play()
} else {
pause()
}
}, [ref, play, pause])
const changeMuted = useCallback(
(newMuted: boolean | ((prev: boolean) => boolean)) => {
if (!ref.current) return
const value =
typeof newMuted === 'function' ? newMuted(ref.current.muted) : newMuted
ref.current.muted = value
},
[ref],
)
return {
play,
pause,
togglePlayPause,
duration,
currentTime,
playing,
muted,
changeMuted,
buffering,
error,
canPlay,
}
}
export function formatTime(time: number) {
if (isNaN(time)) {
return '--'
}
time = Math.round(time)
const minutes = Math.floor(time / 60)
const seconds = String(time % 60).padStart(2, '0')
return `${minutes}:${seconds}`
}
@@ -1,21 +1,26 @@
import React from 'react'
const Context = React.createContext(
{} as {
muted: boolean
setMuted: (muted: boolean) => void
},
)
const Context = React.createContext<{
// native
muted: boolean
setMuted: React.Dispatch<React.SetStateAction<boolean>>
// web
volume: number
setVolume: React.Dispatch<React.SetStateAction<number>>
} | null>(null)
export function Provider({children}: {children: React.ReactNode}) {
const [muted, setMuted] = React.useState(true)
const [volume, setVolume] = React.useState(1)
const value = React.useMemo(
() => ({
muted,
setMuted,
volume,
setVolume,
}),
[muted, setMuted],
[muted, setMuted, volume, setVolume],
)
return <Context.Provider value={value}>{children}</Context.Provider>
@@ -28,5 +33,15 @@ export function useVideoVolumeState() {
'useVideoVolumeState must be used within a VideoVolumeProvider',
)
}
return context
return [context.volume, context.setVolume] as const
}
export function useVideoMuteState() {
const context = React.useContext(Context)
if (!context) {
throw new Error(
'useVideoMuteState must be used within a VideoVolumeProvider',
)
}
return [context.muted, context.setMuted] as const
}