On-device translation
This commit is contained in:
@@ -164,6 +164,7 @@
|
||||
"expo-splash-screen": "~31.0.12",
|
||||
"expo-system-ui": "~6.0.9",
|
||||
"expo-task-manager": "~14.0.9",
|
||||
"expo-translate-text": "^0.1.0",
|
||||
"expo-updates": "~29.0.14",
|
||||
"expo-video": "~3.0.15",
|
||||
"expo-video-thumbnails": "^10.0.8",
|
||||
|
||||
@@ -667,6 +667,9 @@ export type Events = {
|
||||
targetLanguage: string
|
||||
textLength: number
|
||||
}
|
||||
'translate:result': {
|
||||
method: 'on-device' | 'google-translate' | 'fallback-alert'
|
||||
}
|
||||
|
||||
'verification:create': {}
|
||||
'verification:revoke': {}
|
||||
|
||||
@@ -0,0 +1,77 @@
|
||||
import {View} from 'react-native'
|
||||
import {Trans} from '@lingui/macro'
|
||||
import {useLingui} from '@lingui/react'
|
||||
|
||||
import {codeToLanguageName} from '#/locale/helpers'
|
||||
import {useTranslationState} from '#/state/translation'
|
||||
import {atoms as a, useTheme} from '#/alf'
|
||||
import {Loader} from '#/components/Loader'
|
||||
import {Text} from '#/components/Typography'
|
||||
|
||||
export function TranslatedPost({
|
||||
postUri,
|
||||
hideLoading,
|
||||
}: {
|
||||
postUri: string
|
||||
hideLoading?: boolean
|
||||
}) {
|
||||
const state = useTranslationState(postUri)
|
||||
|
||||
if (state.status === 'loading' && !hideLoading) {
|
||||
return <TranslationLoading />
|
||||
}
|
||||
|
||||
if (state.status === 'success') {
|
||||
return (
|
||||
<TranslationResult
|
||||
translatedText={state.translatedText}
|
||||
sourceLanguage={state.sourceLanguage}
|
||||
/>
|
||||
)
|
||||
}
|
||||
|
||||
return null
|
||||
}
|
||||
|
||||
function TranslationLoading() {
|
||||
const t = useTheme()
|
||||
|
||||
return (
|
||||
<View style={[a.flex_row, a.align_center, a.gap_sm, a.py_xs]}>
|
||||
<Loader size="sm" />
|
||||
<Text style={[a.text_sm, t.atoms.text_contrast_medium]}>
|
||||
<Trans>Translating...</Trans>
|
||||
</Text>
|
||||
</View>
|
||||
)
|
||||
}
|
||||
|
||||
function TranslationResult({
|
||||
translatedText,
|
||||
sourceLanguage,
|
||||
}: {
|
||||
translatedText: string
|
||||
sourceLanguage: string
|
||||
}) {
|
||||
const t = useTheme()
|
||||
const {i18n} = useLingui()
|
||||
|
||||
const langName = sourceLanguage
|
||||
? codeToLanguageName(sourceLanguage, i18n.locale)
|
||||
: undefined
|
||||
|
||||
return (
|
||||
<View style={[a.py_xs, a.gap_xs]}>
|
||||
<Text style={[a.text_xs, t.atoms.text_contrast_medium]}>
|
||||
{langName ? (
|
||||
<Trans>Translated from {langName}</Trans>
|
||||
) : (
|
||||
<Trans>Translated</Trans>
|
||||
)}
|
||||
</Text>
|
||||
<Text selectable style={[a.text_md]}>
|
||||
{translatedText}
|
||||
</Text>
|
||||
</View>
|
||||
)
|
||||
}
|
||||
@@ -258,7 +258,7 @@ let PostMenuItems = ({
|
||||
}
|
||||
|
||||
const onPressTranslate = () => {
|
||||
translate(record.text, langPrefs.primaryLanguage)
|
||||
translate(record.text, langPrefs.primaryLanguage, {postUri: post.uri})
|
||||
|
||||
if (
|
||||
bsky.dangerousIsType<AppBskyFeedPost.Record>(
|
||||
|
||||
@@ -1,54 +1,109 @@
|
||||
import {useCallback} from 'react'
|
||||
import * as IntentLauncher from 'expo-intent-launcher'
|
||||
import {Alert} from 'react-native'
|
||||
import {msg} from '@lingui/macro'
|
||||
import {useLingui} from '@lingui/react'
|
||||
|
||||
import {getTranslatorLink} from '#/locale/helpers'
|
||||
import {IS_ANDROID} from '#/env'
|
||||
import {logger} from '#/logger'
|
||||
import {setTranslationState} from '#/state/translation'
|
||||
import {useAnalytics} from '#/analytics'
|
||||
import {useOpenLink} from './useOpenLink'
|
||||
|
||||
/**
|
||||
* Attempts on-device translation via expo-translate-text.
|
||||
* Uses a lazy require to avoid crashing if the native module
|
||||
* isn't linked into the current build.
|
||||
*/
|
||||
async function attemptTranslation(
|
||||
text: string,
|
||||
language: string,
|
||||
postUri: string,
|
||||
): Promise<void> {
|
||||
const {onTranslateTask} =
|
||||
require('expo-translate-text') as typeof import('expo-translate-text')
|
||||
const result = await onTranslateTask({
|
||||
input: text,
|
||||
targetLangCode: language,
|
||||
})
|
||||
|
||||
setTranslationState(postUri, {
|
||||
status: 'success',
|
||||
translatedText:
|
||||
typeof result.translatedTexts === 'string' ? result.translatedTexts : '',
|
||||
sourceLanguage: result.sourceLanguage ?? '',
|
||||
})
|
||||
}
|
||||
|
||||
/**
|
||||
* Native translation hook. Attempts on-device translation using
|
||||
* Apple Translation (iOS 18+) or Google ML Kit (Android).
|
||||
* Falls back to Google Translate URL if the language pack is
|
||||
* unavailable or the user declines to download it.
|
||||
*
|
||||
* Web uses useTranslate.web.ts which always opens Google Translate.
|
||||
*/
|
||||
export function useTranslate() {
|
||||
const openLink = useOpenLink()
|
||||
const {_} = useLingui()
|
||||
const ax = useAnalytics()
|
||||
|
||||
return useCallback(
|
||||
async (text: string, language: string) => {
|
||||
const translateUrl = getTranslatorLink(text, language)
|
||||
if (IS_ANDROID) {
|
||||
try {
|
||||
// use getApplicationIconAsync to determine if the translate app is installed
|
||||
if (
|
||||
!(await IntentLauncher.getApplicationIconAsync(
|
||||
'com.google.android.apps.translate',
|
||||
))
|
||||
) {
|
||||
throw new Error('Translate app not installed')
|
||||
}
|
||||
|
||||
// TODO: this should only be called one at a time, use something like
|
||||
// RQ's `scope` - otherwise can trigger the browser to open unexpectedly when the call throws -sfn
|
||||
await IntentLauncher.startActivityAsync(
|
||||
'android.intent.action.PROCESS_TEXT',
|
||||
{
|
||||
type: 'text/plain',
|
||||
extra: {
|
||||
'android.intent.extra.PROCESS_TEXT': text,
|
||||
'android.intent.extra.PROCESS_TEXT_READONLY': true,
|
||||
},
|
||||
// note: to skip the intermediate app select, we need to specify a
|
||||
// `className`. however, this isn't safe to hardcode, we'd need to query the
|
||||
// package manager for the correct activity. this requires native code, so
|
||||
// skip for now -sfn
|
||||
// packageName: 'com.google.android.apps.translate',
|
||||
// className: 'com.google.android.apps.translate.TranslateActivity',
|
||||
},
|
||||
)
|
||||
} catch (err) {
|
||||
if (__DEV__) console.error(err)
|
||||
// most likely means they don't have the translate app
|
||||
await openLink(translateUrl)
|
||||
}
|
||||
} else {
|
||||
async (text: string, language: string, opts?: {postUri?: string}) => {
|
||||
// No postUri means non-post context (e.g. DMs) — open Google Translate
|
||||
if (!opts?.postUri) {
|
||||
const translateUrl = getTranslatorLink(text, language)
|
||||
await openLink(translateUrl)
|
||||
return
|
||||
}
|
||||
|
||||
const postUri = opts.postUri
|
||||
setTranslationState(postUri, {status: 'loading'})
|
||||
|
||||
try {
|
||||
await attemptTranslation(text, language, postUri)
|
||||
ax.metric('translate:result', {method: 'on-device'})
|
||||
} catch (err) {
|
||||
logger.error('Failed to translate post', {safeMessage: err})
|
||||
setTranslationState(postUri, {status: 'idle'})
|
||||
|
||||
// On-device translation failed (language pack missing or user
|
||||
// dismissed the download prompt). Show options to retry or
|
||||
// fall back to Google Translate.
|
||||
ax.metric('translate:result', {method: 'fallback-alert'})
|
||||
Alert.alert(
|
||||
_(msg`Translation unavailable`),
|
||||
_(msg`The required language pack is not installed on your device.`),
|
||||
[
|
||||
{
|
||||
text: _(msg`Download language pack`),
|
||||
onPress: async () => {
|
||||
setTranslationState(postUri, {status: 'loading'})
|
||||
try {
|
||||
await attemptTranslation(text, language, postUri)
|
||||
ax.metric('translate:result', {method: 'on-device'})
|
||||
} catch (retryErr) {
|
||||
logger.error('Failed to translate post', {
|
||||
safeMessage: retryErr,
|
||||
})
|
||||
setTranslationState(postUri, {status: 'idle'})
|
||||
}
|
||||
},
|
||||
},
|
||||
{
|
||||
text: _(msg`Use Google Translate`),
|
||||
onPress: () => {
|
||||
ax.metric('translate:result', {method: 'google-translate'})
|
||||
openLink(getTranslatorLink(text, language))
|
||||
},
|
||||
},
|
||||
{
|
||||
text: _(msg`Cancel`),
|
||||
style: 'cancel',
|
||||
},
|
||||
],
|
||||
)
|
||||
}
|
||||
},
|
||||
[openLink],
|
||||
[_, ax, openLink],
|
||||
)
|
||||
}
|
||||
|
||||
@@ -0,0 +1,16 @@
|
||||
import {useCallback} from 'react'
|
||||
|
||||
import {getTranslatorLink} from '#/locale/helpers'
|
||||
import {useOpenLink} from './useOpenLink'
|
||||
|
||||
export function useTranslate() {
|
||||
const openLink = useOpenLink()
|
||||
|
||||
return useCallback(
|
||||
async (text: string, language: string, _opts?: {postUri?: string}) => {
|
||||
const translateUrl = getTranslatorLink(text, language)
|
||||
await openLink(translateUrl)
|
||||
},
|
||||
[openLink],
|
||||
)
|
||||
}
|
||||
@@ -30,6 +30,7 @@ import {type ThreadItem} from '#/state/queries/usePostThread/types'
|
||||
import {useSession} from '#/state/session'
|
||||
import {type OnPostSuccessData} from '#/state/shell/composer'
|
||||
import {useMergedThreadgateHiddenReplies} from '#/state/threadgate-hidden-replies'
|
||||
import {clearTranslation, useTranslationState} from '#/state/translation'
|
||||
import {type PostSource} from '#/state/unstable-post-source'
|
||||
import {PreviewableUserAvatar} from '#/view/com/util/UserAvatar'
|
||||
import {ThreadItemAnchorFollowButton} from '#/screens/PostThread/components/ThreadItemAnchorFollowButton'
|
||||
@@ -45,11 +46,13 @@ import {DebugFieldDisplay} from '#/components/DebugFieldDisplay'
|
||||
import {CalendarClock_Stroke2_Corner0_Rounded as CalendarClockIcon} from '#/components/icons/CalendarClock'
|
||||
import {Trash_Stroke2_Corner0_Rounded as TrashIcon} from '#/components/icons/Trash'
|
||||
import {InlineLinkText, Link} from '#/components/Link'
|
||||
import {Loader} from '#/components/Loader'
|
||||
import {ContentHider} from '#/components/moderation/ContentHider'
|
||||
import {LabelsOnMyPost} from '#/components/moderation/LabelsOnMe'
|
||||
import {PostAlerts} from '#/components/moderation/PostAlerts'
|
||||
import {type AppModerationCause} from '#/components/Pills'
|
||||
import {Embed, PostEmbedViewContext} from '#/components/Post/Embed'
|
||||
import {TranslatedPost} from '#/components/Post/TranslatedPost'
|
||||
import {PostControls, PostControlsSkeleton} from '#/components/PostControls'
|
||||
import {useFormatPostStatCount} from '#/components/PostControls/util'
|
||||
import {ProfileHoverCard} from '#/components/ProfileHoverCard'
|
||||
@@ -412,6 +415,7 @@ const ThreadItemAnchorInner = memo(function ThreadItemAnchorInner({
|
||||
shouldProxyLinks={true}
|
||||
/>
|
||||
) : undefined}
|
||||
<TranslatedPost postUri={post.uri} hideLoading />
|
||||
{post.embed && (
|
||||
<View style={[a.py_xs]}>
|
||||
<Embed
|
||||
@@ -558,10 +562,14 @@ function ExpandedPostDetails({
|
||||
[post, langPrefs.primaryLanguage],
|
||||
)
|
||||
|
||||
const translationState = useTranslationState(post.uri)
|
||||
|
||||
const onTranslatePress = useCallback(
|
||||
(e: GestureResponderEvent) => {
|
||||
e.preventDefault()
|
||||
translate(post.record.text || '', langPrefs.primaryLanguage)
|
||||
translate(post.record.text || '', langPrefs.primaryLanguage, {
|
||||
postUri: post.uri,
|
||||
})
|
||||
|
||||
if (
|
||||
bsky.dangerousIsType<AppBskyFeedPost.Record>(
|
||||
@@ -581,6 +589,15 @@ function ExpandedPostDetails({
|
||||
[ax, translate, langPrefs, post],
|
||||
)
|
||||
|
||||
const onHideTranslation = useCallback(
|
||||
(e: GestureResponderEvent) => {
|
||||
e.preventDefault()
|
||||
clearTranslation(post.uri)
|
||||
return false
|
||||
},
|
||||
[post.uri],
|
||||
)
|
||||
|
||||
return (
|
||||
<View style={[a.gap_md, a.pt_md, a.align_start]}>
|
||||
<BackdatedPostIndicator post={post} />
|
||||
@@ -597,18 +614,33 @@ function ExpandedPostDetails({
|
||||
·
|
||||
</Text>
|
||||
|
||||
<InlineLinkText
|
||||
// overridden to open an intent on android, but keep
|
||||
// as anchor tag for accessibility
|
||||
to={getTranslatorLink(
|
||||
post.record.text,
|
||||
langPrefs.primaryLanguage,
|
||||
)}
|
||||
label={_(msg`Translate`)}
|
||||
style={[a.text_sm]}
|
||||
onPress={onTranslatePress}>
|
||||
<Trans>Translate</Trans>
|
||||
</InlineLinkText>
|
||||
{translationState.status === 'loading' ? (
|
||||
<View style={[a.flex_row, a.align_center, a.gap_xs]}>
|
||||
<Loader size="xs" />
|
||||
<Text style={[a.text_sm, t.atoms.text_contrast_medium]}>
|
||||
<Trans>Translating...</Trans>
|
||||
</Text>
|
||||
</View>
|
||||
) : translationState.status === 'success' ? (
|
||||
<InlineLinkText
|
||||
to="#"
|
||||
label={_(msg`Hide translation`)}
|
||||
style={[a.text_sm]}
|
||||
onPress={onHideTranslation}>
|
||||
<Trans>Hide translation</Trans>
|
||||
</InlineLinkText>
|
||||
) : (
|
||||
<InlineLinkText
|
||||
to={getTranslatorLink(
|
||||
post.record.text,
|
||||
langPrefs.primaryLanguage,
|
||||
)}
|
||||
label={_(msg`Translate`)}
|
||||
style={[a.text_sm]}
|
||||
onPress={onTranslatePress}>
|
||||
<Trans>Translate</Trans>
|
||||
</InlineLinkText>
|
||||
)}
|
||||
</>
|
||||
)}
|
||||
</View>
|
||||
|
||||
@@ -0,0 +1,45 @@
|
||||
/**
|
||||
* Per-post translation state store. Uses the EventEmitter + Map pattern
|
||||
* (same as post-shadow.ts) so only components subscribed to a specific
|
||||
* post URI re-render when its translation state changes.
|
||||
*/
|
||||
import {useEffect, useState} from 'react'
|
||||
import EventEmitter from 'eventemitter3'
|
||||
|
||||
export type TranslationState =
|
||||
| {status: 'idle'}
|
||||
| {status: 'loading'}
|
||||
| {status: 'success'; translatedText: string; sourceLanguage: string}
|
||||
|
||||
const IDLE: TranslationState = {status: 'idle'}
|
||||
|
||||
const emitter = new EventEmitter()
|
||||
const translations = new Map<string, TranslationState>()
|
||||
|
||||
export function setTranslationState(postUri: string, state: TranslationState) {
|
||||
translations.set(postUri, state)
|
||||
emitter.emit(postUri)
|
||||
}
|
||||
|
||||
export function clearTranslation(postUri: string) {
|
||||
translations.delete(postUri)
|
||||
emitter.emit(postUri)
|
||||
}
|
||||
|
||||
export function useTranslationState(postUri: string): TranslationState {
|
||||
const [state, setState] = useState<TranslationState>(
|
||||
() => translations.get(postUri) ?? IDLE,
|
||||
)
|
||||
|
||||
useEffect(() => {
|
||||
function onUpdate() {
|
||||
setState(translations.get(postUri) ?? IDLE)
|
||||
}
|
||||
emitter.addListener(postUri, onUpdate)
|
||||
return () => {
|
||||
emitter.removeListener(postUri, onUpdate)
|
||||
}
|
||||
}, [postUri])
|
||||
|
||||
return state
|
||||
}
|
||||
@@ -33,6 +33,7 @@ import {PostAlerts} from '#/components/moderation/PostAlerts'
|
||||
import {Embed, PostEmbedViewContext} from '#/components/Post/Embed'
|
||||
import {PostRepliedTo} from '#/components/Post/PostRepliedTo'
|
||||
import {ShowMoreTextButton} from '#/components/Post/ShowMoreTextButton'
|
||||
import {TranslatedPost} from '#/components/Post/TranslatedPost'
|
||||
import {PostControls} from '#/components/PostControls'
|
||||
import {RichText} from '#/components/RichText'
|
||||
import {SubtleHover} from '#/components/SubtleHover'
|
||||
@@ -217,6 +218,7 @@ function PostInner({
|
||||
)}
|
||||
</View>
|
||||
) : undefined}
|
||||
<TranslatedPost postUri={post.uri} />
|
||||
{post.embed ? (
|
||||
<Embed
|
||||
embed={post.embed}
|
||||
|
||||
@@ -43,6 +43,7 @@ import {Embed} from '#/components/Post/Embed'
|
||||
import {PostEmbedViewContext} from '#/components/Post/Embed/types'
|
||||
import {PostRepliedTo} from '#/components/Post/PostRepliedTo'
|
||||
import {ShowMoreTextButton} from '#/components/Post/ShowMoreTextButton'
|
||||
import {TranslatedPost} from '#/components/Post/TranslatedPost'
|
||||
import {PostControls} from '#/components/PostControls'
|
||||
import {DiscoverDebug} from '#/components/PostControls/DiscoverDebug'
|
||||
import {RichText} from '#/components/RichText'
|
||||
@@ -481,6 +482,7 @@ let PostContent = ({
|
||||
)}
|
||||
</View>
|
||||
) : undefined}
|
||||
<TranslatedPost postUri={post.uri} />
|
||||
{postEmbed ? (
|
||||
<View style={[a.pb_xs]}>
|
||||
<Embed
|
||||
|
||||
@@ -3403,7 +3403,7 @@
|
||||
"@babel/parser" "^7.27.2"
|
||||
"@babel/types" "^7.27.1"
|
||||
|
||||
"@babel/traverse--for-generate-function-map@npm:@babel/traverse@^7.25.3":
|
||||
"@babel/traverse--for-generate-function-map@npm:@babel/traverse@^7.25.3", "@babel/traverse@^7.25.3", "@babel/traverse@^7.25.9":
|
||||
version "7.25.9"
|
||||
resolved "https://registry.yarnpkg.com/@babel/traverse/-/traverse-7.25.9.tgz#a50f8fe49e7f69f53de5bea7e413cd35c5e13c84"
|
||||
integrity sha512-ZCuvfwOwlz/bawvAuvcj8rrithP2/N55Tzz342AkTvq4qaWbGfmCk/tKhNaV2cthijKrPAA8SRJV5WWe7IBMJw==
|
||||
@@ -3464,19 +3464,6 @@
|
||||
debug "^4.3.1"
|
||||
globals "^11.1.0"
|
||||
|
||||
"@babel/traverse@^7.25.3", "@babel/traverse@^7.25.9":
|
||||
version "7.25.9"
|
||||
resolved "https://registry.yarnpkg.com/@babel/traverse/-/traverse-7.25.9.tgz#a50f8fe49e7f69f53de5bea7e413cd35c5e13c84"
|
||||
integrity sha512-ZCuvfwOwlz/bawvAuvcj8rrithP2/N55Tzz342AkTvq4qaWbGfmCk/tKhNaV2cthijKrPAA8SRJV5WWe7IBMJw==
|
||||
dependencies:
|
||||
"@babel/code-frame" "^7.25.9"
|
||||
"@babel/generator" "^7.25.9"
|
||||
"@babel/parser" "^7.25.9"
|
||||
"@babel/template" "^7.25.9"
|
||||
"@babel/types" "^7.25.9"
|
||||
debug "^4.3.1"
|
||||
globals "^11.1.0"
|
||||
|
||||
"@babel/traverse@^7.26.10":
|
||||
version "7.26.10"
|
||||
resolved "https://registry.yarnpkg.com/@babel/traverse/-/traverse-7.26.10.tgz#43cca33d76005dbaa93024fae536cc1946a4c380"
|
||||
@@ -11777,6 +11764,11 @@ expo-task-manager@~14.0.9:
|
||||
dependencies:
|
||||
unimodules-app-loader "~6.0.8"
|
||||
|
||||
expo-translate-text@^0.1.0:
|
||||
version "0.1.0"
|
||||
resolved "https://registry.yarnpkg.com/expo-translate-text/-/expo-translate-text-0.1.0.tgz#16904f1baac126d5a452b4196495d238dcbbb6af"
|
||||
integrity sha512-zrRJ0Do6Gie12yZYzgD4McQNIvb54pfPsBRegkUSuADec9DrXdxJXvw/OieZ+L4oMQu0Fx/NJDnHGQoaa0CVgA==
|
||||
|
||||
expo-updates-interface@~2.0.0:
|
||||
version "2.0.0"
|
||||
resolved "https://registry.yarnpkg.com/expo-updates-interface/-/expo-updates-interface-2.0.0.tgz#7721cb64c37bcb46b23827b2717ef451a9378749"
|
||||
@@ -18691,16 +18683,7 @@ string-natural-compare@^3.0.1:
|
||||
resolved "https://registry.yarnpkg.com/string-natural-compare/-/string-natural-compare-3.0.1.tgz#7a42d58474454963759e8e8b7ae63d71c1e7fdf4"
|
||||
integrity sha512-n3sPwynL1nwKi3WJ6AIsClwBMa0zTi54fn2oLU6ndfTSIO05xaznjSf15PcBZU6FNWbmN5Q6cxT4V5hGvB4taw==
|
||||
|
||||
"string-width-cjs@npm:string-width@^4.2.0":
|
||||
version "4.2.3"
|
||||
resolved "https://registry.yarnpkg.com/string-width/-/string-width-4.2.3.tgz#269c7117d27b05ad2e536830a8ec895ef9c6d010"
|
||||
integrity sha512-wKyQRQpjJ0sIp62ErSZdGsjMJWsap5oRNihHhu6G7JVO/9jIB6UyevL+tXuOqrng8j/cxKTWyWUwvSTriiZz/g==
|
||||
dependencies:
|
||||
emoji-regex "^8.0.0"
|
||||
is-fullwidth-code-point "^3.0.0"
|
||||
strip-ansi "^6.0.1"
|
||||
|
||||
string-width@^4.1.0, string-width@^4.2.0, string-width@^4.2.3:
|
||||
"string-width-cjs@npm:string-width@^4.2.0", string-width@^4.1.0, string-width@^4.2.0, string-width@^4.2.3:
|
||||
version "4.2.3"
|
||||
resolved "https://registry.yarnpkg.com/string-width/-/string-width-4.2.3.tgz#269c7117d27b05ad2e536830a8ec895ef9c6d010"
|
||||
integrity sha512-wKyQRQpjJ0sIp62ErSZdGsjMJWsap5oRNihHhu6G7JVO/9jIB6UyevL+tXuOqrng8j/cxKTWyWUwvSTriiZz/g==
|
||||
@@ -18859,7 +18842,7 @@ string_decoder@~1.1.1:
|
||||
dependencies:
|
||||
safe-buffer "~5.1.0"
|
||||
|
||||
"strip-ansi-cjs@npm:strip-ansi@^6.0.1":
|
||||
"strip-ansi-cjs@npm:strip-ansi@^6.0.1", strip-ansi@^6.0.0, strip-ansi@^6.0.1:
|
||||
version "6.0.1"
|
||||
resolved "https://registry.yarnpkg.com/strip-ansi/-/strip-ansi-6.0.1.tgz#9e26c63d30f53443e9489495b2105d37b67a85d9"
|
||||
integrity sha512-Y38VPSHcqkFrCpFnQ9vuSXmquuv5oXOKpGeT6aGrr3o3Gc9AlVa6JBfUSOCnbxGGZF+/0ooI7KrPuUSztUdU5A==
|
||||
@@ -18873,13 +18856,6 @@ strip-ansi@^5.2.0:
|
||||
dependencies:
|
||||
ansi-regex "^4.1.0"
|
||||
|
||||
strip-ansi@^6.0.0, strip-ansi@^6.0.1:
|
||||
version "6.0.1"
|
||||
resolved "https://registry.yarnpkg.com/strip-ansi/-/strip-ansi-6.0.1.tgz#9e26c63d30f53443e9489495b2105d37b67a85d9"
|
||||
integrity sha512-Y38VPSHcqkFrCpFnQ9vuSXmquuv5oXOKpGeT6aGrr3o3Gc9AlVa6JBfUSOCnbxGGZF+/0ooI7KrPuUSztUdU5A==
|
||||
dependencies:
|
||||
ansi-regex "^5.0.1"
|
||||
|
||||
strip-ansi@^7.0.1:
|
||||
version "7.1.0"
|
||||
resolved "https://registry.yarnpkg.com/strip-ansi/-/strip-ansi-7.1.0.tgz#d5b6568ca689d8561370b0707685d22434faff45"
|
||||
@@ -20276,7 +20252,7 @@ wordwrap@^1.0.0:
|
||||
resolved "https://registry.yarnpkg.com/wordwrap/-/wordwrap-1.0.0.tgz#27584810891456a4171c8d0226441ade90cbcaeb"
|
||||
integrity sha512-gvVzJFlPycKc5dZN4yPkP8w7Dc37BtP1yczEneOb4uq34pXZcvrtRTmWV8W+Ume+XCxKgbjM+nevkyFPMybd4Q==
|
||||
|
||||
"wrap-ansi-cjs@npm:wrap-ansi@^7.0.0":
|
||||
"wrap-ansi-cjs@npm:wrap-ansi@^7.0.0", wrap-ansi@^7.0.0:
|
||||
version "7.0.0"
|
||||
resolved "https://registry.yarnpkg.com/wrap-ansi/-/wrap-ansi-7.0.0.tgz#67e145cff510a6a6984bdf1152911d69d2eb9e43"
|
||||
integrity sha512-YVGIj2kamLSTxw6NsZjoBxfSwsn0ycdesmc4p+Q21c5zPuZ1pl+NfxVdxPtdHvmNVOQ6XSYG4AUtyt/Fi7D16Q==
|
||||
@@ -20294,15 +20270,6 @@ wrap-ansi@^6.2.0:
|
||||
string-width "^4.1.0"
|
||||
strip-ansi "^6.0.0"
|
||||
|
||||
wrap-ansi@^7.0.0:
|
||||
version "7.0.0"
|
||||
resolved "https://registry.yarnpkg.com/wrap-ansi/-/wrap-ansi-7.0.0.tgz#67e145cff510a6a6984bdf1152911d69d2eb9e43"
|
||||
integrity sha512-YVGIj2kamLSTxw6NsZjoBxfSwsn0ycdesmc4p+Q21c5zPuZ1pl+NfxVdxPtdHvmNVOQ6XSYG4AUtyt/Fi7D16Q==
|
||||
dependencies:
|
||||
ansi-styles "^4.0.0"
|
||||
string-width "^4.1.0"
|
||||
strip-ansi "^6.0.0"
|
||||
|
||||
wrap-ansi@^8.0.1, wrap-ansi@^8.1.0:
|
||||
version "8.1.0"
|
||||
resolved "https://registry.yarnpkg.com/wrap-ansi/-/wrap-ansi-8.1.0.tgz#56dc22368ee570face1b49819975d9b9a5ead214"
|
||||
|
||||
Reference in New Issue
Block a user