1
0
mirror of https://github.com/tooot-app/app synced 2025-06-05 22:19:13 +02:00

Merge branch 'main' into release

This commit is contained in:
xmflsct
2022-12-20 00:46:57 +01:00
190 changed files with 2418 additions and 1274 deletions

View File

@ -0,0 +1 @@
../../it/description.txt

View File

@ -0,0 +1 @@
../../it/subtitle.txt

View File

@ -1,5 +1,10 @@
tooot is an open source, simple yet elegant Mastodon mobile client.
tooot is an open source, simple yet elegant Mastodon mobile client. A Mastodon (https://joinmastodon.org/) account is required to use this app.
A Mastodon (https://joinmastodon.org/) account is required to use this app.
tooot supports:
- Cross platform, including iPadOS and MacOS
- Multiple accounts
- Dark mode or adapt to system
- Adjustable toot font size
- Push notification
If you have suggestions, please reach out to @tooot@xmflsct.com or support@tooot.ap.
If you have suggestions, please reach out to @tooot@xmflsct.com or support@tooot.app.

View File

@ -1,10 +1,5 @@
Enjoy toooting! This version includes following improvements and fixes:
- Added Ukrainian (Slava Ukraini)
- Automatic setting detected language when tooting
- Remember public timeline type selection
- Show diffing of edit history
- Allow hiding boosts and replies in home timeline
- Support toot in RTL languages
- Added notification for admins
- Fix whole word filter matching
- Fix tablet cannot delete toot drafts
- Align filter experience with v4.0 and above
- Supports enlarging user's avatar and banner
- Fix iPad weird sizing (not optimisation)
- Experiment (!) support of Pleroma

View File

@ -0,0 +1,10 @@
tooot è un client Mastodon semplice e open source. Per utilizzare questo client, devi disporre di un account Mastodon. (https://joinmastodon.org/).
Tooot supporta:
- Multipiattaforma, inclusi iPadOS e MacOS
- Accesso a più account
- Modalità scura o adattiva
- Dimensione del carattere del testo regolabile
- Notifiche push e altre funzioni
Per suggerimenti o commenti sull'utilizzo, contattare @tooot@xmflsct.com o support@tooot.app.

View File

@ -0,0 +1 @@
Client open source per Mastodon

View File

@ -1,11 +1,10 @@
tooot是一个专门为中文用户社区所打造的开源、简洁长毛象客户端。使用此客户端需要已经拥有一个长毛象https://joinmastodon.org/)账号。
tooot起始于专注中文社区的简洁、开源长毛象手机客户端。使用此客户端需要已经拥有一个长毛象https://joinmastodon.org/)账号。
tooot支持
- iPad
- 跨平台及iPadOS、MacOS
- 多账号登录
- 黑暗或自适应模式
- 可调正文字体大小
- 可调正文字体尺寸
- 消息推送
等功能。
如有使用建议或意见,请联系@tooot@xmflsct.com或者support@tooot.app。

View File

@ -1,10 +1,5 @@
toooting愉快此版本包括以下改进和修复
- 增加乌克兰语Slava Ukraini
- 自动识别发嘟语言
- 记住上次公共时间轴选项
- 显示编辑历史的差异
- 关注列表可隐藏转嘟和回复
- 新增管理员推送通知
- 支持嘟文右到左文字
- 修复过滤整词功能
- 修复平板不能删除草稿
- 改进过滤体验与v4.0以上版本一致
- 支持查看用户的头像和横幅图片
- 修复iPad部分尺寸问题非优化
- 试验性支持Pleroma

View File

@ -1,6 +1,6 @@
{
"name": "tooot",
"version": "4.7.0",
"version": "4.7.1",
"description": "tooot for Mastodon",
"author": "xmflsct <me@xmflsct.com>",
"license": "GPL-3.0-or-later",

View File

@ -263,7 +263,8 @@ declare namespace Mastodon {
verified_at: string | null
}
type Filter = {
type Filter<T extends 'v1' | 'v2'> = T extends 'v2' ? Filter_V2 : Filter_V1
type Filter_V1 = {
id: string
phrase: string
context: ('home' | 'notifications' | 'public' | 'thread' | 'account')[]
@ -271,6 +272,25 @@ declare namespace Mastodon {
irreversible: boolean
whole_word: boolean
}
type Filter_V2 = {
id: string
title: string
context: ('home' | 'notifications' | 'public' | 'thread' | 'account')[]
expires_at?: string
filter_action: 'warn' | 'hide'
keywords: FilterKeyword[]
statuses: FilterStatus[]
}
type FilterKeyword = { id: string; keyword: string; whole_word: boolean }
type FilterStatus = { id: string; status_id: string }
type FilterResult = {
filter: Filter<'v2'>
keyword_matches?: FilterKeyword['keyword'][]
status_matches?: FilterStatus['id'][]
}
type List = {
id: string
@ -406,6 +426,8 @@ declare namespace Mastodon {
id: string
following: boolean
showing_reblogs: boolean
notifying?: boolean
languages?: string[]
followed_by: boolean
blocking: boolean
blocked_by: boolean
@ -459,7 +481,7 @@ declare namespace Mastodon {
sensitive: boolean
spoiler_text?: string
media_attachments: Attachment[]
application: Application
application?: Application
// Attributes
mentions: Mention[]
@ -470,7 +492,7 @@ declare namespace Mastodon {
reblogs_count: number
favourites_count: number
replies_count: number
edited_at?: string // FEATURE edit_post
edited_at?: string
favourited: boolean
reblogged: boolean
muted: boolean
@ -486,6 +508,7 @@ declare namespace Mastodon {
card?: Card
language?: string
text?: string
filtered?: FilterResult[]
}
type StatusHistory = {

View File

@ -56,7 +56,7 @@ const Screens: React.FC<Props> = ({ localCorrupt }) => {
useEffect(() => {
const screenshotListener = addScreenshotListener(() =>
Alert.alert(t('screenshot.title'), t('screenshot.message'), [
{ text: t('screenshot.button'), style: 'destructive' }
{ text: t('common:buttons.confirm'), style: 'destructive' }
])
)
Platform.select({ ios: screenshotListener })

View File

@ -53,7 +53,7 @@ const ComponentHashtag: React.FC<PropsWithChildren & Props> = ({
#{hashtag.name}
</CustomText>
<View
style={{ flexDirection: 'row', alignItems: 'center' }}
style={{ flexDirection: 'row', alignItems: 'center', alignSelf: 'stretch' }}
onLayout={({
nativeEvent: {
layout: { height }
@ -61,7 +61,7 @@ const ComponentHashtag: React.FC<PropsWithChildren & Props> = ({
}) => setHeight(height)}
>
<Sparkline
data={hashtag.history.map(h => parseInt(h.uses)).reverse()}
data={hashtag.history?.map(h => parseInt(h.uses)).reverse()}
width={width}
height={height}
margin={children ? StyleConstants.Spacing.S : undefined}

View File

@ -15,6 +15,7 @@ import { Alert, Image, KeyboardAvoidingView, Platform, TextInput, View } from 'r
import { ScrollView } from 'react-native-gesture-handler'
import { useSelector } from 'react-redux'
import { Placeholder } from 'rn-placeholder'
import validUrl from 'valid-url'
import InstanceInfo from './Info'
import CustomText from '../Text'
import { useNavigation } from '@react-navigation/native'
@ -39,12 +40,26 @@ const ComponentInstance: React.FC<Props> = ({
const navigation = useNavigation<TabMeStackNavigationProp<'Tab-Me-Root' | 'Tab-Me-Switch'>>()
const [domain, setDomain] = useState<string>('')
const [errorCode, setErrorCode] = useState<number | null>(null)
const whitelisted: boolean =
!!domain.length &&
!!errorCode &&
!!validUrl.isHttpsUri(`https://${domain}`) &&
errorCode === 401
const dispatch = useAppDispatch()
const instances = useSelector(getInstances, () => true)
const instanceQuery = useInstanceQuery({
domain,
options: { enabled: !!domain, retry: false }
options: {
enabled: !!domain,
retry: false,
onError: err => {
if (err.status) {
setErrorCode(err.status)
}
}
}
})
const deprecateAuthFollow = useSelector(checkInstanceFeature('deprecate_auth_follow'))
@ -146,7 +161,11 @@ const ComponentInstance: React.FC<Props> = ({
borderBottomWidth: 1,
...StyleConstants.FontStyle.M,
color: colors.primaryDefault,
borderBottomColor: instanceQuery.isError ? colors.red : colors.border,
borderBottomColor: instanceQuery.isError
? whitelisted
? colors.yellow
: colors.red
: colors.border,
...(Platform.OS === 'android' && { paddingRight: 0 })
}}
editable={false}
@ -159,12 +178,23 @@ const ComponentInstance: React.FC<Props> = ({
...StyleConstants.FontStyle.M,
marginRight: StyleConstants.Spacing.M,
color: colors.primaryDefault,
borderBottomColor: instanceQuery.isError ? colors.red : colors.border,
borderBottomColor: instanceQuery.isError
? whitelisted
? colors.yellow
: colors.red
: colors.border,
...(Platform.OS === 'android' && { paddingLeft: 0 })
}}
onChangeText={debounce(text => setDomain(text.replace(/^http(s)?\:\/\//i, '')), 1000, {
onChangeText={debounce(
text => {
setDomain(text.replace(/^http(s)?\:\/\//i, ''))
setErrorCode(null)
},
1000,
{
trailing: true
})}
}
)}
autoCapitalize='none'
clearButtonMode='never'
keyboardType='url'
@ -194,12 +224,24 @@ const ComponentInstance: React.FC<Props> = ({
type='text'
content={t('server.button')}
onPress={processUpdate}
disabled={!instanceQuery.data?.uri}
disabled={!instanceQuery.data?.uri && !whitelisted}
loading={instanceQuery.isFetching || appsMutation.isLoading}
/>
</View>
<View>
{whitelisted ? (
<CustomText
fontStyle='S'
style={{
color: colors.yellow,
paddingHorizontal: StyleConstants.Spacing.Global.PagePadding,
paddingTop: StyleConstants.Spacing.XS
}}
>
{t('server.whitelisted')}
</CustomText>
) : (
<Placeholder>
<InstanceInfo
header={t('server.information.name')}
@ -227,6 +269,7 @@ const ComponentInstance: React.FC<Props> = ({
/>
</View>
</Placeholder>
)}
<View
style={{
flexDirection: 'row',

View File

@ -4,8 +4,8 @@ import { getSettingsFontsize } from '@utils/slices/settingsSlice'
import { StyleConstants } from '@utils/styles/constants'
import { adaptiveScale } from '@utils/styles/scaling'
import { useTheme } from '@utils/styles/ThemeManager'
import React, { useMemo } from 'react'
import { Platform, StyleSheet, TextStyle } from 'react-native'
import React from 'react'
import { Platform, TextStyle } from 'react-native'
import FastImage from 'react-native-fast-image'
import { useSelector } from 'react-redux'
import validUrl from 'valid-url'
@ -36,23 +36,19 @@ const ParseEmojis = React.memo(
)
const { colors, theme } = useTheme()
const styles = useMemo(() => {
return StyleSheet.create({
text: {
return (
<CustomText
style={[
{
color: colors.primaryDefault,
fontSize: adaptedFontsize,
lineHeight: adaptedLineheight
},
image: {
width: adaptedFontsize,
height: adaptedFontsize,
...(Platform.OS === 'android' && { transform: [{ translateY: 2 }] })
}
})
}, [theme, adaptiveFontsize])
return (
<CustomText style={[styles.text, style]} fontWeight={fontBold ? 'Bold' : undefined}>
style
]}
fontWeight={fontBold ? 'Bold' : undefined}
>
{emojis ? (
content
.split(regexEmoji)
@ -73,7 +69,14 @@ const ParseEmojis = React.memo(
return (
<CustomText key={emojiShortcode + i}>
{i === 0 ? ' ' : undefined}
<FastImage source={{ uri }} style={styles.image} />
<FastImage
source={{ uri }}
style={{
width: adaptedFontsize,
height: adaptedFontsize,
transform: [{ translateY: Platform.OS === 'ios' ? -1 : 2 }]
}}
/>
</CustomText>
)
} else {

View File

@ -102,7 +102,7 @@ const renderNode = ({
)
}
} else {
const domain = href?.split(new RegExp(/:\/\/(.[^\/]+)/))
const domain = href?.split(new RegExp(/:\/\/(.[^\/]+\/.{3})/))
// Need example here
const content = node.children && node.children[0] && node.children[0].data
const shouldBeTag = tags && tags.filter(tag => `#${tag.name}` === content).length > 0
@ -128,17 +128,7 @@ const renderNode = ({
}}
>
{content && content !== href ? content : showFullLink ? href : domain?.[1]}
{!shouldBeTag ? (
<Icon
color={colors.blue}
name='ExternalLink'
size={adaptedFontsize}
style={{
marginLeft: StyleConstants.Spacing.XS,
...(Platform.OS === 'android' && { transform: [{ translateY: 2 }] })
}}
/>
) : null}
{!shouldBeTag ? '...' : null}
</CustomText>
)
}

View File

@ -6,21 +6,24 @@ import {
useRelationshipMutation,
useRelationshipQuery
} from '@utils/queryHooks/relationship'
import { QueryKeyTimeline } from '@utils/queryHooks/timeline'
import { useTheme } from '@utils/styles/ThemeManager'
import React from 'react'
import { useTranslation } from 'react-i18next'
import { useQueryClient } from '@tanstack/react-query'
import { useSelector } from 'react-redux'
import { checkInstanceFeature } from '@utils/slices/instancesSlice'
import { StyleConstants } from '@utils/styles/constants'
export interface Props {
id: Mastodon.Account['id']
}
const RelationshipOutgoing = React.memo(
({ id }: Props) => {
const RelationshipOutgoing: React.FC<Props> = ({ id }: Props) => {
const { theme } = useTheme()
const { t } = useTranslation('componentRelationship')
const canFollowNotify = useSelector(checkInstanceFeature('account_follow_notify'))
const query = useRelationshipQuery({ id })
const queryKeyRelationship: QueryKeyRelationship = ['Relationship', { id }]
@ -120,6 +123,27 @@ const RelationshipOutgoing = React.memo(
}
return (
<>
{canFollowNotify && query.data?.following ? (
<Button
type='icon'
content={query.data.notifying ? 'BellOff' : 'Bell'}
round
onPress={() =>
mutation.mutate({
id,
type: 'outgoing',
payload: {
action: 'follow',
state: false,
notify: !query.data.notifying
}
})
}
loading={query.isLoading || mutation.isLoading}
style={{ marginRight: StyleConstants.Spacing.S }}
/>
) : null}
<Button
type='text'
content={content}
@ -127,9 +151,8 @@ const RelationshipOutgoing = React.memo(
loading={query.isLoading || mutation.isLoading}
disabled={query.isError || query.data?.blocked_by}
/>
</>
)
},
() => true
)
}
export default RelationshipOutgoing

View File

@ -98,7 +98,6 @@ const TimelineConversation: React.FC<Props> = ({ conversation, queryKey, highlig
</View>
{conversation.last_status ? (
<>
<View
style={{
paddingTop: highlighted ? StyleConstants.Spacing.S : 0,
@ -107,10 +106,9 @@ const TimelineConversation: React.FC<Props> = ({ conversation, queryKey, highlig
>
<TimelineContent />
<TimelinePoll />
</View>
<TimelineActions />
</>
</View>
) : null}
</Pressable>
</StatusContext.Provider>

View File

@ -9,11 +9,12 @@ import TimelineCard from '@components/Timeline/Shared/Card'
import TimelineContent from '@components/Timeline/Shared/Content'
import TimelineHeaderDefault from '@components/Timeline/Shared/HeaderDefault'
import TimelinePoll from '@components/Timeline/Shared/Poll'
import removeHTML from '@helpers/removeHTML'
import { useNavigation } from '@react-navigation/native'
import { StackNavigationProp } from '@react-navigation/stack'
import { TabLocalStackParamList } from '@utils/navigation/navigators'
import { QueryKeyTimeline } from '@utils/queryHooks/timeline'
import { getInstanceAccount } from '@utils/slices/instancesSlice'
import { checkInstanceFeature, getInstanceAccount } from '@utils/slices/instancesSlice'
import { StyleConstants } from '@utils/styles/constants'
import { useTheme } from '@utils/styles/ThemeManager'
import React, { useRef, useState } from 'react'
@ -22,7 +23,7 @@ import { useSelector } from 'react-redux'
import * as ContextMenu from 'zeego/context-menu'
import StatusContext from './Shared/Context'
import TimelineFeedback from './Shared/Feedback'
import TimelineFiltered, { shouldFilter } from './Shared/Filtered'
import TimelineFiltered, { FilteredProps, shouldFilter } from './Shared/Filtered'
import TimelineFullConversation from './Shared/FullConversation'
import TimelineHeaderAndroid from './Shared/HeaderAndroid'
import TimelineTranslate from './Shared/Translate'
@ -47,12 +48,20 @@ const TimelineDefault: React.FC<Props> = ({
disableOnPress = false,
isConversation = false
}) => {
const status = item.reblog ? item.reblog : item
const rawContent = useRef<string[]>([])
if (highlighted) {
rawContent.current = [
removeHTML(status.content),
status.spoiler_text ? removeHTML(status.spoiler_text) : ''
].filter(c => c.length)
}
const { colors } = useTheme()
const navigation = useNavigation<StackNavigationProp<TabLocalStackParamList>>()
const instanceAccount = useSelector(getInstanceAccount, () => true)
const status = item.reblog ? item.reblog : item
const ownAccount = status.account?.id === instanceAccount?.id
const [spoilerExpanded, setSpoilerExpanded] = useState(
instanceAccount?.preferences?.['reading:expand:spoilers'] || false
@ -60,15 +69,7 @@ const TimelineDefault: React.FC<Props> = ({
const spoilerHidden = status.spoiler_text?.length
? !instanceAccount?.preferences?.['reading:expand:spoilers'] && !spoilerExpanded
: false
const copiableContent = useRef<{ content: string; complete: boolean }>({
content: '',
complete: false
})
const filtered = queryKey && shouldFilter({ copiableContent, status, queryKey })
if (queryKey && filtered && !highlighted) {
return <TimelineFiltered phrase={filtered} />
}
const detectedLanguage = useRef<string>(status.language || '')
const mainStyle: StyleProp<ViewStyle> = {
flex: 1,
@ -102,8 +103,9 @@ const TimelineDefault: React.FC<Props> = ({
paddingTop: highlighted ? StyleConstants.Spacing.S : 0,
paddingLeft: highlighted
? 0
: (disableDetails ? StyleConstants.Avatar.XS : StyleConstants.Avatar.M) +
StyleConstants.Spacing.S,
: (disableDetails || isConversation
? StyleConstants.Avatar.XS
: StyleConstants.Avatar.M) + StyleConstants.Spacing.S,
...(disableDetails && { marginTop: -StyleConstants.Spacing.S })
}}
>
@ -114,9 +116,9 @@ const TimelineDefault: React.FC<Props> = ({
<TimelineFullConversation />
<TimelineTranslate />
<TimelineFeedback />
</View>
<TimelineActions />
</View>
</>
)
@ -124,11 +126,36 @@ const TimelineDefault: React.FC<Props> = ({
visibility: status.visibility,
type: 'status',
url: status.url || status.uri,
copiableContent
rawContent
})
const mStatus = menuStatus({ status, queryKey, rootQueryKey })
const mInstance = menuInstance({ status, queryKey, rootQueryKey })
if (!ownAccount) {
let filterResults: FilteredProps['filterResults'] = []
const [filterRevealed, setFilterRevealed] = useState(false)
const hasFilterServerSide = useSelector(checkInstanceFeature('filter_server_side'))
if (hasFilterServerSide) {
if (status.filtered?.length) {
filterResults = status.filtered?.map(filter => filter.filter)
}
} else {
if (queryKey) {
const checkFilter = shouldFilter({ queryKey, status })
if (checkFilter?.length) {
filterResults = checkFilter
}
}
}
if (queryKey && !highlighted && filterResults?.length && !filterRevealed) {
return !filterResults.filter(result => result.filter_action === 'hide').length ? (
<Pressable onPress={() => setFilterRevealed(!filterRevealed)}>
<TimelineFiltered filterResults={filterResults} />
</Pressable>
) : null
}
}
return (
<StatusContext.Provider
value={{
@ -138,7 +165,8 @@ const TimelineDefault: React.FC<Props> = ({
reblogStatus: item.reblog ? item : undefined,
ownAccount,
spoilerHidden,
copiableContent,
rawContent,
detectedLanguage,
highlighted,
inThread: queryKey?.[1].page === 'Toot',
disableDetails,

View File

@ -13,7 +13,7 @@ import { useNavigation } from '@react-navigation/native'
import { StackNavigationProp } from '@react-navigation/stack'
import { TabLocalStackParamList } from '@utils/navigation/navigators'
import { QueryKeyTimeline } from '@utils/queryHooks/timeline'
import { getInstanceAccount } from '@utils/slices/instancesSlice'
import { checkInstanceFeature, getInstanceAccount } from '@utils/slices/instancesSlice'
import { StyleConstants } from '@utils/styles/constants'
import { useTheme } from '@utils/styles/ThemeManager'
import React, { useCallback, useRef, useState } from 'react'
@ -21,7 +21,7 @@ import { Pressable, View } from 'react-native'
import { useSelector } from 'react-redux'
import * as ContextMenu from 'zeego/context-menu'
import StatusContext from './Shared/Context'
import TimelineFiltered, { shouldFilter } from './Shared/Filtered'
import TimelineFiltered, { FilteredProps, shouldFilter } from './Shared/Filtered'
import TimelineFullConversation from './Shared/FullConversation'
import TimelineHeaderAndroid from './Shared/HeaderAndroid'
@ -47,21 +47,6 @@ const TimelineNotifications: React.FC<Props> = ({ notification, queryKey }) => {
const spoilerHidden = notification.status?.spoiler_text?.length
? !instanceAccount.preferences?.['reading:expand:spoilers'] && !spoilerExpanded
: false
const copiableContent = useRef<{ content: string; complete: boolean }>({
content: '',
complete: false
})
const filtered =
notification.status &&
shouldFilter({
copiableContent,
status: notification.status,
queryKey
})
if (notification.status && filtered) {
return <TimelineFiltered phrase={filtered} />
}
const { colors } = useTheme()
const navigation = useNavigation<StackNavigationProp<TabLocalStackParamList>>()
@ -112,11 +97,11 @@ const TimelineNotifications: React.FC<Props> = ({ notification, queryKey }) => {
<TimelineAttachment />
<TimelineCard />
<TimelineFullConversation />
<TimelineActions />
</View>
) : null}
</View>
<TimelineActions />
</>
)
}
@ -124,20 +109,44 @@ const TimelineNotifications: React.FC<Props> = ({ notification, queryKey }) => {
const mShare = menuShare({
visibility: notification.status?.visibility,
type: 'status',
url: notification.status?.url || notification.status?.uri,
copiableContent
url: notification.status?.url || notification.status?.uri
})
const mStatus = menuStatus({ status: notification.status, queryKey })
const mInstance = menuInstance({ status: notification.status, queryKey })
if (!ownAccount) {
let filterResults: FilteredProps['filterResults'] = []
const [filterRevealed, setFilterRevealed] = useState(false)
const hasFilterServerSide = useSelector(checkInstanceFeature('filter_server_side'))
if (notification.status) {
if (hasFilterServerSide) {
if (notification.status.filtered?.length) {
filterResults = notification.status.filtered.map(filter => filter.filter)
}
} else {
const checkFilter = shouldFilter({ queryKey, status: notification.status })
if (checkFilter?.length) {
filterResults = checkFilter
}
}
if (filterResults?.length && !filterRevealed) {
return !filterResults.filter(result => result.filter_action === 'hide').length ? (
<Pressable onPress={() => setFilterRevealed(!filterRevealed)}>
<TimelineFiltered filterResults={filterResults} />
</Pressable>
) : null
}
}
}
return (
<StatusContext.Provider
value={{
queryKey,
status,
ownAccount,
spoilerHidden,
copiableContent
spoilerHidden
}}
>
<ContextMenu.Root>

View File

@ -55,7 +55,7 @@ const TimelineRefresh: React.FC<Props> = ({
firstPage?.links?.prev && {
...(firstPage.links.prev.isOffset
? { offset: firstPage.links.prev.id }
: { max_id: firstPage.links.prev.id }),
: { min_id: firstPage.links.prev.id }),
// https://github.com/facebook/react-native/issues/25239#issuecomment-731100372
limit: '3'
},

View File

@ -2,6 +2,7 @@ import Icon from '@components/Icon'
import { displayMessage } from '@components/Message'
import CustomText from '@components/Text'
import { useActionSheet } from '@expo/react-native-action-sheet'
import { androidActionSheetStyles } from '@helpers/androidActionSheetStyles'
import { useNavigation } from '@react-navigation/native'
import { StackNavigationProp } from '@react-navigation/stack'
import { RootStackParamList } from '@utils/navigation/navigators'
@ -99,7 +100,8 @@ const TimelineActions: React.FC = () => {
t('shared.actions.reblogged.options.unlisted'),
t('common:buttons.cancel')
],
cancelButtonIndex: 2
cancelButtonIndex: 2,
...androidActionSheetStyles(colors)
},
(selectedIndex: number) => {
switch (selectedIndex) {
@ -263,11 +265,6 @@ const TimelineActions: React.FC = () => {
}, [status.bookmarked])
return (
<View
style={{
paddingLeft: highlighted ? 0 : StyleConstants.Avatar.M + StyleConstants.Spacing.S
}}
>
<View style={{ flexDirection: 'row' }}>
<Pressable
{...(highlighted
@ -320,7 +317,6 @@ const TimelineActions: React.FC = () => {
children={childrenBookmark}
/>
</View>
</View>
)
}

View File

@ -70,7 +70,6 @@ const TimelineAttachment = () => {
preview_url: attachment.preview_url,
url: attachment.url,
remote_url: attachment.remote_url,
blurhash: attachment.blurhash,
width: attachment.meta?.original?.width,
height: attachment.meta?.original?.height
}
@ -90,7 +89,6 @@ const TimelineAttachment = () => {
preview_url: attachment.preview_url,
url: attachment.url,
remote_url: attachment.remote_url,
blurhash: attachment.blurhash,
width: attachment.meta?.original?.width,
height: attachment.meta?.original?.height
}

View File

@ -48,8 +48,7 @@ const TimelineAvatar: React.FC<Props> = ({ account }) => {
style={{
borderRadius: StyleConstants.Avatar.M,
overflow: 'hidden',
marginRight: StyleConstants.Spacing.S,
marginLeft: isConversation ? StyleConstants.Avatar.M - StyleConstants.Avatar.XS : undefined
marginRight: StyleConstants.Spacing.S
}}
/>
)

View File

@ -145,6 +145,7 @@ const TimelineCard: React.FC = () => {
/>
) : null}
<View style={{ flex: 1, padding: StyleConstants.Spacing.S }}>
{status.card?.title.length ? (
<CustomText
fontStyle='S'
numberOfLines={2}
@ -155,9 +156,10 @@ const TimelineCard: React.FC = () => {
fontWeight='Bold'
testID='title'
>
{status.card?.title}
{status.card.title}
</CustomText>
{status.card?.description ? (
) : null}
{status.card?.description.length ? (
<CustomText
fontStyle='S'
numberOfLines={1}
@ -170,9 +172,11 @@ const TimelineCard: React.FC = () => {
{status.card.description}
</CustomText>
) : null}
{status.card?.url.length ? (
<CustomText fontStyle='S' numberOfLines={1} style={{ color: colors.secondary }}>
{status.card?.url}
{status.card.url}
</CustomText>
) : null}
</View>
</>
)
@ -187,10 +191,6 @@ const TimelineCard: React.FC = () => {
style={{
flex: 1,
flexDirection: 'row',
minHeight:
(isStatus && foundStatus) || (isAccount && foundAccount)
? undefined
: StyleConstants.Font.LineHeight.M * 5,
marginTop: StyleConstants.Spacing.M,
borderWidth: StyleSheet.hairlineWidth,
borderRadius: StyleConstants.Spacing.S,

View File

@ -1,8 +1,12 @@
import { ParseHTML } from '@components/Parse'
import CustomText from '@components/Text'
import { getInstanceAccount } from '@utils/slices/instancesSlice'
import { StyleConstants } from '@utils/styles/constants'
import { useTheme } from '@utils/styles/ThemeManager'
import React, { useContext } from 'react'
import { useTranslation } from 'react-i18next'
import { Platform } from 'react-native'
import { Platform, StyleSheet, View } from 'react-native'
import { Path, Svg } from 'react-native-svg'
import { useSelector } from 'react-redux'
import { isRtlLang } from 'rtl-detect'
import StatusContext from './Context'
@ -16,6 +20,7 @@ const TimelineContent: React.FC<Props> = ({ notificationOwnToot = false, setSpoi
const { status, highlighted, inThread, disableDetails } = useContext(StatusContext)
if (!status || typeof status.content !== 'string' || !status.content.length) return null
const { colors } = useTheme()
const { t } = useTranslation('componentTimeline')
const instanceAccount = useSelector(getInstanceAccount, () => true)
@ -39,6 +44,11 @@ const TimelineContent: React.FC<Props> = ({ notificationOwnToot = false, setSpoi
: undefined
}
/>
{inThread ? (
<CustomText fontStyle='S' style={{ textAlign: 'center', color: colors.secondary, paddingVertical: StyleConstants.Spacing.XS }}>
{t('shared.content.expandHint')}
</CustomText>
) : null}
<ParseHTML
content={status.content}
size={highlighted ? 'L' : 'M'}

View File

@ -1,7 +1,9 @@
import { QueryKeyTimeline } from '@utils/queryHooks/timeline'
import { createContext } from 'react'
type ContextType = {
export type HighlightedStatusContextType = {}
type StatusContextType = {
queryKey?: QueryKeyTimeline
rootQueryKey?: QueryKeyTimeline
@ -10,10 +12,8 @@ type ContextType = {
reblogStatus?: Mastodon.Status // When it is a reblog, pass the root status
ownAccount?: boolean
spoilerHidden?: boolean
copiableContent?: React.MutableRefObject<{
content: string
complete: boolean
}>
rawContent?: React.MutableRefObject<string[]> // When highlighted, for translate, edit history
detectedLanguage?: React.MutableRefObject<string>
highlighted?: boolean
inThread?: boolean
@ -21,6 +21,6 @@ type ContextType = {
disableOnPress?: boolean
isConversation?: boolean
}
const StatusContext = createContext<ContextType>({} as ContextType)
const StatusContext = createContext<StatusContextType>({} as StatusContextType)
export default StatusContext

View File

@ -11,7 +11,7 @@ import { StyleSheet, View } from 'react-native'
import StatusContext from './Context'
const TimelineFeedback = () => {
const { status, highlighted } = useContext(StatusContext)
const { status, highlighted, detectedLanguage } = useContext(StatusContext)
if (!status || !highlighted) return null
const { t } = useTranslation('componentTimeline')
@ -80,7 +80,12 @@ const TimelineFeedback = () => {
accessibilityHint={t('shared.actionsUsers.history.accessibilityHint')}
accessibilityRole='button'
style={[styles.text, { marginRight: 0, color: colors.blue }]}
onPress={() => navigation.push('Tab-Shared-History', { id: status.id })}
onPress={() =>
navigation.push('Tab-Shared-History', {
id: status.id,
detectedLanguage: detectedLanguage?.current || status.language || ''
})
}
>
{t('shared.actionsUsers.history.text', {
count: data.length - 1

View File

@ -1,19 +1,46 @@
import CustomText from '@components/Text'
import removeHTML from '@helpers/removeHTML'
import { store } from '@root/store'
import { QueryKeyTimeline } from '@utils/queryHooks/timeline'
import { getInstance, getInstanceAccount } from '@utils/slices/instancesSlice'
import { getInstance } from '@utils/slices/instancesSlice'
import { StyleConstants } from '@utils/styles/constants'
import { useTheme } from '@utils/styles/ThemeManager'
import htmlparser2 from 'htmlparser2-without-node-native'
import React from 'react'
import { useTranslation } from 'react-i18next'
import { View } from 'react-native'
const TimelineFiltered = React.memo(
({ phrase }: { phrase: string }) => {
export interface FilteredProps {
filterResults: { title: string; filter_action: Mastodon.Filter<'v2'>['filter_action'] }[]
}
const TimelineFiltered: React.FC<FilteredProps> = ({ filterResults }) => {
const { colors } = useTheme()
const { t } = useTranslation('componentTimeline')
const main = () => {
if (!filterResults?.length) {
return <></>
}
switch (typeof filterResults[0]) {
case 'string': // v1 filter
return <>{t('shared.filtered.match', { context: 'v1', phrase: filterResults[0] })}</>
default:
return (
<>
{t('shared.filtered.match', {
context: 'v2',
count: filterResults.length,
filters: filterResults.map(result => result.title).join(t('common:separator'))
})}
<CustomText
style={{ color: colors.blue }}
children={`\n${t('shared.filtered.reveal')}`}
/>
</>
)
}
}
return (
<View style={{ backgroundColor: colors.backgroundDefault }}>
<CustomText
@ -25,67 +52,47 @@ const TimelineFiltered = React.memo(
paddingLeft: StyleConstants.Avatar.M + StyleConstants.Spacing.S
}}
>
{t('shared.filtered', { phrase })}
{main()}
</CustomText>
</View>
)
},
() => true
)
}
export const shouldFilter = ({
copiableContent,
status,
queryKey
queryKey,
status
}: {
copiableContent: React.MutableRefObject<{
content: string
complete: boolean
}>
status: Mastodon.Status
queryKey: QueryKeyTimeline
}): string | null => {
status: Pick<Mastodon.Status, 'content' | 'spoiler_text'>
}): FilteredProps['filterResults'] | undefined => {
const page = queryKey[1]
const instance = getInstance(store.getState())
const ownAccount = getInstanceAccount(store.getState())?.id === status.account?.id
let shouldFilter: string | null = null
let returnFilter: FilteredProps['filterResults'] | undefined
if (!ownAccount) {
let rawContent = ''
const parser = new htmlparser2.Parser({
ontext: (text: string) => {
if (!copiableContent.current.complete) {
copiableContent.current.content = copiableContent.current.content + text
}
rawContent = rawContent + text
}
})
if (status.spoiler_text) {
parser.write(status.spoiler_text)
rawContent = rawContent + `\n\n`
}
parser.write(status.content)
parser.end()
const checkFilter = (filter: Mastodon.Filter) => {
const rawContentCombined = [
removeHTML(status.content),
status.spoiler_text ? removeHTML(status.spoiler_text) : ''
]
.filter(c => c.length)
.join(`\n`)
const checkFilter = (filter: Mastodon.Filter<'v1'>) => {
const escapedPhrase = filter.phrase.replace(/[.*+?^${}()|[\]\\]/g, '\\$&') // $& means the whole matched string
switch (filter.whole_word) {
case true:
if (new RegExp(`\\b${escapedPhrase}\\b`, 'i').test(rawContent)) {
shouldFilter = filter.phrase
if (new RegExp(`\\b${escapedPhrase}\\b`, 'i').test(rawContentCombined)) {
returnFilter = [{ title: filter.phrase, filter_action: 'warn' }]
}
break
case false:
if (new RegExp(escapedPhrase, 'i').test(rawContent)) {
shouldFilter = filter.phrase
if (new RegExp(escapedPhrase, 'i').test(rawContentCombined)) {
returnFilter = [{ title: filter.phrase, filter_action: 'warn' }]
}
break
}
}
instance?.filters?.forEach(filter => {
if (shouldFilter) {
if (returnFilter) {
return
}
if (filter.expires_at) {
@ -100,30 +107,27 @@ export const shouldFilter = ({
case 'List':
case 'Account':
if (filter.context.includes('home')) {
checkFilter(filter)
checkFilter(filter as Mastodon.Filter<'v1'>)
}
break
case 'Notifications':
if (filter.context.includes('notifications')) {
checkFilter(filter)
checkFilter(filter as Mastodon.Filter<'v1'>)
}
break
case 'LocalPublic':
if (filter.context.includes('public')) {
checkFilter(filter)
checkFilter(filter as Mastodon.Filter<'v1'>)
}
break
case 'Toot':
if (filter.context.includes('thread')) {
checkFilter(filter)
checkFilter(filter as Mastodon.Filter<'v1'>)
}
}
})
copiableContent.current.complete = true
}
return shouldFilter
return returnFilter
}
export default TimelineFiltered

View File

@ -10,7 +10,7 @@ import * as DropdownMenu from 'zeego/dropdown-menu'
import StatusContext from './Context'
const TimelineHeaderAndroid: React.FC = () => {
const { queryKey, rootQueryKey, status, disableDetails, disableOnPress } =
const { queryKey, rootQueryKey, status, disableDetails, disableOnPress, rawContent } =
useContext(StatusContext)
if (Platform.OS !== 'android' || !status || disableDetails || disableOnPress) return null
@ -21,7 +21,8 @@ const TimelineHeaderAndroid: React.FC = () => {
const mShare = menuShare({
visibility: status.visibility,
type: 'status',
url: status.url || status.uri
url: status.url || status.uri,
rawContent
})
const mAccount = menuAccount({
type: 'status',

View File

@ -16,7 +16,7 @@ import HeaderSharedMuted from './HeaderShared/Muted'
import HeaderSharedVisibility from './HeaderShared/Visibility'
const TimelineHeaderDefault: React.FC = () => {
const { queryKey, rootQueryKey, status, copiableContent, highlighted, disableDetails } =
const { queryKey, rootQueryKey, status, highlighted, disableDetails, rawContent } =
useContext(StatusContext)
if (!status) return null
@ -28,7 +28,7 @@ const TimelineHeaderDefault: React.FC = () => {
visibility: status.visibility,
type: 'status',
url: status.url || status.uri,
copiableContent
rawContent
})
const mAccount = menuAccount({
type: 'status',

View File

@ -13,39 +13,25 @@ import { Circle } from 'react-native-animated-spinkit'
import StatusContext from './Context'
const TimelineTranslate = () => {
const { status, highlighted, copiableContent } = useContext(StatusContext)
if (!status || !highlighted) return null
const { status, highlighted, rawContent, detectedLanguage } = useContext(StatusContext)
if (!status || !highlighted || !rawContent?.current.length) return null
const { t } = useTranslation('componentTimeline')
const { colors } = useTheme()
const backupTextProcessing = (): string[] => {
const text = status.spoiler_text ? [status.spoiler_text, status.content] : [status.content]
for (const i in text) {
for (const emoji of status.emojis) {
text[i] = text[i].replaceAll(`:${emoji.shortcode}:`, ' ')
}
text[i] = text[i]
.replace(/(<([^>]+)>)/gi, ' ')
.replace(/@.*? /gi, ' ')
.replace(/#.*? /gi, ' ')
.replace(/http(s):\/\/.*? /gi, ' ')
}
return text
}
const text = copiableContent?.current.content
? [copiableContent?.current.content]
: backupTextProcessing()
const [detectedLanguage, setDetectedLanguage] = useState<{
const [detected, setDetected] = useState<{
language: string
confidence: number
}>({ language: status.language || '', confidence: 0 })
useEffect(() => {
const detect = async () => {
const result = await detectLanguage(text.join('\n\n'))
result && setDetectedLanguage(result)
const result = await detectLanguage(rawContent.current.join('\n\n'))
if (result) {
setDetected(result)
if (detectedLanguage) {
detectedLanguage.current = result.language
}
}
}
detect()
}, [])
@ -57,18 +43,18 @@ const TimelineTranslate = () => {
const [enabled, setEnabled] = useState(false)
const { refetch, data, isFetching, isSuccess, isError } = useTranslateQuery({
source: detectedLanguage.language,
source: detected.language,
target: targetLanguage,
text,
text: rawContent.current,
options: { enabled }
})
const devView = () => {
return __DEV__ ? (
<CustomText fontStyle='S' style={{ color: colors.secondary }}>{` Source: ${
detectedLanguage?.language
detected?.language
}; Confidence: ${
detectedLanguage?.confidence.toString().slice(0, 5) || 'null'
detected?.confidence.toString().slice(0, 5) || 'null'
}; Target: ${targetLanguage}`}</CustomText>
) : null
}
@ -78,13 +64,13 @@ const TimelineTranslate = () => {
}
if (
Platform.OS === 'ios' &&
Localization.locale.slice(0, 2).includes(detectedLanguage.language.slice(0, 2))
Localization.locale.slice(0, 2).includes(detected.language.slice(0, 2))
) {
return devView()
}
if (
Platform.OS === 'android' &&
settingsLanguage?.slice(0, 2).includes(detectedLanguage.language.slice(0, 2))
settingsLanguage?.slice(0, 2).includes(detected.language.slice(0, 2))
) {
return devView()
}

View File

@ -16,7 +16,7 @@ import {
import { getInstanceAccount } from '@utils/slices/instancesSlice'
import { useEffect, useState } from 'react'
import { useTranslation } from 'react-i18next'
import { Platform } from 'react-native'
import { Alert, Platform } from 'react-native'
import { useQueryClient } from '@tanstack/react-query'
import { useSelector } from 'react-redux'
@ -186,12 +186,22 @@ const menuAccount = ({
key: 'account-block',
item: {
onSelect: () =>
Alert.alert(t('account.block.alert.title', { username: account.username }), undefined, [
{
text: t('common:buttons.confirm'),
style: 'destructive',
onPress: () =>
timelineMutation.mutate({
type: 'updateAccountProperty',
queryKey,
id: account.id,
payload: { property: 'block', currentValue: data?.blocking }
}),
})
},
{
text: t('common:buttons.cancel')
}
]),
disabled: Platform.OS !== 'android' ? !data || !isFetched : false,
destructive: !data?.blocking,
hidden: false
@ -204,7 +214,15 @@ const menuAccount = ({
{
key: 'account-reports',
item: {
onSelect: () => {
onSelect: () =>
Alert.alert(
t('account.reports.alert.title', { username: account.username }),
undefined,
[
{
text: t('common:buttons.confirm'),
style: 'destructive',
onPress: () => {
timelineMutation.mutate({
type: 'updateAccountProperty',
queryKey,
@ -217,7 +235,13 @@ const menuAccount = ({
id: account.id,
payload: { property: 'block', currentValue: false }
})
}
},
{
text: t('common:buttons.cancel')
}
]
),
disabled: false,
destructive: true,
hidden: false

View File

@ -49,7 +49,7 @@ const menuInstance = ({
t('instance.block.alert.message'),
[
{
text: t('instance.block.alert.buttons.confirm'),
text: t('common:buttons.confirm'),
style: 'destructive',
onPress: () => {
mutation.mutate({

View File

@ -7,10 +7,7 @@ const menuShare = (
params:
| {
visibility?: Mastodon.Status['visibility']
copiableContent?: React.MutableRefObject<{
content?: string | undefined
complete: boolean
}>
rawContent?: React.MutableRefObject<string[]>
type: 'status'
url?: string
}
@ -48,17 +45,17 @@ const menuShare = (
icon: 'square.and.arrow.up'
})
}
if (params.type === 'status' && Platform.OS === 'ios')
if (params.type === 'status')
menus[0].push({
key: 'copy',
item: {
onSelect: () => {
Clipboard.setString(params.copiableContent?.current.content || '')
Clipboard.setString(params.rawContent?.current.join(`\n\n`) || '')
displayMessage({ type: 'success', message: t(`copy.succeed`) })
},
disabled: false,
destructive: false,
hidden: !params.copiableContent?.current.content?.length
hidden: !params.rawContent?.current.length
},
title: t('copy.action'),
icon: 'doc.on.doc'

View File

@ -109,7 +109,7 @@ const menuStatus = ({
onSelect: () =>
Alert.alert(t('status.deleteEdit.alert.title'), t('status.deleteEdit.alert.message'), [
{
text: t('status.deleteEdit.alert.buttons.confirm'),
text: t('common:buttons.confirm'),
style: 'destructive',
onPress: async () => {
let replyToStatus: Mastodon.Status | undefined = undefined
@ -153,7 +153,7 @@ const menuStatus = ({
onSelect: () =>
Alert.alert(t('status.delete.alert.title'), t('status.delete.alert.message'), [
{
text: t('status.delete.alert.buttons.confirm'),
text: t('common:buttons.confirm'),
style: 'destructive',
onPress: async () => {
mutation.mutate({

View File

@ -0,0 +1,5 @@
export const androidActionSheetStyles = (colors: any) => ({
containerStyle: { backgroundColor: colors.backgroundDefault },
textStyle: { color: colors.primaryDefault },
titleTextStyle: { color: colors.secondary }
})

View File

@ -1,4 +1,8 @@
[
{
"feature": "account_follow_notify",
"version": 3.3
},
{
"feature": "notification_type_status",
"version": 3.3
@ -38,5 +42,9 @@
{
"feature": "notification_type_admin_report",
"version": 4.0
},
{
"feature": "filter_server_side",
"version": 4.0
}
]

View File

@ -1,5 +1,21 @@
import { QueryClient } from '@tanstack/react-query'
const queryClient = new QueryClient({ defaultOptions: { queries: { staleTime: 1000 * 60 * 5 } } })
const queryClient = new QueryClient({
defaultOptions: {
queries: {
staleTime: 1000 * 60 * 5,
retry: (failureCount, error: any) => {
if (error?.status === 404) {
return false
}
if (failureCount <= 3) {
return true
} else {
return false
}
}
}
}
})
export default queryClient

View File

@ -6,6 +6,9 @@ const removeHTML = (text: string): string => {
const parser = new htmlparser2.Parser({
ontext: (text: string) => {
raw = raw + text
},
onclosetag: (tag: string) => {
if (['p', 'br'].includes(tag)) raw = raw + `\n`
}
})

View File

@ -7,7 +7,8 @@
"continue": "Continua",
"create": "Crea",
"delete": "Esborra",
"done": "Fet"
"done": "Fet",
"confirm": "Confirma"
},
"customEmoji": {
"accessibilityLabel": "Emoji personalitzat {{emoji}}"

View File

@ -13,10 +13,16 @@
},
"block": {
"action_false": "Bloqueja l'usuari",
"action_true": "Deixa de bloquejar l'usuari"
"action_true": "Deixa de bloquejar l'usuari",
"alert": {
"title": ""
}
},
"reports": {
"action": "Denuncia i bloqueja l'usuari"
"action": "Denuncia i bloqueja l'usuari",
"alert": {
"title": ""
}
}
},
"at": {
@ -32,11 +38,8 @@
"block": {
"action": "Bloquejar la instància {{instance}}",
"alert": {
"title": "Confirma el bloqueig de la instància {{instance}}?",
"message": "Pots silenciar o bloquejar a un usuari.\n\nDesprés de bloquejar una instància, tot el seu contingut, amb els seus seguidors, seran esborrats!",
"buttons": {
"confirm": "Confirma"
}
"title": "Vols bloquejar la instància {{instance}}?",
"message": "Pots silenciar o bloquejar a un usuari.\n\nDesprés de bloquejar una instància, tot el seu contingut, amb els seus seguidors, seran esborrats!"
}
}
},
@ -56,21 +59,15 @@
"delete": {
"action": "Elimina la publicació",
"alert": {
"title": "Confirma l'eliminació?",
"message": "Tots els impulsos i favorits s'esborraran, incloses totes les respostes.",
"buttons": {
"confirm": "Confirma"
}
"title": "Vols eliminar-ho?",
"message": "Tots els impulsos i favorits s'esborraran, incloses totes les respostes."
}
},
"deleteEdit": {
"action": "Elimina la publicació i torna a publicar",
"action": "Elimina i torna-ho a publicar",
"alert": {
"title": "Confirma l'eliminació i tornar a publicar?",
"message": "Tots els impulsos i favorits s'esborraran, incloses totes les respostes.",
"buttons": {
"confirm": "Confirma"
}
"title": "Vols eliminar i tornar-ho a publicar?",
"message": "Tots els impulsos i favorits s'esborraran, incloses totes les respostes."
}
},
"mute": {

View File

@ -1,8 +1,9 @@
{
"server": {
"textInput": {
"placeholder": ""
"placeholder": "Domini de la instància"
},
"whitelisted": "Pot ser una instància que estigui a la llista blanca de la qual tooot no pot obtenir les dades abans d'iniciar la sessió.",
"button": "Inicia la sessió",
"information": {
"name": "Nom",

View File

@ -1,6 +1,6 @@
{
"title": "Selecciona origen multimèdia",
"message": "Les dades multimèdia EXIF no s'han penjat",
"message": "Les dades multimèdia EXIF no es penjen",
"options": {
"image": "Penja fotos",
"image_max": "Penja fotos (màx. {{max}})",

View File

@ -31,7 +31,7 @@
"notification": "{{name}} ha impulsat la teva publicació"
},
"update": "L'impuls ha sigut editat",
"admin.sign_up": "",
"admin.sign_up": "{{name}} s'ha unit a la instància",
"admin.report": ""
},
"actions": {
@ -55,7 +55,7 @@
"accessibilityLabel": "Afegeix aquesta publicació a marcadors",
"function": "Afegeix la publicació a marcadors"
},
"openReport": ""
"openReport": "Obre la denúncia"
},
"actionsUsers": {
"reblogged_by": {
@ -91,7 +91,12 @@
"content": {
"expandHint": "Contingut ocult"
},
"filtered": "Filtrat: {{phrase}}.",
"filtered": {
"reveal": "Mostra-ho de totes maneres",
"match_v1": "Filtrat: {{phrase}}.",
"match_v2_one": "Filtrat per {{filters}}.",
"match_v2_other": "Filtrat per {{count}} filtres, {{filters}}."
},
"fullConversation": "Llegeix conversacions",
"translate": {
"default": "Tradueix",

View File

@ -1,8 +1,7 @@
{
"screenshot": {
"title": "Protecció de la privacitat",
"message": "Si us plau, no revelis la identitat d'altres usuaris, així com el nom d'usuari, avatar, etc. Gràcies!",
"button": "Confirma"
"message": "Si us plau, no revelis la identitat d'altres usuaris, així com el nom d'usuari, avatar, etc. Gràcies!"
},
"localCorrupt": {
"message": "La sessió ha sigut expirada. Si us plau, torna a iniciar la sessió"

View File

@ -11,11 +11,11 @@
},
"right": {
"button": {
"default": "Publicació",
"default": "Publica",
"conversation": "Envia un missatge directe",
"reply": "Resposta de la publicació",
"reply": "Publica la resposta",
"deleteEdit": "Publicació",
"edit": "Publicació",
"edit": "Publica l'edició",
"share": "Publicació"
},
"alert": {

View File

@ -3,15 +3,15 @@
"local": {
"name": "Seguint",
"options": {
"showBoosts": "",
"showReplies": ""
"showBoosts": "Mostra les publicacions",
"showReplies": "Mostra les respostes"
}
},
"public": {
"segments": {
"federated": "Federat",
"local": "Local",
"trending": "En tendència"
"trending": "Tendència"
}
},
"notifications": {
@ -31,13 +31,13 @@
"title": "Mostra les notificacions",
"options": {
"follow": "$t(screenTabs:me.push.follow.heading)",
"follow_request": "Sol·licitud de seguiment",
"follow_request": "Sol·licituds de seguiment",
"favourite": "$t(screenTabs:me.push.favourite.heading)",
"reblog": "$t(screenTabs:me.push.reblog.heading)",
"mention": "$t(screenTabs:me.push.mention.heading)",
"poll": "$t(screenTabs:me.push.poll.heading)",
"status": "Publicació d'usuaris subscrits",
"update": "L'impuls ha sigut editat",
"status": "Publicacions d'usuaris subscrits",
"update": "Edicions d'impulsos",
"admin.sign_up": "$t(screenTabs:me.push.admin.sign_up.heading)",
"admin.report": "$t(screenTabs:me.push.admin.report.heading)"
}
@ -163,7 +163,7 @@
"total_other": "{{count}} camps"
},
"visibility": {
"title": "Visibilitat de la publicació",
"title": "Visibilitat",
"options": {
"public": "Públic",
"unlisted": "Sense llistar",
@ -171,7 +171,7 @@
}
},
"sensitive": {
"title": "Publica contingut multimèdia sensible"
"title": "Continguts multimèdia sensibles"
},
"lock": {
"title": "Fes el compte privat",
@ -211,28 +211,28 @@
"heading": "Per defecte"
},
"follow": {
"heading": "Nou seguidor"
"heading": "Seguidors nous"
},
"follow_request": {
"heading": "Sol·licitud de seguiment"
"heading": "Sol·licituds de seguiment"
},
"favourite": {
"heading": "Favorits"
},
"reblog": {
"heading": "Impulsat"
"heading": "Impulsos"
},
"mention": {
"heading": "T'ha mencionat"
"heading": "Mencions"
},
"poll": {
"heading": "Actualització d'una votació"
"heading": "Actualitzacions d'una votació"
},
"status": {
"heading": "Publicació d'usuaris subscrits"
"heading": "Publicacions d'usuaris subscrits"
},
"update": {
"heading": "L'impuls ha sigut editat"
"heading": "Edicions d'impulsos"
},
"admin.sign_up": {
"heading": "Administració: Registra"
@ -399,7 +399,7 @@
"reblogged_by": "{{count}} impulsats",
"favourited_by": "{{count}} favorits"
},
"resultIncomplete": ""
"resultIncomplete": "Els resultats d'una instància remota són incomplets"
}
}
}

View File

@ -7,7 +7,8 @@
"continue": "",
"create": "",
"delete": "",
"done": ""
"done": "",
"confirm": ""
},
"customEmoji": {
"accessibilityLabel": ""

View File

@ -13,10 +13,16 @@
},
"block": {
"action_false": "",
"action_true": ""
"action_true": "",
"alert": {
"title": ""
}
},
"reports": {
"action": ""
"action": "",
"alert": {
"title": ""
}
}
},
"at": {
@ -33,10 +39,7 @@
"action": "",
"alert": {
"title": "",
"message": "",
"buttons": {
"confirm": ""
}
"message": ""
}
}
},
@ -57,20 +60,14 @@
"action": "",
"alert": {
"title": "",
"message": "",
"buttons": {
"confirm": ""
}
"message": ""
}
},
"deleteEdit": {
"action": "",
"alert": {
"title": "",
"message": "",
"buttons": {
"confirm": ""
}
"message": ""
}
},
"mute": {

View File

@ -3,6 +3,7 @@
"textInput": {
"placeholder": ""
},
"whitelisted": "",
"button": "",
"information": {
"name": "",

View File

@ -91,7 +91,12 @@
"content": {
"expandHint": ""
},
"filtered": "",
"filtered": {
"reveal": "",
"match_v1": "",
"match_v2_one": "",
"match_v2_other": ""
},
"fullConversation": "",
"translate": {
"default": "",

View File

@ -1,8 +1,7 @@
{
"screenshot": {
"title": "",
"message": "",
"button": ""
"message": ""
},
"localCorrupt": {
"message": ""

View File

@ -7,7 +7,8 @@
"continue": "Weiter",
"create": "Erstellen",
"delete": "Löschen",
"done": "Fertig"
"done": "Fertig",
"confirm": "Bestätigen"
},
"customEmoji": {
"accessibilityLabel": "Eigenes Emoji {{emoji}}"

View File

@ -13,10 +13,16 @@
},
"block": {
"action_false": "Nutzer blockieren",
"action_true": "User entblocken"
"action_true": "User entblocken",
"alert": {
"title": ""
}
},
"reports": {
"action": "Nutzer melden und blockieren"
"action": "Nutzer melden und blockieren",
"alert": {
"title": ""
}
}
},
"at": {
@ -33,10 +39,7 @@
"action": "Instanz {{instance}} blockieren",
"alert": {
"title": "{{instance}} wirklich blockieren?",
"message": "Üblicherweise kannst du einen User stummschalten oder blockieren.\nBlockierst du hingegegen eine Instanz, wird deren gesamter Inhalt samt Usern, die dir von dieser Instanz folgen, entfernt!",
"buttons": {
"confirm": "Bestätigen"
}
"message": "Üblicherweise kannst du einen User stummschalten oder blockieren.\nBlockierst du hingegegen eine Instanz, wird deren gesamter Inhalt samt Usern, die dir von dieser Instanz folgen, entfernt!"
}
}
},
@ -57,20 +60,14 @@
"action": "Tröt löschen",
"alert": {
"title": "Löschen bestätigen?",
"message": "Alle Boosts, Sterne und Antworten werden entfernt.",
"buttons": {
"confirm": "Bestätigen"
}
"message": "Alle Boosts, Sterne und Antworten werden entfernt."
}
},
"deleteEdit": {
"action": "Tröt neu entwerfen",
"alert": {
"title": "Beitrag wirklich entfernen?",
"message": "Alle Boosts und Likes inklusive der Antworten werden gelöscht.",
"buttons": {
"confirm": "Bestätigen"
}
"message": "Alle Boosts und Likes inklusive der Antworten werden gelöscht."
}
},
"mute": {

View File

@ -3,6 +3,7 @@
"textInput": {
"placeholder": "Domain der Instanz"
},
"whitelisted": "",
"button": "Login",
"information": {
"name": "Name",

View File

@ -91,7 +91,12 @@
"content": {
"expandHint": "Ausgeblendeter Inhalt"
},
"filtered": "Gefiltert: {{phrase}}.",
"filtered": {
"reveal": "Trotzdem anzeigen",
"match_v1": "Gefiltert: {{phrase}}.",
"match_v2_one": "Gefiltert durch {{filters}}.",
"match_v2_other": "Gefiltert durch {{count}} Filter, {{filters}}."
},
"fullConversation": "Unterhaltung anzeigen",
"translate": {
"default": "Übersetzen",

View File

@ -1,8 +1,7 @@
{
"screenshot": {
"title": "Datenschutz",
"message": "Bitte geben Sie nicht die Identität anderer Nutzer preis, wie z. B. Benutzername, Avatar, etc. Vielen Dank!",
"button": "Bestätigen"
"message": "Bitte geben Sie nicht die Identität anderer Nutzer preis, wie z. B. Benutzername, Avatar, etc. Vielen Dank!"
},
"localCorrupt": {
"message": "Login abgelaufen, bitte erneut anmelden"

View File

@ -7,7 +7,8 @@
"continue": "Continue",
"create": "Create",
"delete": "Delete",
"done": "Done"
"done": "Done",
"confirm": "Confirm"
},
"customEmoji": {
"accessibilityLabel": "Custom emoji {{emoji}}"

View File

@ -13,10 +13,16 @@
},
"block": {
"action_false": "Block user",
"action_true": "Unblock user"
"action_true": "Unblock user",
"alert": {
"title": "Confirm blocking user @{{username}} ?"
}
},
"reports": {
"action": "Report and block user"
"action": "Report and block user",
"alert": {
"title": "Confirm report and blocking user @{{username}} ?"
}
}
},
"at": {
@ -33,10 +39,7 @@
"action": "Block instance {{instance}}",
"alert": {
"title": "Confirm blocking instance {{instance}} ?",
"message": "Mostly you can mute or block certain user.\n\nAfter blocking instance, all its content including followers from this instance will be removed!",
"buttons": {
"confirm": "Confirm"
}
"message": "Mostly you can mute or block certain user.\n\nAfter blocking instance, all its content including followers from this instance will be removed!"
}
}
},
@ -57,20 +60,14 @@
"action": "Delete toot",
"alert": {
"title": "Confirm deleting?",
"message": "All boosts and favourites will be cleared, including all replies.",
"buttons": {
"confirm": "Confirm"
}
"message": "All boosts and favourites will be cleared, including all replies."
}
},
"deleteEdit": {
"action": "Delete toot and repost",
"alert": {
"title": "Confirm deleting and repost?",
"message": "All boosts and favourites will be cleared, including all replies.",
"buttons": {
"confirm": "Confirm"
}
"message": "All boosts and favourites will be cleared, including all replies."
}
},
"mute": {

View File

@ -3,6 +3,7 @@
"textInput": {
"placeholder": "Instance's domain"
},
"whitelisted": "This may be a whitelisted instance that tooot cannot retrieve data from before logging in.",
"button": "Login",
"information": {
"name": "Name",

View File

@ -91,7 +91,12 @@
"content": {
"expandHint": "Hidden content"
},
"filtered": "Filtered: {{phrase}}.",
"filtered": {
"reveal": "Show anyway",
"match_v1": "Filtered: {{phrase}}.",
"match_v2_one": "Filtered by {{filters}}.",
"match_v2_other": "Filtered by {{count}} filters, {{filters}}."
},
"fullConversation": "Read conversations",
"translate": {
"default": "Translate",

View File

@ -1,8 +1,7 @@
{
"screenshot": {
"title": "Privacy Protection",
"message": "Please do not disclose other user's identity, such as username, avatar, etc. Thank you!",
"button": "Confirm"
"message": "Please do not disclose other user's identity, such as username, avatar, etc. Thank you!"
},
"localCorrupt": {
"message": "Login expired, please login again"

View File

@ -7,7 +7,8 @@
"continue": "Continuar",
"create": "Crear",
"delete": "Borrar",
"done": "Hecho"
"done": "Hecho",
"confirm": "Confirmar"
},
"customEmoji": {
"accessibilityLabel": "Emoji personalizado {{emoji}}"

View File

@ -13,10 +13,16 @@
},
"block": {
"action_false": "Bloquear usuario",
"action_true": "Desbloquear usuario"
"action_true": "Desbloquear usuario",
"alert": {
"title": ""
}
},
"reports": {
"action": "Reportar y bloquear usuario"
"action": "Reportar y bloquear usuario",
"alert": {
"title": ""
}
}
},
"at": {
@ -33,10 +39,7 @@
"action": "Bloquear instancia {{instance}}",
"alert": {
"title": "¿Confirmar bloqueo de la instancia {{instance}}?",
"message": "Puedes silenciar o bloquear a un usuario.\n\nTras bloquear una instancia, todo su contenido junto con sus seguidores se eliminarán.",
"buttons": {
"confirm": "Confirmar"
}
"message": "Puedes silenciar o bloquear a un usuario.\n\nTras bloquear una instancia, todo su contenido junto con sus seguidores se eliminarán."
}
}
},
@ -57,20 +60,14 @@
"action": "Eliminar toot",
"alert": {
"title": "¿Confirmar eliminación?",
"message": "Todos los boosts y favoritos se eliminarán, incluidas todas las respuestas.",
"buttons": {
"confirm": "Confirmar"
}
"message": "Todos los boosts y favoritos se eliminarán, incluidas todas las respuestas."
}
},
"deleteEdit": {
"action": "Eliminar toot y volver a publicar",
"alert": {
"title": "¿Confirmar eliminación y volver a publicar?",
"message": "Todos los boosts y favoritos se eliminarán, incluidas todas las respuestas.",
"buttons": {
"confirm": "Confirmar"
}
"message": "Todos los boosts y favoritos se eliminarán, incluidas todas las respuestas."
}
},
"mute": {

View File

@ -1,8 +1,9 @@
{
"server": {
"textInput": {
"placeholder": ""
"placeholder": "Dominio de la instancia"
},
"whitelisted": "Puede ser que la instancia esté en una lista blanca por la que tooot no pueda obtener los datos antes de iniciar la sesión.",
"button": "Iniciar sesión",
"information": {
"name": "Nombre",

View File

@ -31,7 +31,7 @@
"notification": "{{name}} ha impulsado tu toot"
},
"update": "El impulso ha sido editado",
"admin.sign_up": "",
"admin.sign_up": "{{name}} se unió a la instancia",
"admin.report": ""
},
"actions": {
@ -55,7 +55,7 @@
"accessibilityLabel": "Añadir este toot en marcadores",
"function": "Añadir toot a marcadores"
},
"openReport": ""
"openReport": "Abrir denuncia"
},
"actionsUsers": {
"reblogged_by": {
@ -91,7 +91,12 @@
"content": {
"expandHint": "Contenido oculto"
},
"filtered": "Filtrado: {{phrase}}.",
"filtered": {
"reveal": "Mostrar de todos modos",
"match_v1": "Filtrado: {{phrase}}.",
"match_v2_one": "Filtrado por {{filters}}.",
"match_v2_other": "Filtrado por {{count}} filtros, {{filters}}."
},
"fullConversation": "Leer conversaciones",
"translate": {
"default": "Traducir",

View File

@ -1,8 +1,7 @@
{
"screenshot": {
"title": "Protección de la privacidad",
"message": "Por favor, no revele la identidad de otros usuarios, como el nombre de usuario, avatar, etc. ¡Gracias!",
"button": "Confirmar"
"message": "Por favor, no revele la identidad de otros usuarios, como el nombre de usuario, avatar, etc. ¡Gracias!"
},
"localCorrupt": {
"message": "La sesión se ha expirado. Por favor, vuelve a iniciar sesión"

View File

@ -15,7 +15,7 @@
"conversation": "Mensaje privado",
"reply": "Respuesta al toot",
"deleteEdit": "Toot",
"edit": "Toot",
"edit": "Edita el toot",
"share": "Toot"
},
"alert": {

View File

@ -399,7 +399,7 @@
"reblogged_by": "{{count}} impulsados",
"favourited_by": "{{count}} favoritos"
},
"resultIncomplete": ""
"resultIncomplete": "Los resultados de una instancia remota están incompletos"
}
}
}

View File

@ -3,11 +3,12 @@
"OK": "Ok",
"apply": "Confirmer",
"cancel": "Annuler",
"discard": "Ne pas tenir compte",
"discard": "Abandonner",
"continue": "Continuer",
"create": "",
"delete": "",
"done": ""
"create": "Créer",
"delete": "Supprimer",
"done": "Fait",
"confirm": "Confirmer"
},
"customEmoji": {
"accessibilityLabel": "Émoji personnalisé {{emoji}}"
@ -20,12 +21,12 @@
"message": ""
},
"error": {
"message": "Échec de la connexion, veuillez réessayer"
"message": "{{function}} a échoué, veuillez réessayer"
}
},
"separator": ", ",
"discard": {
"title": "Modifications non sauvegardées",
"message": "Votre modification n'a pas été enregistrée. Voulez-vous annuler l'enregistrement des modifications ?"
"message": "Votre modification n'a pas été enregistrée. Voulez-vous renoncer à enregistrer les modifications ?"
}
}

View File

@ -4,24 +4,30 @@
"title": "Actions de l'utilisateur",
"following": {
"action_false": "Suivre l'utilisateur",
"action_true": ""
"action_true": "Ne plus suivre l'utilisateur"
},
"inLists": "",
"inLists": "Gérer l'utilisateur des listes",
"mute": {
"action_false": "Rendre muet l'utilisateur",
"action_true": "Rendre la parole"
},
"block": {
"action_false": "Bloquer l'utilisateur",
"action_true": "Débloquer l'utilisateur"
"action_true": "Débloquer l'utilisateur",
"alert": {
"title": ""
}
},
"reports": {
"action": ""
"action": "Signaler et bloquer un utilisateur",
"alert": {
"title": ""
}
}
},
"at": {
"direct": "Message direct",
"public": ""
"public": "Message public"
},
"copy": {
"action": "Copier le Pouet",
@ -33,10 +39,7 @@
"action": "Bloquer l'instance {{instance}}",
"alert": {
"title": "Confirmer le blocage de l'instance {{instance}}?",
"message": "Vous pouvez masquer ou bloquer certains utilisateurs.\n\nAprès avoir bloqué l'instance, tout son contenu, y compris les followers de cette instance, sera supprimé !",
"buttons": {
"confirm": "Confirmer"
}
"message": "Vous pouvez masquer ou bloquer certains utilisateurs.\n\nAprès avoir bloqué l'instance, tout son contenu, y compris les followers de cette instance, sera supprimé !"
}
}
},
@ -57,20 +60,14 @@
"action": "Supprimer le pouet",
"alert": {
"title": "Confirmer la suppression ?",
"message": "Tous les boosts et favoris seront effacés, y compris toutes les réponses.",
"buttons": {
"confirm": "Confirmer"
}
"message": "Tous les boosts et favoris seront effacés, y compris toutes les réponses."
}
},
"deleteEdit": {
"action": "Supprimer le pouet et le republié",
"alert": {
"title": "Confirmer la suppression et le repost ?",
"message": "Tous les boosts et favoris seront effacés, y compris toutes les réponses.",
"buttons": {
"confirm": "Confirmer"
}
"message": "Tous les boosts et favoris seront effacés, y compris toutes les réponses."
}
},
"mute": {

View File

@ -1,8 +1,9 @@
{
"server": {
"textInput": {
"placeholder": ""
"placeholder": "URL de linstance"
},
"whitelisted": "Il peut sagir dune instance sur liste blanche à partir de laquelle tooot ne peut pas récupérer de données avant de se connecter.",
"button": "Connexion",
"information": {
"name": "Nom",
@ -20,7 +21,7 @@
"update": {
"alert": {
"title": "Connecté à cette instance",
"message": "Vous pouvez vous connecter à un autre compte, en maintenant un compte connecté existant"
"message": "Vous pouvez vous connecter à un autre compte, en conservant le compte connecté existant"
}
}
}

View File

@ -1,6 +1,6 @@
{
"HTML": {
"accessibilityHint": "Appuyez pour agrandir ou réduire le contenu",
"accessibilityHint": "Touchez pour développer ou réduire le contenu",
"expanded": "{{hint}}{{moreLines}}",
"moreLines": " ({{count}} lignes en plus)",
"defaultHint": "Pouet long"

View File

@ -5,7 +5,7 @@
"button": "Réessayer"
},
"success": {
"message": "La chronologie est vide"
"message": "Le fil est vide"
}
},
"end": {
@ -31,8 +31,8 @@
"notification": "{{name}} a partagé votre message"
},
"update": "Le reblog a été modifié",
"admin.sign_up": "",
"admin.report": ""
"admin.sign_up": "{{name}} a rejoint l'instance",
"admin.report": "{{name}} a signalé:"
},
"actions": {
"reply": {
@ -40,7 +40,7 @@
},
"reblogged": {
"accessibilityLabel": "Partager ce pouet",
"function": "Pouet de Boost",
"function": "Pouet de boost",
"options": {
"title": "Choisir la visibilité du boost",
"public": "Boost public",
@ -49,13 +49,13 @@
},
"favourited": {
"accessibilityLabel": "Ajouter ce pouet aux favoris",
"function": "Mettre le pouet en favori"
"function": "Pouet en favoris"
},
"bookmarked": {
"accessibilityLabel": "Ajouter ce pouet aux signets",
"function": "Pouet de signet"
"accessibilityLabel": "Ajouter ce pouet aux marque-pages",
"function": "Pouet en marque-pages"
},
"openReport": ""
"openReport": "Ouvrir un rapport"
},
"actionsUsers": {
"reblogged_by": {
@ -91,7 +91,12 @@
"content": {
"expandHint": "Contenu masqué"
},
"filtered": "Filtré: {{phrase}}.",
"filtered": {
"reveal": "Montrer quand même",
"match_v1": "Filtré : {{phrase}}.",
"match_v2_one": "Filtré par {{filters}}.",
"match_v2_other": "Filtré par {{count}} filtres, {{filters}}."
},
"fullConversation": "Conversations lues",
"translate": {
"default": "Traduire",
@ -104,7 +109,7 @@
"shared": {
"account": {
"name": {
"accessibilityHint": "Nom de l'utilisateur"
"accessibilityHint": "Nom d'affichage de l'utilisateur"
},
"account": {
"accessibilityHint": "Compte de l'utilisateur"
@ -119,7 +124,7 @@
},
"visibility": {
"direct": {
"accessibilityLabel": "Envoyer un message direct"
"accessibilityLabel": "Envoyer un message privé"
},
"private": {
"accessibilityLabel": "Visible uniquement pour les abonné·e·s"

View File

@ -1,8 +1,7 @@
{
"screenshot": {
"title": "Protection de la confidentialité",
"message": "Veuillez ne pas divulguer l'identité d'un autre utilisateur, tel que le nom d'utilisateur, l'avatar, etc. Merci!",
"button": "Confirmer"
"message": "Veuillez ne pas divulguer l'identité d'un autre utilisateur, tel que le nom d'utilisateur, l'avatar, etc. Merci!"
},
"localCorrupt": {
"message": "Session expirée, veuillez ré-essayer"

View File

@ -12,7 +12,7 @@
"right": {
"button": {
"default": "Pouet",
"conversation": "Pouet DM",
"conversation": "Pouet MP",
"reply": "Réponse de pouet",
"deleteEdit": "Pouet",
"edit": "Pouet",
@ -24,8 +24,8 @@
"button": "Réessayer"
},
"removeReply": {
"title": "Le pouet répondu est introuvable",
"description": "Le pouet répondu a peut-être été supprimé. Voulez-vous le supprimer de votre référence ?",
"title": "Le pouet réponse est introuvable",
"description": "Le pouet réponse a peut-être été supprimé. Voulez-vous le supprimer de votre référence ?",
"confirm": "Supprimer la référence"
}
}
@ -62,7 +62,7 @@
}
},
"emojis": {
"accessibilityHint": "Tapotez pour ajouter des émojis au pouet"
"accessibilityHint": "Appuyez pour ajouter des émojis au pouet"
},
"poll": {
"option": {
@ -90,7 +90,7 @@
}
},
"expiration": {
"heading": "Validité",
"heading": "Durée",
"options": {
"300": "5 minutes",
"1800": "30 minutes",
@ -106,7 +106,7 @@
"actions": {
"attachment": {
"accessibilityLabel": "Téléchargez une pièce-jointe",
"accessibilityHint": "La fonction de sondage sera désactivée lorsqu'il y a une pièce jointe",
"accessibilityHint": "La fonction de sondage sera désactivée s'il y a une pièce jointe",
"failed": {
"alert": {
"title": "Le téléchargement a échoué",

View File

@ -3,15 +3,15 @@
"local": {
"name": "Suit",
"options": {
"showBoosts": "",
"showReplies": ""
"showBoosts": "Afficher les boosts",
"showReplies": "Afficher les réponses"
}
},
"public": {
"segments": {
"federated": "Fédéré",
"local": "Local",
"trending": ""
"trending": "Tendance"
}
},
"notifications": {
@ -28,7 +28,7 @@
"filters": {
"accessibilityLabel": "Filtrer",
"accessibilityHint": "Filtrer les types de notifications affichés",
"title": "",
"title": "Afficher les notifications",
"options": {
"follow": "$t(screenTabs:me.push.follow.heading)",
"follow_request": "Demande d'abonnement",
@ -55,7 +55,7 @@
"name": "Favoris"
},
"followedTags": {
"name": ""
"name": "Hashtags suivis"
},
"fontSize": {
"name": "Taille de la police de Pouet"
@ -67,25 +67,25 @@
"name": "Liste : {{list}}"
},
"listAccounts": {
"name": ""
"name": "Utilisateurs dans la liste : {{list}}"
},
"listAdd": {
"name": ""
"name": "Créer une liste"
},
"listEdit": {
"name": ""
"name": "Modifier les détails de la liste"
},
"lists": {
"name": "Listes"
},
"push": {
"name": "Push de Notification"
"name": "Push de notification"
},
"profile": {
"name": "Modifier le profil"
},
"profileName": {
"name": "Editer le nom d'affichage"
"name": "Éditer le nom d'affichage"
},
"profileNote": {
"name": "Éditer la description"
@ -114,33 +114,33 @@
}
},
"listAccounts": {
"heading": "",
"error": "",
"empty": ""
"heading": "Gérer les utilisateurs",
"error": "Supprimer l'utilisateur de la liste",
"empty": "Aucun utilisateur ajouté dans cette liste"
},
"listEdit": {
"heading": "",
"title": "",
"heading": "Modifier les détails de la liste",
"title": "Titre",
"repliesPolicy": {
"heading": "",
"heading": "Montrer les réponses à :",
"options": {
"none": "",
"list": "",
"followed": ""
"none": "Personne",
"list": "Un membre de la liste",
"followed": "N'importe quel utilisateur suivi"
}
}
},
"listDelete": {
"heading": "",
"heading": "Supprimer la liste",
"confirm": {
"title": "",
"message": ""
"title": "Supprimer la liste \"{{list}}\" ?",
"message": "Cette action ne peut être annulé."
}
},
"profile": {
"feedback": {
"succeed": "{{type}} mis à jour",
"failed": "{{type}} Échec de la mise à jour, veuillez ré-essayer"
"failed": "La mise à jour de {{type}} a échoué, veuillez réessayer"
},
"root": {
"name": {
@ -178,13 +178,13 @@
"description": "Nécessite que vous approuviez manuellement chaque abonné·e"
},
"bot": {
"title": "Compte Bot",
"title": "Compte bot",
"description": "Ce compte effectue principalement des actions automatisées et peut ne pas être surveillé"
}
},
"fields": {
"group": "Groupe {{index}}",
"label": "Étiquette",
"label": "Libellé",
"content": "Contenu"
},
"mediaSelectionFailed": "Le traitement de l'image a échoué. Veuillez réessayer."
@ -196,8 +196,8 @@
"settings": "Activer dans les paramètres"
},
"missingServerKey": {
"message": "",
"description": ""
"message": "Le serveur n'est pas configuré pour le push",
"description": "Veuillez contacter l'administrateur de votre serveur pour configurer le support push"
},
"global": {
"heading": "Activer pour {{acct}}",
@ -205,13 +205,13 @@
},
"decode": {
"heading": "Détails du message",
"description": "Les messages acheminés par le serveur de tooot sont chiffrés, mais vous pouvez choisir de décoder le message sur le serveur. Le code source de notre serveur est open source et aucune politique de log."
"description": "Les messages acheminés par le serveur de tooot sont chiffrés, mais vous pouvez choisir de décoder le message sur le serveur. Le code source de notre serveur est open source et il n'y a aucune politique de log."
},
"default": {
"heading": "Par défaut"
},
"follow": {
"heading": "Nouvel abonné"
"heading": "Nouveau⋅elle abonné⋅e"
},
"follow_request": {
"heading": "Demande d'abonnement"
@ -229,16 +229,16 @@
"heading": "Mise à jour du sondage"
},
"status": {
"heading": "Pouet des utilisateurs inscrits"
"heading": "Pouet des utilisateurs auxquels vous êtes abonnés"
},
"update": {
"heading": ""
"heading": "Le reblog a été édité"
},
"admin.sign_up": {
"heading": ""
"heading": "Admin : s'inscrire"
},
"admin.report": {
"heading": ""
"heading": "Admin : signalement"
},
"howitworks": "Apprenez comment cela fonctionne"
},
@ -261,7 +261,7 @@
"button": "Se déconnecter",
"alert": {
"title": "Déconnexion?",
"message": "Après vous être déconnecté, vous devez vous reconnecter",
"message": "Après vous être déconnecté, vous devrez vous reconnecter",
"buttons": {
"logout": "Déconnexion"
}
@ -317,7 +317,7 @@
"contact": {
"heading": "Contacter tooot"
},
"version": "Version {{version}}",
"version": "Version v{{version}}",
"instanceVersion": "Version de Mastodon v{{version}}"
},
"switch": {
@ -346,12 +346,12 @@
"suspended": "Compte suspendu par les modérateurs de votre serveur"
},
"accountInLists": {
"name": "",
"inLists": "",
"notInLists": ""
"name": "Listes de @{{username}}",
"inLists": "Dans les listes",
"notInLists": "Autres listes"
},
"attachments": {
"name": ""
"name": "Média de <0 /><1></1>"
},
"hashtag": {
"follow": "Suivre",
@ -377,7 +377,7 @@
}
},
"trending": {
"tags": ""
"tags": "Tags tendance"
}
},
"sections": {
@ -399,7 +399,7 @@
"reblogged_by": "{{count}} boosté",
"favourited_by": "{{count}} mis en favori"
},
"resultIncomplete": ""
"resultIncomplete": "Les résultats d'une instance distante sont incomplets"
}
}
}

View File

@ -7,7 +7,8 @@
"continue": "Continua",
"create": "",
"delete": "",
"done": ""
"done": "",
"confirm": "Ho capito"
},
"customEmoji": {
"accessibilityLabel": "Emoji personalizzata {{emoji}}"

View File

@ -13,10 +13,16 @@
},
"block": {
"action_false": "Blocca utente",
"action_true": "Sblocca utente"
"action_true": "Sblocca utente",
"alert": {
"title": ""
}
},
"reports": {
"action": ""
"action": "",
"alert": {
"title": ""
}
}
},
"at": {
@ -33,10 +39,7 @@
"action": "Blocca istanza {{instance}}",
"alert": {
"title": "Confermi di voler bloccare l'istanza {{instance}}?",
"message": "Sarebbe meglio mutare o bloccare singoli utenti.\n\nSe blocchi un'istanza, tutti i suoi contenuti a te relativi, inclusi tutti i tuoi seguaci da questa, saranno rimossi.",
"buttons": {
"confirm": "Ho capito"
}
"message": "Sarebbe meglio mutare o bloccare singoli utenti.\n\nSe blocchi un'istanza, tutti i suoi contenuti a te relativi, inclusi tutti i tuoi seguaci da questa, saranno rimossi."
}
}
},
@ -57,20 +60,14 @@
"action": "Cancella toot",
"alert": {
"title": "Conferma?",
"message": "Tutti i retoot, gli apprezzamenti, e le risposte, saranno cancellati.",
"buttons": {
"confirm": "Ho capito"
}
"message": "Tutti i retoot, gli apprezzamenti, e le risposte, saranno cancellati."
}
},
"deleteEdit": {
"action": "Cancella e ripubblica toot",
"alert": {
"title": "Confermi cancellazione e ripubblicazione?",
"message": "Tutti i retoot, gli apprezzamenti, e le risposte, saranno cancellati.",
"buttons": {
"confirm": "Ho capito"
}
"message": "Tutti i retoot, gli apprezzamenti, e le risposte, saranno cancellati."
}
},
"mute": {

View File

@ -3,6 +3,7 @@
"textInput": {
"placeholder": ""
},
"whitelisted": "",
"button": "Accedi",
"information": {
"name": "Nome",

View File

@ -91,7 +91,12 @@
"content": {
"expandHint": "Contenuto nascosto"
},
"filtered": "Filtrato: {{phrase}}.",
"filtered": {
"reveal": "",
"match_v1": "",
"match_v2_one": "",
"match_v2_other": ""
},
"fullConversation": "Leggi la conversazione",
"translate": {
"default": "Traduci",

View File

@ -1,8 +1,7 @@
{
"screenshot": {
"title": "Tutela della privacy",
"message": "Per favore, non rivelare l'identità degli altri utenti (nome, foto profilo, ecc..). Grazie!",
"button": "Ho capito"
"message": "Per favore, non rivelare l'identità degli altri utenti (nome, foto profilo, ecc..). Grazie!"
},
"localCorrupt": {
"message": "La sessione è scaduta, devi riaccedere"

View File

@ -5,9 +5,10 @@
"cancel": "キャンセル",
"discard": "変更を破棄",
"continue": "続ける",
"create": "",
"create": "作成",
"delete": "削除",
"done": "完了"
"done": "完了",
"confirm": "確認"
},
"customEmoji": {
"accessibilityLabel": "カスタム絵文字 {{emoji}}"

View File

@ -13,10 +13,16 @@
},
"block": {
"action_false": "ユーザーをブロック",
"action_true": "ユーザーのブロックを解除"
"action_true": "ユーザーのブロックを解除",
"alert": {
"title": ""
}
},
"reports": {
"action": "ユーザーの報告とブロック"
"action": "ユーザーの報告とブロック",
"alert": {
"title": ""
}
}
},
"at": {
@ -33,10 +39,7 @@
"action": "インスタンスをブロック {{instance}}",
"alert": {
"title": "インスタンス {{instance}} をブロックしますか?",
"message": "ほとんどの場合、特定のユーザーをミュートまたはブロックすることができます。\n\nインスタンスをブロックすると、このインスタンスからフォロワーを含むすべてのコンテンツが削除されます",
"buttons": {
"confirm": "確定"
}
"message": "ほとんどの場合、特定のユーザーをミュートまたはブロックすることができます。\n\nインスタンスをブロックすると、このインスタンスからフォロワーを含むすべてのコンテンツが削除されます"
}
}
},
@ -57,20 +60,14 @@
"action": "トゥートを削除",
"alert": {
"title": "削除しますか?",
"message": "この投稿へのすべてのお気に入り登録やブーストは消去され、すべての返信は孤立することになります。",
"buttons": {
"confirm": "確定"
}
"message": "この投稿へのすべてのお気に入り登録やブーストは消去され、すべての返信は孤立することになります。"
}
},
"deleteEdit": {
"action": "トゥートを削除し、再投稿する",
"alert": {
"title": "削除して再投稿しますか?",
"message": "この投稿へのすべてのお気に入り登録やブーストは消去され、すべての返信は孤立することになります。",
"buttons": {
"confirm": "確定"
}
"message": "この投稿へのすべてのお気に入り登録やブーストは消去され、すべての返信は孤立することになります。"
}
},
"mute": {

View File

@ -3,6 +3,7 @@
"textInput": {
"placeholder": "インスタンスのドメイン"
},
"whitelisted": "",
"button": "ログイン",
"information": {
"name": "名前",

View File

@ -91,7 +91,12 @@
"content": {
"expandHint": "内容を非表示にする"
},
"filtered": "フィルター: {{phrase}}.",
"filtered": {
"reveal": "",
"match_v1": "",
"match_v2_one": "",
"match_v2_other": ""
},
"fullConversation": "スレッドを読む",
"translate": {
"default": "翻訳",

View File

@ -1,8 +1,7 @@
{
"screenshot": {
"title": "プライバシー保護",
"message": "ユーザー名やアバターなど、他のユーザーを特定する情報は公開しないでください。",
"button": "確認"
"message": "ユーザー名やアバターなど、他のユーザーを特定する情報は公開しないでください。"
},
"localCorrupt": {
"message": "ログインの有効期限が切れました。もう一度ログインしてください。"

View File

@ -5,9 +5,10 @@
"cancel": "취소",
"discard": "취소",
"continue": "계속",
"create": "",
"create": "생성",
"delete": "삭제",
"done": "완료"
"done": "완료",
"confirm": "확인"
},
"customEmoji": {
"accessibilityLabel": "커스텀 에모지 {{emoji}}"

View File

@ -13,10 +13,16 @@
},
"block": {
"action_false": "사용자 차단",
"action_true": "사용자 차단 해제"
"action_true": "사용자 차단 해제",
"alert": {
"title": ""
}
},
"reports": {
"action": "사용자 신고 및 차단"
"action": "사용자 신고 및 차단",
"alert": {
"title": ""
}
}
},
"at": {
@ -33,10 +39,7 @@
"action": "{{instance}} 인스턴스 차단",
"alert": {
"title": "정말 {{instance}} 인스턴스를 차단할까요?",
"message": "보통은 사용자 뮤트나 차단으로 충분해요.\n\n인스턴스를 차단하면, 팔로워를 포함한 인스턴스의 모든 콘텐츠가 삭제됩니다!",
"buttons": {
"confirm": "확인"
}
"message": "보통은 사용자 뮤트나 차단으로 충분해요.\n\n인스턴스를 차단하면, 팔로워를 포함한 인스턴스의 모든 콘텐츠가 삭제됩니다!"
}
}
},
@ -57,20 +60,14 @@
"action": "툿 삭제",
"alert": {
"title": "정말 삭제할까요?",
"message": "답장을 포함한 모든 부스트와 즐겨찾기가 지워져요.",
"buttons": {
"confirm": "확인"
}
"message": "답장을 포함한 모든 부스트와 즐겨찾기가 지워져요."
}
},
"deleteEdit": {
"action": "툿 삭제 후 다시 게시",
"alert": {
"title": "툿을 정말 삭제하고 다시 게시할까요?",
"message": "답장을 포함한 모든 부스트와 즐겨찾기가 지워져요.",
"buttons": {
"confirm": "확인"
}
"message": "답장을 포함한 모든 부스트와 즐겨찾기가 지워져요."
}
},
"mute": {

View File

@ -1,8 +1,9 @@
{
"server": {
"textInput": {
"placeholder": ""
"placeholder": "인스턴스 도메인"
},
"whitelisted": "화이트리스트 등록이 필요한 인스턴스에서 tooot이 정보를 읽어올 수 없는 문제일 수 있습니다.",
"button": "로그인",
"information": {
"name": "이름",

View File

@ -31,8 +31,8 @@
"notification": "{{name}} 님이 내 툿을 부스트했어요"
},
"update": "부스트한 툿이 수정됨",
"admin.sign_up": "",
"admin.report": ""
"admin.sign_up": "{{name}} 님이 인스턴스에 가입함",
"admin.report": "{{name}} 님의 신고:"
},
"actions": {
"reply": {
@ -55,7 +55,7 @@
"accessibilityLabel": "툿 북마크에 추가",
"function": "툿 북마크"
},
"openReport": ""
"openReport": "신고 열기"
},
"actionsUsers": {
"reblogged_by": {
@ -91,7 +91,12 @@
"content": {
"expandHint": "숨겨진 콘텐츠"
},
"filtered": "필터: {{phrase}}.",
"filtered": {
"reveal": "무시하고 보기",
"match_v1": "필터됨: {{phrase}}.",
"match_v2_one": "{{filters}}에 의해 필터됨.",
"match_v2_other": "{{count}}개의 필터 {{filters}}에 의해 필터됨."
},
"fullConversation": "대화 보기",
"translate": {
"default": "번역",

View File

@ -1,8 +1,7 @@
{
"screenshot": {
"title": "개인정보 보호",
"message": "다른 사용자의 이름이나, 프로필 사진 등의 정보를 유출하지 말아주세요. 고마워요!",
"button": "확인"
"message": "다른 사용자의 이름이나, 프로필 사진 등의 정보를 유출하지 말아주세요. 고마워요!"
},
"localCorrupt": {
"message": "로그인이 만료되었어요. 다시 로그인해주세요"

View File

@ -3,15 +3,15 @@
"local": {
"name": "팔로우 중",
"options": {
"showBoosts": "",
"showReplies": ""
"showBoosts": "부스트 표시",
"showReplies": "답글 표시"
}
},
"public": {
"segments": {
"federated": "연합",
"local": "로컬",
"trending": ""
"trending": "유행"
}
},
"notifications": {
@ -28,7 +28,7 @@
"filters": {
"accessibilityLabel": "필터",
"accessibilityHint": "받는 알림 종류 선택",
"title": "",
"title": "알림 표시",
"options": {
"follow": "$t(screenTabs:me.push.follow.heading)",
"follow_request": "팔로우 요청",
@ -55,7 +55,7 @@
"name": "즐겨찾기"
},
"followedTags": {
"name": ""
"name": "팔로우 중인 해시태그"
},
"fontSize": {
"name": "툿 폰트 크기"
@ -235,10 +235,10 @@
"heading": "부스트한 툿이 수정됨"
},
"admin.sign_up": {
"heading": ""
"heading": "관리자: 신규 가입"
},
"admin.report": {
"heading": ""
"heading": "관리자: 신고 요청"
},
"howitworks": "메시지 라우팅 방식 더 알아보기"
},
@ -399,7 +399,7 @@
"reblogged_by": "{{count}} 부스트",
"favourited_by": "{{count}} 즐겨찾기"
},
"resultIncomplete": ""
"resultIncomplete": "원격 인스턴스의 응답 형태가 올바르지 않아요"
}
}
}

View File

@ -7,7 +7,8 @@
"continue": "Ga verder",
"create": "Maak",
"delete": "Verwijder",
"done": "Gereed"
"done": "Gereed",
"confirm": "Bevestig"
},
"customEmoji": {
"accessibilityLabel": "Aangepaste emoji {{emoji}}"

View File

@ -13,10 +13,16 @@
},
"block": {
"action_false": "Gebruiker blokkeren",
"action_true": "Gebruiker deblokkeren"
"action_true": "Gebruiker deblokkeren",
"alert": {
"title": "Bevestig blokkeren van @{{username}} ?"
}
},
"reports": {
"action": "Rapporteren en blokkeren"
"action": "Rapporteren en blokkeren",
"alert": {
"title": "Bevestig rapporteren en blokkeren van @{{username}} ?"
}
}
},
"at": {
@ -33,10 +39,7 @@
"action": "Blokkeer instantie {{instance}}",
"alert": {
"title": "Bevestig blokkeren van {{instance}} ?",
"message": "Meestal kunt u bepaalde gebruiker dempen of blokkeren.\n\nNa het blokkeren van de instantie, wordt alle inhoud inclusief volgers van deze instantie verwijderd!",
"buttons": {
"confirm": "Bevestig"
}
"message": "Meestal kunt u bepaalde gebruiker dempen of blokkeren.\n\nNa het blokkeren van de instantie, wordt alle inhoud inclusief volgers van deze instantie verwijderd!"
}
}
},
@ -57,20 +60,14 @@
"action": "Toot verwijderen",
"alert": {
"title": "Verwijderen bevestigen?",
"message": "Alle boosts en favorieten zullen worden gewist, inclusief alle antwoorden.",
"buttons": {
"confirm": "Bevestig"
}
"message": "Alle boosts en favorieten zullen worden gewist, inclusief alle antwoorden."
}
},
"deleteEdit": {
"action": "Toot verwijderen en opnieuw plaatsen",
"alert": {
"title": "Bevestig verwijderen en opnieuw plaatsen?",
"message": "Alle boosts en favorieten zullen worden gewist, inclusief alle antwoorden.",
"buttons": {
"confirm": "Bevestig"
}
"message": "Alle boosts en favorieten zullen worden gewist, inclusief alle antwoorden."
}
},
"mute": {

View File

@ -3,6 +3,7 @@
"textInput": {
"placeholder": "Domeinnaam van instantie"
},
"whitelisted": "Dit kan een gewhiteliste instantie zijn waarvan tooot geen gegevens kan ophalen voordat er ingelogd is.",
"button": "Inloggen",
"information": {
"name": "Naam",

View File

@ -91,7 +91,12 @@
"content": {
"expandHint": "Verborgen inhoud"
},
"filtered": "Gefilterd: {{phrase}}.",
"filtered": {
"reveal": "Toch weergeven",
"match_v1": "Gefilterd: {{phrase}}.",
"match_v2_one": "Gefilterd door {{filters}}.",
"match_v2_other": "Gefilterd door {{count}} filters, {{filters}}."
},
"fullConversation": "Gesprekken lezen",
"translate": {
"default": "Vertaal",

View File

@ -1,8 +1,7 @@
{
"screenshot": {
"title": "Privacy Bescherming",
"message": "Gelieve de identiteit van een andere gebruiker niet openbaar te maken, zoals gebruikersnaam of avatar en meer. Bedankt!",
"button": "Bevestig"
"message": "Gelieve de identiteit van een andere gebruiker niet openbaar te maken, zoals gebruikersnaam of avatar en meer. Bedankt!"
},
"localCorrupt": {
"message": "Sessie verlopen. Log opnieuw in"

View File

@ -7,7 +7,8 @@
"continue": "Dalej",
"create": "",
"delete": "Usuń",
"done": ""
"done": "",
"confirm": ""
},
"customEmoji": {
"accessibilityLabel": "Własne emoji {{emoji}}"

View File

@ -13,10 +13,16 @@
},
"block": {
"action_false": "Zablokuj użytkownika",
"action_true": "Odblokuj użytkownika"
"action_true": "Odblokuj użytkownika",
"alert": {
"title": ""
}
},
"reports": {
"action": "Zgłoś i zablokuj"
"action": "Zgłoś i zablokuj",
"alert": {
"title": ""
}
}
},
"at": {
@ -33,10 +39,7 @@
"action": "Zablokuj instancję {{instance}}",
"alert": {
"title": "Na pewno zablokować {{instance}}?",
"message": "Zazwyczaj wycisza się (albo blokuje) konkretnych użytkowników. \n\nGdy zablokujesz instancję, cała jej zawartość (włączając np. obserwujące Cię osoby, które do niej należą) zostanie usunięta!",
"buttons": {
"confirm": "Na pewno?"
}
"message": "Zazwyczaj wycisza się (albo blokuje) konkretnych użytkowników. \n\nGdy zablokujesz instancję, cała jej zawartość (włączając np. obserwujące Cię osoby, które do niej należą) zostanie usunięta!"
}
}
},
@ -57,20 +60,14 @@
"action": "Usuń wpis",
"alert": {
"title": "Na pewno usunąć?",
"message": "Wszystkie podbite i polubione wpisy zostaną wyczyszczone - wraz z odpowiedziami.",
"buttons": {
"confirm": "Na pewno?"
}
"message": "Wszystkie podbite i polubione wpisy zostaną wyczyszczone - wraz z odpowiedziami."
}
},
"deleteEdit": {
"action": "",
"alert": {
"title": "",
"message": "",
"buttons": {
"confirm": ""
}
"message": ""
}
},
"mute": {

Some files were not shown because too many files have changed in this diff Show More