Merge remote-tracking branch 'origin/develop' into settings-and-filtering

* origin/develop: (169 commits)
  Improve the user card for deactivated users
  Update CHANGELOG.md
  Update CHANGELOG.md
  Allow canceling a follow request
  Simple policy reasons for instance specific policies
  entity_normalizer: Escape name when parsing user
  Translated using Weblate (Spanish)
  Translated using Weblate (Catalan)
  Translated using Weblate (Korean)
  Translated using Weblate (Japanese (ja_PEDANTIC))
  Translated using Weblate (Indonesian)
  Translated using Weblate (Esperanto)
  Translated using Weblate (Vietnamese)
  Translated using Weblate (Italian)
  Translated using Weblate (Vietnamese)
  Translated using Weblate (Indonesian)
  Translated using Weblate (Italian)
  Translated using Weblate (Vietnamese)
  Translated using Weblate (Indonesian)
  Translated using Weblate (Chinese (Simplified))
  ...
This commit is contained in:
Henry Jameson 2022-01-24 19:12:17 +02:00
commit 9ea0f10abb
106 changed files with 5304 additions and 1167 deletions

View file

@ -1,5 +1,5 @@
{
"presets": ["@babel/preset-env"],
"plugins": ["@babel/plugin-transform-runtime", "lodash", "@vue/babel-plugin-transform-vue-jsx"],
"presets": ["@babel/preset-env", "@vue/babel-preset-jsx"],
"plugins": ["@babel/plugin-transform-runtime", "lodash"],
"comments": false
}

View file

@ -3,13 +3,31 @@ All notable changes to this project will be documented in this file.
The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/).
## [Unreleased]
## [2.4.2] - 2022-01-09
### Added
- Added Apply and Reset buttons to the bottom of theme tab to minimize UI travel
- Implemented user option to always show floating New Post button (normally mobile-only)
- Display reasons for instance specific policies
- Added functionality to cancel follow request
### Fixed
- Fixed link to external profile not working on user profiles
- Fixed mobile shoutbox display
- Fixed favicon badge not working in Chrome
- Escape html more properly in subject/display name
## [2.4.0] - 2021-08-08
### Added
- Added a quick settings to timeline header for easier access
- Added option to mark posts as sensitive by default
- Added quick filters for notifications
- Implemented user option to change sidebar position to the right side
- Implemented user option to hide floating shout panel
- Implemented "edit profile" button if viewing own profile which opens profile settings
### Fixed
- Fixed follow request count showing in the wrong location in mobile view
## [2.3.0] - 2021-03-01

View file

@ -3,6 +3,7 @@ Contributors of this project.
- Constance Variable (lambadalambda@social.heldscal.la): Code
- Coco Snuss (cocosnuss@social.heldscal.la): Code
- wakarimasen (wakarimasen@shitposter.club): NSFW hiding image
- eris (eris@disqordia.space): Code
- dtluna (dtluna@social.heldscal.la): Code
- sonyam (sonyam@social.heldscal.la): Background images
- hakui (hakui@freezepeach.xyz): CSS and styling

View file

@ -47,8 +47,8 @@
"@babel/preset-env": "^7.7.6",
"@babel/register": "^7.7.4",
"@ungap/event-target": "^0.1.0",
"@vue/babel-helper-vue-jsx-merge-props": "^1.0.0",
"@vue/babel-plugin-transform-vue-jsx": "^1.1.2",
"@vue/babel-helper-vue-jsx-merge-props": "^1.2.1",
"@vue/babel-preset-jsx": "^1.2.4",
"@vue/test-utils": "^1.0.0-beta.26",
"autoprefixer": "^6.4.0",
"babel-eslint": "^7.0.0",

View file

@ -73,6 +73,9 @@ export default {
this.$store.state.instance.instanceSpecificPanelContent
},
showFeaturesPanel () { return this.$store.state.instance.showFeaturesPanel },
shoutboxPosition () {
return this.$store.getters.mergedConfig.showNewPostButton || false
},
hideShoutbox () {
return this.$store.getters.mergedConfig.hideShoutbox
},

View file

@ -88,6 +88,10 @@ a {
font-family: sans-serif;
font-family: var(--interfaceFont, sans-serif);
&.-sublime {
background: transparent;
}
i[class*=icon-],
.svg-inline--fa {
color: $fallback--text;

View file

@ -53,6 +53,7 @@
v-if="currentUser && shout && !hideShoutbox"
:floating="true"
class="floating-shout mobile-hidden"
:class="{ 'left': shoutboxPosition }"
/>
<MobilePostStatusButton />
<UserReportingModal />

View file

@ -1,5 +1,6 @@
import UserCard from '../user_card/user_card.vue'
import UserAvatar from '../user_avatar/user_avatar.vue'
import RichContent from 'src/components/rich_content/rich_content.jsx'
import generateProfileLink from 'src/services/user_profile_link_generator/user_profile_link_generator'
const BasicUserCard = {
@ -13,7 +14,8 @@ const BasicUserCard = {
},
components: {
UserCard,
UserAvatar
UserAvatar,
RichContent
},
methods: {
toggleUserExpanded () {

View file

@ -25,17 +25,11 @@
:title="user.name"
class="basic-user-card-user-name"
>
<!-- eslint-disable vue/no-v-html -->
<span
v-if="user.name_html"
<RichContent
class="basic-user-card-user-name-value"
v-html="user.name_html"
:html="user.name"
:emoji="user.emoji"
/>
<!-- eslint-enable vue/no-v-html -->
<span
v-else
class="basic-user-card-user-name-value"
>{{ user.name }}</span>
</div>
<div>
<router-link

View file

@ -1,5 +1,5 @@
import { mapState } from 'vuex'
import StatusContent from '../status_content/status_content.vue'
import StatusBody from '../status_content/status_content.vue'
import fileType from 'src/services/file_type/file_type.service'
import UserAvatar from '../user_avatar/user_avatar.vue'
import AvatarList from '../avatar_list/avatar_list.vue'
@ -16,7 +16,7 @@ const ChatListItem = {
AvatarList,
Timeago,
ChatTitle,
StatusContent
StatusBody
},
computed: {
...mapState({
@ -38,12 +38,14 @@ const ChatListItem = {
},
messageForStatusContent () {
const message = this.chat.lastMessage
const messageEmojis = message ? message.emojis : []
const isYou = message && message.account_id === this.currentUser.id
const content = message ? (this.attachmentInfo || message.content) : ''
const messagePreview = isYou ? `<i>${this.$t('chats.you')}</i> ${content}` : content
return {
summary: '',
statusnet_html: messagePreview,
emojis: messageEmojis,
raw_html: messagePreview,
text: messagePreview,
attachments: []
}

View file

@ -77,18 +77,15 @@
border-radius: var(--avatarAltRadius, $fallback--avatarAltRadius);
}
.StatusContent {
img.emoji {
width: 1.4em;
height: 1.4em;
}
.chat-preview-body {
--emoji-size: 1.4em;
}
.time-wrapper {
line-height: 1.4em;
}
.single-line {
.chat-preview-body {
padding-right: 1em;
}
}

View file

@ -29,7 +29,8 @@
</div>
</div>
<div class="chat-preview">
<StatusContent
<StatusBody
class="chat-preview-body"
:status="messageForStatusContent"
:single-line="true"
/>

View file

@ -57,8 +57,9 @@ const ChatMessage = {
messageForStatusContent () {
return {
summary: '',
statusnet_html: this.message.content,
text: this.message.content,
emojis: this.message.emojis,
raw_html: this.message.content || '',
text: this.message.content || '',
attachments: this.message.attachments
}
},

View file

@ -89,8 +89,9 @@
}
.without-attachment {
.status-content {
&::after {
.message-content {
// TODO figure out how to do it properly
.RichContent::after {
margin-right: 5.4em;
content: " ";
display: inline-block;
@ -162,6 +163,7 @@
.visible {
opacity: 1;
}
}
.chat-message-date-separator {

View file

@ -71,6 +71,7 @@
</Popover>
</div>
<StatusContent
class="message-content"
:status="messageForStatusContent"
:full-content="true"
>

View file

@ -1,9 +1,9 @@
<template>
<div
ref="root"
v-click-outside="onClickOutside"
class="emoji-input"
:class="{ 'with-picker': !hideEmojiButton }"
ref='root'
>
<slot />
<template v-if="enableEmojiPicker">

View file

@ -1,6 +1,6 @@
import { requestFollow, requestUnfollow } from '../../services/follow_manipulate/follow_manipulate'
export default {
props: ['relationship', 'labelFollowing', 'buttonClass'],
props: ['relationship', 'user', 'labelFollowing', 'buttonClass'],
data () {
return {
inProgress: false
@ -14,7 +14,7 @@ export default {
if (this.inProgress || this.relationship.following) {
return this.$t('user_card.follow_unfollow')
} else if (this.relationship.requested) {
return this.$t('user_card.follow_again')
return this.$t('user_card.follow_cancel')
} else {
return this.$t('user_card.follow')
}
@ -29,11 +29,14 @@ export default {
} else {
return this.$t('user_card.follow')
}
},
disabled () {
return this.inProgress || this.user.deactivated
}
},
methods: {
onClick () {
this.relationship.following ? this.unfollow() : this.follow()
this.relationship.following || this.relationship.requested ? this.unfollow() : this.follow()
},
follow () {
this.inProgress = true

View file

@ -2,7 +2,7 @@
<button
class="btn button-default follow-button"
:class="{ toggled: isPressed }"
:disabled="inProgress"
:disabled="disabled"
:title="title"
@click="onClick"
>

View file

@ -0,0 +1,36 @@
import { extractTagFromUrl } from 'src/services/matcher/matcher.service.js'
const HashtagLink = {
name: 'HashtagLink',
props: {
url: {
required: true,
type: String
},
content: {
required: true,
type: String
},
tag: {
required: false,
type: String,
default: ''
}
},
methods: {
onClick () {
const tag = this.tag || extractTagFromUrl(this.url)
if (tag) {
const link = this.generateTagLink(tag)
this.$router.push(link)
} else {
window.open(this.url, '_blank')
}
},
generateTagLink (tag) {
return `/tag/${tag}`
}
}
}
export default HashtagLink

View file

@ -0,0 +1,6 @@
.HashtagLink {
position: relative;
white-space: normal;
display: inline-block;
color: var(--link);
}

View file

@ -0,0 +1,19 @@
<template>
<span
class="HashtagLink"
>
<!-- eslint-disable vue/no-v-html -->
<a
:href="url"
class="original"
target="_blank"
@click.prevent="onClick"
v-html="content"
/>
<!-- eslint-enable vue/no-v-html -->
</span>
</template>
<script src="./hashtag_link.js"/>
<style lang="scss" src="./hashtag_link.scss"/>

View file

@ -0,0 +1,95 @@
import generateProfileLink from 'src/services/user_profile_link_generator/user_profile_link_generator'
import { mapGetters, mapState } from 'vuex'
import { highlightClass, highlightStyle } from '../../services/user_highlighter/user_highlighter.js'
import { library } from '@fortawesome/fontawesome-svg-core'
import {
faAt
} from '@fortawesome/free-solid-svg-icons'
library.add(
faAt
)
const MentionLink = {
name: 'MentionLink',
props: {
url: {
required: true,
type: String
},
content: {
required: true,
type: String
},
userId: {
required: false,
type: String
},
userScreenName: {
required: false,
type: String
}
},
methods: {
onClick () {
const link = generateProfileLink(
this.userId || this.user.id,
this.userScreenName || this.user.screen_name
)
this.$router.push(link)
}
},
computed: {
user () {
return this.url && this.$store && this.$store.getters.findUserByUrl(this.url)
},
isYou () {
// FIXME why user !== currentUser???
return this.user && this.user.id === this.currentUser.id
},
userName () {
return this.user && this.userNameFullUi.split('@')[0]
},
userNameFull () {
return this.user && this.user.screen_name
},
userNameFullUi () {
return this.user && this.user.screen_name_ui
},
highlight () {
return this.user && this.mergedConfig.highlight[this.user.screen_name]
},
highlightType () {
return this.highlight && ('-' + this.highlight.type)
},
highlightClass () {
if (this.highlight) return highlightClass(this.user)
},
style () {
if (this.highlight) {
const {
backgroundColor,
backgroundPosition,
backgroundImage,
...rest
} = highlightStyle(this.highlight)
return rest
}
},
classnames () {
return [
{
'-you': this.isYou,
'-highlighted': this.highlight
},
this.highlightType
]
},
...mapGetters(['mergedConfig']),
...mapState({
currentUser: state => state.users.currentUser
})
}
}
export default MentionLink

View file

@ -0,0 +1,91 @@
.MentionLink {
position: relative;
white-space: normal;
display: inline-block;
color: var(--link);
& .new,
& .original {
display: inline-block;
border-radius: 2px;
}
.full {
position: absolute;
display: inline-block;
pointer-events: none;
opacity: 0;
top: 100%;
left: 0;
height: 100%;
word-wrap: normal;
white-space: nowrap;
transition: opacity 0.2s ease;
z-index: 1;
margin-top: 0.25em;
padding: 0.5em;
user-select: all;
}
.short {
user-select: none;
}
& .short,
& .full {
white-space: nowrap;
}
.new {
&.-you {
& .shortName,
& .full {
font-weight: 600;
}
}
.at {
color: var(--link);
opacity: 0.8;
display: inline-block;
height: 50%;
line-height: 1;
padding: 0 0.1em;
vertical-align: -25%;
margin: 0;
}
&.-striped {
& .userName,
& .full {
background-image:
repeating-linear-gradient(
135deg,
var(--____highlight-tintColor),
var(--____highlight-tintColor) 5px,
var(--____highlight-tintColor2) 5px,
var(--____highlight-tintColor2) 10px
);
}
}
&.-solid {
& .userName,
& .full {
background-image: linear-gradient(var(--____highlight-tintColor2), var(--____highlight-tintColor2));
}
}
&.-side {
& .userName,
& .userNameFull {
box-shadow: 0 -5px 3px -4px inset var(--____highlight-solidColor);
}
}
}
&:hover .new .full {
opacity: 1;
pointer-events: initial;
}
}

View file

@ -0,0 +1,56 @@
<template>
<span
class="MentionLink"
>
<!-- eslint-disable vue/no-v-html -->
<a
v-if="!user"
:href="url"
class="original"
target="_blank"
v-html="content"
/>
<!-- eslint-enable vue/no-v-html -->
<span
v-if="user"
class="new"
:style="style"
:class="classnames"
>
<a
class="short button-unstyled"
:href="url"
@click.prevent="onClick"
>
<!-- eslint-disable vue/no-v-html -->
<FAIcon
size="sm"
icon="at"
class="at"
/><span class="shortName"><span
class="userName"
v-html="userName"
/></span>
<span
v-if="isYou"
class="you"
>{{ $t('status.you') }}</span>
<!-- eslint-enable vue/no-v-html -->
</a>
<span
v-if="userName !== userNameFull"
class="full popover-default"
:class="[highlightType]"
>
<span
class="userNameFull"
v-text="'@' + userNameFull"
/>
</span>
</span>
</span>
</template>
<script src="./mention_link.js"/>
<style lang="scss" src="./mention_link.scss"/>

View file

@ -0,0 +1,37 @@
import MentionLink from 'src/components/mention_link/mention_link.vue'
import { mapGetters } from 'vuex'
export const MENTIONS_LIMIT = 5
const MentionsLine = {
name: 'MentionsLine',
props: {
mentions: {
required: true,
type: Array
}
},
data: () => ({ expanded: false }),
components: {
MentionLink
},
computed: {
mentionsComputed () {
return this.mentions.slice(0, MENTIONS_LIMIT)
},
extraMentions () {
return this.mentions.slice(MENTIONS_LIMIT)
},
manyMentions () {
return this.extraMentions.length > 0
},
...mapGetters(['mergedConfig'])
},
methods: {
toggleShowMore () {
this.expanded = !this.expanded
}
}
}
export default MentionsLine

View file

@ -0,0 +1,11 @@
.MentionsLine {
.showMoreLess {
white-space: normal;
color: var(--link);
}
.fullExtraMentions,
.mention-link:not(:last-child) {
margin-right: 0.25em;
}
}

View file

@ -0,0 +1,43 @@
<template>
<span class="MentionsLine">
<MentionLink
v-for="mention in mentionsComputed"
:key="mention.index"
class="mention-link"
:content="mention.content"
:url="mention.url"
:first-mention="false"
/><span
v-if="manyMentions"
class="extraMentions"
>
<span
v-if="expanded"
class="fullExtraMentions"
>
<MentionLink
v-for="mention in extraMentions"
:key="mention.index"
class="mention-link"
:content="mention.content"
:url="mention.url"
:first-mention="false"
/>
</span><button
v-if="!expanded"
class="button-unstyled showMoreLess"
@click="toggleShowMore"
>
{{ $t('status.plus_more', { number: extraMentions.length }) }}
</button><button
v-if="expanded"
class="button-unstyled showMoreLess"
@click="toggleShowMore"
>
{{ $t('general.show_less') }}
</button>
</span>
</span>
</template>
<script src="./mentions_line.js" ></script>
<style lang="scss" src="./mentions_line.scss" />

View file

@ -44,6 +44,9 @@ const MobilePostStatusButton = {
return this.autohideFloatingPostButton && (this.hidden || this.inputActive)
},
isPersistent () {
return !!this.$store.getters.mergedConfig.showNewPostButton
},
autohideFloatingPostButton () {
return !!this.$store.getters.mergedConfig.autohideFloatingPostButton
}

View file

@ -2,7 +2,7 @@
<div v-if="isLoggedIn">
<button
class="button-default new-status-button"
:class="{ 'hidden': isHidden }"
:class="{ 'hidden': isHidden, 'always-show': isPersistent }"
@click="openPostForm"
>
<FAIcon icon="pen" />
@ -47,7 +47,7 @@
}
@media all and (min-width: 801px) {
.new-status-button {
.new-status-button:not(.always-show) {
display: none;
}
}

View file

@ -1,17 +1,56 @@
import { mapState } from 'vuex'
import { get } from 'lodash'
/**
* This is for backwards compatibility. We originally didn't recieve
* extra info like a reason why an instance was rejected/quarantined/etc.
* Because we didn't want to break backwards compatibility it was decided
* to add an extra "info" key.
*/
const toInstanceReasonObject = (instances, info, key) => {
return instances.map(instance => {
if (info[key] && info[key][instance] && info[key][instance]['reason']) {
return { instance: instance, reason: info[key][instance]['reason'] }
}
return { instance: instance, reason: '' }
})
}
const MRFTransparencyPanel = {
computed: {
...mapState({
federationPolicy: state => get(state, 'instance.federationPolicy'),
mrfPolicies: state => get(state, 'instance.federationPolicy.mrf_policies', []),
quarantineInstances: state => get(state, 'instance.federationPolicy.quarantined_instances', []),
acceptInstances: state => get(state, 'instance.federationPolicy.mrf_simple.accept', []),
rejectInstances: state => get(state, 'instance.federationPolicy.mrf_simple.reject', []),
ftlRemovalInstances: state => get(state, 'instance.federationPolicy.mrf_simple.federated_timeline_removal', []),
mediaNsfwInstances: state => get(state, 'instance.federationPolicy.mrf_simple.media_nsfw', []),
mediaRemovalInstances: state => get(state, 'instance.federationPolicy.mrf_simple.media_removal', []),
quarantineInstances: state => toInstanceReasonObject(
get(state, 'instance.federationPolicy.quarantined_instances', []),
get(state, 'instance.federationPolicy.quarantined_instances_info', []),
'quarantined_instances'
),
acceptInstances: state => toInstanceReasonObject(
get(state, 'instance.federationPolicy.mrf_simple.accept', []),
get(state, 'instance.federationPolicy.mrf_simple_info', []),
'accept'
),
rejectInstances: state => toInstanceReasonObject(
get(state, 'instance.federationPolicy.mrf_simple.reject', []),
get(state, 'instance.federationPolicy.mrf_simple_info', []),
'reject'
),
ftlRemovalInstances: state => toInstanceReasonObject(
get(state, 'instance.federationPolicy.mrf_simple.federated_timeline_removal', []),
get(state, 'instance.federationPolicy.mrf_simple_info', []),
'federated_timeline_removal'
),
mediaNsfwInstances: state => toInstanceReasonObject(
get(state, 'instance.federationPolicy.mrf_simple.media_nsfw', []),
get(state, 'instance.federationPolicy.mrf_simple_info', []),
'media_nsfw'
),
mediaRemovalInstances: state => toInstanceReasonObject(
get(state, 'instance.federationPolicy.mrf_simple.media_removal', []),
get(state, 'instance.federationPolicy.mrf_simple_info', []),
'media_removal'
),
keywordsFtlRemoval: state => get(state, 'instance.federationPolicy.mrf_keyword.federated_timeline_removal', []),
keywordsReject: state => get(state, 'instance.federationPolicy.mrf_keyword.reject', []),
keywordsReplace: state => get(state, 'instance.federationPolicy.mrf_keyword.replace', [])

View file

@ -0,0 +1,21 @@
.mrf-section {
margin: 1em;
table {
width:100%;
text-align: left;
padding-left:10px;
padding-bottom:20px;
th, td {
width: 180px;
max-width: 360px;
overflow: hidden;
vertical-align: text-top;
}
th+th, td+td {
width: auto;
}
}
}

View file

@ -31,13 +31,24 @@
<p>{{ $t("about.mrf.simple.accept_desc") }}</p>
<ul>
<li
v-for="instance in acceptInstances"
:key="instance"
v-text="instance"
/>
</ul>
<table>
<tr>
<th>{{ $t("about.mrf.simple.instance") }}</th>
<th>{{ $t("about.mrf.simple.reason") }}</th>
</tr>
<tr
v-for="entry in acceptInstances"
:key="entry.instance + '_accept'"
>
<td>{{ entry.instance }}</td>
<td v-if="entry.reason === ''">
{{ $t("about.mrf.simple.not_applicable") }}
</td>
<td v-else>
{{ entry.reason }}
</td>
</tr>
</table>
</div>
<div v-if="rejectInstances.length">
@ -45,13 +56,24 @@
<p>{{ $t("about.mrf.simple.reject_desc") }}</p>
<ul>
<li
v-for="instance in rejectInstances"
:key="instance"
v-text="instance"
/>
</ul>
<table>
<tr>
<th>{{ $t("about.mrf.simple.instance") }}</th>
<th>{{ $t("about.mrf.simple.reason") }}</th>
</tr>
<tr
v-for="entry in rejectInstances"
:key="entry.instance + '_reject'"
>
<td>{{ entry.instance }}</td>
<td v-if="entry.reason === ''">
{{ $t("about.mrf.simple.not_applicable") }}
</td>
<td v-else>
{{ entry.reason }}
</td>
</tr>
</table>
</div>
<div v-if="quarantineInstances.length">
@ -59,13 +81,24 @@
<p>{{ $t("about.mrf.simple.quarantine_desc") }}</p>
<ul>
<li
v-for="instance in quarantineInstances"
:key="instance"
v-text="instance"
/>
</ul>
<table>
<tr>
<th>{{ $t("about.mrf.simple.instance") }}</th>
<th>{{ $t("about.mrf.simple.reason") }}</th>
</tr>
<tr
v-for="entry in quarantineInstances"
:key="entry.instance + '_quarantine'"
>
<td>{{ entry.instance }}</td>
<td v-if="entry.reason === ''">
{{ $t("about.mrf.simple.not_applicable") }}
</td>
<td v-else>
{{ entry.reason }}
</td>
</tr>
</table>
</div>
<div v-if="ftlRemovalInstances.length">
@ -73,13 +106,24 @@
<p>{{ $t("about.mrf.simple.ftl_removal_desc") }}</p>
<ul>
<li
v-for="instance in ftlRemovalInstances"
:key="instance"
v-text="instance"
/>
</ul>
<table>
<tr>
<th>{{ $t("about.mrf.simple.instance") }}</th>
<th>{{ $t("about.mrf.simple.reason") }}</th>
</tr>
<tr
v-for="entry in ftlRemovalInstances"
:key="entry.instance + '_ftl_removal'"
>
<td>{{ entry.instance }}</td>
<td v-if="entry.reason === ''">
{{ $t("about.mrf.simple.not_applicable") }}
</td>
<td v-else>
{{ entry.reason }}
</td>
</tr>
</table>
</div>
<div v-if="mediaNsfwInstances.length">
@ -87,13 +131,24 @@
<p>{{ $t("about.mrf.simple.media_nsfw_desc") }}</p>
<ul>
<li
v-for="instance in mediaNsfwInstances"
:key="instance"
v-text="instance"
/>
</ul>
<table>
<tr>
<th>{{ $t("about.mrf.simple.instance") }}</th>
<th>{{ $t("about.mrf.simple.reason") }}</th>
</tr>
<tr
v-for="entry in mediaNsfwInstances"
:key="entry.instance + '_media_nsfw'"
>
<td>{{ entry.instance }}</td>
<td v-if="entry.reason === ''">
{{ $t("about.mrf.simple.not_applicable") }}
</td>
<td v-else>
{{ entry.reason }}
</td>
</tr>
</table>
</div>
<div v-if="mediaRemovalInstances.length">
@ -101,13 +156,24 @@
<p>{{ $t("about.mrf.simple.media_removal_desc") }}</p>
<ul>
<li
v-for="instance in mediaRemovalInstances"
:key="instance"
v-text="instance"
/>
</ul>
<table>
<tr>
<th>{{ $t("about.mrf.simple.instance") }}</th>
<th>{{ $t("about.mrf.simple.reason") }}</th>
</tr>
<tr
v-for="entry in mediaRemovalInstances"
:key="entry.instance + '_media_removal'"
>
<td>{{ entry.instance }}</td>
<td v-if="entry.reason === ''">
{{ $t("about.mrf.simple.not_applicable") }}
</td>
<td v-else>
{{ entry.reason }}
</td>
</tr>
</table>
</div>
<h2 v-if="hasKeywordPolicies">
@ -161,7 +227,6 @@
<script src="./mrf_transparency_panel.js"></script>
<style lang="scss">
.mrf-section {
margin: 1em;
}
@import '../../_variables.scss';
@import './mrf_transparency_panel.scss';
</style>

View file

@ -4,6 +4,7 @@ import Status from '../status/status.vue'
import UserAvatar from '../user_avatar/user_avatar.vue'
import UserCard from '../user_card/user_card.vue'
import Timeago from '../timeago/timeago.vue'
import RichContent from 'src/components/rich_content/rich_content.jsx'
import { isStatusNotification } from '../../services/notification_utils/notification_utils.js'
import { highlightClass, highlightStyle } from '../../services/user_highlighter/user_highlighter.js'
import generateProfileLink from 'src/services/user_profile_link_generator/user_profile_link_generator'
@ -44,7 +45,8 @@ const Notification = {
UserAvatar,
UserCard,
Timeago,
Status
Status,
RichContent
},
methods: {
toggleUserExpanded () {

View file

@ -2,11 +2,7 @@
// TODO Copypaste from Status, should unify it somehow
.Notification {
border-bottom: 1px solid;
border-color: $fallback--border;
border-color: var(--border, $fallback--border);
word-wrap: break-word;
word-break: break-word;
--emoji-size: 14px;
&.-muted {
padding: 0.25em 0.6em;

View file

@ -52,12 +52,14 @@
<span class="notification-details">
<div class="name-and-action">
<!-- eslint-disable vue/no-v-html -->
<bdi
v-if="!!notification.from_profile.name_html"
class="username"
:title="'@'+notification.from_profile.screen_name_ui"
v-html="notification.from_profile.name_html"
/>
<bdi v-if="!!notification.from_profile.name_html">
<RichContent
class="username"
:title="'@'+notification.from_profile.screen_name_ui"
:html="notification.from_profile.name_html"
:emoji="notification.from_profile.emoji"
/>
</bdi>
<!-- eslint-enable vue/no-v-html -->
<span
v-else

View file

@ -143,13 +143,6 @@
max-width: 100%;
text-overflow: ellipsis;
white-space: nowrap;
img {
width: 14px;
height: 14px;
vertical-align: middle;
object-fit: contain
}
}
.timeago {

View file

@ -1,10 +1,14 @@
import Timeago from '../timeago/timeago.vue'
import Timeago from 'components/timeago/timeago.vue'
import RichContent from 'components/rich_content/rich_content.jsx'
import { forEach, map } from 'lodash'
export default {
name: 'Poll',
props: ['basePoll'],
components: { Timeago },
props: ['basePoll', 'emoji'],
components: {
Timeago,
RichContent
},
data () {
return {
loading: false,

View file

@ -17,8 +17,11 @@
<span class="result-percentage">
{{ percentageForOption(option.votes_count) }}%
</span>
<!-- eslint-disable-next-line vue/no-v-html -->
<span v-html="option.title_html" />
<RichContent
:html="option.title_html"
:handle-links="false"
:emoji="emoji"
/>
</div>
<div
class="result-fill"
@ -42,8 +45,11 @@
:value="index"
>
<label class="option-vote">
<!-- eslint-disable-next-line vue/no-v-html -->
<div v-html="option.title_html" />
<RichContent
:html="option.title_html"
:handle-links="false"
:emoji="emoji"
/>
</label>
</div>
</div>

View file

@ -0,0 +1,327 @@
import Vue from 'vue'
import { unescape, flattenDeep } from 'lodash'
import { getTagName, processTextForEmoji, getAttrs } from 'src/services/html_converter/utility.service.js'
import { convertHtmlToTree } from 'src/services/html_converter/html_tree_converter.service.js'
import { convertHtmlToLines } from 'src/services/html_converter/html_line_converter.service.js'
import StillImage from 'src/components/still-image/still-image.vue'
import MentionsLine, { MENTIONS_LIMIT } from 'src/components/mentions_line/mentions_line.vue'
import HashtagLink from 'src/components/hashtag_link/hashtag_link.vue'
import './rich_content.scss'
/**
* RichContent, The Über-powered component for rendering Post HTML.
*
* This takes post HTML and does multiple things to it:
* - Groups all mentions into <MentionsLine>, this affects all mentions regardles
* of where they are (beginning/middle/end), even single mentions are converted
* to a <MentionsLine> containing single <MentionLink>.
* - Replaces emoji shortcodes with <StillImage>'d images.
*
* There are two problems with this component's architecture:
* 1. Parsing HTML and rendering are inseparable. Attempts to separate the two
* proven to be a massive overcomplication due to amount of things done here.
* 2. We need to output both render and some extra data, which seems to be imp-
* possible in vue. Current solution is to emit 'parseReady' event when parsing
* is done within render() function.
*
* Apart from that one small hiccup with emit in render this _should_ be vue3-ready
*/
export default Vue.component('RichContent', {
name: 'RichContent',
props: {
// Original html content
html: {
required: true,
type: String
},
attentions: {
required: false,
default: () => []
},
// Emoji object, as in status.emojis, note the "s" at the end...
emoji: {
required: true,
type: Array
},
// Whether to handle links or not (posts: yes, everything else: no)
handleLinks: {
required: false,
type: Boolean,
default: false
},
// Meme arrows
greentext: {
required: false,
type: Boolean,
default: false
}
},
// NEVER EVER TOUCH DATA INSIDE RENDER
render (h) {
// Pre-process HTML
const { newHtml: html } = preProcessPerLine(this.html, this.greentext)
let currentMentions = null // Current chain of mentions, we group all mentions together
// This is used to recover spacing removed when parsing mentions
let lastSpacing = ''
const lastTags = [] // Tags that appear at the end of post body
const writtenMentions = [] // All mentions that appear in post body
const invisibleMentions = [] // All mentions that go beyond the limiter (see MentionsLine)
// to collapse too many mentions in a row
const writtenTags = [] // All tags that appear in post body
// unique index for vue "tag" property
let mentionIndex = 0
let tagsIndex = 0
const renderImage = (tag) => {
return <StillImage
{...{ attrs: getAttrs(tag) }}
class="img"
/>
}
const renderHashtag = (attrs, children, encounteredTextReverse) => {
const linkData = getLinkData(attrs, children, tagsIndex++)
writtenTags.push(linkData)
if (!encounteredTextReverse) {
lastTags.push(linkData)
}
return <HashtagLink {...{ props: linkData }}/>
}
const renderMention = (attrs, children) => {
const linkData = getLinkData(attrs, children, mentionIndex++)
linkData.notifying = this.attentions.some(a => a.statusnet_profile_url === linkData.url)
writtenMentions.push(linkData)
if (currentMentions === null) {
currentMentions = []
}
currentMentions.push(linkData)
if (currentMentions.length > MENTIONS_LIMIT) {
invisibleMentions.push(linkData)
}
if (currentMentions.length === 1) {
return <MentionsLine mentions={ currentMentions } />
} else {
return ''
}
}
// Processor to use with html_tree_converter
const processItem = (item, index, array, what) => {
// Handle text nodes - just add emoji
if (typeof item === 'string') {
const emptyText = item.trim() === ''
if (item.includes('\n')) {
currentMentions = null
}
if (emptyText) {
// don't include spaces when processing mentions - we'll include them
// in MentionsLine
lastSpacing = item
return currentMentions !== null ? item.trim() : item
}
currentMentions = null
if (item.includes(':')) {
item = ['', processTextForEmoji(
item,
this.emoji,
({ shortcode, url }) => {
return <StillImage
class="emoji img"
src={url}
title={`:${shortcode}:`}
alt={`:${shortcode}:`}
/>
}
)]
}
return item
}
// Handle tag nodes
if (Array.isArray(item)) {
const [opener, children, closer] = item
const Tag = getTagName(opener)
const attrs = getAttrs(opener)
const previouslyMentions = currentMentions !== null
/* During grouping of mentions we trim all the empty text elements
* This padding is added to recover last space removed in case
* we have a tag right next to mentions
*/
const mentionsLinePadding =
// Padding is only needed if we just finished parsing mentions
previouslyMentions &&
// Don't add padding if content is string and has padding already
!(children && typeof children[0] === 'string' && children[0].match(/^\s/))
? lastSpacing
: ''
switch (Tag) {
case 'br':
currentMentions = null
break
case 'img': // replace images with StillImage
return ['', [mentionsLinePadding, renderImage(opener)], '']
case 'a': // replace mentions with MentionLink
if (!this.handleLinks) break
if (attrs['class'] && attrs['class'].includes('mention')) {
// Handling mentions here
return renderMention(attrs, children)
} else {
currentMentions = null
break
}
case 'span':
if (this.handleLinks && attrs['class'] && attrs['class'].includes('h-card')) {
return ['', children.map(processItem), '']
}
}
if (children !== undefined) {
return [
'',
[
mentionsLinePadding,
[opener, children.map(processItem), closer]
],
''
]
} else {
return ['', [mentionsLinePadding, item], '']
}
}
}
// Processor for back direction (for finding "last" stuff, just easier this way)
let encounteredTextReverse = false
const processItemReverse = (item, index, array, what) => {
// Handle text nodes - just add emoji
if (typeof item === 'string') {
const emptyText = item.trim() === ''
if (emptyText) return item
if (!encounteredTextReverse) encounteredTextReverse = true
return unescape(item)
} else if (Array.isArray(item)) {
// Handle tag nodes
const [opener, children] = item
const Tag = opener === '' ? '' : getTagName(opener)
switch (Tag) {
case 'a': // replace mentions with MentionLink
if (!this.handleLinks) break
const attrs = getAttrs(opener)
// should only be this
if (
(attrs['class'] && attrs['class'].includes('hashtag')) || // Pleroma style
(attrs['rel'] === 'tag') // Mastodon style
) {
return renderHashtag(attrs, children, encounteredTextReverse)
} else {
attrs.target = '_blank'
const newChildren = [...children].reverse().map(processItemReverse).reverse()
return <a {...{ attrs }}>
{ newChildren }
</a>
}
case '':
return [...children].reverse().map(processItemReverse).reverse()
}
// Render tag as is
if (children !== undefined) {
const newChildren = Array.isArray(children)
? [...children].reverse().map(processItemReverse).reverse()
: children
return <Tag {...{ attrs: getAttrs(opener) }}>
{ newChildren }
</Tag>
} else {
return <Tag/>
}
}
return item
}
const pass1 = convertHtmlToTree(html).map(processItem)
const pass2 = [...pass1].reverse().map(processItemReverse).reverse()
// DO NOT USE SLOTS they cause a re-render feedback loop here.
// slots updated -> rerender -> emit -> update up the tree -> rerender -> ...
// at least until vue3?
const result = <span class="RichContent">
{ pass2 }
</span>
const event = {
lastTags,
writtenMentions,
writtenTags,
invisibleMentions
}
// DO NOT MOVE TO UPDATE. BAD IDEA.
this.$emit('parseReady', event)
return result
}
})
const getLinkData = (attrs, children, index) => {
const stripTags = (item) => {
if (typeof item === 'string') {
return item
} else {
return item[1].map(stripTags).join('')
}
}
const textContent = children.map(stripTags).join('')
return {
index,
url: attrs.href,
tag: attrs['data-tag'],
content: flattenDeep(children).join(''),
textContent
}
}
/** Pre-processing HTML
*
* Currently this does one thing:
* - add green/cyantexting
*
* @param {String} html - raw HTML to process
* @param {Boolean} greentext - whether to enable greentexting or not
*/
export const preProcessPerLine = (html, greentext) => {
const greentextHandle = new Set(['p', 'div'])
const lines = convertHtmlToLines(html)
const newHtml = lines.reverse().map((item, index, array) => {
if (!item.text) return item
const string = item.text
// Greentext stuff
if (
// Only if greentext is engaged
greentext &&
// Only handle p's and divs. Don't want to affect blockquotes, code etc
item.level.every(l => greentextHandle.has(l)) &&
// Only if line begins with '>' or '<'
(string.includes('&gt;') || string.includes('&lt;'))
) {
const cleanedString = string.replace(/<[^>]+?>/gi, '') // remove all tags
.replace(/@\w+/gi, '') // remove mentions (even failed ones)
.trim()
if (cleanedString.startsWith('&gt;')) {
return `<span class='greentext'>${string}</span>`
} else if (cleanedString.startsWith('&lt;')) {
return `<span class='cyantext'>${string}</span>`
}
}
return string
}).reverse().join('')
return { newHtml }
}

View file

@ -0,0 +1,64 @@
.RichContent {
blockquote {
margin: 0.2em 0 0.2em 2em;
font-style: italic;
}
pre {
overflow: auto;
}
code,
samp,
kbd,
var,
pre {
font-family: var(--postCodeFont, monospace);
}
p {
margin: 0 0 1em 0;
}
p:last-child {
margin: 0 0 0 0;
}
h1 {
font-size: 1.1em;
line-height: 1.2em;
margin: 1.4em 0;
}
h2 {
font-size: 1.1em;
margin: 1em 0;
}
h3 {
font-size: 1em;
margin: 1.2em 0;
}
h4 {
margin: 1.1em 0;
}
.img {
display: inline-block;
}
.emoji {
display: inline-block;
width: var(--emoji-size, 32px);
height: var(--emoji-size, 32px);
}
.img,
video {
max-width: 100%;
max-height: 400px;
vertical-align: middle;
object-fit: contain;
}
}

View file

@ -16,10 +16,18 @@ export default {
return [firstSegment + 'DefaultValue', ...rest].join('.')
},
state () {
return get(this.$parent, this.path)
const value = get(this.$parent, this.path)
if (value === undefined) {
return this.defaultState
} else {
return value
}
},
defaultState () {
return get(this.$parent, this.pathDefault)
},
isChanged () {
return get(this.$parent, this.path) !== get(this.$parent, this.pathDefault)
return this.state !== this.defaultState
}
},
methods: {

View file

@ -17,13 +17,18 @@ export default {
return [firstSegment + 'DefaultValue', ...rest].join('.')
},
state () {
return get(this.$parent, this.path)
const value = get(this.$parent, this.path)
if (value === undefined) {
return this.defaultState
} else {
return value
}
},
defaultState () {
return get(this.$parent, this.pathDefault)
},
isChanged () {
return get(this.$parent, this.path) !== get(this.$parent, this.pathDefault)
return this.state !== this.defaultState
}
},
methods: {

View file

@ -191,6 +191,16 @@
{{ $t('settings.sensitive_by_default') }}
</BooleanSetting>
</li>
<li>
<BooleanSetting path="alwaysShowNewPostButton">
{{ $t('settings.always_show_post_button') }}
</BooleanSetting>
</li>
<li>
<BooleanSetting path="autohideFloatingPostButton">
{{ $t('settings.autohide_floating_post_button') }}
</BooleanSetting>
</li>
<li>
<BooleanSetting path="padEmoji">
{{ $t('settings.pad_emoji') }}

View file

@ -24,7 +24,7 @@ library.add(
const ProfileTab = {
data () {
return {
newName: this.$store.state.users.currentUser.name,
newName: this.$store.state.users.currentUser.name_unescaped,
newBio: unescape(this.$store.state.users.currentUser.description),
newLocked: this.$store.state.users.currentUser.locked,
newNoRichText: this.$store.state.users.currentUser.no_rich_text,

View file

@ -73,7 +73,8 @@ export default {
getExportedObject: () => this.exportedTheme
}),
availableStyles: [],
selected: this.$store.getters.mergedConfig.theme,
selected: '',
selectedTheme: this.$store.getters.mergedConfig.theme,
themeWarning: undefined,
tempImportFile: undefined,
engineVersion: 0,
@ -207,7 +208,7 @@ export default {
}
},
selectedVersion () {
return Array.isArray(this.selected) ? 1 : 2
return Array.isArray(this.selectedTheme) ? 1 : 2
},
currentColors () {
return Object.keys(SLOT_INHERITANCE)
@ -474,7 +475,7 @@ export default {
this.loadThemeFromLocalStorage(false, true)
break
case 'file':
console.err('Forcing snapshout from file is not supported yet')
console.error('Forcing snapshot from file is not supported yet')
break
}
this.dismissWarning()
@ -745,6 +746,16 @@ export default {
}
},
selected () {
this.selectedTheme = Object.entries(this.availableStyles).find(([k, s]) => {
if (Array.isArray(s)) {
console.log(s[0] === this.selected, this.selected)
return s[0] === this.selected
} else {
return s.name === this.selected
}
})[1]
},
selectedTheme () {
this.dismissWarning()
if (this.selectedVersion === 1) {
if (!this.keepRoundness) {
@ -762,17 +773,17 @@ export default {
if (!this.keepColor) {
this.clearV1()
this.bgColorLocal = this.selected[1]
this.fgColorLocal = this.selected[2]
this.textColorLocal = this.selected[3]
this.linkColorLocal = this.selected[4]
this.cRedColorLocal = this.selected[5]
this.cGreenColorLocal = this.selected[6]
this.cBlueColorLocal = this.selected[7]
this.cOrangeColorLocal = this.selected[8]
this.bgColorLocal = this.selectedTheme[1]
this.fgColorLocal = this.selectedTheme[2]
this.textColorLocal = this.selectedTheme[3]
this.linkColorLocal = this.selectedTheme[4]
this.cRedColorLocal = this.selectedTheme[5]
this.cGreenColorLocal = this.selectedTheme[6]
this.cBlueColorLocal = this.selectedTheme[7]
this.cOrangeColorLocal = this.selectedTheme[8]
}
} else if (this.selectedVersion >= 2) {
this.normalizeLocalState(this.selected.theme, 2, this.selected.source)
this.normalizeLocalState(this.selectedTheme.theme, 2, this.selectedTheme.source)
}
}
}

View file

@ -270,6 +270,9 @@
.apply-container {
justify-content: center;
position: absolute;
bottom: 8px;
right: 5px;
}
.radius-item,

View file

@ -63,7 +63,7 @@
<option
v-for="style in availableStyles"
:key="style.name"
:value="style"
:value="style.name || style[0]"
:style="{
backgroundColor: style[1] || (style.theme || style.source).colors.bg,
color: style[3] || (style.theme || style.source).colors.text

View file

@ -79,12 +79,19 @@
.floating-shout {
position: fixed;
right: 0px;
bottom: 0px;
z-index: 1000;
max-width: 25em;
}
.floating-shout.left {
left: 0px;
}
.floating-shout:not(.left) {
right: 0px;
}
.shout-panel {
.shout-heading {
cursor: pointer;

View file

@ -49,6 +49,7 @@ const SideDrawer = {
currentUser () {
return this.$store.state.users.currentUser
},
shout () { return this.$store.state.shout.channel.state === 'joined' },
unseenNotifications () {
return unseenNotificationsFromStore(this.$store)
},

View file

@ -106,10 +106,10 @@
</router-link>
</li>
<li
v-if="chat"
v-if="shout"
@click="toggleDrawer"
>
<router-link :to="{ name: 'chat-panel' }">
<router-link :to="{ name: 'shout-panel' }">
<FAIcon
fixed-width
class="fa-scale-110 fa-old-padding"
@ -273,9 +273,7 @@
--icon: var(--popoverIcon, $fallback--icon);
.badge {
position: absolute;
right: 0.7rem;
top: 1em;
margin-left: 10px;
}
}

View file

@ -9,9 +9,12 @@ import UserAvatar from '../user_avatar/user_avatar.vue'
import AvatarList from '../avatar_list/avatar_list.vue'
import Timeago from '../timeago/timeago.vue'
import StatusContent from '../status_content/status_content.vue'
import RichContent from 'src/components/rich_content/rich_content.jsx'
import StatusPopover from '../status_popover/status_popover.vue'
import UserListPopover from '../user_list_popover/user_list_popover.vue'
import EmojiReactions from '../emoji_reactions/emoji_reactions.vue'
import MentionsLine from 'src/components/mentions_line/mentions_line.vue'
import MentionLink from 'src/components/mention_link/mention_link.vue'
import generateProfileLink from 'src/services/user_profile_link_generator/user_profile_link_generator'
import { highlightClass, highlightStyle } from '../../services/user_highlighter/user_highlighter.js'
import { muteWordHits } from '../../services/status_parser/status_parser.js'
@ -68,7 +71,10 @@ const Status = {
StatusPopover,
UserListPopover,
EmojiReactions,
StatusContent
StatusContent,
RichContent,
MentionLink,
MentionsLine
},
props: [
'statusoid',
@ -92,7 +98,8 @@ const Status = {
userExpanded: false,
mediaPlaying: [],
suspendable: true,
error: null
error: null,
headTailLinks: null
}
},
computed: {
@ -132,12 +139,15 @@ const Status = {
},
replyProfileLink () {
if (this.isReply) {
return this.generateUserProfileLink(this.status.in_reply_to_user_id, this.replyToName)
const user = this.$store.getters.findUser(this.status.in_reply_to_user_id)
// FIXME Why user not found sometimes???
return user ? user.statusnet_profile_url : 'NOT_FOUND'
}
},
retweet () { return !!this.statusoid.retweeted_status },
retweeterUser () { return this.statusoid.user },
retweeter () { return this.statusoid.user.name || this.statusoid.user.screen_name_ui },
retweeterHtml () { return this.statusoid.user.name_html },
retweeterHtml () { return this.statusoid.user.name },
retweeterProfileLink () { return this.generateUserProfileLink(this.statusoid.user.id, this.statusoid.user.screen_name) },
status () {
if (this.retweet) {
@ -156,6 +166,25 @@ const Status = {
muteWordHits () {
return muteWordHits(this.status, this.muteWords)
},
mentionsLine () {
if (!this.headTailLinks) return []
const writtenSet = new Set(this.headTailLinks.writtenMentions.map(_ => _.url))
return this.status.attentions.filter(attn => {
// no reply user
return attn.id !== this.status.in_reply_to_user_id &&
// no self-replies
attn.statusnet_profile_url !== this.status.user.statusnet_profile_url &&
// don't include if mentions is written
!writtenSet.has(attn.statusnet_profile_url)
}).map(attn => ({
url: attn.statusnet_profile_url,
content: attn.screen_name,
userId: attn.id
}))
},
hasMentionsLine () {
return this.mentionsLine.length > 0
},
muted () {
if (this.statusoid.user.id === this.currentUser.id) return false
const reasonsToMute = this.userIsMuted ||
@ -321,6 +350,9 @@ const Status = {
},
removeMediaPlaying (id) {
this.mediaPlaying = this.mediaPlaying.filter(mediaId => mediaId !== id)
},
setHeadTailLinks (headTailLinks) {
this.headTailLinks = headTailLinks
}
},
watch: {

View file

@ -1,10 +1,10 @@
@import '../../_variables.scss';
$status-margin: 0.75em;
.Status {
min-width: 0;
white-space: normal;
&:hover {
--_still-image-img-visibility: visible;
@ -93,12 +93,8 @@ $status-margin: 0.75em;
margin-right: 0.4em;
text-overflow: ellipsis;
.emoji {
width: 14px;
height: 14px;
vertical-align: middle;
object-fit: contain;
}
--_still_image-label-scale: 0.25;
--emoji-size: 14px;
}
.status-favicon {
@ -155,35 +151,24 @@ $status-margin: 0.75em;
}
}
.glued-label {
display: inline-flex;
white-space: nowrap;
}
.timeago {
margin-right: 0.2em;
}
.heading-reply-row {
& .heading-reply-row {
position: relative;
align-content: baseline;
font-size: 12px;
line-height: 18px;
line-height: 160%;
max-width: 100%;
display: flex;
flex-wrap: wrap;
align-items: stretch;
}
.reply-to-and-accountname {
display: flex;
height: 18px;
margin-right: 0.5em;
max-width: 100%;
.reply-to-link {
white-space: nowrap;
word-break: break-word;
text-overflow: ellipsis;
overflow-x: hidden;
}
}
& .reply-to-popover,
& .reply-to-no-popover {
min-width: 0;
@ -220,21 +205,27 @@ $status-margin: 0.75em;
}
}
.reply-to {
& .mentions,
& .reply-to {
white-space: nowrap;
position: relative;
padding-right: 0.25em;
}
.reply-to-text {
& .mentions-text,
& .reply-to-text {
color: var(--faint);
overflow: hidden;
text-overflow: ellipsis;
white-space: nowrap;
}
.replies-separator {
margin-left: 0.4em;
.mentions-line {
display: inline;
}
.replies {
margin-top: 0.25em;
line-height: 18px;
font-size: 12px;
display: flex;

View file

@ -1,5 +1,4 @@
<template>
<!-- eslint-disable vue/no-v-html -->
<div
v-if="!hideStatus"
class="Status"
@ -89,8 +88,12 @@
<router-link
v-if="retweeterHtml"
:to="retweeterProfileLink"
v-html="retweeterHtml"
/>
>
<RichContent
:html="retweeterHtml"
:emoji="retweeterUser.emoji"
/>
</router-link>
<router-link
v-else
:to="retweeterProfileLink"
@ -145,8 +148,12 @@
v-if="status.user.name_html"
class="status-username"
:title="status.user.name"
v-html="status.user.name_html"
/>
>
<RichContent
:html="status.user.name"
:emoji="status.user.emoji"
/>
</h4>
<h4
v-else
class="status-username"
@ -214,11 +221,13 @@
</button>
</span>
</div>
<div class="heading-reply-row">
<div
<div
v-if="isReply || hasMentionsLine"
class="heading-reply-row"
>
<span
v-if="isReply"
class="reply-to-and-accountname"
class="glued-label"
>
<StatusPopover
v-if="!isPreview"
@ -238,7 +247,7 @@
flip="horizontal"
/>
<span
class="faint-link reply-to-text"
class="reply-to-text"
>
{{ $t('status.reply_to') }}
</span>
@ -251,50 +260,76 @@
>
<span class="reply-to-text">{{ $t('status.reply_to') }}</span>
</span>
<router-link
class="reply-to-link"
:title="replyToName"
:to="replyProfileLink"
>
{{ replyToName }}
</router-link>
<span
v-if="replies && replies.length"
class="faint replies-separator"
>
-
</span>
</div>
<div
v-if="inConversation && !isPreview && replies && replies.length"
class="replies"
<MentionLink
:content="replyToName"
:url="replyProfileLink"
:user-id="status.in_reply_to_user_id"
:user-screen-name="status.in_reply_to_screen_name"
:first-mention="false"
/>
</span>
<!-- This little wrapper is made for sole purpose of "gluing" -->
<!-- "Mentions" label to the first mention -->
<span
v-if="hasMentionsLine"
class="glued-label"
>
<span class="faint">{{ $t('status.replies_list') }}</span>
<StatusPopover
v-for="reply in replies"
:key="reply.id"
:status-id="reply.id"
<span
class="mentions"
:aria-label="$t('tool_tip.mentions')"
@click.prevent="gotoOriginal(status.in_reply_to_status_id)"
>
<button
class="button-unstyled -link reply-link"
@click.prevent="gotoOriginal(reply.id)"
<span
class="mentions-text"
>
{{ reply.name }}
</button>
</StatusPopover>
</div>
{{ $t('status.mentions') }}
</span>
</span>
<MentionsLine
v-if="hasMentionsLine"
:mentions="mentionsLine.slice(0, 1)"
class="mentions-line-first"
/>
</span>
<MentionsLine
v-if="hasMentionsLine"
:mentions="mentionsLine.slice(1)"
class="mentions-line"
/>
</div>
</div>
<StatusContent
ref="content"
:status="status"
:no-heading="noHeading"
:highlight="highlight"
:focused="isFocused"
@mediaplay="addMediaPlaying($event)"
@mediapause="removeMediaPlaying($event)"
@parseReady="setHeadTailLinks"
/>
<div
v-if="inConversation && !isPreview && replies && replies.length"
class="replies"
>
<span class="faint">{{ $t('status.replies_list') }}</span>
<StatusPopover
v-for="reply in replies"
:key="reply.id"
:status-id="reply.id"
>
<button
class="button-unstyled -link reply-link"
@click.prevent="gotoOriginal(reply.id)"
>
{{ reply.name }}
</button>
</StatusPopover>
</div>
<transition name="fade">
<div
v-if="!hidePostStats && isFocused && combinedFavsAndRepeatsUsers.length > 0"
@ -402,7 +437,6 @@
</div>
</template>
</div>
<!-- eslint-enable vue/no-v-html -->
</template>
<script src="./status.js" ></script>

View file

@ -0,0 +1,127 @@
import fileType from 'src/services/file_type/file_type.service'
import RichContent from 'src/components/rich_content/rich_content.jsx'
import { mapGetters } from 'vuex'
import { library } from '@fortawesome/fontawesome-svg-core'
import {
faFile,
faMusic,
faImage,
faLink,
faPollH
} from '@fortawesome/free-solid-svg-icons'
library.add(
faFile,
faMusic,
faImage,
faLink,
faPollH
)
const StatusContent = {
name: 'StatusContent',
props: [
'status',
'focused',
'noHeading',
'fullContent',
'singleLine'
],
data () {
return {
showingTall: this.fullContent || (this.inConversation && this.focused),
showingLongSubject: false,
// not as computed because it sets the initial state which will be changed later
expandingSubject: !this.$store.getters.mergedConfig.collapseMessageWithSubject,
postLength: this.status.text.length,
parseReadyDone: false
}
},
computed: {
localCollapseSubjectDefault () {
return this.mergedConfig.collapseMessageWithSubject
},
// This is a bit hacky, but we want to approximate post height before rendering
// so we count newlines (masto uses <p> for paragraphs, GS uses <br> between them)
// as well as approximate line count by counting characters and approximating ~80
// per line.
//
// Using max-height + overflow: auto for status components resulted in false positives
// very often with japanese characters, and it was very annoying.
tallStatus () {
const lengthScore = this.status.raw_html.split(/<p|<br/).length + this.postLength / 80
return lengthScore > 20
},
longSubject () {
return this.status.summary.length > 240
},
// When a status has a subject and is also tall, we should only have one show more/less button. If the default is to collapse statuses with subjects, we just treat it like a status with a subject; otherwise, we just treat it like a tall status.
mightHideBecauseSubject () {
return !!this.status.summary && this.localCollapseSubjectDefault
},
mightHideBecauseTall () {
return this.tallStatus && !(this.status.summary && this.localCollapseSubjectDefault)
},
hideSubjectStatus () {
return this.mightHideBecauseSubject && !this.expandingSubject
},
hideTallStatus () {
return this.mightHideBecauseTall && !this.showingTall
},
showingMore () {
return (this.mightHideBecauseTall && this.showingTall) || (this.mightHideBecauseSubject && this.expandingSubject)
},
attachmentTypes () {
return this.status.attachments.map(file => fileType.fileType(file.mimetype))
},
...mapGetters(['mergedConfig'])
},
components: {
RichContent
},
mounted () {
this.status.attentions && this.status.attentions.forEach(attn => {
const { id } = attn
this.$store.dispatch('fetchUserIfMissing', id)
})
},
methods: {
onParseReady (event) {
if (this.parseReadyDone) return
this.parseReadyDone = true
this.$emit('parseReady', event)
const { writtenMentions, invisibleMentions } = event
writtenMentions
.filter(mention => !mention.notifying)
.forEach(mention => {
const { content, url } = mention
const cleanedString = content.replace(/<[^>]+?>/gi, '') // remove all tags
if (!cleanedString.startsWith('@')) return
const handle = cleanedString.slice(1)
const host = url.replace(/^https?:\/\//, '').replace(/\/.+?$/, '')
this.$store.dispatch('fetchUserIfMissing', `${handle}@${host}`)
})
/* This is a bit of a hack to make current tall status detector work
* with rich mentions. Invisible mentions are detected at RichContent level
* and also we generate plaintext version of mentions by stripping tags
* so here we subtract from post length by each mention that became invisible
* via MentionsLine
*/
this.postLength = invisibleMentions.reduce((acc, mention) => {
return acc - mention.textContent.length - 1
}, this.postLength)
},
toggleShowMore () {
if (this.mightHideBecauseTall) {
this.showingTall = !this.showingTall
} else if (this.mightHideBecauseSubject) {
this.expandingSubject = !this.expandingSubject
}
},
generateTagLink (tag) {
return `/tag/${tag}`
}
}
}
export default StatusContent

View file

@ -0,0 +1,118 @@
@import '../../_variables.scss';
.StatusBody {
.emoji {
--_still_image-label-scale: 0.5;
}
& .text,
& .summary {
font-family: var(--postFont, sans-serif);
white-space: pre-wrap;
overflow-wrap: break-word;
word-wrap: break-word;
word-break: break-word;
line-height: 1.4em;
}
.summary {
display: block;
font-style: italic;
padding-bottom: 0.5em;
}
.text {
&.-single-line {
white-space: nowrap;
text-overflow: ellipsis;
overflow: hidden;
height: 1.4em;
}
}
.summary-wrapper {
margin-bottom: 0.5em;
border-style: solid;
border-width: 0 0 1px 0;
border-color: var(--border, $fallback--border);
flex-grow: 0;
&.-tall {
position: relative;
.summary {
max-height: 2em;
overflow: hidden;
white-space: nowrap;
text-overflow: ellipsis;
}
}
}
.text-wrapper {
display: flex;
flex-direction: column;
flex-wrap: nowrap;
&.-tall-status {
position: relative;
height: 220px;
overflow-x: hidden;
overflow-y: hidden;
z-index: 1;
.media-body {
min-height: 0;
mask:
linear-gradient(to top, white, transparent) bottom/100% 70px no-repeat,
linear-gradient(to top, white, white);
/* Autoprefixed seem to ignore this one, and also syntax is different */
-webkit-mask-composite: xor;
mask-composite: exclude;
}
}
}
& .tall-status-hider,
& .tall-subject-hider,
& .status-unhider,
& .cw-status-hider {
display: inline-block;
word-break: break-all;
width: 100%;
text-align: center;
}
.tall-status-hider {
position: absolute;
height: 70px;
margin-top: 150px;
line-height: 110px;
z-index: 2;
}
.tall-subject-hider {
// position: absolute;
padding-bottom: 0.5em;
}
& .status-unhider,
& .cw-status-hider {
word-break: break-all;
svg {
color: inherit;
}
}
.greentext {
color: $fallback--cGreen;
color: var(--postGreentext, $fallback--cGreen);
}
.cyantext {
color: var(--postCyantext, $fallback--cBlue);
}
}

View file

@ -0,0 +1,97 @@
<template>
<div class="StatusBody">
<div class="body">
<div
v-if="status.summary_raw_html"
class="summary-wrapper"
:class="{ '-tall': (longSubject && !showingLongSubject) }"
>
<RichContent
class="media-body summary"
:html="status.summary_raw_html"
:emoji="status.emojis"
/>
<button
v-if="longSubject && showingLongSubject"
class="button-unstyled -link tall-subject-hider"
@click.prevent="showingLongSubject=false"
>
{{ $t("status.hide_full_subject") }}
</button>
<button
v-else-if="longSubject"
class="button-unstyled -link tall-subject-hider"
@click.prevent="showingLongSubject=true"
>
{{ $t("status.show_full_subject") }}
</button>
</div>
<div
:class="{'-tall-status': hideTallStatus}"
class="text-wrapper"
>
<button
v-if="hideTallStatus"
class="button-unstyled -link tall-status-hider"
:class="{ '-focused': focused }"
@click.prevent="toggleShowMore"
>
{{ $t("general.show_more") }}
</button>
<RichContent
v-if="!hideSubjectStatus && !(singleLine && status.summary_raw_html)"
:class="{ '-single-line': singleLine }"
class="text media-body"
:html="status.raw_html"
:emoji="status.emojis"
:handle-links="true"
:greentext="mergedConfig.greentext"
:attentions="status.attentions"
@parseReady="onParseReady"
/>
<button
v-if="hideSubjectStatus"
class="button-unstyled -link cw-status-hider"
@click.prevent="toggleShowMore"
>
{{ $t("status.show_content") }}
<FAIcon
v-if="attachmentTypes.includes('image')"
icon="image"
/>
<FAIcon
v-if="attachmentTypes.includes('video')"
icon="video"
/>
<FAIcon
v-if="attachmentTypes.includes('audio')"
icon="music"
/>
<FAIcon
v-if="attachmentTypes.includes('unknown')"
icon="file"
/>
<FAIcon
v-if="status.poll && status.poll.options"
icon="poll-h"
/>
<FAIcon
v-if="status.card"
icon="link"
/>
</button>
<button
v-if="showingMore && !fullContent"
class="button-unstyled -link status-unhider"
@click.prevent="toggleShowMore"
>
{{ tallStatus ? $t("general.show_less") : $t("status.hide_content") }}
</button>
</div>
</div>
<slot v-if="!hideSubjectStatus" />
</div>
</template>
<script src="./status_body.js" ></script>
<style lang="scss" src="./status_body.scss" />

View file

@ -1,11 +1,9 @@
import Attachment from '../attachment/attachment.vue'
import Poll from '../poll/poll.vue'
import Gallery from '../gallery/gallery.vue'
import StatusBody from 'src/components/status_body/status_body.vue'
import LinkPreview from '../link-preview/link-preview.vue'
import generateProfileLink from 'src/services/user_profile_link_generator/user_profile_link_generator'
import fileType from 'src/services/file_type/file_type.service'
import { processHtml } from 'src/services/tiny_post_html_processor/tiny_post_html_processor.service.js'
import { mentionMatchesUrl, extractTagFromUrl } from 'src/services/matcher/matcher.service.js'
import { mapGetters, mapState } from 'vuex'
import { library } from '@fortawesome/fontawesome-svg-core'
import {
@ -35,52 +33,11 @@ const StatusContent = {
'fullContent',
'singleLine'
],
data () {
return {
showingTall: this.fullContent || (this.inConversation && this.focused),
showingLongSubject: false,
// not as computed because it sets the initial state which will be changed later
expandingSubject: !this.$store.getters.mergedConfig.collapseMessageWithSubject
}
},
computed: {
localCollapseSubjectDefault () {
return this.mergedConfig.collapseMessageWithSubject
},
hideAttachments () {
return (this.mergedConfig.hideAttachments && !this.inConversation) ||
(this.mergedConfig.hideAttachmentsInConv && this.inConversation)
},
// This is a bit hacky, but we want to approximate post height before rendering
// so we count newlines (masto uses <p> for paragraphs, GS uses <br> between them)
// as well as approximate line count by counting characters and approximating ~80
// per line.
//
// Using max-height + overflow: auto for status components resulted in false positives
// very often with japanese characters, and it was very annoying.
tallStatus () {
const lengthScore = this.status.statusnet_html.split(/<p|<br/).length + this.status.text.length / 80
return lengthScore > 20
},
longSubject () {
return this.status.summary.length > 240
},
// When a status has a subject and is also tall, we should only have one show more/less button. If the default is to collapse statuses with subjects, we just treat it like a status with a subject; otherwise, we just treat it like a tall status.
mightHideBecauseSubject () {
return !!this.status.summary && this.localCollapseSubjectDefault
},
mightHideBecauseTall () {
return this.tallStatus && !(this.status.summary && this.localCollapseSubjectDefault)
},
hideSubjectStatus () {
return this.mightHideBecauseSubject && !this.expandingSubject
},
hideTallStatus () {
return this.mightHideBecauseTall && !this.showingTall
},
showingMore () {
return (this.mightHideBecauseTall && this.showingTall) || (this.mightHideBecauseSubject && this.expandingSubject)
},
nsfwClickthrough () {
if (!this.status.nsfw) {
return false
@ -118,45 +75,11 @@ const StatusContent = {
file => !fileType.fileMatchesSomeType(this.galleryTypes, file)
)
},
attachmentTypes () {
return this.status.attachments.map(file => fileType.fileType(file.mimetype))
},
maxThumbnails () {
return this.mergedConfig.maxThumbnails
},
postBodyHtml () {
const html = this.status.statusnet_html
if (this.mergedConfig.greentext) {
try {
if (html.includes('&gt;')) {
// This checks if post has '>' at the beginning, excluding mentions so that @mention >impying works
return processHtml(html, (string) => {
if (string.includes('&gt;') &&
string
.replace(/<[^>]+?>/gi, '') // remove all tags
.replace(/@\w+/gi, '') // remove mentions (even failed ones)
.trim()
.startsWith('&gt;')) {
return `<span class='greentext'>${string}</span>`
} else {
return string
}
})
} else {
return html
}
} catch (e) {
console.err('Failed to process status html', e)
return html
}
} else {
return html
}
},
...mapGetters(['mergedConfig']),
...mapState({
betterShadow: state => state.interface.browserSupport.cssFilter,
currentUser: state => state.users.currentUser
})
},
@ -164,48 +87,10 @@ const StatusContent = {
Attachment,
Poll,
Gallery,
LinkPreview
LinkPreview,
StatusBody
},
methods: {
linkClicked (event) {
const target = event.target.closest('.status-content a')
if (target) {
if (target.className.match(/mention/)) {
const href = target.href
const attn = this.status.attentions.find(attn => mentionMatchesUrl(attn, href))
if (attn) {
event.stopPropagation()
event.preventDefault()
const link = this.generateUserProfileLink(attn.id, attn.screen_name)
this.$router.push(link)
return
}
}
if (target.rel.match(/(?:^|\s)tag(?:$|\s)/) || target.className.match(/hashtag/)) {
// Extract tag name from dataset or link url
const tag = target.dataset.tag || extractTagFromUrl(target.href)
if (tag) {
const link = this.generateTagLink(tag)
this.$router.push(link)
return
}
}
window.open(target.href, '_blank')
}
},
toggleShowMore () {
if (this.mightHideBecauseTall) {
this.showingTall = !this.showingTall
} else if (this.mightHideBecauseSubject) {
this.expandingSubject = !this.expandingSubject
}
},
generateUserProfileLink (id, name) {
return generateProfileLink(id, name, this.$store.state.instance.restrictedNicknames)
},
generateTagLink (tag) {
return `/tag/${tag}`
},
setMedia () {
const attachments = this.attachmentSize === 'hide' ? this.status.attachments : this.galleryAttachments
return () => this.$store.dispatch('setMedia', attachments)

View file

@ -1,133 +1,55 @@
<template>
<!-- eslint-disable vue/no-v-html -->
<div class="StatusContent">
<slot name="header" />
<div
v-if="status.summary_html"
class="summary-wrapper"
:class="{ 'tall-subject': (longSubject && !showingLongSubject) }"
<StatusBody
:status="status"
:single-line="singleLine"
@parseReady="$emit('parseReady', $event)"
>
<div v-if="status.poll && status.poll.options">
<Poll
:base-poll="status.poll"
:emoji="status.emojis"
/>
</div>
<div
class="media-body summary"
@click.prevent="linkClicked"
v-html="status.summary_html"
/>
<button
v-if="longSubject && showingLongSubject"
class="button-unstyled -link tall-subject-hider"
@click.prevent="showingLongSubject=false"
v-if="status.attachments.length !== 0"
class="attachments media-body"
>
{{ $t("status.hide_full_subject") }}
</button>
<button
v-else-if="longSubject"
class="button-unstyled -link tall-subject-hider"
:class="{ 'tall-subject-hider_focused': focused }"
@click.prevent="showingLongSubject=true"
>
{{ $t("status.show_full_subject") }}
</button>
</div>
<div
:class="{'tall-status': hideTallStatus}"
class="status-content-wrapper"
>
<button
v-if="hideTallStatus"
class="button-unstyled -link tall-status-hider"
:class="{ 'tall-status-hider_focused': focused }"
@click.prevent="toggleShowMore"
>
{{ $t("general.show_more") }}
</button>
<attachment
v-for="attachment in nonGalleryAttachments"
:key="attachment.id"
class="non-gallery"
:size="attachmentSize"
:nsfw="nsfwClickthrough"
:attachment="attachment"
:allow-play="true"
:set-media="setMedia()"
@play="$emit('mediaplay', attachment.id)"
@pause="$emit('mediapause', attachment.id)"
/>
<gallery
v-if="galleryAttachments.length > 0"
:nsfw="nsfwClickthrough"
:attachments="galleryAttachments"
:set-media="setMedia()"
/>
</div>
<div
v-if="!hideSubjectStatus"
:class="{ 'single-line': singleLine }"
class="status-content media-body"
@click.prevent="linkClicked"
v-html="postBodyHtml"
/>
<button
v-if="hideSubjectStatus"
class="button-unstyled -link cw-status-hider"
@click.prevent="toggleShowMore"
v-if="status.card && !noHeading"
class="link-preview media-body"
>
{{ $t("status.show_content") }}
<FAIcon
v-if="attachmentTypes.includes('image')"
icon="image"
<link-preview
:card="status.card"
:size="attachmentSize"
:nsfw="nsfwClickthrough"
/>
<FAIcon
v-if="attachmentTypes.includes('video')"
icon="video"
/>
<FAIcon
v-if="attachmentTypes.includes('audio')"
icon="music"
/>
<FAIcon
v-if="attachmentTypes.includes('unknown')"
icon="file"
/>
<FAIcon
v-if="status.poll && status.poll.options"
icon="poll-h"
/>
<FAIcon
v-if="status.card"
icon="link"
/>
</button>
<button
v-if="showingMore && !fullContent"
class="button-unstyled -link status-unhider"
@click.prevent="toggleShowMore"
>
{{ tallStatus ? $t("general.show_less") : $t("status.hide_content") }}
</button>
</div>
<div v-if="status.poll && status.poll.options && !hideSubjectStatus">
<poll :base-poll="status.poll" />
</div>
<div
v-if="status.attachments.length !== 0 && (!hideSubjectStatus || showingLongSubject)"
class="attachments media-body"
>
<attachment
v-for="attachment in nonGalleryAttachments"
:key="attachment.id"
class="non-gallery"
:size="attachmentSize"
:nsfw="nsfwClickthrough"
:attachment="attachment"
:allow-play="true"
:set-media="setMedia()"
@play="$emit('mediaplay', attachment.id)"
@pause="$emit('mediapause', attachment.id)"
/>
<gallery
v-if="galleryAttachments.length > 0"
:nsfw="nsfwClickthrough"
:attachments="galleryAttachments"
:set-media="setMedia()"
/>
</div>
<div
v-if="status.card && !hideSubjectStatus && !noHeading"
class="link-preview media-body"
>
<link-preview
:card="status.card"
:size="attachmentSize"
:nsfw="nsfwClickthrough"
/>
</div>
</div>
</StatusBody>
<slot name="footer" />
</div>
<!-- eslint-enable vue/no-v-html -->
</template>
<script src="./status_content.js" ></script>
@ -139,156 +61,5 @@ $status-margin: 0.75em;
.StatusContent {
flex: 1;
min-width: 0;
.status-content-wrapper {
display: flex;
flex-direction: column;
flex-wrap: nowrap;
}
.tall-status {
position: relative;
height: 220px;
overflow-x: hidden;
overflow-y: hidden;
z-index: 1;
.status-content {
min-height: 0;
mask: linear-gradient(to top, white, transparent) bottom/100% 70px no-repeat,
linear-gradient(to top, white, white);
/* Autoprefixed seem to ignore this one, and also syntax is different */
-webkit-mask-composite: xor;
mask-composite: exclude;
}
}
.tall-status-hider {
display: inline-block;
word-break: break-all;
position: absolute;
height: 70px;
margin-top: 150px;
width: 100%;
text-align: center;
line-height: 110px;
z-index: 2;
}
.status-unhider, .cw-status-hider {
width: 100%;
text-align: center;
display: inline-block;
word-break: break-all;
svg {
color: inherit;
}
}
img, video {
max-width: 100%;
max-height: 400px;
vertical-align: middle;
object-fit: contain;
&.emoji {
width: 32px;
height: 32px;
}
}
.summary-wrapper {
margin-bottom: 0.5em;
border-style: solid;
border-width: 0 0 1px 0;
border-color: var(--border, $fallback--border);
flex-grow: 0;
}
.summary {
font-style: italic;
padding-bottom: 0.5em;
}
.tall-subject {
position: relative;
.summary {
max-height: 2em;
overflow: hidden;
white-space: nowrap;
text-overflow: ellipsis;
}
}
.tall-subject-hider {
display: inline-block;
word-break: break-all;
// position: absolute;
width: 100%;
text-align: center;
padding-bottom: 0.5em;
}
.status-content {
font-family: var(--postFont, sans-serif);
line-height: 1.4em;
white-space: pre-wrap;
overflow-wrap: break-word;
word-wrap: break-word;
word-break: break-word;
blockquote {
margin: 0.2em 0 0.2em 2em;
font-style: italic;
}
pre {
overflow: auto;
}
code, samp, kbd, var, pre {
font-family: var(--postCodeFont, monospace);
}
p {
margin: 0 0 1em 0;
}
p:last-child {
margin: 0 0 0 0;
}
h1 {
font-size: 1.1em;
line-height: 1.2em;
margin: 1.4em 0;
}
h2 {
font-size: 1.1em;
margin: 1.0em 0;
}
h3 {
font-size: 1em;
margin: 1.2em 0;
}
h4 {
margin: 1.1em 0;
}
&.single-line {
white-space: nowrap;
text-overflow: ellipsis;
overflow: hidden;
height: 1.4em;
}
}
}
.greentext {
color: $fallback--cGreen;
color: var(--postGreentext, $fallback--cGreen);
}
</style>

View file

@ -30,7 +30,7 @@
position: relative;
line-height: 0;
overflow: hidden;
display: flex;
display: inline-flex;
align-items: center;
canvas {
@ -47,12 +47,13 @@
img {
width: 100%;
min-height: 100%;
height: 100%;
object-fit: contain;
}
&.animated {
&::before {
zoom: var(--_still_image-label-scale, 1);
content: 'gif';
position: absolute;
line-height: 10px;

View file

@ -5,6 +5,7 @@ import FollowButton from '../follow_button/follow_button.vue'
import ModerationTools from '../moderation_tools/moderation_tools.vue'
import AccountActions from '../account_actions/account_actions.vue'
import Select from '../select/select.vue'
import RichContent from 'src/components/rich_content/rich_content.jsx'
import generateProfileLink from 'src/services/user_profile_link_generator/user_profile_link_generator'
import { mapGetters } from 'vuex'
import { library } from '@fortawesome/fontawesome-svg-core'
@ -12,14 +13,16 @@ import {
faBell,
faRss,
faSearchPlus,
faExternalLinkAlt
faExternalLinkAlt,
faEdit
} from '@fortawesome/free-solid-svg-icons'
library.add(
faRss,
faBell,
faSearchPlus,
faExternalLinkAlt
faExternalLinkAlt,
faEdit
)
export default {
@ -118,7 +121,8 @@ export default {
AccountActions,
ProgressButton,
FollowButton,
Select
Select,
RichContent
},
methods: {
muteUser () {
@ -153,6 +157,9 @@ export default {
this.$store.state.instance.restrictedNicknames
)
},
openProfileTab () {
this.$store.dispatch('openSettingsModalTab', 'profile')
},
zoomAvatar () {
const attachment = {
url: this.user.profile_image_url_original,

View file

@ -38,22 +38,25 @@
</router-link>
<div class="user-summary">
<div class="top-line">
<!-- eslint-disable vue/no-v-html -->
<div
v-if="user.name_html"
<RichContent
:title="user.name"
class="user-name"
v-html="user.name_html"
:html="user.name"
:emoji="user.emoji"
/>
<!-- eslint-enable vue/no-v-html -->
<div
v-else
:title="user.name"
class="user-name"
>
{{ user.name }}
</div>
<button
v-if="!isOtherUser && user.is_local"
class="button-unstyled edit-profile-button"
@click.stop="openProfileTab"
>
<FAIcon
fixed-width
class="icon"
icon="edit"
:title="$t('user_card.edit_profile')"
/>
</button>
<a
v-if="isOtherUser && !user.is_local"
:href="user.statusnet_profile_url"
target="_blank"
@ -63,7 +66,7 @@
class="icon"
icon="external-link-alt"
/>
</button>
</a>
<AccountActions
v-if="isOtherUser && loggedIn"
:user="user"
@ -79,6 +82,12 @@
@{{ user.screen_name_ui }}
</router-link>
<template v-if="!hideBio">
<span
v-if="user.deactivated"
class="alert user-role"
>
{{ $t('user_card.deactivated') }}
</span>
<span
v-if="!!visibleRole"
class="alert user-role"
@ -157,7 +166,10 @@
class="user-interactions"
>
<div class="btn-group">
<FollowButton :relationship="relationship" />
<FollowButton
:relationship="relationship"
:user="user"
/>
<template v-if="relationship.following">
<ProgressButton
v-if="!relationship.subscribing"
@ -192,6 +204,7 @@
<button
v-if="relationship.muting"
class="btn button-default btn-block toggled"
:disabled="user.deactivated"
@click="unmuteUser"
>
{{ $t('user_card.muted') }}
@ -199,6 +212,7 @@
<button
v-else
class="btn button-default btn-block"
:disabled="user.deactivated"
@click="muteUser"
>
{{ $t('user_card.mute') }}
@ -207,6 +221,7 @@
<div>
<button
class="btn button-default btn-block"
:disabled="user.deactivated"
@click="mentionUser"
>
{{ $t('user_card.mention') }}
@ -255,20 +270,12 @@
<span>{{ hideFollowersCount ? $t('user_card.hidden') : user.followers_count }}</span>
</div>
</div>
<!-- eslint-disable vue/no-v-html -->
<p
v-if="!hideBio && user.description_html"
<RichContent
v-if="!hideBio"
class="user-card-bio"
@click.prevent="linkClicked"
v-html="user.description_html"
:html="user.description_html"
:emoji="user.emoji"
/>
<!-- eslint-enable vue/no-v-html -->
<p
v-else-if="!hideBio"
class="user-card-bio"
>
{{ user.description }}
</p>
</div>
</div>
</template>
@ -281,9 +288,10 @@
.user-card {
position: relative;
&:hover .Avatar {
&:hover {
--_still-image-img-visibility: visible;
--_still-image-canvas-visibility: hidden;
--_still-image-label-visibility: hidden;
}
.panel-heading {
@ -327,12 +335,12 @@
}
}
p {
margin-bottom: 0;
}
&-bio {
text-align: center;
display: block;
line-height: 18px;
padding: 1em;
margin: 0;
a {
color: $fallback--link;
@ -344,11 +352,6 @@
vertical-align: middle;
max-width: 100%;
max-height: 400px;
&.emoji {
width: 32px;
height: 32px;
}
}
}
@ -426,7 +429,7 @@
}
}
.external-link-button {
.external-link-button, .edit-profile-button {
cursor: pointer;
width: 2.5em;
text-align: center;
@ -450,13 +453,6 @@
// big one
z-index: 1;
img {
width: 26px;
height: 26px;
vertical-align: middle;
object-fit: contain
}
.top-line {
display: flex;
}
@ -469,12 +465,7 @@
margin-right: 1em;
font-size: 15px;
img {
object-fit: contain;
height: 16px;
width: 16px;
vertical-align: middle;
}
--emoji-size: 14px;
}
.bottom-line {
@ -578,6 +569,10 @@
}
}
.sidebar .edit-profile-button {
display: none;
}
.user-counts {
display: flex;
line-height:16px;

View file

@ -4,6 +4,7 @@ import FollowCard from '../follow_card/follow_card.vue'
import Timeline from '../timeline/timeline.vue'
import Conversation from '../conversation/conversation.vue'
import TabSwitcher from 'src/components/tab_switcher/tab_switcher.js'
import RichContent from 'src/components/rich_content/rich_content.jsx'
import List from '../list/list.vue'
import withLoadMore from '../../hocs/with_load_more/with_load_more'
import { library } from '@fortawesome/fontawesome-svg-core'
@ -164,7 +165,8 @@ const UserProfile = {
FriendList,
FollowCard,
TabSwitcher,
Conversation
Conversation,
RichContent
}
}

View file

@ -20,20 +20,24 @@
:key="index"
class="user-profile-field"
>
<!-- eslint-disable vue/no-v-html -->
<dt
:title="user.fields_text[index].name"
class="user-profile-field-name"
@click.prevent="linkClicked"
v-html="field.name"
/>
>
<RichContent
:html="field.name"
:emoji="user.emoji"
/>
</dt>
<dd
:title="user.fields_text[index].value"
class="user-profile-field-value"
@click.prevent="linkClicked"
v-html="field.value"
/>
<!-- eslint-enable vue/no-v-html -->
>
<RichContent
:html="field.value"
:emoji="user.emoji"
/>
</dd>
</dl>
</div>
<tab-switcher

View file

@ -10,11 +10,12 @@
"text_limit": "Límit de text",
"title": "Funcionalitats",
"who_to_follow": "A qui seguir",
"pleroma_chat_messages": "Xat de Pleroma"
"pleroma_chat_messages": "Xat de Pleroma",
"upload_limit": "Límit de càrrega"
},
"finder": {
"error_fetching_user": "No s'ha pogut carregar l'usuari/a",
"find_user": "Find user"
"find_user": "Trobar usuari"
},
"general": {
"apply": "Aplica",
@ -32,7 +33,16 @@
"error_retry": "Si us plau, prova de nou",
"generic_error": "Hi ha hagut un error",
"loading": "Carregant…",
"more": "Més"
"more": "Més",
"flash_content": "Fes clic per mostrar el contingut Flash utilitzant Ruffle (experimental, pot no funcionar).",
"flash_security": "Tingues en compte que això pot ser potencialment perillós, ja que el contingut Flash encara és un codi arbitrari.",
"flash_fail": "No s'ha pogut carregar el contingut del flaix, consulta la consola per als detalls.",
"role": {
"moderator": "Moderador/a",
"admin": "Administrador/a"
},
"dismiss": "Descartar",
"peek": "Donar un cop d'ull"
},
"login": {
"login": "Inicia sessió",
@ -45,15 +55,20 @@
"enter_recovery_code": "Posa un codi de recuperació",
"authentication_code": "Codi d'autenticació",
"hint": "Entra per participar a la conversa",
"description": "Entra amb OAuth"
"description": "Entra amb OAuth",
"heading": {
"totp": "Autenticació de dos factors",
"recovery": "Recuperació de dos factors"
},
"enter_two_factor_code": "Introdueix un codi de dos factors"
},
"nav": {
"chat": "Xat local públic",
"friend_requests": "Soŀlicituds de connexió",
"friend_requests": "Sol·licituds de seguiment",
"mentions": "Mencions",
"public_tl": "Flux públic del node",
"public_tl": "Línia temporal pública",
"timeline": "Flux personal",
"twkn": "Flux de la xarxa coneguda",
"twkn": "Xarxa coneguda",
"chats": "Xats",
"timelines": "Línies de temps",
"preferences": "Preferències",
@ -62,19 +77,25 @@
"dms": "Missatges directes",
"interactions": "Interaccions",
"back": "Enrere",
"administration": "Administració"
"administration": "Administració",
"about": "Quant a",
"bookmarks": "Marcadors",
"user_search": "Cerca d'usuaris",
"home_timeline": "Línea temporal personal"
},
"notifications": {
"broken_favorite": "No es coneix aquest estat. S'està cercant.",
"broken_favorite": "Publicació desconeguda, s'està cercant…",
"favorited_you": "ha marcat un estat teu",
"followed_you": "ha començat a seguir-te",
"load_older": "Carrega més notificacions",
"notifications": "Notificacions",
"read": "Read!",
"read": "Llegit!",
"repeated_you": "ha repetit el teu estat",
"migrated_to": "migrat a",
"no_more_notifications": "No més notificacions",
"follow_request": "et vol seguir"
"follow_request": "et vol seguir",
"reacted_with": "ha reaccionat amb {0}",
"error": "Error obtenint notificacions: {0}"
},
"post_status": {
"account_not_locked_warning": "El teu compte no està {0}. Qualsevol persona pot seguir-te per llegir les teves entrades reservades només a seguidores.",
@ -83,24 +104,33 @@
"content_type": {
"text/plain": "Text pla",
"text/markdown": "Markdown",
"text/html": "HTML"
"text/html": "HTML",
"text/bbcode": "BBCode"
},
"content_warning": "Assumpte (opcional)",
"default": "Em sento…",
"default": "Acabe d'aterrar a L.A.",
"direct_warning": "Aquesta entrada només serà visible per les usuràries que etiquetis",
"posting": "Publicació",
"scope": {
"direct": "Directa - Publica només per les usuàries etiquetades",
"private": "Només seguidors/es - Publica només per comptes que et segueixin",
"public": "Pública - Publica als fluxos públics",
"unlisted": "Silenciosa - No la mostris en fluxos públics"
"direct": "Directa - publica només per als usuaris etiquetats",
"private": "Només seguidors/es - publica només per comptes que et segueixin",
"public": "Pública - publica als fluxos públics",
"unlisted": "Silenciosa - no la mostris en fluxos públics"
},
"scope_notice": {
"private": "Aquesta entrada serà visible només per a qui et segueixi",
"public": "Aquesta entrada serà visible per a tothom"
"public": "Aquesta entrada serà visible per a tothom",
"unlisted": "Aquesta entrada no es veurà ni a la Línia de temps local ni a la Línia de temps federada"
},
"preview_empty": "Buida",
"preview": "Vista prèvia"
"preview": "Vista prèvia",
"direct_warning_to_first_only": "Aquesta publicació només serà visible per als usuaris mencionats al principi del missatge.",
"empty_status_error": "No es pot publicar un estat buit sense fitxers adjunts",
"media_description": "Descripció multimèdia",
"direct_warning_to_all": "Aquesta publicació serà visible per a tots els usuaris mencionats.",
"new_status": "Publicar un nou estat",
"post": "Publicació",
"media_description_error": "Ha fallat la pujada del contingut. Prova de nou"
},
"registration": {
"bio": "Presentació",
@ -118,13 +148,19 @@
"username_required": "no es pot deixar en blanc"
},
"fullname_placeholder": "p. ex. Lain Iwakura",
"username_placeholder": "p. ex. lain"
"username_placeholder": "p. ex. lain",
"captcha": "CAPTCHA",
"register": "Registrar-se",
"reason": "Raó per a registrar-se",
"bio_placeholder": "p.e.\nHola, sóc la Lain.\nSóc una noia anime que viu a un suburbi de Japó. Potser em coneixes per Wired.",
"reason_placeholder": "Aquesta instància aprova els registres manualment.\nExplica a l'administració per què vols registrar-te.",
"new_captcha": "Clica a la imatge per obtenir un nou captcha"
},
"settings": {
"attachmentRadius": "Adjunts",
"attachments": "Adjunts",
"avatar": "Avatar",
"avatarAltRadius": "Avatars en les notificacions",
"avatarAltRadius": "Avatars (notificacions)",
"avatarRadius": "Avatars",
"background": "Fons de pantalla",
"bio": "Presentació",
@ -134,8 +170,8 @@
"cOrange": "Taronja (marca com a preferit)",
"cRed": "Vermell (canceŀla)",
"change_password": "Canvia la contrasenya",
"change_password_error": "No s'ha pogut canviar la contrasenya",
"changed_password": "S'ha canviat la contrasenya",
"change_password_error": "No s'ha pogut canviar la contrasenya.",
"changed_password": "S'ha canviat la contrasenya correctament!",
"collapse_subject": "Replega les entrades amb títol",
"confirm_new_password": "Confirma la nova contrasenya",
"current_avatar": "L'avatar actual",
@ -176,7 +212,7 @@
"new_password": "Contrasenya nova",
"notification_visibility": "Notifica'm quan algú",
"notification_visibility_follows": "Comença a seguir-me",
"notification_visibility_likes": "Marca com a preferida una entrada meva",
"notification_visibility_likes": "Favorits",
"notification_visibility_mentions": "Em menciona",
"notification_visibility_repeats": "Republica una entrada meva",
"no_rich_text_description": "Neteja el formatat de text de totes les entrades",
@ -193,7 +229,7 @@
"profile_banner": "Fons de perfil",
"profile_tab": "Perfil",
"radii_help": "Configura l'arrodoniment de les vores (en píxels)",
"replies_in_timeline": "Replies in timeline",
"replies_in_timeline": "Respostes al flux",
"reply_visibility_all": "Mostra totes les respostes",
"reply_visibility_following": "Mostra només les respostes a entrades meves o d'usuàries que jo segueixo",
"reply_visibility_self": "Mostra només les respostes a entrades meves",
@ -216,7 +252,7 @@
"true": "sí"
},
"show_moderator_badge": "Mostra una insígnia de Moderació en el meu perfil",
"show_admin_badge": "Mostra una insígnia d'Administració en el meu perfil",
"show_admin_badge": "Mostra una insígnia \"d'Administració\" en el meu perfil",
"hide_followers_description": "No mostris qui m'està seguint",
"hide_follows_description": "No mostris a qui segueixo",
"notification_visibility_emoji_reactions": "Reaccions",
@ -254,25 +290,270 @@
"allow_following_move": "Permet el seguiment automàtic quan un compte a qui seguim es mou",
"mfa": {
"scan": {
"secret_code": "Clau"
"secret_code": "Clau",
"title": "Escanejar",
"desc": "S'està usant l'aplicació two-factor, escaneja aquest codi QR o introdueix la clau de text:"
},
"authentication_methods": "Mètodes d'autenticació",
"waiting_a_recovery_codes": "Rebent còpies de seguretat dels codis…",
"recovery_codes": "Codis de recuperació.",
"warning_of_generate_new_codes": "Quan generes nous codis de recuperació, els antics ja no funcionaran més.",
"generate_new_recovery_codes": "Genera nous codis de recuperació"
"generate_new_recovery_codes": "Genera nous codis de recuperació",
"otp": "OTP",
"confirm_and_enable": "Confirmar i habilitar OTP",
"recovery_codes_warning": "Anote els codis o guarda'ls en un lloc segur, o no els veuràs una altra volta. Si perds l'accés a la teua aplicació 2FA i els codis de recuperació, no podràs accedir al compte.",
"title": "Autenticació de dos factors",
"setup_otp": "Configurar OTP",
"wait_pre_setup_otp": "preconfiguració OTP",
"verify": {
"desc": "Per habilitar l'autenticació two-factor, introdueix el codi des de la teva aplicació two-factor:"
}
},
"enter_current_password_to_confirm": "Posar la contrasenya actual per confirmar la teva identitat",
"security": "Seguretat",
"app_name": "Nom de l'aplicació"
"app_name": "Nom de l'aplicació",
"subject_line_mastodon": "Com a mastodon: copiar com és",
"mute_export_button": "Exportar silenciats a un fitxer csv",
"mute_import_error": "Error al importar silenciats",
"mutes_imported": "Silenciats importats! Processar-los portarà una estona.",
"import_mutes_from_a_csv_file": "Importar silenciats des d'un fitxer csv",
"word_filter": "Filtre de paraules",
"hide_media_previews": "Ocultar les vistes prèvies multimèdia",
"hide_filtered_statuses": "Amagar estats filtrats",
"play_videos_in_modal": "Reproduir vídeos en un marc emergent",
"file_export_import": {
"errors": {
"invalid_file": "El fitxer seleccionat no és vàlid com a còpia de seguretat de la configuració. No s'ha realitzat cap canvi.",
"file_too_new": "Versió important incompatible: {fileMajor}, aquest PleromaFE (configuració versió {feMajor}) és massa antiga per gestionar-lo",
"file_too_old": "Versió important incompatible: {fileMajor}, la versió del fitxer és massa antiga i no està implementada (s'ha establert un mínim ver. {feMajor})",
"file_slightly_new": "La versió menor del fitxer és diferent, alguns paràmetres podrien no carregar-se"
},
"backup_settings": "Còpia de seguretat de la configuració a un fitxer",
"backup_settings_theme": "Còpia de seguretat de la configuració i tema a un fitxer",
"restore_settings": "Restaurar configuració des d'un fitxer",
"backup_restore": "Còpia de seguretat de la configuració"
},
"user_mutes": "Usuaris",
"subject_line_email": "Com a l'email: \"re: tema\"",
"search_user_to_block": "Busca a qui vols bloquejar",
"save": "Guardar els canvis",
"use_contain_fit": "No retallar els adjunts en miniatures",
"reset_profile_background": "Restablir fons del perfil",
"reset_profile_banner": "Restablir banner del perfil",
"emoji_reactions_on_timeline": "Mostrar reaccions emoji al flux",
"max_thumbnails": "Quantitat màxima de miniatures per publicació",
"hide_user_stats": "Amagar les estadístiques de l'usuari (p. ex. el nombre de seguidors)",
"reset_banner_confirm": "Realment vols restablir el banner?",
"reset_background_confirm": "Realment vols restablir el fons del perfil?",
"subject_input_always_show": "Sempre mostrar el camp del tema",
"subject_line_noop": "No copiar",
"subject_line_behavior": "Copiar el tema a les respostes",
"search_user_to_mute": "Busca a qui vols silenciar",
"mute_export": "Exportar silenciats",
"scope_copy": "Copiar visibilitat quan contestes (En els missatges directes sempre es copia)",
"reset_avatar": "Restablir avatar",
"right_sidebar": "Mostrar barra lateral a la dreta",
"no_blocks": "No hi han bloquejats",
"no_mutes": "No hi han silenciats",
"hide_follows_count_description": "No mostrar el nombre de comptes que segueixo",
"mute_import": "Importar silenciats",
"hide_all_muted_posts": "Ocultar publicacions silenciades",
"hide_wallpaper": "Amagar el fons de la instància",
"notification_visibility_moves": "Usuari Migrat",
"reply_visibility_following_short": "Mostrar respostes als meus seguidors",
"reply_visibility_self_short": "Mostrar respostes només a un mateix",
"autohide_floating_post_button": "Ocultar automàticament el botó 'Nova Publicació' (mòbil)",
"minimal_scopes_mode": "Minimitzar les opcions de visibilitat de la publicació",
"sensitive_by_default": "Marcar publicacions com a sensibles per defecte",
"useStreamingApi": "Rebre publicacions i notificacions en temps real",
"hide_isp": "Ocultar el panell especific de la instància",
"preload_images": "Precarregar les imatges",
"setting_changed": "La configuració és diferent a la predeterminada",
"hide_followers_count_description": "No mostrar el nombre de seguidors",
"reset_avatar_confirm": "Realment vols restablir l'avatar?",
"accent": "Accent",
"useStreamingApiWarning": "(No recomanat, experimental, pot ometre publicacions)",
"style": {
"fonts": {
"family": "Nom de la font",
"size": "Mida (en píxels)",
"custom": "Personalitza",
"_tab_label": "Fonts",
"help": "Selecciona la font per als elements de la interfície. Per a \"personalitzat\" deus escriure el nom de la font exactament com apareix al sistema.",
"components": {
"post": "Text de les publicacions",
"postCode": "Text monoespai en publicació (text enriquit)",
"input": "Camps d'entrada",
"interface": "Interfície"
},
"weight": "Pes (negreta)"
},
"preview": {
"input": "Acabo d'aterrar a Los Angeles.",
"button": "Botó",
"mono": "contingut",
"content": "Contingut",
"header": "Previsualització",
"header_faint": "Això està bé",
"error": "Exemple d'error",
"faint_link": "Manual d'ajuda",
"checkbox": "He llegit els termes i condicions",
"link": "un bonic enllaç",
"fine_print": "Llegiu el nostre {0} per no aprendre res útil!",
"text": "Un grapat més de {0} i {1}"
},
"shadows": {
"spread": "Difon",
"filter_hint": {
"drop_shadow_syntax": "{0} no suporta el paràmetre {1} i la paraula clau {2}.",
"avatar_inset": "Tingues en compte que combinar ombres interiors i no interiors als avatars podria donar resultats inesperats amb avatars transparents.",
"inset_classic": "Les ombres interiors estaran usant {0}",
"always_drop_shadow": "Advertència, aquesta ombra sempre utilitza {0} quan el navegador ho suporta.",
"spread_zero": "Ombres amb propagació > 0 apareixeran com si estigueren posades a zero"
},
"components": {
"popup": "Texts i finestres emergents (popups & tooltips)",
"panel": "Panell",
"panelHeader": "Capçalera del panell",
"avatar": "Avatar de l'usuari (en vista de perfil)",
"input": "Camp d'entrada",
"buttonHover": "Botó (surant)",
"buttonPressed": "Botó (pressionat)",
"topBar": "Barra superior",
"buttonPressedHover": "Botó (surant i pressionat)",
"avatarStatus": "Avatar de l'usuari (en vista de publicació)",
"button": "Botó"
},
"hintV3": "per a les ombres també pots usar la notació {0} per a utilitzar un altre espai de color.",
"blur": "Difuminat",
"component": "Component",
"override": "Sobreescriure",
"shadow_id": "Ombra #{value}",
"_tab_label": "Ombra i il·luminació",
"inset": "Ombra interior"
},
"switcher": {
"use_snapshot": "Versió antiga",
"help": {
"future_version_imported": "El fitxer importat es va crear per a una versió del front-end més recent.",
"migration_snapshot_ok": "Per a estar segurs, s'ha carregat la instantània del tema. Pots intentar carregar les dades del tema.",
"migration_napshot_gone": "Per alguna raó, faltava la instantània, algunes coses podrien veure's diferents del que recordes.",
"snapshot_source_mismatch": "Conflicte de versions: probablement el front-end s'ha revertit i actualitzat una altra volta, si has canviat el tema en una versió anterior, segurament vols utilitzar la versió antiga; d'altra banda utilitza la nova versió.",
"v2_imported": "El fitxer que has importat va ser creat per a un front-end més antic. Intentem maximitzar la compatibilitat, però podrien haver inconsistències.",
"fe_upgraded": "El motor de temes de PleromaFE es va actualitzar després de l'actualització de la versió.",
"snapshot_missing": "No hi havia cap instantània del tema al fitxer, per tant podria veure's diferent del previst originalment.",
"upgraded_from_v2": "PleromaFE s'ha actualitzat, el tema pot veure's un poc diferent de com recordes.",
"fe_downgraded": "Versió de PleromaFE revertida.",
"older_version_imported": "El fitxer que has importat va ser creat en una versió del front-end més antiga.",
"snapshot_present": "S'ha carregat la instantània del tema, de manera que tots els valors estan sobreescrits. En canvi, podeu carregar les dades reals del tema."
},
"keep_as_is": "Mantindre com està",
"save_load_hint": "Les opcions \"Mantindre\" conserven les opcions configurades actualment al seleccionar o carregar temes, també emmagatzema aquestes opcions quan s'exporta un tema. Quan es desactiven totes les caselles de verificació, el tema exportat ho guardarà tot.",
"keep_color": "Mantindre colors",
"keep_opacity": "Mantindre opacitat",
"keep_shadows": "Mantindre ombres",
"keep_fonts": "Mantindre fonts",
"keep_roundness": "Mantindre rodoneses",
"clear_all": "Netejar tot",
"reset": "Reinciar",
"load_theme": "Carregar tema",
"use_source": "Nova versió",
"clear_opacity": "Netejar opacitat"
},
"common": {
"contrast": {
"hint": "El ràtio de contrast és {ratio}. {level} {context}",
"level": {
"bad": "no compleix amb cap pauta d'accecibilitat",
"aaa": "Compleix amb el nivell AA (recomanat)",
"aa": "Compleix amb el nivell AA (mínim)"
},
"context": {
"18pt": "per a textos grans (+18pt)",
"text": "per a textos"
}
},
"opacity": "Opacitat",
"color": "Color"
},
"advanced_colors": {
"badge": "Fons de insígnies",
"inputs": "Camps d'entrada",
"wallpaper": "Fons de pantalla",
"pressed": "Pressionat",
"chat": {
"outgoing": "Eixint",
"border": "Borde",
"incoming": "Entrants"
},
"borders": "Bordes",
"panel_header": "Capçalera del panell",
"buttons": "Botons",
"faint_text": "Text esvaït",
"poll": "Gràfica de l'enquesta",
"toggled": "Commutat",
"alert": "Fons d'alertes",
"alert_error": "Error",
"alert_warning": "Precaució",
"post": "Publicacions/Biografies d'usuaris",
"badge_notification": "Notificacions",
"selectedMenu": "Element del menú seleccionat",
"tabs": "Pestanyes",
"_tab_label": "Avançat",
"alert_neutral": "Neutral",
"popover": "Suggeriments, menús, superposicions",
"top_bar": "Barra superior",
"highlight": "Elements destacats",
"disabled": "Deshabilitat",
"icons": "Icones",
"selectedPost": "Publicació seleccionada",
"underlay": "Subratllat"
},
"common_colors": {
"main": "Colors comuns",
"rgbo": "Icones, accents, insígnies",
"foreground_hint": "mira la pestanya \"Avançat\" per a un control més detallat",
"_tab_label": "Comú"
},
"radii": {
"_tab_label": "Rodonesa"
}
},
"version": {
"frontend_version": "Versió \"Frontend\"",
"backend_version": "Versió \"backend\"",
"title": "Versió"
},
"theme_help_v2_1": "També pots anular alguns components de color i opacitat activant la casella. Usa el botó \"Esborrar tot\" per esborrar totes les anulacions.",
"type_domains_to_mute": "Buscar dominis per a silenciar",
"greentext": "Text verd (meme arrows)",
"fun": "Divertit",
"notification_setting_filters": "Filtres",
"virtual_scrolling": "Optimitzar la representació del flux",
"notification_setting_block_from_strangers": "Bloqueja les notificacions dels usuaris que no segueixes",
"enable_web_push_notifications": "Habilitar notificacions del navegador",
"notification_blocks": "Bloquejar a un usuari para totes les notificacions i també les cancel·la.",
"more_settings": "Més opcions",
"notification_setting_privacy": "Privacitat",
"upload_a_photo": "Pujar una foto",
"notification_setting_hide_notification_contents": "Amagar el remitent i els continguts de les notificacions push",
"notifications": "Notificacions",
"notification_mutes": "Per a deixar de rebre notificacions d'un usuari en concret, silencia'l-ho.",
"theme_help_v2_2": "Les icones per baix d'algunes entrades són indicadors del contrast del fons/text, desplaça el ratolí per a més informació. Tingues en compte que quan s'utilitzen indicadors de contrast de transparència es mostra el pitjor cas possible.",
"hide_shoutbox": "Oculta la casella de gàbia de grills",
"always_show_post_button": "Mostra sempre el botó flotant de publicació nova",
"pad_emoji": "Acompanya els emojis amb espais en afegir des del selector",
"mentions_new_style": "Enllaços d'esment més elegants",
"mentions_new_place": "Posa les mencions en una línia separada",
"post_status_content_type": "Format de publicació"
},
"time": {
"day": "{0} dia",
"days": "{0} dies",
"day_short": "{0} dia",
"days_short": "{0} dies",
"hour": "{0} hour",
"hours": "{0} hours",
"hour": "{0} hora",
"hours": "{0} hores",
"hour_short": "{0}h",
"hours_short": "{0}h",
"in_future": "in {0}",
@ -287,12 +568,12 @@
"months_short": "{0} mesos",
"now": "ara mateix",
"now_short": "ara mateix",
"second": "{0} second",
"seconds": "{0} seconds",
"second": "{0} segon",
"seconds": "{0} segons",
"second_short": "{0}s",
"seconds_short": "{0}s",
"week": "{0} setm.",
"weeks": "{0} setm.",
"week": "{0} setmana",
"weeks": "{0} setmanes",
"week_short": "{0} setm.",
"weeks_short": "{0} setm.",
"year": "{0} any",
@ -308,7 +589,13 @@
"no_retweet_hint": "L'entrada és només per a seguidores o és \"directa\", i per tant no es pot republicar",
"repeated": "republicat",
"show_new": "Mostra els nous",
"up_to_date": "Actualitzat"
"up_to_date": "Actualitzat",
"socket_reconnected": "Connexió a temps real establerta",
"socket_broke": "Connexió a temps real perduda: codi CloseEvent {0}",
"error": "Error de càrrega de la línia de temps: {0}",
"no_statuses": "No hi ha entrades",
"reload": "Recarrega",
"no_more_statuses": "No hi ha més entrades"
},
"user_card": {
"approve": "Aprova",
@ -324,13 +611,62 @@
"muted": "Silenciat",
"per_day": "per dia",
"remote_follow": "Seguiment remot",
"statuses": "Estats"
"statuses": "Estats",
"unblock_progress": "Desbloquejant…",
"unmute": "Deixa de silenciar",
"follow_progress": "Sol·licitant…",
"admin_menu": {
"force_nsfw": "Marca totes les entrades amb \"No segur per a entorns laborals\"",
"strip_media": "Esborra els audiovisuals de les entrades",
"disable_any_subscription": "Deshabilita completament seguir algú",
"quarantine": "Deshabilita la federació a les entrades de les usuàries",
"moderation": "Moderació",
"delete_user_confirmation": "Estàs completament segur/a? Aquesta acció no es pot desfer.",
"revoke_admin": "Revoca l'Admin",
"activate_account": "Activa el compte",
"deactivate_account": "Desactiva el compte",
"revoke_moderator": "Revoca Moderació",
"delete_account": "Esborra el compte",
"disable_remote_subscription": "Deshabilita seguir algú des d'una instància remota",
"delete_user": "Esborra la usuària",
"grant_admin": "Concedir permisos d'Administració",
"grant_moderator": "Concedir permisos de Moderació",
"force_unlisted": "Força que les publicacions no estiguin llistades",
"sandbox": "Força que els missatges siguin només seguidors"
},
"edit_profile": "Edita el perfil",
"hidden": "Amagat",
"follow_sent": "Petició enviada!",
"unmute_progress": "Deixant de silenciar…",
"bot": "Bot",
"mute_progress": "Silenciant…",
"favorites": "Favorits",
"mention": "Menció",
"follow_unfollow": "Deixa de seguir",
"subscribe": "Subscriu-te",
"show_repeats": "Mostra les repeticions",
"report": "Report",
"its_you": "Ets tu!",
"unblock": "Desbloqueja",
"block_progress": "Bloquejant…",
"message": "Missatge",
"unsubscribe": "Anul·la la subscripció",
"hide_repeats": "Amaga les repeticions",
"highlight": {
"disabled": "Sense ressaltat",
"solid": "Fons sòlid",
"striped": "Fons a ratlles",
"side": "Ratlla lateral"
},
"media": "Media"
},
"user_profile": {
"timeline_title": "Flux personal"
"timeline_title": "Flux personal",
"profile_loading_error": "Disculpes, hi ha hagut un error carregant aquest perfil.",
"profile_does_not_exist": "Disculpes, aquest perfil no existeix."
},
"who_to_follow": {
"more": "More",
"more": "Més",
"who_to_follow": "A qui seguir"
},
"selectable_list": {
@ -338,14 +674,25 @@
},
"remote_user_resolver": {
"error": "No trobat.",
"searching_for": "Cercant per"
"searching_for": "Cercant per",
"remote_user_resolver": "Resolució d'usuari remot"
},
"interactions": {
"load_older": "Carrega antigues interaccions",
"favs_repeats": "Repeticions i favorits"
"favs_repeats": "Repeticions i favorits",
"follows": "Nous seguidors",
"moves": "Migració d'usuaris"
},
"emoji": {
"stickers": "Adhesius"
"stickers": "Adhesius",
"keep_open": "Mantindre el selector obert",
"custom": "Emojis personalitzats",
"unicode": "Emojis unicode",
"load_all_hint": "Carregat el primer emoji {saneAmount}, carregar tots els emoji pot causar problemes de rendiment.",
"emoji": "Emoji",
"search_emoji": "Buscar un emoji",
"add_emoji": "Inserir un emoji",
"load_all": "Carregant tots els {emojiAmount} emoji"
},
"polls": {
"expired": "L'enquesta va acabar fa {0}",
@ -357,7 +704,11 @@
"votes": "vots",
"option": "Opció",
"add_option": "Afegeix opció",
"add_poll": "Afegeix enquesta"
"add_poll": "Afegeix enquesta",
"expiry": "Temps de vida de l'enquesta",
"people_voted_count": "{count} persona ha votat | {count} persones han votat",
"votes_count": "{count} vot | {count} vots",
"not_enough_options": "L'enquesta no té suficients opcions úniques"
},
"media_modal": {
"next": "Següent",
@ -365,7 +716,8 @@
},
"importer": {
"error": "Ha succeït un error mentre s'importava aquest arxiu.",
"success": "Importat amb èxit."
"success": "Importat amb èxit.",
"submit": "Enviar"
},
"image_cropper": {
"cancel": "Cancel·la",
@ -379,7 +731,9 @@
},
"domain_mute_card": {
"mute_progress": "Silenciant…",
"mute": "Silencia"
"mute": "Silencia",
"unmute": "Deixar de silenciar",
"unmute_progress": "Deixant de silenciar…"
},
"about": {
"staff": "Equip responsable",
@ -391,16 +745,136 @@
"reject": "Rebutja",
"accept_desc": "Aquesta instància només accepta missatges de les següents instàncies:",
"accept": "Accepta",
"simple_policies": "Polítiques específiques de la instància"
"simple_policies": "Polítiques específiques de la instància",
"ftl_removal_desc": "Aquesta instància elimina les següents instàncies del flux de la xarxa coneguda:",
"ftl_removal": "Eliminació de la línia de temps coneguda",
"media_nsfw_desc": "Aquesta instància obliga el contingut multimèdia a establir-se com a sensible dins de les publicacions en les següents instàncies:",
"media_removal": "Eliminació de la multimèdia",
"media_removal_desc": "Aquesta instància elimina els suports multimèdia de les publicacions en les següents instàncies:",
"media_nsfw": "Forçar contingut multimèdia com a sensible"
},
"mrf_policies_desc": "Les polítiques MRF controlen el comportament federat de la instància. Les següents polítiques estan habilitades:",
"mrf_policies": "Polítiques MRF habilitades",
"keyword": {
"replace": "Reemplaça",
"reject": "Rebutja",
"keyword_policies": "Polítiques de paraules clau"
"keyword_policies": "Filtratge per paraules clau",
"is_replaced_by": "→",
"ftl_removal": "Eliminació de la línia de temps federada"
},
"federation": "Federació"
}
},
"shoutbox": {
"title": "Gàbia de Grills"
},
"status": {
"delete": "Esborra l'entrada",
"delete_confirm": "Segur que vols esborrar aquesta entrada?",
"thread_muted_and_words": ", té les paraules:",
"show_full_subject": "Mostra tot el tema",
"show_content": "Mostra el contingut",
"repeats": "Repeticions",
"bookmark": "Marcadors",
"status_unavailable": "Entrada no disponible",
"expand": "Expandeix",
"copy_link": "Copia l'enllaç a l'entrada",
"hide_full_subject": "Amaga tot el tema",
"favorites": "Favorits",
"replies_list": "Contestacions:",
"mute_conversation": "Silencia la conversa",
"thread_muted": "Fil silenciat",
"hide_content": "Amaga el contingut",
"status_deleted": "S'ha esborrat aquesta entrada",
"nsfw": "No segur per a entorns laborals",
"unbookmark": "Desmarca",
"external_source": "Font externa",
"unpin": "Deixa de destacar al perfil",
"pinned": "Destacat",
"reply_to": "Contesta a",
"pin": "Destaca al perfil",
"unmute_conversation": "Deixa de silenciar la conversa",
"mentions": "Mencions",
"you": "(Tu)",
"plus_more": "+{number} més"
},
"user_reporting": {
"additional_comments": "Comentaris addicionals",
"forward_description": "Aquest compte és d'un altre servidor. Vols enviar una còpia del report allà també?",
"forward_to": "Endavant a {0}",
"generic_error": "Hi ha hagut un error mentre s'estava processant la teva sol·licitud.",
"title": "Reportant {0}",
"add_comment_description": "Aquest report serà enviat a la moderació a la instància. Pots donar una explicació de per què estàs reportant aquest compte:",
"submit": "Envia"
},
"tool_tip": {
"add_reaction": "Afegeix una Reacció",
"accept_follow_request": "Accepta la sol·licitud de seguir",
"repeat": "Repeteix",
"reply": "Respon",
"favorite": "Favorit",
"user_settings": "Configuració d'usuària",
"reject_follow_request": "Rebutja la sol·licitud de seguir",
"bookmark": "Marcador",
"media_upload": "Pujar multimèdia"
},
"search": {
"no_results": "No hi ha resultats",
"people": "Persones",
"hashtags": "Etiquetes",
"people_talking": "{count} persones parlant",
"person_talking": "{count} persones parlant"
},
"upload": {
"file_size_units": {
"B": "B",
"KiB": "KiB",
"GiB": "GiB",
"TiB": "TiB",
"MiB": "MiB"
},
"error": {
"base": "La pujada ha fallat.",
"file_too_big": "Fitxer massa gran [{filesize}{filesizeunit} / {allowedsize}{allowedsizeunit}]",
"default": "Prova de nou d'aquí una estona",
"message": "La pujada ha fallat: {0}"
}
},
"errors": {
"storage_unavailable": "Pleroma no ha pogut accedir a l'emmagatzematge del navegador. El teu inici de sessió o configuració no es desaran i et pots trobar algun altre problema. Prova a habilitar les galetes."
},
"password_reset": {
"password_reset": "Reinicia la contrasenya",
"forgot_password": "Has oblidat la contrasenya?",
"too_many_requests": "Has arribat al límit d'intents. Prova de nou d'aquí una estona.",
"password_reset_required_but_mailer_is_disabled": "Has de reiniciar la teva contrasenya però el reinici de la contrasenya està deshabilitat. Si us plau, contacta l'administració de la teva instància.",
"placeholder": "El teu correu electrònic o nom d'usuària",
"instruction": "Introdueix la teva adreça de correu electrònic o nom d'usuària. T'enviarem un enllaç per reiniciar la teva contrasenya.",
"return_home": "Torna a la pàgina principal",
"password_reset_required": "Has de reiniciar la teva contrasenya per iniciar la sessió.",
"password_reset_disabled": "El reinici de la contrasenya està deshabilitat. Si us plau, contacta l'administració de la teva instància.",
"check_email": "Comprova que has rebut al correu electrònic un enllaç per reiniciar la teva contrasenya."
},
"file_type": {
"image": "Imatge",
"file": "Fitxer",
"video": "Vídeo",
"audio": "Àudio"
},
"chats": {
"chats": "Xats",
"new": "Nou xat",
"delete_confirm": "Realment vols esborrar aquest missatge?",
"error_sending_message": "Alguna cosa ha fallat quan s'enviava el missatge.",
"more": "Més",
"delete": "Esborra",
"empty_message_error": "No es pot publicar un missatge buit",
"you": "Tu:",
"message_user": "Missatge {nickname}",
"error_loading_chat": "Alguna cosa ha fallat quan es carregava el xat.",
"empty_chat_list_placeholder": "Encara no tens cap xat. Crea un nou xat!"
},
"display_date": {
"today": "Avui"
}
}

View file

@ -407,7 +407,6 @@
"follow": "Sledovat",
"follow_sent": "Požadavek odeslán!",
"follow_progress": "Odeslílám požadavek…",
"follow_again": "Odeslat požadavek znovu?",
"follow_unfollow": "Přestat sledovat",
"followees": "Sledovaní",
"followers": "Sledující",

View file

@ -9,7 +9,7 @@
"scope_options": "Reichweitenoptionen",
"text_limit": "Zeichenlimit",
"title": "Funktionen",
"who_to_follow": "Wem folgen?",
"who_to_follow": "Vorschläge",
"upload_limit": "Maximale Upload Größe",
"pleroma_chat_messages": "Pleroma Chat"
},
@ -39,7 +39,10 @@
"close": "Schliessen",
"retry": "Versuche es erneut",
"error_retry": "Bitte versuche es erneut",
"loading": "Lade…"
"loading": "Lade…",
"flash_content": "Klicken, um den Flash-Inhalt mit Ruffle anzuzeigen (Die Funktion ist experimentell und funktioniert daher möglicherweise nicht).",
"flash_security": "Diese Funktion stellt möglicherweise eine Risiko dar, weil Flash-Inhalte weiterhin potentiell gefährlich sind.",
"flash_fail": "Falsh-Inhalt konnte nicht geladen werden, Details werden in der Konsole angezeigt."
},
"login": {
"login": "Anmelden",
@ -538,7 +541,9 @@
"reset_background_confirm": "Hintergrund wirklich zurücksetzen?",
"reset_banner_confirm": "Banner wirklich zurücksetzen?",
"reset_avatar_confirm": "Avatar wirklich zurücksetzen?",
"reset_profile_banner": "Profilbanner zurücksetzen"
"reset_profile_banner": "Profilbanner zurücksetzen",
"hide_shoutbox": "Shoutbox der Instanz verbergen",
"right_sidebar": "Seitenleiste rechts anzeigen"
},
"timeline": {
"collapse": "Einklappen",
@ -564,7 +569,6 @@
"follow": "Folgen",
"follow_sent": "Anfrage gesendet!",
"follow_progress": "Anfragen…",
"follow_again": "Anfrage erneut senden?",
"follow_unfollow": "Folgen beenden",
"followees": "Folgt",
"followers": "Folgende",
@ -779,7 +783,7 @@
"error_sending_message": "Beim Senden der Nachricht ist ein Fehler aufgetreten.",
"error_loading_chat": "Beim Laden des Chats ist ein Fehler aufgetreten.",
"delete_confirm": "Soll diese Nachricht wirklich gelöscht werden?",
"empty_message_error": "Die Nachricht darf nicht leer sein.",
"empty_message_error": "Die Nachricht darf nicht leer sein",
"delete": "Löschen",
"message_user": "Nachricht an {nickname} senden",
"empty_chat_list_placeholder": "Es sind noch keine Chats vorhanden. Jetzt einen Chat starten!",

View file

@ -13,6 +13,9 @@
"mrf_policies_desc": "MRF policies manipulate the federation behaviour of the instance. The following policies are enabled:",
"simple": {
"simple_policies": "Instance-specific policies",
"instance": "Instance",
"reason": "Reason",
"not_applicable": "N/A",
"accept": "Accept",
"accept_desc": "This instance only accepts messages from the following instances:",
"reject": "Reject",
@ -352,6 +355,7 @@
"hide_isp": "Hide instance-specific panel",
"hide_shoutbox": "Hide instance shoutbox",
"right_sidebar": "Show sidebar on the right side",
"always_show_post_button": "Always show floating New Post button",
"hide_wallpaper": "Hide instance wallpaper",
"preload_images": "Preload images",
"use_one_click_nsfw": "Open NSFW attachments with just one click",
@ -704,6 +708,7 @@
"unbookmark": "Unbookmark",
"delete_confirm": "Do you really want to delete this status?",
"reply_to": "Reply to",
"mentions": "Mentions",
"replies_list": "Replies:",
"mute_conversation": "Mute conversation",
"unmute_conversation": "Unmute conversation",
@ -718,18 +723,22 @@
"hide_content": "Hide content",
"status_deleted": "This post was deleted",
"nsfw": "NSFW",
"expand": "Expand"
"expand": "Expand",
"you": "(You)",
"plus_more": "+{number} more"
},
"user_card": {
"approve": "Approve",
"block": "Block",
"blocked": "Blocked!",
"deactivated": "Deactivated",
"deny": "Deny",
"edit_profile": "Edit profile",
"favorites": "Favorites",
"follow": "Follow",
"follow_cancel": "Cancel request",
"follow_sent": "Request sent!",
"follow_progress": "Requesting…",
"follow_again": "Send request again?",
"follow_unfollow": "Unfollow",
"followees": "Following",
"followers": "Followers",

View file

@ -39,7 +39,10 @@
"role": {
"moderator": "Reguligisto",
"admin": "Administranto"
}
},
"flash_content": "Klaku por montri enhavon de Flash per Ruffle. (Eksperimente, eble ne funkcios.)",
"flash_security": "Sciu, ke tio povas esti danĝera, ĉar la enhavo de Flash ja estas arbitra programo.",
"flash_fail": "Malsukcesis enlegi enhavon de Flash; vidu detalojn en konzolo."
},
"image_cropper": {
"crop_picture": "Tondi bildon",
@ -87,7 +90,8 @@
"interactions": "Interagoj",
"administration": "Administrado",
"bookmarks": "Legosignoj",
"timelines": "Historioj"
"timelines": "Historioj",
"home_timeline": "Hejma historio"
},
"notifications": {
"broken_favorite": "Nekonata stato, serĉante ĝin…",
@ -119,10 +123,10 @@
"direct_warning": "Ĉi tiu afiŝo estos videbla nur por ĉiuj menciitaj uzantoj.",
"posting": "Afiŝante",
"scope": {
"direct": "Rekta Afiŝi nur al menciitaj uzantoj",
"private": "Nur abonantoj Afiŝi nur al abonantoj",
"public": "Publika Afiŝi al publikaj historioj",
"unlisted": "Nelistigita Ne afiŝi al publikaj historioj"
"direct": "Rekta afiŝi nur al menciitaj uzantoj",
"private": "Nur abonantoj afiŝi nur al abonantoj",
"public": "Publika afiŝi al publikaj historioj",
"unlisted": "Nelistigita ne afiŝi al publikaj historioj"
},
"scope_notice": {
"unlisted": "Ĉi tiu afiŝo ne estos videbla en la Publika historio kaj La tuta konata reto",
@ -135,7 +139,8 @@
"preview": "Antaŭrigardo",
"direct_warning_to_first_only": "Ĉi tiu afiŝo estas nur videbla al uzantoj menciitaj je la komenco de la mesaĝo.",
"direct_warning_to_all": "Ĉi tiu afiŝo estos videbla al ĉiuj menciitaj uzantoj.",
"media_description": "Priskribo de vidaŭdaĵo"
"media_description": "Priskribo de vidaŭdaĵo",
"post": "Afiŝo"
},
"registration": {
"bio": "Priskribo",
@ -143,7 +148,7 @@
"fullname": "Prezenta nomo",
"password_confirm": "Konfirmo de pasvorto",
"registration": "Registriĝo",
"token": "Invita ĵetono",
"token": "Invita peco",
"captcha": "TESTO DE HOMECO",
"new_captcha": "Klaku la bildon por akiri novan teston",
"username_placeholder": "ekz. lain",
@ -158,7 +163,8 @@
"password_confirmation_match": "samu la pasvorton"
},
"reason_placeholder": "Ĉi-node oni aprobas registriĝojn permane.\nSciigu la administrantojn kial vi volas registriĝi.",
"reason": "Kialo registriĝi"
"reason": "Kialo registriĝi",
"register": "Registriĝi"
},
"settings": {
"app_name": "Nomo de aplikaĵo",
@ -244,9 +250,9 @@
"show_admin_badge": "Montri la insignon de administranto en mia profilo",
"show_moderator_badge": "Montri la insignon de reguligisto en mia profilo",
"nsfw_clickthrough": "Ŝalti traklakan kaŝadon de kunsendaĵoj kaj antaŭmontroj de ligiloj por konsternaj statoj",
"oauth_tokens": "Ĵetonoj de OAuth",
"token": "Ĵetono",
"refresh_token": "Ĵetono de aktualigo",
"oauth_tokens": "Pecoj de OAuth",
"token": "Peco",
"refresh_token": "Aktualiga peco",
"valid_until": "Valida ĝis",
"revoke_token": "Senvalidigi",
"panelRadius": "Bretoj",
@ -532,7 +538,25 @@
"hide_all_muted_posts": "Kaŝi silentigitajn afiŝojn",
"hide_media_previews": "Kaŝi antaŭrigardojn al vidaŭdaĵoj",
"word_filter": "Vortofiltro",
"reply_visibility_self_short": "Montri nur respondojn por mi"
"reply_visibility_self_short": "Montri nur respondojn por mi",
"file_export_import": {
"errors": {
"file_slightly_new": "Etversio de dosiero malsamas, iuj agordoj eble ne funkcios",
"file_too_old": "Nekonforma ĉefa versio: {fileMajor}, versio de dosiero estas tro malnova kaj nesubtenata (minimuma estas {feMajor})",
"file_too_new": "Nekonforma ĉefa versio: {fileMajor}, ĉi tiu PleromaFE (agordoj je versio {feMajor}) tro malnovas por tio",
"invalid_file": "La elektita dosiero ne estas subtenata savkopio de agordoj de Pleroma. Nenio ŝanĝiĝis."
},
"restore_settings": "Rehavi agordojn el dosiero",
"backup_settings_theme": "Savkopii agordojn kaj haŭton al dosiero",
"backup_settings": "Savkopii agordojn al dosiero",
"backup_restore": "Savkopio de agordoj"
},
"right_sidebar": "Montri flankan breton dekstre",
"save": "Konservi ŝanĝojn",
"hide_shoutbox": "Kaŝi kriujon de nodo",
"always_show_post_button": "Ĉiam montri ŝvebantan butonon por nova afiŝo",
"mentions_new_style": "Pli mojosaj menciligiloj",
"mentions_new_place": "Meti menciojn sur apartan linion"
},
"timeline": {
"collapse": "Maletendi",
@ -546,7 +570,9 @@
"no_more_statuses": "Neniuj pliaj statoj",
"no_statuses": "Neniuj statoj",
"reload": "Enlegi ree",
"error": "Eraris akirado de historio: {0}"
"error": "Eraris akirado de historio: {0}",
"socket_reconnected": "Realtempa konekto fariĝis",
"socket_broke": "Realtempa konekto perdiĝis: CloseEvent code {0}"
},
"user_card": {
"approve": "Aprobi",
@ -557,7 +583,6 @@
"follow": "Aboni",
"follow_sent": "Peto sendiĝis!",
"follow_progress": "Petante…",
"follow_again": "Ĉu sendi peton ree?",
"follow_unfollow": "Malaboni",
"followees": "Abonatoj",
"followers": "Abonantoj",
@ -609,7 +634,8 @@
"striped": "Stria fono",
"solid": "Unueca fono",
"disabled": "Senemfaze"
}
},
"edit_profile": "Redakti profilon"
},
"user_profile": {
"timeline_title": "Historio de uzanto",
@ -696,7 +722,7 @@
"media_nsfw": "Devige marki vidaŭdaĵojn konsternaj",
"media_removal_desc": "Ĉi tiu nodo forigas vidaŭdaĵojn de afiŝoj el la jenaj nodoj:",
"media_removal": "Forigo de vidaŭdaĵoj",
"ftl_removal": "Forigo el la historio de «La tuta konata reto»",
"ftl_removal": "Forigo el la historio de «Konata reto»",
"quarantine_desc": "Ĉi tiu nodo sendos nur publikajn afiŝojn al la jenaj nodoj:",
"quarantine": "Kvaranteno",
"reject_desc": "Ĉi tiu nodo ne akceptos mesaĝojn de la jenaj nodoj:",
@ -704,7 +730,7 @@
"accept_desc": "Ĉi tiu nodo nur akceptas mesaĝojn de la jenaj nodoj:",
"accept": "Akcepti",
"simple_policies": "Specialaj politikoj de la nodo",
"ftl_removal_desc": "Ĉi tiu nodo forigas la jenajn nodojn el la historio de «La tuta konata reto»:"
"ftl_removal_desc": "Ĉi tiu nodo forigas la jenajn nodojn el la historio de «Konata reto»:"
},
"mrf_policies": "Ŝaltis politikon de Mesaĝa ŝanĝilaro (MRF)",
"keyword": {
@ -760,7 +786,10 @@
"status_deleted": "Ĉi tiu afiŝo foriĝis",
"nsfw": "Konsterna",
"expand": "Etendi",
"external_source": "Ekstera fonto"
"external_source": "Ekstera fonto",
"mentions": "Mencioj",
"you": "(Vi)",
"plus_more": "+{number} pli"
},
"time": {
"years_short": "{0}j",

View file

@ -43,7 +43,10 @@
"role": {
"admin": "Administrador/a",
"moderator": "Moderador/a"
}
},
"flash_content": "Haga clic para mostrar contenido Flash usando Ruffle (experimental, puede que no funcione).",
"flash_security": "Tenga en cuenta que esto puede ser potencialmente peligroso ya que el contenido Flash sigue siendo código arbitrario.",
"flash_fail": "No se pudo cargar el contenido flash, consulte la consola para obtener más detalles."
},
"image_cropper": {
"crop_picture": "Recortar la foto",
@ -147,7 +150,7 @@
"favs_repeats": "Favoritos y repetidos",
"follows": "Nuevos seguidores",
"load_older": "Cargar interacciones más antiguas",
"moves": "Usuario Migrado"
"moves": "Usuario migrado"
},
"post_status": {
"new_status": "Publicar un nuevo estado",
@ -181,7 +184,7 @@
"preview_empty": "Vacío",
"preview": "Vista previa",
"media_description": "Descripción multimedia",
"post": "Publicación"
"post": "Publicar"
},
"registration": {
"bio": "Biografía",
@ -585,13 +588,21 @@
"save": "Guardar los cambios",
"file_export_import": {
"errors": {
"invalid_file": "El archivo seleccionado no es válido como copia de seguridad de Pleroma. No se han realizado cambios."
"invalid_file": "El archivo seleccionado no es válido como copia de seguridad de Pleroma. No se han realizado cambios.",
"file_too_new": "Versión principal incompatible: {fileMajor}, este \"FrontEnd\" de Pleroma (versión de configuración {feMajor}) es demasiado antiguo para manejarlo",
"file_too_old": "Versión principal incompatible: {fileMajor}, la versión del archivo es demasiado antigua y no es compatible (versión mínima {FeMajor})",
"file_slightly_new": "La versión secundaria del archivo es diferente, es posible que algunas configuraciones no se carguen"
},
"restore_settings": "Restaurar ajustes desde archivo",
"backup_settings_theme": "Copia de seguridad de la configuración y tema a archivo",
"backup_settings": "Copia de seguridad de la configuración a archivo",
"backup_settings_theme": "Descargar la copia de seguridad de la configuración y del tema",
"backup_settings": "Descargar la copia de seguridad de la configuración",
"backup_restore": "Copia de seguridad de la configuración"
}
},
"hide_shoutbox": "Ocultar cuadro de diálogo de la instancia",
"right_sidebar": "Mostrar la barra lateral a la derecha",
"always_show_post_button": "Muestra siempre el botón flotante de Nueva Plubicación",
"mentions_new_style": "Enlaces de menciones más elegantes",
"mentions_new_place": "Situa las menciones en una línea separada"
},
"time": {
"day": "{0} día",
@ -668,7 +679,10 @@
"status_deleted": "Esta publicación ha sido eliminada",
"nsfw": "NSFW (No apropiado para el trabajo)",
"expand": "Expandir",
"external_source": "Fuente externa"
"external_source": "Fuente externa",
"mentions": "Menciones",
"you": "(Tú)",
"plus_more": "+{number} más"
},
"user_card": {
"approve": "Aprobar",
@ -679,7 +693,6 @@
"follow": "Seguir",
"follow_sent": "¡Solicitud enviada!",
"follow_progress": "Solicitando…",
"follow_again": "¿Enviar solicitud de nuevo?",
"follow_unfollow": "Dejar de seguir",
"followees": "Siguiendo",
"followers": "Seguidores",
@ -735,7 +748,8 @@
"solid": "Fondo sólido",
"disabled": "Sin resaltado"
},
"bot": "Bot"
"bot": "Bot",
"edit_profile": "Edita el perfil"
},
"user_profile": {
"timeline_title": "Línea temporal del usuario",

View file

@ -43,7 +43,10 @@
"role": {
"moderator": "Moderatzailea",
"admin": "Administratzailea"
}
},
"flash_content": "Klik egin Flash edukia erakusteko Ruffle erabilita (esperimentala, baliteke ez ibiltzea).",
"flash_security": "Kontuan izan arriskutsua izan daitekeela, Flash edukia kode arbitrarioa baita.",
"flash_fail": "Ezin izan da Flash edukia kargatu. Ikusi kontsola xehetasunetarako."
},
"image_cropper": {
"crop_picture": "Moztu argazkia",
@ -96,7 +99,8 @@
"preferences": "Hobespenak",
"chats": "Txatak",
"timelines": "Denbora-lerroak",
"bookmarks": "Laster-markak"
"bookmarks": "Laster-markak",
"home_timeline": "Denbora-lerro pertsonala"
},
"notifications": {
"broken_favorite": "Egoera ezezaguna, bilatzen…",
@ -136,7 +140,8 @@
"add_emoji": "Emoji bat gehitu",
"custom": "Ohiko emojiak",
"unicode": "Unicode emojiak",
"load_all": "{emojiAmount} emoji guztiak kargatzen"
"load_all": "{emojiAmount} emoji guztiak kargatzen",
"load_all_hint": "Lehenengo {saneAmount} emojia kargatuta, emoji guztiak kargatzeak errendimendu arazoak sor ditzake."
},
"stickers": {
"add_sticker": "Pegatina gehitu"
@ -144,7 +149,8 @@
"interactions": {
"favs_repeats": "Errepikapen eta gogokoak",
"follows": "Jarraitzaile berriak",
"load_older": "Kargatu elkarrekintza zaharragoak"
"load_older": "Kargatu elkarrekintza zaharragoak",
"moves": "Erabiltzailea migratuta"
},
"post_status": {
"new_status": "Mezu berri bat idatzi",
@ -172,14 +178,20 @@
"private": "Jarraitzaileentzako bakarrik: bidali jarraitzaileentzat bakarrik",
"public": "Publikoa: bistaratu denbora-lerro publikoetan",
"unlisted": "Zerrendatu gabea: ez bidali denbora-lerro publikoetara"
}
},
"media_description_error": "Ezin izan da artxiboa eguneratu, saiatu berriro",
"preview": "Aurrebista",
"media_description": "Media deskribapena",
"preview_empty": "Hutsik",
"post": "Bidali",
"empty_status_error": "Ezin da argitaratu ezer idatzi gabe edo eranskinik gabe"
},
"registration": {
"bio": "Biografia",
"email": "E-posta",
"fullname": "Erakutsi izena",
"password_confirm": "Pasahitza berretsi",
"registration": "Izena ematea",
"registration": "Sortu kontua",
"token": "Gonbidapen txartela",
"captcha": "CAPTCHA",
"new_captcha": "Klikatu irudia captcha berri bat lortzeko",
@ -193,7 +205,10 @@
"password_required": "Ezin da hutsik utzi",
"password_confirmation_required": "Ezin da hutsik utzi",
"password_confirmation_match": "Pasahitzaren berdina izan behar du"
}
},
"reason": "Kontua sortzeko arrazoia",
"reason_placeholder": "Instantzia honek kontu berriak eskuz onartzen ditu.\nJakinarazi administrazioari zergatik erregistratu nahi duzun.",
"register": "Erregistratu"
},
"selectable_list": {
"select_all": "Hautatu denak"
@ -210,7 +225,7 @@
"title": "Bi-faktore autentifikazioa",
"generate_new_recovery_codes": "Sortu berreskuratze kode berriak",
"warning_of_generate_new_codes": "Berreskuratze kode berriak sortzean, zure berreskuratze kode zaharrak ez dute balioko.",
"recovery_codes": "Berreskuratze kodea",
"recovery_codes": "Berreskuratze kodea.",
"waiting_a_recovery_codes": "Babes-kopia kodeak jasotzen…",
"recovery_codes_warning": "Idatzi edo gorde kodeak leku seguruan - bestela ez dituzu berriro ikusiko. Zure 2FA aplikaziorako sarbidea eta berreskuratze kodeak galduz gero, zure kontutik blokeatuta egongo zara.",
"authentication_methods": "Autentifikazio metodoa",
@ -468,7 +483,7 @@
"button": "Botoia",
"text": "Hamaika {0} eta {1}",
"mono": "edukia",
"input": "Jadanik Los Angeles-en",
"input": "Jadanik Los Angeles-en.",
"faint_link": "laguntza",
"fine_print": "Irakurri gure {0} ezer erabilgarria ikasteko!",
"header_faint": "Ondo dago",
@ -480,7 +495,11 @@
"title": "Bertsioa",
"backend_version": "Backend bertsioa",
"frontend_version": "Frontend bertsioa"
}
},
"save": "Aldaketak gorde",
"setting_changed": "Ezarpena lehenetsitakoaren desberdina da",
"allow_following_move": "Baimendu jarraipen automatikoa, jarraitzen duzun kontua beste instantzia batera eramaten denean",
"new_email": "E-posta berria"
},
"time": {
"day": "{0} egun",
@ -550,7 +569,6 @@
"follow": "Jarraitu",
"follow_sent": "Eskaera bidalita!",
"follow_progress": "Eskatzen…",
"follow_again": "Eskaera berriro bidali?",
"follow_unfollow": "Jarraitzeari utzi",
"followees": "Jarraitzen",
"followers": "Jarraitzaileak",
@ -691,5 +709,12 @@
},
"shoutbox": {
"title": "Oihu-kutxa"
},
"errors": {
"storage_unavailable": "Pleromak ezin izan du nabigatzailearen biltegira sartu. Hasiera-saioa edo tokiko ezarpenak ez dira gordeko eta ustekabeko arazoak sor ditzake. Saiatu cookie-ak gaitzen."
},
"remote_user_resolver": {
"searching_for": "Bilatzen",
"error": "Ez da aurkitu."
}
}

View file

@ -579,7 +579,8 @@
"hide_full_subject": "Piilota koko otsikko",
"show_content": "Näytä sisältö",
"hide_content": "Piilota sisältö",
"status_deleted": "Poistettu viesti"
"status_deleted": "Poistettu viesti",
"you": "(sinä)"
},
"user_card": {
"approve": "Hyväksy",
@ -589,7 +590,6 @@
"follow": "Seuraa",
"follow_sent": "Pyyntö lähetetty!",
"follow_progress": "Pyydetään…",
"follow_again": "Lähetä pyyntö uudestaan?",
"follow_unfollow": "Älä seuraa",
"followees": "Seuraa",
"followers": "Seuraajat",

View file

@ -43,7 +43,10 @@
"role": {
"moderator": "Modo'",
"admin": "Admin"
}
},
"flash_content": "Clique pour afficher le contenu Flash avec Ruffle (Expérimental, peut ne pas fonctionner).",
"flash_security": "Cela reste potentiellement dangereux, Flash restant du code arbitraire.",
"flash_fail": "Échec de chargement du contenu Flash, voir la console pour les détails."
},
"image_cropper": {
"crop_picture": "Rogner l'image",
@ -282,7 +285,7 @@
"new_password": "Nouveau mot de passe",
"notification_visibility": "Types de notifications à afficher",
"notification_visibility_follows": "Suivis",
"notification_visibility_likes": "J'aime",
"notification_visibility_likes": "Favoris",
"notification_visibility_mentions": "Mentionnés",
"notification_visibility_repeats": "Partages",
"no_rich_text_description": "Ne formatez pas le texte",
@ -553,7 +556,21 @@
"hide_wallpaper": "Cacher le fond d'écran",
"hide_all_muted_posts": "Cacher les messages masqués",
"word_filter": "Filtrage par mots",
"save": "Enregistrer les changements"
"save": "Enregistrer les changements",
"file_export_import": {
"backup_settings_theme": "Sauvegarder les paramètres et le thème dans un fichier",
"errors": {
"invalid_file": "Le fichier sélectionné n'est pas un format supporté pour les sauvegarde Pleroma. Aucun changement n'a été fait.",
"file_too_new": "Version majeure incompatible. {fileMajor}, ce PleromaFE ({feMajor}) est trop ancien",
"file_too_old": "Version majeure incompatible : {fileMajor}, la version du fichier est trop vielle et n'est plus supportée (vers. min. {feMajor})",
"file_slightly_new": "La version mineure du fichier est différente, quelques paramètres on pût ne pas chargés"
},
"backup_restore": "Sauvegarde des Paramètres",
"backup_settings": "Sauvegarder les paramètres dans un fichier",
"restore_settings": "Restaurer les paramètres depuis un fichier"
},
"hide_shoutbox": "Cacher la shoutbox de l'instance",
"right_sidebar": "Afficher le paneau latéral à droite"
},
"timeline": {
"collapse": "Fermer",
@ -607,7 +624,6 @@
"follow": "Suivre",
"follow_sent": "Demande envoyée !",
"follow_progress": "Demande en cours…",
"follow_again": "Renvoyer la demande ?",
"follow_unfollow": "Désabonner",
"followees": "Suivis",
"followers": "Vous suivent",
@ -663,7 +679,8 @@
"side": "Coté rayé",
"striped": "Fond rayé"
},
"bot": "Robot"
"bot": "Robot",
"edit_profile": "Éditer le profil"
},
"user_profile": {
"timeline_title": "Flux du compte",

View file

@ -312,7 +312,6 @@
"follow": "עקוב",
"follow_sent": "בקשה נשלחה!",
"follow_progress": "מבקש…",
"follow_again": "שלח בקשה שוב?",
"follow_unfollow": "בטל עקיבה",
"followees": "נעקבים",
"followers": "עוקבים",

631
src/i18n/id.json Normal file
View file

@ -0,0 +1,631 @@
{
"settings": {
"style": {
"preview": {
"link": "sebuah tautan yang kecil nan bagus",
"header": "Pratinjau",
"error": "Contoh kesalahan",
"button": "Tombol",
"input": "Baru saja mendarat di L.A.",
"faint_link": "manual berguna",
"fine_print": "Baca {0} kami untuk belajar sesuatu yang tak ada gunanya!",
"header_faint": "Ini baik-baik saja",
"checkbox": "Saya telah membaca sekilas syarat dan ketentuan"
},
"advanced_colors": {
"alert_neutral": "Neutral",
"alert_warning": "Peringatan",
"alert_error": "Kesalahan",
"_tab_label": "Lanjutan",
"post": "Postingan/Bio pengguna",
"popover": "Tooltip, menu, popover",
"badge_notification": "Notifikasi",
"top_bar": "Bar atas",
"borders": "",
"buttons": "Tombol",
"wallpaper": "Latar belakang",
"panel_header": "Header panel",
"icons": "Ikon-ikon",
"disabled": "Dinonaktifkan"
},
"common_colors": {
"main": "Warna umum",
"_tab_label": "Umum"
},
"common": {
"contrast": {
"context": {
"text": "untuk teks",
"18pt": "Untuk teks besar (18pt+)"
}
},
"color": "Warna"
},
"switcher": {
"help": {
"upgraded_from_v2": "PleromaFE telah diperbarui, tema dapat terlihat sedikit berbeda dari apa yang Anda ingat.",
"future_version_imported": "Berkas yang Anda impor dibuat pada versi FE yang lebih baru.",
"older_version_imported": "Berkas yang Anda impor dibuat pada versi FE yang lebih lama.",
"fe_upgraded": "Mesin tema PleromaFE diperbarui setelah pembaruan versi."
},
"use_source": "Versi baru",
"use_snapshot": "Versi lama",
"load_theme": "Muat tema"
},
"fonts": {
"_tab_label": "Font",
"components": {
"interface": "Antarmuka",
"post": "Teks postingan"
},
"family": "Nama font",
"size": "Ukuran (dalam px)",
"weight": "Berat (ketebalan)"
},
"shadows": {
"components": {
"panel": "Panel",
"panelHeader": "Header panel"
}
}
},
"notification_setting_privacy": "Privasi",
"notifications": "Notifikasi",
"values": {
"true": "ya",
"false": "tidak"
},
"user_settings": "Pengaturan Pengguna",
"upload_a_photo": "Unggah foto",
"theme": "Tema",
"text": "Teks",
"settings": "Pengaturan",
"security_tab": "Keamanan",
"saving_ok": "Pengaturan disimpan",
"profile_tab": "Profil",
"profile_background": "Latar belakang profil",
"token": "Token",
"oauth_tokens": "Token OAuth",
"show_moderator_badge": "Tampilkan lencana \"Moderator\" di profil saya",
"show_admin_badge": "Tampilkan lencana \"Admin\" di profil saya",
"new_password": "Kata sandi baru",
"new_email": "Surel baru",
"name_bio": "Nama & bio",
"name": "Nama",
"profile_fields": {
"value": "Isi",
"name": "Label",
"label": "Metadata profil"
},
"limited_availability": "Tidak tersedia di browser Anda",
"invalid_theme_imported": "Berkas yang dipilih bukan sebuah tema yang didukung Pleroma. Tidak ada perbuahan yang dibuat pada tema Anda.",
"interfaceLanguage": "Bahasa antarmuka",
"interface": "Antarmuka",
"instance_default_simple": "(bawaan)",
"instance_default": "(bawaan: {value})",
"general": "Umum",
"delete_account_error": "Ada masalah ketika menghapus akun Anda. Jika ini terus terjadi harap hubungi adminstrator instansi Anda.",
"delete_account_description": "Hapus data Anda secara permanen dan menonaktifkan akun Anda.",
"delete_account": "Hapus akun",
"data_import_export_tab": "Impor / ekspor data",
"current_password": "Kata sandi saat ini",
"confirm_new_password": "Konfirmasi kata sandi baru",
"version": {
"title": "Versi",
"backend_version": "Versi backend",
"frontend_version": "Versi frontend"
},
"security": "Keamanan",
"changed_password": "Kata sandi berhasil diubah!",
"change_password_error": "Ada masalah ketika mengubah kata sandi Anda.",
"change_password": "Ubah kata sandi",
"changed_email": "Surel berhasil diubah!",
"change_email_error": "Ada masalah ketika mengubah surel Anda.",
"change_email": "Ubah surel",
"cRed": "Merah (Batal)",
"cBlue": "Biru (Balas, ikuti)",
"btnRadius": "Tombol",
"bot": "Ini adalah akun bot",
"block_export": "Ekspor blokiran",
"bio": "Bio",
"background": "Latar belakang",
"avatarRadius": "Avatar",
"avatar": "Avatar",
"attachments": "Lampiran",
"mfa": {
"scan": {
"title": "Pindai"
},
"confirm_and_enable": "Konfirmasi & aktifkan OTP",
"setup_otp": "Siapkan OTP",
"otp": "OTP",
"recovery_codes_warning": "Tulis kode-kode nya atau simpan mereka di tempat yang aman - jika tidak Anda tidak akan melihat mereka lagi. Jika Anda tidak dapat mengakses aplikasi 2FA Anda dan kode pemulihan Anda hilang Anda tidak akan bisa mengakses akun Anda.",
"authentication_methods": "Metode otentikasi",
"recovery_codes": "Kode pemulihan.",
"warning_of_generate_new_codes": "Ketika Anda menghasilkan kode pemulihan baru, kode lama Anda berhenti bekerja.",
"generate_new_recovery_codes": "Hasilkan kode pemulihan baru",
"title": "Otentikasi Dua-faktor",
"waiting_a_recovery_codes": "Menerima kode cadangan…",
"verify": {
"desc": "Untuk mengaktifkan otentikasi dua-faktor, masukkan kode dari aplikasi dua-faktor Anda:"
}
},
"app_name": "Nama aplikasi",
"save": "Simpan perubahan",
"valid_until": "Valid hingga",
"follow_import_error": "Terjadi kesalahan ketika mengimpor pengikut",
"emoji_reactions_on_timeline": "Tampilkan reaksi emoji pada linimasa",
"chatMessageRadius": "Pesan obrolan",
"cOrange": "Jingga (Favorit)",
"avatarAltRadius": "Avatar (notifikasi)",
"hide_shoutbox": "Sembunyikan kotak suara instansi",
"hide_followers_count_description": "Jangan tampilkan jumlah pengikut",
"hide_follows_count_description": "Jangan tampilkan jumlah mengikuti",
"hide_followers_description": "Jangan tampilkan siapa yang mengikuti saya",
"hide_follows_description": "Jangan tampilkan siapa yang saya ikuti",
"notification_visibility_emoji_reactions": "Reaksi",
"notification_visibility_follows": "Diikuti",
"notification_visibility_moves": "Pengguna Bermigrasi",
"notification_visibility_repeats": "Ulangan",
"notification_visibility_mentions": "Sebutan",
"notification_visibility_likes": "Favorit",
"notification_visibility": "Jenis notifikasi yang perlu ditampilkan",
"links": "Tautan",
"hide_user_stats": "Sembunyikan statistik pengguna (contoh. jumlah pengikut)",
"hide_post_stats": "Sembunyikan statistik postingan (contoh. jumlah favorit)",
"use_one_click_nsfw": "Buka lampiran NSFW hanya dengan satu klik",
"hide_wallpaper": "Sembunyikan latar belakang instansi",
"blocks_imported": "Blokiran diimpor! Pemrosesannya mungkin memakan sedikit waktu.",
"block_import_error": "Terjadi kesalahan ketika mengimpor blokiran",
"block_import": "Impor blokiran",
"block_export_button": "Ekspor blokiran Anda menjadi berkas csv",
"blocks_tab": "Blokiran",
"delete_account_instructions": "Ketik kata sandi Anda pada input di bawah untuk mengkonfirmasi penghapusan akun.",
"mutes_and_blocks": "Bisuan dan Blokiran",
"enter_current_password_to_confirm": "Masukkan kata sandi Anda saat ini untuk mengkonfirmasi identitas Anda",
"filtering": "Penyaringan",
"word_filter": "Penyaring kata",
"avatar_size_instruction": "Ukuran minimum gambar avatar yang disarankan adalah 150x150 piksel.",
"attachmentRadius": "Lampiran",
"cGreen": "Hijau (Retweet)",
"max_thumbnails": "Jumlah thumbnail maksimum per postingan",
"loop_video": "Ulang-ulang video",
"loop_video_silent_only": "Ulang-ulang video tanpa suara (seperti \"gif\" Mastodon)",
"pause_on_unfocused": "Jeda aliran ketika tab di dalam fokus",
"reply_visibility_following": "Hanya tampilkan balasan yang ditujukan kepada saya atau orang yang saya ikuti",
"reply_visibility_following_short": "Tampilkan balasan ke orang yang saya ikuti",
"saving_err": "Terjadi kesalahan ketika menyimpan pengaturan",
"search_user_to_block": "Cari siapa yang Anda ingin blokir",
"search_user_to_mute": "Cari siapa yang ingin Anda bisukan",
"set_new_avatar": "Tetapkan avatar baru",
"set_new_profile_background": "Tetapkan latar belakang profil baru",
"subject_line_behavior": "Salin subyek ketika membalas",
"subject_line_email": "Seperti surel: \"re: subyek\"",
"subject_line_mastodon": "Seperti mastodon: salin saja",
"subject_line_noop": "Jangan salin",
"useStreamingApiWarning": "(Tidak disarankan, eksperimental, diketahui dapat melewati postingan-postingan)",
"fun": "Seru",
"enable_web_push_notifications": "Aktifkan notifikasi push web",
"more_settings": "Lebih banyak pengaturan",
"reply_visibility_all": "Tampilkan semua balasan",
"reply_visibility_self": "Hanya tampilkan balasan yang ditujukan kepada saya",
"hide_muted_posts": "Sembunyikan postingan-postingan dari pengguna yang dibisukan",
"import_blocks_from_a_csv_file": "Impor blokiran dari berkas csv",
"domain_mutes": "Domain",
"composing": "Menulis",
"no_blocks": "Tidak ada yang diblokir",
"no_mutes": "Tidak ada yang dibisukan"
},
"about": {
"mrf": {
"keyword": {
"reject": "Tolak",
"is_replaced_by": "→"
},
"simple": {
"quarantine_desc": "Instansi ini hanya akan mengirim postingan publik ke instansi-instansi berikut:",
"quarantine": "Karantina",
"reject_desc": "Instansi ini tidak akan menerima pesan dari instansi-instansi berikut:",
"reject": "Tolak",
"accept_desc": "Instansi ini hanya menerima pesan dari instansi-instansi berikut:",
"accept": "Terima",
"media_removal": "Penghapusan Media",
"media_removal_desc": "Instansi ini menghapus media dari postingan yang berasal dari instansi-instansi berikut:"
},
"federation": "Federasi",
"mrf_policies": "Kebijakan MRF yang diaktifkan"
},
"staff": "Staf"
},
"time": {
"day": "{0} hari",
"days": "{0} hari",
"day_short": "{0}h",
"days_short": "{0}h",
"hour": "{0} jam",
"hours": "{0} jam",
"hour_short": "{0}j",
"hours_short": "{0}j",
"in_future": "dalam {0}",
"in_past": "{0} yang lalu",
"minute": "{0} menit",
"minutes": "{0} menit",
"minute_short": "{0}m",
"minutes_short": "{0}m",
"month": "{0} bulan",
"months": "{0} bulan",
"month_short": "{0}b",
"months_short": "{0}b",
"now": "baru saja",
"now_short": "sekarang",
"second": "{0} detik",
"seconds": "{0} detik",
"second_short": "{0}d",
"seconds_short": "{0}d",
"week": "{0} pekan",
"weeks": "{0} pekan",
"week_short": "{0}p",
"weeks_short": "{0}p",
"year": "{0} tahun",
"years": "{0} tahun",
"year_short": "{0}t",
"years_short": "{0}t"
},
"timeline": {
"conversation": "Percakapan",
"error": "Terjadi kesalahan memuat linimasa: {0}",
"no_retweet_hint": "Postingan ditandai sebagai hanya-pengikut atau langsung dan tidak dapat diulang",
"repeated": "diulangi",
"reload": "Muat ulang",
"no_more_statuses": "Tidak ada status lagi",
"no_statuses": "Tidak ada status"
},
"status": {
"favorites": "Favorit",
"repeats": "Ulangan",
"delete": "Hapus status",
"pin": "Sematkan di profil",
"unpin": "Berhenti menyematkan dari profil",
"pinned": "Disematkan",
"delete_confirm": "Apakah Anda benar-benar ingin menghapus status ini?",
"reply_to": "Balas ke",
"replies_list": "Balasan:",
"mute_conversation": "Bisukan percakapan",
"unmute_conversation": "Berhenti membisikan percakapan",
"status_unavailable": "Status tidak tersedia",
"thread_muted_and_words": ", memiliki kata:",
"hide_content": "",
"show_content": "",
"status_deleted": "Postingan ini telah dihapus",
"nsfw": "NSFW"
},
"user_card": {
"block": "Blokir",
"blocked": "Diblokir!",
"deny": "Tolak",
"edit_profile": "Sunting profil",
"favorites": "Favorit",
"follow": "Ikuti",
"follow_sent": "Permintaan dikirim!",
"follow_progress": "Meminta…",
"mute": "Bisukan",
"muted": "Dibisukan",
"per_day": "per hari",
"report": "Laporkan",
"statuses": "Status",
"unblock": "Berhenti memblokir",
"block_progress": "Memblokir…",
"unmute": "Berhenti membisukan",
"mute_progress": "Membisukan…",
"hide_repeats": "Sembunyikan ulangan",
"show_repeats": "Tampilkan ulangan",
"bot": "Bot",
"admin_menu": {
"moderation": "Moderasi",
"activate_account": "Aktifkan akun",
"deactivate_account": "Nonaktifkan akun",
"delete_account": "Hapus akun",
"force_nsfw": "Tandai semua postingan sebagai NSFW",
"strip_media": "Hapus media dari postingan-postingan",
"delete_user": "Hapus pengguna",
"delete_user_confirmation": "Apakah Anda benar-benar yakin? Tindakan ini tidak dapat dibatalkan."
},
"follow_unfollow": "Berhenti mengikuti",
"followees": "Mengikuti",
"followers": "Pengikut",
"following": "Diikuti!",
"follows_you": "Mengikuti Anda!",
"hidden": "Disembunyikan",
"its_you": "Ini Anda!",
"media": "Media",
"mention": "Sebut",
"message": "Kirimkan pesan"
},
"user_profile": {
"timeline_title": "Linimasa pengguna",
"profile_does_not_exist": "Maaf, profil ini tidak ada.",
"profile_loading_error": "Maaf, terjadi kesalahan ketika memuat profil ini."
},
"user_reporting": {
"title": "Melaporkan {0}",
"add_comment_description": "Laporan ini akan dikirim ke moderator instansi Anda. Anda dapat menyediakan penjelasan mengapa Anda melaporkan akun ini di bawah:",
"additional_comments": "Komentar tambahan",
"forward_description": "Akun ini berada di server lain. Kirim salinan dari laporannya juga?",
"submit": "Kirim",
"generic_error": "Sebuah kesalahan terjadi ketika memproses permintaan Anda."
},
"notifications": {
"favorited_you": "memfavoritkan status Anda",
"reacted_with": "bereaksi dengan {0}",
"no_more_notifications": "Tidak ada notifikasi lagi",
"repeated_you": "mengulangi status Anda",
"read": "Dibaca!",
"notifications": "Notifikasi",
"follow_request": "ingin mengikuti Anda",
"followed_you": "mengikuti Anda",
"error": "Terjadi kesalahan ketika memuat notifikasi: {0}",
"migrated_to": "bermigrasi ke",
"load_older": "Muat notifikasi yang lebih lama",
"broken_favorite": "Status tak diketahui, mencarinya…"
},
"who_to_follow": {
"more": "Lebih banyak"
},
"tool_tip": {
"media_upload": "Unggah media",
"repeat": "Ulangi",
"reply": "Balas",
"favorite": "Favorit",
"add_reaction": "Tambahkan Reaksi",
"user_settings": "Pengaturan Pengguna"
},
"upload": {
"error": {
"base": "Pengunggahan gagal.",
"message": "Pengunggahan gagal: {0}",
"file_too_big": "Berkas terlalu besar [{filesize}{filesizeunit} / {allowedsize}{allowedsizeunit}]",
"default": "Coba lagi nanti"
},
"file_size_units": {
"B": "B",
"KiB": "KiB",
"MiB": "MiB",
"GiB": "GiB",
"TiB": "TiB"
}
},
"search": {
"people": "Orang",
"hashtags": "Tagar",
"person_talking": "{count} orang berbicara",
"people_talking": "{count} orang berbicara",
"no_results": "Tidak ada hasil"
},
"password_reset": {
"forgot_password": "Lupa kata sandi?",
"placeholder": "Surel atau nama pengguna Anda",
"return_home": "Kembali ke halaman beranda",
"too_many_requests": "Anda telah mencapai batas percobaan, coba lagi nanti.",
"instruction": "Masukkan surel atau nama pengguna Anda. Kami akan mengirimkan Anda tautan untuk mengatur ulang kata sandi.",
"password_reset": "Pengatur-ulangan kata sandi",
"password_reset_disabled": "Pengatur-ulangan kata sandi dinonaktifkan. Hubungi administrator instansi Anda.",
"password_reset_required": "Anda harus mengatur ulang kata sandi Anda untuk masuk.",
"password_reset_required_but_mailer_is_disabled": "Anda harus mengatur ulang kata sandi, tetapi pengatur-ulangan kata sandi dinonaktifkan. Silakan hubungi administrator instansi Anda."
},
"chats": {
"you": "Anda:",
"message_user": "Kirim Pesan ke {nickname}",
"delete": "Hapus",
"chats": "Obrolan",
"new": "Obrolan Baru",
"empty_message_error": "Tidak dapat memposting pesan yang kosong",
"more": "Lebih banyak",
"delete_confirm": "Apakah Anda benar-benar ingin menghapus pesan ini?",
"error_loading_chat": "Sesuatu yang salah terjadi ketika memuat obrolan.",
"error_sending_message": "Sesuatu yang salah terjadi ketika mengirim pesan.",
"empty_chat_list_placeholder": "Anda belum memiliki obrolan. Buat sbeuah obrolan baru!"
},
"file_type": {
"audio": "Audio",
"video": "Video",
"image": "Gambar",
"file": "Berkas"
},
"registration": {
"bio_placeholder": "contoh.\nHai, aku Lain.\nAku seorang putri anime yang tinggal di pinggiran kota Jepang. Kamu mungkin mengenal aku dari Wired.",
"validations": {
"password_confirmation_required": "tidak boleh kosong",
"password_required": "tidak boleh kosong",
"email_required": "tidak boleh kosong",
"fullname_required": "tidak boleh kosong",
"username_required": "tidak boleh kosong"
},
"register": "Daftar",
"fullname_placeholder": "contoh. Lain Iwakura",
"username_placeholder": "contoh. lain",
"new_captcha": "Klik gambarnya untuk mendapatkan captcha baru",
"captcha": "CAPTCHA",
"token": "Token undangan",
"password_confirm": "Konfirmasi kata sandi",
"email": "Surel",
"bio": "Bio",
"reason_placeholder": "Instansi ini menerima pendaftaran secara manual.\nBeritahu administrasinya mengapa Anda ingin mendaftar.",
"reason": "Alasan mendaftar",
"registration": "Pendaftaran"
},
"post_status": {
"preview_empty": "Kosong",
"default": "Baru saja mendarat di L.A.",
"content_warning": "Subyek (opsional)",
"content_type": {
"text/bbcode": "BBCode",
"text/markdown": "Markdown",
"text/html": "HTML",
"text/plain": "Teks biasa"
},
"media_description": "Keterangan media",
"attachments_sensitive": "Tandai lampiran sebagai sensitif",
"scope": {
"public": "Publik - posting ke linimasa publik",
"private": "Hanya-pengikut - posting hanya kepada pengikut",
"direct": "Langsung - posting hanya kepada pengguna yang disebut"
},
"preview": "Pratinjau",
"post": "Posting",
"posting": "Memposting",
"direct_warning_to_first_only": "Postingan ini akan terlihat oleh pengguna yang disebutkan di awal pesan.",
"direct_warning_to_all": "Postingan ini akan terlihat oleh pengguna yang disebutkan.",
"scope_notice": {
"private": "Postingan ini akan terlihat hanya oleh pengikut Anda",
"public": "Postingan ini akan terlihat oleh siapa saja"
},
"media_description_error": "Gagal memperbarui media, coba lagi",
"empty_status_error": "Tidak dapat memposting status kosong tanpa berkas",
"account_not_locked_warning_link": "terkunci",
"account_not_locked_warning": "Akun Anda tidak {0}. Siapapun dapat mengikuti Anda untuk melihat postingan hanya-pengikut Anda.",
"new_status": "Posting status baru"
},
"general": {
"apply": "Terapkan",
"flash_fail": "Gagal memuat konten flash, lihat console untuk keterangan.",
"flash_security": "Harap ingat ini dapat menjadi berbahaya karena konten Flash masih termasuk arbitrary code.",
"flash_content": "Klik untuk menampilkan konten Flash menggunakan Ruffle (Eksperimental, mungkin tidak bekerja).",
"role": {
"moderator": "Moderator",
"admin": "Admin"
},
"peek": "Intip",
"close": "Tutup",
"verify": "Verifikasi",
"confirm": "Konfirmasi",
"enable": "Aktifkan",
"disable": "Nonaktifkan",
"cancel": "Batal",
"show_less": "Tampilkan lebih sedikit",
"show_more": "Tampilkan lebih banyak",
"optional": "opsional",
"retry": "Coba lagi",
"error_retry": "Harap coba lagi",
"generic_error": "Terjadi kesalahan",
"loading": "Memuat…",
"more": "Lebih banyak",
"submit": "Kirim"
},
"remote_user_resolver": {
"error": "Tidak ditemukan."
},
"emoji": {
"load_all": "Memuat semua {emojiAmount} emoji",
"load_all_hint": "Memuat {saneAmount} emoji pertama, memuat semua emoji dapat menyebabkan masalah performa.",
"unicode": "Emoji unicode",
"add_emoji": "Sisipkan emoji",
"search_emoji": "Cari emoji",
"emoji": "Emoji",
"stickers": "Stiker",
"keep_open": "Tetap buka pemilih",
"custom": "Emoji kustom"
},
"polls": {
"expired": "Japat berakhir {0} yang lalu",
"expires_in": "Japat berakhir dalam {0}",
"expiry": "Usia japat",
"type": "Jenis japat",
"vote": "Pilih",
"votes_count": "{count} suara | {count} suara",
"people_voted_count": "{count} orang memilih | {count} orang memilih",
"votes": "suara",
"option": "Opsi",
"add_option": "Tambahkan opsi",
"add_poll": "Tambahkan japat",
"not_enough_options": "Terlalu sedikit opsi yang unik pada japat"
},
"nav": {
"preferences": "Preferensi",
"search": "Cari",
"user_search": "Pencarian Pengguna",
"home_timeline": "Linimasa beranda",
"timeline": "Linimasa",
"public_tl": "Linimasa publik",
"interactions": "Interaksi",
"mentions": "Sebutan",
"back": "Kembali",
"administration": "Administrasi",
"about": "Tentang",
"timelines": "Linimasa",
"chats": "Obrolan",
"dms": "Pesan langsung",
"friend_requests": "Ingin mengikuti"
},
"media_modal": {
"next": "Selanjutnya",
"previous": "Sebelum"
},
"login": {
"recovery_code": "Kode pemulihan",
"enter_recovery_code": "Masukkan kode pemulihan",
"authentication_code": "Kode otentikasi",
"hint": "Masuk untuk ikut berdiskusi",
"username": "Nama pengguna",
"register": "Daftar",
"placeholder": "contoh: lain",
"password": "Kata sandi",
"logout": "Keluar",
"description": "Masuk dengan OAuth",
"login": "Masuk",
"heading": {
"totp": "Otentikasi dua-faktor"
},
"enter_two_factor_code": "Masukkan kode dua-faktor"
},
"importer": {
"error": "Terjadi kesalahan ketika mnengimpor berkas ini.",
"success": "Berhasil mengimpor.",
"submit": "Kirim"
},
"image_cropper": {
"cancel": "Batal",
"save_without_cropping": "Simpan tanpa memotong",
"save": "Simpan",
"crop_picture": "Potong gambar"
},
"finder": {
"find_user": "Cari pengguna",
"error_fetching_user": "Terjadi kesalahan ketika memuat pengguna"
},
"features_panel": {
"title": "Fitur-fitur",
"text_limit": "Batas teks",
"gopher": "Gopher",
"pleroma_chat_messages": "Pleroma Obrolan",
"chat": "Obrolan",
"upload_limit": "Batas unggahan"
},
"exporter": {
"processing": "Memproses, Anda akan segera diminta untuk mengunduh berkas Anda",
"export": "Ekspor"
},
"domain_mute_card": {
"unmute": "Berhenti membisukan",
"mute_progress": "Membisukan…",
"mute": "Bisukan",
"unmute_progress": "Memberhentikan pembisuan…"
},
"display_date": {
"today": "Hari Ini"
},
"selectable_list": {
"select_all": "Pilih semua"
},
"interactions": {
"moves": "Pengguna yang bermigrasi",
"follows": "Pengikut baru",
"favs_repeats": "Ulangan dan favorit",
"load_older": "Muat interaksi yang lebih tua"
},
"errors": {
"storage_unavailable": "Pleroma tidak dapat mengakses penyimpanan browser. Login Anda atau pengaturan lokal Anda tidak akan tersimpan dan masalah yang tidak terduga dapat terjadi. Coba mengaktifkan kuki."
},
"shoutbox": {
"title": "Kotak Suara"
}
}

View file

@ -21,7 +21,10 @@
"role": {
"moderator": "Moderatore",
"admin": "Amministratore"
}
},
"flash_fail": "Contenuto Flash non caricato, vedi console del browser.",
"flash_content": "Mostra contenuto Flash tramite Ruffle (funzione in prova).",
"flash_security": "Può essere pericoloso perché i contenuti in Flash sono eseguibili."
},
"nav": {
"mentions": "Menzioni",
@ -65,13 +68,13 @@
"current_avatar": "La tua icona attuale",
"current_profile_banner": "Il tuo stendardo attuale",
"filtering": "Filtri",
"filtering_explanation": "Tutti i post contenenti queste parole saranno silenziati, una per riga",
"filtering_explanation": "Tutti i messaggi contenenti queste parole saranno silenziati, una per riga",
"hide_attachments_in_convo": "Nascondi gli allegati presenti nelle conversazioni",
"hide_attachments_in_tl": "Nascondi gli allegati presenti nelle sequenze",
"name": "Nome",
"name_bio": "Nome ed introduzione",
"nsfw_clickthrough": "Fai click per visualizzare gli allegati offuscati",
"profile_background": "Sfondo della tua pagina",
"profile_background": "Sfondo del tuo profilo",
"profile_banner": "Gonfalone del tuo profilo",
"set_new_avatar": "Scegli una nuova icona",
"set_new_profile_background": "Scegli un nuovo sfondo",
@ -365,8 +368,8 @@
"search_user_to_mute": "Cerca utente da silenziare",
"search_user_to_block": "Cerca utente da bloccare",
"autohide_floating_post_button": "Nascondi automaticamente il pulsante di composizione (mobile)",
"show_moderator_badge": "Mostra l'insegna di moderatore sulla mia pagina",
"show_admin_badge": "Mostra l'insegna di amministratore sulla mia pagina",
"show_moderator_badge": "Mostra l'insegna di moderatore sul mio profilo",
"show_admin_badge": "Mostra l'insegna di amministratore sul mio profilo",
"hide_followers_count_description": "Non mostrare quanti seguaci ho",
"hide_follows_count_description": "Non mostrare quanti utenti seguo",
"hide_followers_description": "Non mostrare i miei seguaci",
@ -443,7 +446,12 @@
"backup_settings_theme": "Archivia impostazioni e tema localmente",
"backup_settings": "Archivia impostazioni localmente",
"backup_restore": "Archiviazione impostazioni"
}
},
"right_sidebar": "Mostra barra laterale a destra",
"hide_shoutbox": "Nascondi muro dei graffiti",
"mentions_new_style": "Menzioni abbreviate",
"mentions_new_place": "Segrega le menzioni",
"always_show_post_button": "Non nascondere il pulsante di composizione"
},
"timeline": {
"error_fetching": "Errore nell'aggiornamento",
@ -511,7 +519,6 @@
"its_you": "Sei tu!",
"hidden": "Nascosto",
"follow_unfollow": "Disconosci",
"follow_again": "Reinvio richiesta?",
"follow_progress": "Richiedo…",
"follow_sent": "Richiesta inviata!",
"favorites": "Preferiti",
@ -522,7 +529,8 @@
"striped": "A righe",
"solid": "Un colore",
"disabled": "Nessun risalto"
}
},
"edit_profile": "Modifica profilo"
},
"chat": {
"title": "Chat"
@ -660,7 +668,7 @@
},
"domain_mute_card": {
"mute": "Silenzia",
"mute_progress": "Silenzio…",
"mute_progress": "Procedo…",
"unmute": "Ascolta",
"unmute_progress": "Procedo…"
},
@ -701,7 +709,7 @@
},
"interactions": {
"favs_repeats": "Condivisi e Graditi",
"load_older": "Carica vecchie interazioni",
"load_older": "Carica interazioni precedenti",
"moves": "Utenti migrati",
"follows": "Nuovi seguìti"
},
@ -752,7 +760,10 @@
"status_deleted": "Questo messagio è stato cancellato",
"nsfw": "DISDICEVOLE",
"external_source": "Vai all'origine",
"expand": "Espandi"
"expand": "Espandi",
"mentions": "Menzioni",
"you": "(Tu)",
"plus_more": "+{number} altri"
},
"time": {
"years_short": "{0} a",
@ -769,8 +780,8 @@
"second": "{0} secondo",
"now_short": "adesso",
"now": "adesso",
"months_short": "{0} ms",
"month_short": "{0} ms",
"months_short": "{0} mes",
"month_short": "{0} mes",
"months": "{0} mesi",
"month": "{0} mese",
"minutes_short": "{0} min",

View file

@ -567,7 +567,6 @@
"follow": "フォロー",
"follow_sent": "リクエストを、おくりました!",
"follow_progress": "リクエストしています…",
"follow_again": "ふたたびリクエストをおくりますか?",
"follow_unfollow": "フォローをやめる",
"followees": "フォロー",
"followers": "フォロワー",

View file

@ -43,7 +43,10 @@
"role": {
"moderator": "モデレーター",
"admin": "管理者"
}
},
"flash_security": "Flashコンテンツが任意の命令を実行させることにより、コンピューターが危険にさらされることがあります。",
"flash_fail": "Flashコンテンツの読み込みに失敗しました。コンソールで詳細を確認できます。",
"flash_content": "試験的機能クリックしてFlashコンテンツを再生します。"
},
"image_cropper": {
"crop_picture": "画像を切り抜く",
@ -586,14 +589,18 @@
"word_filter": "単語フィルタ",
"file_export_import": {
"errors": {
"invalid_file": "これはPleromaの設定をバックアップしたファイルではありません。"
"invalid_file": "これはPleromaの設定をバックアップしたファイルではありません。",
"file_slightly_new": "ファイルのマイナーバージョンが異なり、一部の設定が読み込まれないことがあります"
},
"restore_settings": "設定をファイルから復元する",
"backup_settings_theme": "テーマを含む設定をファイルにバックアップする",
"backup_settings": "設定をファイルにバックアップする",
"backup_restore": "設定をバックアップ"
},
"save": "変更を保存"
"save": "変更を保存",
"hide_shoutbox": "Shoutboxを表示しない",
"always_show_post_button": "投稿ボタンを常に表示",
"right_sidebar": "サイドバーを右に表示"
},
"time": {
"day": "{0}日",
@ -641,7 +648,9 @@
"no_more_statuses": "これで終わりです",
"no_statuses": "ステータスはありません",
"reload": "再読み込み",
"error": "タイムラインの読み込みに失敗しました: {0}"
"error": "タイムラインの読み込みに失敗しました: {0}",
"socket_reconnected": "リアルタイム接続が確立されました",
"socket_broke": "コード{0}によりリアルタイム接続が切断されました"
},
"status": {
"favorites": "お気に入り",
@ -668,7 +677,10 @@
"copy_link": "リンクをコピー",
"status_unavailable": "利用できません",
"unbookmark": "ブックマーク解除",
"bookmark": "ブックマーク"
"bookmark": "ブックマーク",
"mentions": "メンション",
"you": "(あなた)",
"plus_more": "ほか{number}件"
},
"user_card": {
"approve": "受け入れ",
@ -679,7 +691,6 @@
"follow": "フォロー",
"follow_sent": "リクエストを送りました!",
"follow_progress": "リクエストしています…",
"follow_again": "再びリクエストを送りますか?",
"follow_unfollow": "フォローをやめる",
"followees": "フォロー",
"followers": "フォロワー",
@ -735,7 +746,8 @@
"striped": "背景を縞模様にする",
"side": "端に線を付ける",
"disabled": "強調しない"
}
},
"edit_profile": "プロフィールを編集"
},
"user_profile": {
"timeline_title": "ユーザータイムライン",

View file

@ -428,7 +428,6 @@
"follow": "팔로우",
"follow_sent": "요청 보내짐!",
"follow_progress": "요청 중…",
"follow_again": "요청을 다시 보낼까요?",
"follow_unfollow": "팔로우 중지",
"followees": "팔로우 중",
"followers": "팔로워",
@ -492,7 +491,9 @@
"votes_count": "{count} 표 | {count} 표",
"people_voted_count": "{count} 명 투표 | {count} 명 투표",
"option": "선택지",
"add_option": "선택지 추가"
"add_option": "선택지 추가",
"expired": "투표는 {0} 전에 마감되었습니다",
"expires_in": "투표는 {0}에 마감됩니다"
},
"media_modal": {
"next": "다음",

View file

@ -516,7 +516,6 @@
"follow": "Følg",
"follow_sent": "Forespørsel sendt!",
"follow_progress": "Forespør…",
"follow_again": "Gjenta forespørsel?",
"follow_unfollow": "Avfølg",
"followees": "Følger",
"followers": "Følgere",

View file

@ -565,9 +565,9 @@
"deny": "Weigeren",
"favorites": "Favorieten",
"follow": "Volgen",
"follow_cancel": "Aanvraag annuleren",
"follow_sent": "Aanvraag verzonden!",
"follow_progress": "Aanvragen…",
"follow_again": "Aanvraag opnieuw zenden?",
"follow_unfollow": "Stop volgen",
"followees": "Aan het volgen",
"followers": "Volgers",
@ -670,6 +670,9 @@
"mrf_policies": "Ingeschakelde MRF-regels",
"simple": {
"simple_policies": "Instantiespecifieke regels",
"instance": "Instantie",
"reason": "Reden",
"not_applicable": "n.v.t.",
"accept": "Accepteren",
"accept_desc": "Deze instantie accepteert alleen berichten van de volgende instanties:",
"reject": "Afwijzen",

View file

@ -465,7 +465,6 @@
"follow": "Seguir",
"follow_sent": "Demanda enviada!",
"follow_progress": "Demanda…",
"follow_again": "Tornar enviar la demanda?",
"follow_unfollow": "Quitar de seguir",
"followees": "Abonaments",
"followers": "Seguidors",

View file

@ -19,8 +19,8 @@
"reject_desc": "Ta instancja odrzuca posty z wymienionych instancji:",
"quarantine": "Kwarantanna",
"quarantine_desc": "Ta instancja wysyła tylko publiczne posty do wymienionych instancji:",
"ftl_removal": "Usunięcie z \"Całej znanej sieci\"",
"ftl_removal_desc": "Ta instancja usuwa wymienionych instancje z \"Całej znanej sieci\":",
"ftl_removal": "Usunięcie z „Całej znanej sieci”",
"ftl_removal_desc": "Ta instancja usuwa wymienionych instancje z „Całej znanej sieci”:",
"media_removal": "Usuwanie multimediów",
"media_removal_desc": "Ta instancja usuwa multimedia z postów od wymienionych instancji:",
"media_nsfw": "Multimedia ustawione jako wrażliwe",
@ -75,7 +75,13 @@
"loading": "Ładowanie…",
"retry": "Spróbuj ponownie",
"peek": "Spójrz",
"error_retry": "Spróbuj ponownie"
"error_retry": "Spróbuj ponownie",
"flash_content": "Naciśnij, aby wyświetlić zawartości Flash z użyciem Ruffle (eksperymentalnie, może nie działać).",
"flash_fail": "Nie udało się załadować treści flash, zajrzyj do konsoli, aby odnaleźć szczegóły.",
"role": {
"moderator": "Moderator",
"admin": "Administrator"
}
},
"image_cropper": {
"crop_picture": "Przytnij obrazek",
@ -118,7 +124,7 @@
"friend_requests": "Prośby o możliwość obserwacji",
"mentions": "Wzmianki",
"interactions": "Interakcje",
"dms": "Wiadomości prywatne",
"dms": "Wiadomości bezpośrednie",
"public_tl": "Publiczna oś czasu",
"timeline": "Oś czasu",
"twkn": "Znana sieć",
@ -128,7 +134,8 @@
"preferences": "Preferencje",
"bookmarks": "Zakładki",
"chats": "Czaty",
"timelines": "Osie czasu"
"timelines": "Osie czasu",
"home_timeline": "Główna oś czasu"
},
"notifications": {
"broken_favorite": "Nieznany status, szukam go…",
@ -156,7 +163,9 @@
"expiry": "Czas trwania ankiety",
"expires_in": "Ankieta kończy się za {0}",
"expired": "Ankieta skończyła się {0} temu",
"not_enough_options": "Zbyt mało unikalnych opcji w ankiecie"
"not_enough_options": "Zbyt mało unikalnych opcji w ankiecie",
"people_voted_count": "{count} osoba zagłosowała | {count} osoby zagłosowały | {count} osób zagłosowało",
"votes_count": "{count} głos | {count} głosy | {count} głosów"
},
"emoji": {
"stickers": "Naklejki",
@ -197,16 +206,17 @@
"unlisted": "Ten post nie będzie widoczny na publicznej osi czasu i całej znanej sieci"
},
"scope": {
"direct": "Bezpośredni Tylko dla wspomnianych użytkowników",
"private": "Tylko dla obserwujących Umieść dla osób, które cię obserwują",
"public": "Publiczny Umieść na publicznych osiach czasu",
"unlisted": "Niewidoczny Nie umieszczaj na publicznych osiach czasu"
"direct": "Bezpośredni tylko dla wspomnianych użytkowników",
"private": "Tylko dla obserwujących umieść dla osób, które cię obserwują",
"public": "Publiczny umieść na publicznych osiach czasu",
"unlisted": "Niewidoczny nie umieszczaj na publicznych osiach czasu"
},
"preview_empty": "Pusty",
"preview": "Podgląd",
"empty_status_error": "Nie można wysłać pustego wpisu bez plików",
"media_description_error": "Nie udało się zaktualizować mediów, spróbuj ponownie",
"media_description": "Opis mediów"
"media_description": "Opis mediów",
"post": "Opublikuj"
},
"registration": {
"bio": "Bio",
@ -227,7 +237,10 @@
"password_required": "nie może być puste",
"password_confirmation_required": "nie może być puste",
"password_confirmation_match": "musi być takie jak hasło"
}
},
"reason": "Powód rejestracji",
"reason_placeholder": "Ta instancja ręcznie zatwierdza rejestracje.\nPoinformuj administratora, dlaczego chcesz się zarejestrować.",
"register": "Zarejestruj się"
},
"remote_user_resolver": {
"remote_user_resolver": "Wyszukiwarka użytkowników nietutejszych",
@ -281,7 +294,7 @@
"cGreen": "Zielony (powtórzenia)",
"cOrange": "Pomarańczowy (ulubione)",
"cRed": "Czerwony (anuluj)",
"change_email": "Zmień email",
"change_email": "Zmień e-mail",
"change_email_error": "Wystąpił problem podczas zmiany emaila.",
"changed_email": "Pomyślnie zmieniono email!",
"change_password": "Zmień hasło",
@ -345,7 +358,7 @@
"use_contain_fit": "Nie przycinaj załączników na miniaturach",
"name": "Imię",
"name_bio": "Imię i bio",
"new_email": "Nowy email",
"new_email": "Nowy e-mail",
"new_password": "Nowe hasło",
"notification_visibility": "Rodzaje powiadomień do wyświetlania",
"notification_visibility_follows": "Obserwacje",
@ -361,8 +374,8 @@
"hide_followers_description": "Nie pokazuj kto mnie obserwuje",
"hide_follows_count_description": "Nie pokazuj licznika obserwowanych",
"hide_followers_count_description": "Nie pokazuj licznika obserwujących",
"show_admin_badge": "Pokazuj odznakę Administrator na moim profilu",
"show_moderator_badge": "Pokazuj odznakę Moderator na moim profilu",
"show_admin_badge": "Pokazuj odznakę Administrator na moim profilu",
"show_moderator_badge": "Pokazuj odznakę Moderator na moim profilu",
"nsfw_clickthrough": "Włącz domyślne ukrywanie załączników o treści nieprzyzwoitej (NSFW)",
"oauth_tokens": "Tokeny OAuth",
"token": "Token",
@ -600,7 +613,27 @@
"mute_import": "Import wyciszeń",
"mute_export_button": "Wyeksportuj swoje wyciszenia do pliku .csv",
"mute_export": "Eksport wyciszeń",
"hide_wallpaper": "Ukryj tło instancji"
"hide_wallpaper": "Ukryj tło instancji",
"save": "Zapisz zmiany",
"setting_changed": "Opcja różni się od domyślnej",
"right_sidebar": "Pokaż pasek boczny po prawej",
"file_export_import": {
"errors": {
"invalid_file": "Wybrany plik nie jest obsługiwaną kopią zapasową ustawień Pleromy. Nie dokonano żadnych zmian."
},
"backup_restore": "Kopia zapasowa ustawień",
"backup_settings": "Kopia zapasowa ustawień do pliku",
"backup_settings_theme": "Kopia zapasowa ustawień i motywu do pliku",
"restore_settings": "Przywróć ustawienia z pliku"
},
"more_settings": "Więcej ustawień",
"word_filter": "Filtr słów",
"hide_media_previews": "Ukryj podgląd mediów",
"hide_all_muted_posts": "Ukryj wyciszone słowa",
"reply_visibility_following_short": "Pokazuj odpowiedzi obserwującym",
"reply_visibility_self_short": "Pokazuj odpowiedzi tylko do mnie",
"sensitive_by_default": "Domyślnie oznaczaj wpisy jako wrażliwe",
"hide_shoutbox": "Ukryj shoutbox instancji"
},
"time": {
"day": "{0} dzień",
@ -648,7 +681,9 @@
"no_more_statuses": "Brak kolejnych statusów",
"no_statuses": "Brak statusów",
"reload": "Odśwież",
"error": "Błąd pobierania osi czasu: {0}"
"error": "Błąd pobierania osi czasu: {0}",
"socket_broke": "Utracono połączenie w czasie rzeczywistym: kod CloseEvent {0}",
"socket_reconnected": "Osiągnięto połączenie w czasie rzeczywistym"
},
"status": {
"favorites": "Ulubione",
@ -686,7 +721,6 @@
"follow": "Obserwuj",
"follow_sent": "Wysłano prośbę!",
"follow_progress": "Wysyłam prośbę…",
"follow_again": "Wysłać prośbę ponownie?",
"follow_unfollow": "Przestań obserwować",
"followees": "Obserwowani",
"followers": "Obserwujący",
@ -731,7 +765,12 @@
"delete_user": "Usuń użytkownika",
"delete_user_confirmation": "Czy jesteś absolutnie pewny(-a)? Ta operacja nie może być cofnięta."
},
"message": "Napisz"
"message": "Napisz",
"edit_profile": "Edytuj profil",
"highlight": {
"disabled": "Bez wyróżnienia"
},
"bot": "Bot"
},
"user_profile": {
"timeline_title": "Oś czasu użytkownika",

View file

@ -575,7 +575,6 @@
"follow": "Seguir",
"follow_sent": "Pedido enviado!",
"follow_progress": "Enviando…",
"follow_again": "Enviar solicitação novamente?",
"follow_unfollow": "Deixar de seguir",
"followees": "Seguindo",
"followers": "Seguidores",

View file

@ -550,7 +550,6 @@
"follow": "Читать",
"follow_sent": "Запрос отправлен!",
"follow_progress": "Запрашиваем…",
"follow_again": "Запросить еще раз?",
"follow_unfollow": "Перестать читать",
"followees": "Читаемые",
"followers": "Читатели",

View file

@ -310,7 +310,6 @@
"user_card.follow": "Follow",
"user_card.follow_sent": "Request sent!",
"user_card.follow_progress": "Requesting…",
"user_card.follow_again": "Send request again?",
"user_card.follow_unfollow": "Unfollow",
"user_card.followees": "Following",
"user_card.followers": "Followers",

View file

@ -21,7 +21,10 @@
"role": {
"moderator": "Модератор",
"admin": "Адміністратор"
}
},
"flash_content": "Натисніть для перегляду змісту Flash за допомогою Ruffle (експериментально, може не працювати).",
"flash_security": "Ця функція може становити ризик, оскільки Flash-вміст все ще є потенційно небезпечним.",
"flash_fail": "Не вдалося завантажити Flash-вміст, докладнішу інформацію дивись у консолі."
},
"finder": {
"error_fetching_user": "Користувача не знайдено",
@ -633,7 +636,9 @@
"backup_settings_theme": "Резервне копіювання налаштувань та теми у файл",
"backup_settings": "Резервне копіювання налаштувань у файл",
"backup_restore": "Резервне копіювання налаштувань"
}
},
"right_sidebar": "Показувати бокову панель справа",
"hide_shoutbox": "Приховати оголошення інстансу"
},
"selectable_list": {
"select_all": "Вибрати все"
@ -743,7 +748,6 @@
"message": "Повідомлення",
"follow": "Підписатись",
"follow_unfollow": "Відписатись",
"follow_again": "Відправити запит знову?",
"follow_sent": "Запит відправлено!",
"blocked": "Заблоковано!",
"admin_menu": {
@ -799,7 +803,8 @@
"solid": "Суцільний фон",
"disabled": "Не виділяти"
},
"bot": "Бот"
"bot": "Бот",
"edit_profile": "Редагувати профіль"
},
"status": {
"copy_link": "Скопіювати посилання на допис",

872
src/i18n/vi.json Normal file
View file

@ -0,0 +1,872 @@
{
"about": {
"mrf": {
"federation": "Liên hợp",
"keyword": {
"keyword_policies": "Chính sách quan trọng",
"reject": "Từ chối",
"replace": "Thay thế",
"is_replaced_by": "→",
"ftl_removal": "Giới hạn chung"
},
"mrf_policies": "Kích hoạt chính sách MRF",
"simple": {
"simple_policies": "Quy tắc máy chủ",
"accept": "Đồng ý",
"accept_desc": "Máy chủ này chỉ chấp nhận tin nhắn từ những máy chủ:",
"reject": "Từ chối",
"quarantine": "Bảo hành",
"quarantine_desc": "Máy chủ này sẽ gửi tút công khai đến những máy chủ:",
"ftl_removal": "Giới hạn chung",
"media_removal": "Ẩn Media",
"media_removal_desc": "Media từ những máy chủ sau sẽ bị ẩn:",
"media_nsfw": "Áp đặt nhạy cảm",
"media_nsfw_desc": "Nội dung từ những máy chủ sau sẽ bị tự động gắn nhãn nhạy cảm:",
"reject_desc": "Máy chủ này không chấp nhận tin nhắn từ những máy chủ:",
"ftl_removal_desc": "Nội dung từ những máy chủ sau sẽ bị ẩn:"
},
"mrf_policies_desc": "Các chính sách MRF kiểm soát sự liên hợp của máy chủ. Các chính sách sau được bật:"
},
"staff": "Nhân viên"
},
"domain_mute_card": {
"mute": "Ẩn",
"mute_progress": "Đang ẩn…",
"unmute": "Ngưng ẩn",
"unmute_progress": "Đang ngưng ẩn…"
},
"exporter": {
"export": "Xuất dữ liệu",
"processing": "Đang chuẩn bị tập tin cho bạn tải về"
},
"features_panel": {
"chat": "Chat",
"pleroma_chat_messages": "Pleroma Chat",
"gopher": "Gopher",
"media_proxy": "Proxy media",
"text_limit": "Giới hạn ký tự",
"title": "Tính năng",
"who_to_follow": "Đề xuất theo dõi",
"upload_limit": "Giới hạn tải lên",
"scope_options": "Đa dạng kiểu đăng"
},
"finder": {
"error_fetching_user": "Lỗi khi nạp người dùng",
"find_user": "Tìm người dùng"
},
"shoutbox": {
"title": "Chat cùng nhau"
},
"general": {
"apply": "Áp dụng",
"submit": "Gửi tặng",
"more": "Nhiều hơn",
"loading": "Đang tải…",
"generic_error": "Đã có lỗi xảy ra",
"error_retry": "Xin hãy thử lại",
"retry": "Thử lại",
"optional": "tùy chọn",
"show_more": "Xem thêm",
"show_less": "Thu gọn",
"dismiss": "Bỏ qua",
"cancel": "Hủy bỏ",
"disable": "Tắt",
"enable": "Bật",
"confirm": "Xác nhận",
"verify": "Xác thực",
"close": "Đóng",
"peek": "Thu gọn",
"role": {
"admin": "Quản trị viên",
"moderator": "Kiểm duyệt viên"
},
"flash_security": "Lưu ý rằng điều này có thể tiềm ẩn nguy hiểm vì nội dung Flash là mã lập trình tùy ý.",
"flash_fail": "Tải nội dung Flash thất bại, tham khảo chi tiết trong console.",
"flash_content": "Nhấn để hiện nội dung Flash bằng Ruffle (Thử nghiệm, có thể không dùng được)."
},
"image_cropper": {
"crop_picture": "Cắt hình ảnh",
"save": "Lưu",
"save_without_cropping": "Bỏ qua cắt",
"cancel": "Hủy bỏ"
},
"importer": {
"submit": "Gửi đi",
"success": "Đã nhập dữ liệu thành công.",
"error": "Có lỗi xảy ra khi nhập dữ liệu từ tập tin này."
},
"login": {
"login": "Đăng nhập",
"description": "Đăng nhập bằng OAuth",
"logout": "Đăng xuất",
"password": "Mật khẩu",
"placeholder": "vd: cobetronxinh",
"register": "Đăng ký",
"username": "Tên người dùng",
"hint": "Đăng nhập để cùng trò chuyện",
"authentication_code": "Mã truy cập",
"enter_recovery_code": "Nhập mã khôi phục",
"recovery_code": "Mã khôi phục",
"heading": {
"totp": "Xác thực hai bước",
"recovery": "Khôi phục hai bước"
},
"enter_two_factor_code": "Nhập mã xác thực hai bước"
},
"media_modal": {
"previous": "Trước đó",
"next": "Kế tiếp"
},
"nav": {
"about": "Về máy chủ này",
"administration": "Vận hành bởi",
"back": "Quay lại",
"friend_requests": "Yêu cầu theo dõi",
"mentions": "Lượt nhắc đến",
"interactions": "Giao tiếp",
"dms": "Nhắn tin",
"public_tl": "Bảng tin máy chủ",
"timeline": "Bảng tin",
"home_timeline": "Bảng tin của bạn",
"twkn": "Thế giới",
"bookmarks": "Đã lưu",
"user_search": "Tìm kiếm người dùng",
"search": "Tìm kiếm",
"who_to_follow": "Đề xuất theo dõi",
"preferences": "Thiết lập",
"timelines": "Bảng tin",
"chats": "Chat"
},
"notifications": {
"broken_favorite": "Trạng thái chưa rõ, đang tìm kiếm…",
"favorited_you": "thích tút của bạn",
"followed_you": "theo dõi bạn",
"follow_request": "yêu cầu theo dõi bạn",
"load_older": "Xem những thông báo cũ hơn",
"notifications": "Thông báo",
"read": "Đọc!",
"repeated_you": "chia sẻ tút của bạn",
"no_more_notifications": "Không còn thông báo nào",
"migrated_to": "chuyển sang",
"reacted_with": "chạm tới {0}",
"error": "Lỗi khi nạp thông báo {0}"
},
"polls": {
"add_poll": "Tạo bình chọn",
"option": "Lựa chọn",
"votes": "người bình chọn",
"people_voted_count": "{count} người bình chọn | {count} người bình chọn",
"vote": "Bình chọn",
"type": "Kiểu bình chọn",
"single_choice": "Chỉ được chọn một lựa chọn",
"multiple_choices": "Cho phép chọn nhiều lựa chọn",
"expiry": "Thời hạn bình chọn",
"expires_in": "Bình chọn kết thúc sau {0}",
"not_enough_options": "Không đủ lựa chọn tối thiểu",
"add_option": "Thêm lựa chọn",
"votes_count": "{count} bình chọn | {count} bình chọn",
"expired": "Bình chọn đã kết thúc {0} trước"
},
"emoji": {
"stickers": "Sticker",
"emoji": "Emoji",
"keep_open": "Mở khung lựa chọn",
"search_emoji": "Tìm emoji",
"add_emoji": "Nhập emoji",
"custom": "Tùy chỉnh emoji",
"unicode": "Unicode emoji",
"load_all_hint": "Tải trước {saneAmount} emoji, tải toàn bộ emoji có thể gây xử lí chậm.",
"load_all": "Đang tải {emojiAmount} emoji"
},
"interactions": {
"favs_repeats": "Tương tác",
"follows": "Lượt theo dõi mới",
"moves": "Người dùng chuyển đi",
"load_older": "Xem tương tác cũ hơn"
},
"post_status": {
"new_status": "Đăng tút",
"account_not_locked_warning": "Tài khoản của bạn chưa {0}. Bất kỳ ai cũng có thể xem những tút dành cho người theo dõi của bạn.",
"account_not_locked_warning_link": "đã khóa",
"attachments_sensitive": "Đánh dấu media là nhạy cảm",
"media_description": "Mô tả media",
"content_type": {
"text/plain": "Văn bản",
"text/html": "HTML",
"text/markdown": "Markdown",
"text/bbcode": "BBCode"
},
"content_warning": "Tiêu đề (tùy chọn)",
"default": "Đời người con gái không muốn yêu ai được không?",
"direct_warning_to_first_only": "Người đầu tiên được nhắc đến mới có thể thấy tút này.",
"posting": "Đang đăng tút",
"post": "Đăng",
"preview": "Xem trước",
"preview_empty": "Trống",
"empty_status_error": "Không thể đăng một tút trống và không có media",
"media_description_error": "Cập nhật media thất bại, thử lại sau",
"scope_notice": {
"private": "Chỉ những người theo dõi bạn mới thấy tút này",
"unlisted": "Tút này sẽ không hiện trong bảng tin máy chủ và thế giới",
"public": "Mọi người đều có thể thấy tút này"
},
"scope": {
"public": "Công khai - hiện trên bảng tin máy chủ",
"private": "Riêng tư - Chỉ dành cho người theo dõi",
"unlisted": "Hạn chế - không hiện trên bảng tin",
"direct": "Tin nhắn - chỉ người được nhắc đến mới thấy"
},
"direct_warning_to_all": "Những ai được nhắc đến sẽ đều thấy tút này."
},
"registration": {
"bio": "Tiểu sử",
"email": "Email",
"fullname": "Tên hiển thị",
"password_confirm": "Xác nhận mật khẩu",
"registration": "Đăng ký",
"token": "Lời mời",
"captcha": "CAPTCHA",
"new_captcha": "Nhấn vào hình ảnh để đổi captcha mới",
"username_placeholder": "vd: cobetronxinh",
"fullname_placeholder": "vd: Cô Bé Tròn Xinh",
"bio_placeholder": "vd:\nHi, I'm Cô Bé Tròn Xinh.\nIm an anime girl living in suburban Vietnam. You may know me from the school.",
"reason": "Lý do đăng ký",
"reason_placeholder": "Máy chủ này phê duyệt đăng ký thủ công.\nHãy cho quản trị viên biết lý do bạn muốn đăng ký.",
"register": "Đăng ký",
"validations": {
"username_required": "không được để trống",
"fullname_required": "không được để trống",
"email_required": "không được để trống",
"password_confirmation_required": "không được để trống",
"password_confirmation_match": "phải trùng khớp với mật khẩu",
"password_required": "không được để trống"
}
},
"remote_user_resolver": {
"remote_user_resolver": "Giải quyết người dùng từ xa",
"searching_for": "Tìm kiếm",
"error": "Không tìm thấy."
},
"selectable_list": {
"select_all": "Chọn tất cả"
},
"settings": {
"app_name": "Tên app",
"save": "Lưu thay đổi",
"security": "Bảo mật",
"enter_current_password_to_confirm": "Nhập mật khẩu để xác thực",
"mfa": {
"otp": "OTP",
"setup_otp": "Thiết lập OTP",
"wait_pre_setup_otp": "hậu thiết lập OTP",
"confirm_and_enable": "Xác nhận và kích hoạt OTP",
"title": "Xác thực hai bước",
"recovery_codes": "Những mã khôi phục.",
"waiting_a_recovery_codes": "Đang nhận mã khôi phục…",
"authentication_methods": "Phương pháp xác thực",
"scan": {
"title": "Quét",
"desc": "Sử dụng app xác thực hai bước để quét mã QR hoặc nhập mã khôi phục:",
"secret_code": "Mã"
},
"verify": {
"desc": "Để bật xác thực hai bước, nhập mã từ app của bạn:"
},
"generate_new_recovery_codes": "Tạo mã khôi phục mới",
"warning_of_generate_new_codes": "Khi tạo mã khôi phục mới, những mã khôi phục cũ sẽ không sử dụng được nữa.",
"recovery_codes_warning": "Hãy viết lại mã và cất ở một nơi an toàn - những mã này sẽ không xuất hiện lại nữa. Nếu mất quyền sử dụng app 2FA app và mã khôi phục, tài khoản của bạn sẽ không thể truy cập."
},
"allow_following_move": "Cho phép tự động theo dõi lại khi tài khoản đang theo dõi chuyển sang máy chủ khác",
"attachmentRadius": "Tập tin tải lên",
"attachments": "Tập tin tải lên",
"avatar": "Ảnh đại diện",
"avatarAltRadius": "Ảnh đại diện (thông báo)",
"avatarRadius": "Ảnh đại diện",
"background": "Ảnh nền",
"bio": "Tiểu sử",
"block_export": "Xuất danh sách chặn",
"block_import": "Nhập danh sách chặn",
"block_import_error": "Lỗi khi nhập danh sách chặn",
"mute_export": "Xuất danh sách ẩn",
"mute_export_button": "Xuất danh sách ẩn ra tập tin CSV",
"mute_import": "Nhập danh sách ẩn",
"mute_import_error": "Lỗi khi nhập danh sách ẩn",
"mutes_imported": "Đã nhập danh sách ẩn! Sẽ mất một lúc nữa để hoàn thành.",
"import_mutes_from_a_csv_file": "Nhập danh sách ẩn từ tập tin CSV",
"blocks_tab": "Danh sách chặn",
"bot": "Đây là tài khoản Bot",
"btnRadius": "Nút",
"cBlue": "Xanh (Trả lời, theo dõi)",
"cOrange": "Cam (Thích)",
"cRed": "Đỏ (Hủy bỏ)",
"change_email": "Đổi email",
"change_email_error": "Có lỗi xảy ra khi đổi email.",
"changed_email": "Đã đổi email thành công!",
"change_password": "Đổi mật khẩu",
"changed_password": "Đổi mật khẩu thành công!",
"chatMessageRadius": "Tin nhắn chat",
"follows_imported": "Đã nhập danh sách theo dõi! Sẽ mất một lúc nữa để hoàn thành.",
"collapse_subject": "Thu gọn những tút có tựa đề",
"composing": "Thu gọn",
"current_password": "Mật khẩu cũ",
"mutes_and_blocks": "Ẩn và Chặn",
"data_import_export_tab": "Nhập / Xuất dữ liệu",
"default_vis": "Kiểu đăng tút mặc định",
"delete_account": "Xóa tài khoản",
"delete_account_error": "Có lỗi khi xóa tài khoản. Xin liên hệ quản trị viên máy chủ để tìm hiểu.",
"delete_account_instructions": "Nhập mật khẩu bên dưới để xác nhận.",
"domain_mutes": "Máy chủ",
"avatar_size_instruction": "Kích cỡ tối thiểu 150x150 pixels.",
"pad_emoji": "Nhớ chừa khoảng cách khi chèn emoji",
"emoji_reactions_on_timeline": "Hiện tương tác emoji trên bảng tin",
"export_theme": "Lưu mẫu",
"filtering": "Bộ lọc",
"filtering_explanation": "Những tút chứa từ sau sẽ bị ẩn, mỗi chữ một hàng",
"word_filter": "Bộ lọc từ ngữ",
"follow_export": "Xuất danh sách theo dõi",
"follow_import": "Nhập danh sách theo dõi",
"follow_import_error": "Lỗi khi nhập danh sách theo dõi",
"accent": "Màu chủ đạo",
"foreground": "Màu phối",
"general": "Chung",
"hide_attachments_in_convo": "Ẩn tập tin đính kèm trong thảo luận",
"hide_media_previews": "Ẩn xem trước media",
"hide_all_muted_posts": "Ẩn những tút đã ẩn",
"hide_muted_posts": "Ẩn tút từ các người dùng đã ẩn",
"max_thumbnails": "Số ảnh xem trước tối đa cho mỗi tút",
"hide_isp": "Ẩn thanh bên của máy chủ",
"hide_shoutbox": "Ẩn thanh chat máy chủ",
"hide_wallpaper": "Ẩn ảnh nền máy chủ",
"preload_images": "Tải trước hình ảnh",
"use_one_click_nsfw": "Xem nội dung nhạy cảm bằng cách nhấn vào",
"hide_user_stats": "Ẩn số liệu người dùng (vd: số người theo dõi)",
"hide_filtered_statuses": "Ẩn những tút đã lọc",
"import_followers_from_a_csv_file": "Nhập danh sách theo dõi từ tập tin CSV",
"import_theme": "Tải mẫu có sẵn",
"inputRadius": "Chỗ nhập vào",
"checkboxRadius": "Hộp kiểm",
"instance_default": "(mặc định: {value})",
"instance_default_simple": "(mặc định)",
"interface": "Giao diện",
"interfaceLanguage": "Ngôn ngữ",
"limited_availability": "Trình duyệt không hỗ trợ",
"links": "Liên kết",
"lock_account_description": "Tự phê duyệt yêu cầu theo dõi",
"loop_video": "Lặp lại video",
"loop_video_silent_only": "Chỉ lặp lại những video không có âm thanh",
"mutes_tab": "Ẩn",
"play_videos_in_modal": "Phát video trong khung hình riêng",
"file_export_import": {
"backup_restore": "Sao lưu",
"backup_settings": "Thiết lập sao lưu",
"restore_settings": "Khôi phục thiết lập từ tập tin",
"errors": {
"invalid_file": "Tập tin đã chọn không hỗ trợ bởi Pleroma. Giữ nguyên mọi thay đổi.",
"file_too_old": "Phiên bản không tương thích: {fileMajor}, phiên bản tập tin quá cũ và không được hỗ trợ (min. set. ver. {feMajor})",
"file_slightly_new": "Phiên bản tập tin khác biệt, không thể áp dụng một vài thay đổi",
"file_too_new": "Phiên bản không tương thích: {fileMajor}, phiên bản PleromaFE(settings ver {feMajor}) của máy chủ này quá cũ để sử dụng"
},
"backup_settings_theme": "Thiết lập sao lưu dữ liệu và giao diện"
},
"profile_fields": {
"label": "Metadata",
"add_field": "Thêm mục",
"name": "Nhãn",
"value": "Nội dung"
},
"use_contain_fit": "Không cắt ảnh đính kèm trong bản xem trước",
"name": "Tên",
"name_bio": "Tên & tiểu sử",
"new_email": "Email mới",
"new_password": "Mật khẩu mới",
"notification_visibility_follows": "Theo dõi",
"notification_visibility_mentions": "Lượt nhắc",
"notification_visibility_repeats": "Chia sẻ",
"notification_visibility_moves": "Chuyển máy chủ",
"notification_visibility_emoji_reactions": "Tương tác",
"no_blocks": "Không có chặn",
"no_mutes": "Không có ẩn",
"hide_follows_description": "Ẩn danh sách những người tôi theo dõi",
"hide_followers_description": "Ẩn danh sách những người theo dõi tôi",
"hide_followers_count_description": "Ẩn số lượng người theo dõi tôi",
"show_admin_badge": "Hiện huy hiệu \"Quản trị viên\" trên trang của tôi",
"show_moderator_badge": "Hiện huy hiệu \"Kiểm duyệt viên\" trên trang của tôi",
"oauth_tokens": "OAuth tokens",
"token": "Token",
"refresh_token": "Làm tươi token",
"valid_until": "Có giá trị tới",
"revoke_token": "Gỡ",
"panelRadius": "Panels",
"pause_on_unfocused": "Dừng phát khi đang lướt các tút khác",
"presets": "Mẫu có sẵn",
"profile_background": "Ảnh nền trang cá nhân",
"profile_banner": "Ảnh bìa trang cá nhân",
"profile_tab": "Trang cá nhân",
"radii_help": "Thiết lập góc bo tròn (bằng pixels)",
"replies_in_timeline": "Trả lời trong bảng tin",
"reply_visibility_all": "Hiện toàn bộ trả lời",
"reply_visibility_self": "Chỉ hiện những trả lời có nhắc tới tôi",
"reply_visibility_following_short": "Hiện trả lời có những người tôi theo dõi",
"reply_visibility_self_short": "Hiện trả lời của bản thân",
"setting_changed": "Thiết lập khác với mặc định",
"block_export_button": "Xuất danh sách chặn ra tập tin CSV",
"blocks_imported": "Đã nhập danh sách chặn! Sẽ mất một lúc nữa để hoàn thành.",
"cGreen": "Green (Chia sẻ)",
"change_password_error": "Có lỗi xảy ra khi đổi mật khẩu.",
"confirm_new_password": "Xác nhận mật khẩu mới",
"delete_account_description": "Xóa vĩnh viễn mọi dữ liệu và vô hiệu hóa tài khoản của bạn.",
"discoverable": "Hiện tài khoản trong công cụ tìm kiếm và những tính năng khác",
"follow_export_button": "Xuất danh sách theo dõi ra tập tin CSV",
"hide_attachments_in_tl": "Ẩn tập tin đính kèm trong bảng tin",
"right_sidebar": "Hiện thanh bên bên phải",
"hide_post_stats": "Ẩn tương tác của tút (vd: số lượt thích)",
"import_blocks_from_a_csv_file": "Nhập danh sách chặn từ tập tin CSV",
"invalid_theme_imported": "Tập tin đã chọn không hỗ trợ bởi Pleroma. Giao diện của bạn sẽ giữ nguyên.",
"notification_visibility": "Những loại thông báo sẽ hiện",
"notification_visibility_likes": "Thích",
"no_rich_text_description": "Không hiện rich text trong các tút",
"hide_follows_count_description": "Ẩn số lượng người tôi theo dõi",
"nsfw_clickthrough": "Cho phép nhấn vào xem các tút nhạy cảm",
"reply_visibility_following": "Chỉ hiện những trả lời có nhắc tới tôi hoặc từ những người mà tôi theo dõi",
"autohide_floating_post_button": "Ẩn nút viết tút khi xem bảng tin (di động)",
"saving_err": "Thiết lập lỗi lưu",
"saving_ok": "Đã lưu các thay đổi",
"search_user_to_block": "Tìm người bạn muốn chặn",
"search_user_to_mute": "Tìm người bạn muốn ẩn",
"security_tab": "Bảo mật",
"scope_copy": "Chép phạm vi khi trả lời (tin nhắn luôn được chép sẵn)",
"minimal_scopes_mode": "Tùy chọn thu nhỏ phạm vi tút",
"set_new_avatar": "Đổi ảnh đại diện",
"set_new_profile_background": "Đổi ảnh nền",
"set_new_profile_banner": "Đổi ảnh bìa",
"reset_profile_background": "Đặt lại ảnh nền",
"reset_profile_banner": "Đặt lại ảnh bìa",
"reset_banner_confirm": "Bạn có chắc chắn muốn đặt lại ảnh bìa?",
"reset_background_confirm": "Bạn có chắc chắn muốn đặt lại ảnh nền?",
"settings": "Cài đặt",
"subject_input_always_show": "Luôn hiện vùng tiêu đề",
"subject_line_behavior": "Chép tiêu đề khi trả lời",
"subject_line_email": "Giống email: \"re: subject\"",
"subject_line_mastodon": "Giống Mastodon: copy as is",
"subject_line_noop": "Đừng chép",
"sensitive_by_default": "Mặc định tút là nhạy cảm",
"stop_gifs": "Chỉ phát GIF khi chạm vào",
"streaming": "Tự động tải tút mới khi cuộn lên trên",
"user_mutes": "Người dùng",
"useStreamingApiWarning": "(Tính năng thử nghiệm, không đề xuất sử dụng)",
"text": "Văn bản",
"theme": "Theme",
"theme_help": "Dùng mã màu hex (#rrggbb) để tự chế theme.",
"tooltipRadius": "Tooltips/alerts",
"type_domains_to_mute": "Tìm máy chủ để ẩn",
"upload_a_photo": "Tải ảnh lên",
"user_settings": "Thiết lập người dùng",
"values": {
"false": "không",
"true": "có"
},
"virtual_scrolling": "Render bảng tin",
"fun": "Vui nhộn",
"greentext": "Mũi tên meme",
"notifications": "Thông báo",
"notification_setting_filters": "Bộ lọc",
"notification_setting_block_from_strangers": "Chặn thông báo từ những người bạn không theo dõi",
"notification_setting_privacy": "Riêng tư",
"notification_setting_hide_notification_contents": "Ẩn người gửi và nội dung thông báo đẩy",
"notification_mutes": "Sử dụng ẩn nếu muốn dừng nhận thông báo từ một người cụ thể.",
"notification_blocks": "Chặn một người ngừng toàn bộ thông báo cũng giống như hủy đăng ký họ.",
"more_settings": "Cài đặt khác",
"style": {
"switcher": {
"keep_shadows": "Giữ bóng đổ",
"keep_color": "Giữ màu",
"keep_opacity": "Giữ trong suốt",
"keep_roundness": "Giữ bo tròn góc",
"reset": "Đặt lại",
"clear_all": "Xóa hết",
"clear_opacity": "Xóa trong suốt",
"load_theme": "Tải theme",
"keep_as_is": "Giữ như là",
"use_snapshot": "Bản cũ",
"use_source": "Bản mới",
"help": {
"upgraded_from_v2": "PleromaFE đã được nâng cấp, theme có thể khác hơn một chút so với bản cũ.",
"v2_imported": "Tập tin bạn nhập là từ phiên bản PleromaFE cũ. Chúng tôi sẽ cố làm nó tương thích nhưng có thể sẽ có xung đột.",
"older_version_imported": "Tập tin bạn vừa nhập được tạo ra từ phiên bản PleromaFE cũ.",
"snapshot_present": "Đã tải theme snapshot, mọi giá trị sẽ bị chép đè. Thay vào đó, bạn có thể tải dữ liệu chắc chắn của theme.",
"fe_upgraded": "Theme của PleromaFE được nâng cấp sau mỗi phiên bản.",
"fe_downgraded": "Theme của phiên bản PleromaFE đã được hạ cấp.",
"migration_snapshot_ok": "Theme snapshot đã tải xong. Bạn có thể thử tải dữ liệu theme.",
"migration_napshot_gone": "Nếu thiếu snapshot, một số thứ sẽ khác với ban đầu.",
"future_version_imported": "Tập tin bạn vừa nhập được tạo ra từ phiên bản PleromaFE mới.",
"snapshot_missing": "Không có theme snapshot trong tập tin cho nên có thể nó sẽ khác với bản gốc đôi chút.",
"snapshot_source_mismatch": "Xung đột phiên bản: hầu hết Pleroma FE đã hạ cấp và cập nhật lại, nếu bạn đổi theme sử dụng phiên bản cũ hơn của FE, bạn gần như muốn sử dụng phiên bản cũ, thay vào đó sử dụng phiên bản mới."
},
"keep_fonts": "Giữ phông chữ",
"save_load_hint": "Giúp giữ nguyên các tùy chọn hiện tại khi chọn hoặc tải theme khác, nó cũng lưu trữ các tùy chọn đã nói khi xuất một theme. Khi tất cả các hộp kiểm bị bỏ trống, việc xuất theme sẽ lưu mọi thứ."
},
"common": {
"color": "Màu sắc",
"opacity": "Trong suốt",
"contrast": {
"hint": "Tỉ lệ tương phản là {ratio}, nó {level} {context}",
"level": {
"aa": "đạt mức AA (tối thiểu)",
"aaa": "đạt mức AAA (đề xuất)",
"bad": "không đạt yêu cầu"
},
"context": {
"18pt": "cỡ chữ lớn (18pt+)",
"text": "cho chữ"
}
}
},
"common_colors": {
"_tab_label": "Chung",
"main": "Màu sắc chung",
"foreground_hint": "Mở tab \"Nâng cao\" để có nhiều tùy chọn hơn",
"rgbo": "Icons, accents, badges"
},
"advanced_colors": {
"_tab_label": "Nâng cao",
"alert": "Nền cảnh báo",
"alert_error": "Lỗi",
"alert_warning": "Cảnh báo",
"alert_neutral": "Neutral",
"post": "Tút/Tiểu sử",
"badge": "Nền huy hiệu",
"popover": "Tooltips, menus, popovers",
"badge_notification": "Thông báo",
"panel_header": "Tiêu đề panel",
"top_bar": "Thanh trên cùng",
"borders": "Đường biên",
"buttons": "Nút bấm",
"faint_text": "Chữ mờ",
"underlay": "Lớp dưới",
"wallpaper": "Wallpaper",
"poll": "Biểu đồ cuộc bình chọn",
"icons": "Biểu tượng",
"highlight": "Những thành phần nổi bật",
"pressed": "Khi nhấn xuống",
"selectedPost": "Chọn tút",
"selectedMenu": "Chọn menu",
"toggled": "Toggled",
"tabs": "Tab",
"chat": {
"incoming": "Tin nhắn đến",
"outgoing": "Tin nhắn đi",
"border": "Đường biên"
},
"inputs": "Khung soạn thảo",
"disabled": "Vô hiệu hóa"
},
"radii": {
"_tab_label": "Góc bo tròn"
},
"shadows": {
"component": "Thành phần",
"shadow_id": "Đổ bóng #{value}",
"blur": "Làm mờ",
"spread": "Mở rộng",
"inset": "Thu vào",
"filter_hint": {
"always_drop_shadow": "Chú ý, màu bóng đổ này luôn sử dụng {0} nếu trình duyệt hỗ trợ.",
"drop_shadow_syntax": "{0} không hỗ trợ {1} phần và từ khóa {2}.",
"spread_zero": "Bóng đổ > 0 sẽ xuất hiện nếu chọn nó thành không",
"inset_classic": "Bóng đổ inset sẽ sử dụng {0}",
"avatar_inset": "Nếu trộn lẫn bóng đổ inset và non-inset trên ảnh đại diện có thể khiến ảnh đại diện biến thành trong suốt."
},
"components": {
"panel": "Panel",
"panelHeader": "Panel ảnh bìa",
"topBar": "Thanh trên cùng",
"avatar": "Ảnh đại diện (ở trang cá nhân)",
"avatarStatus": "Ảnh đại diện (ở tút)",
"popup": "Popups và tooltips",
"button": "Nút bấm",
"buttonHover": "Nút bấm (khi rê chuột)",
"buttonPressed": "Nút bấm (khi nhấn chuột)",
"buttonPressedHover": "Nút bấm (khi nhấn+giữ)",
"input": "Khung soạn thảo"
},
"_tab_label": "Đổ bóng và tô sáng",
"override": "Chép đè",
"hintV3": "Với bóng đổ, bạn có thể sử dụng ký hiệu {0} để dùng slot màu khác."
},
"fonts": {
"_tab_label": "Phông chữ",
"components": {
"interface": "Giao diện chung",
"input": "Khung soạn thảo",
"post": "Tút",
"postCode": "Chữ monospaced (rich text)"
},
"family": "Tên phông",
"size": "Kích cỡ (px)",
"weight": "Độ đậm",
"custom": "Tùy chỉnh",
"help": "Chọn phông chữ hiển thị. Để \"tùy chọn\", bạn phải nhập chính xác tên phông chữ trên hệ thống."
},
"preview": {
"header": "Xem trước",
"content": "Nội dung",
"error": "Lỗi mẫu ví dụ",
"button": "Nút bấm",
"text": "Một đống {0} và {1}",
"mono": "nội dung",
"input": "Đời người con gái không muốn yêu ai được không?",
"faint_link": "tài liệu hướng dẫn",
"checkbox": "Tôi đã đọc lướt qua quy tắc và chính sách bảo mật",
"link": "Link đẹp đó em yêu",
"fine_print": "Đọc {0} để tìm hiểu thêm!",
"header_faint": "OK nè"
}
},
"version": {
"title": "Phiên bản",
"frontend_version": "Frontend",
"backend_version": "Backend"
},
"reset_avatar": "Đặt lại ảnh đại diện",
"reset_avatar_confirm": "Bạn có chắc chắn muốn đặt lại ảnh đại diện?",
"post_status_content_type": "Loại tút đăng",
"useStreamingApi": "Nhận tút và thông báo theo thời gian thực",
"theme_help_v2_1": "Bạn cũng có thể xóa hết màu thành phần và làm theme trong suốt, chọn nút \"Xóa hết\".",
"theme_help_v2_2": "Các biểu tượng bên dưới các mục có độ tương phản nền/văn bản, hãy rê chuột qua để biết thông tin chi tiết. Xin lưu ý rằng, khi sử dụng các độ tương phản trong suốt có thể khiến đọc chữ không ra.",
"enable_web_push_notifications": "Cho phép thông báo đẩy trên web",
"mentions_new_style": "Lượt nhắc màu mè",
"mentions_new_place": "Đặt lượt nhắc ở dòng riêng",
"always_show_post_button": "Luôn hiện nút viết tút mới"
},
"errors": {
"storage_unavailable": "Pleroma không thể truy cập lưu trữ trình duyệt. Thông tin đăng nhập và những thiết lập tạm thời sẽ bị mất. Hãy cho phép cookies."
},
"time": {
"day": "{0} ngày",
"days": "{0} ngày",
"day_short": "{0} ngày",
"days_short": "{0} ngày",
"hour": "{0} giờ",
"hours": "{0} giờ",
"hour_short": "{0} giờ",
"hours_short": "{0} giờ",
"in_future": "lúc {0}",
"in_past": "{0} trước",
"minute": "{0} phút",
"minutes": "{0} phút",
"minute_short": "{0} phút",
"minutes_short": "{0} phút",
"month": "{0} tháng",
"months": "{0} tháng",
"month_short": "{0} tháng",
"months_short": "{0} tháng",
"now": "vừa xong",
"second": "{0} giây",
"seconds": "{0} giây",
"second_short": "{0}s",
"seconds_short": "{0}s",
"week": "{0} tuần",
"weeks": "{0} tuần",
"week_short": "{0} tuần",
"weeks_short": "{0} tuần",
"year": "{0} năm",
"years": "{0} năm",
"year_short": "{0} năm",
"years_short": "{0} năm",
"now_short": "vừa xong"
},
"timeline": {
"collapse": "Thu gọn",
"error": "Lỗi khi nạp bảng tin {0}",
"load_older": "Xem tút cũ hơn",
"repeated": "chia sẻ",
"show_new": "Hiện mới",
"reload": "Tải lại",
"up_to_date": "Đã tải những tút mới nhất",
"no_more_statuses": "Không còn tút nào",
"no_statuses": "Trống trơn!",
"socket_reconnected": "Thiết lập kết nối thời gian thực",
"conversation": "Thảo luận",
"no_retweet_hint": "Không thể chia sẻ tin nhắn và những tút riêng tư",
"socket_broke": "Mất kết nối thời gian thực: CloseEvent {0}"
},
"status": {
"repeats": "Chia sẻ",
"delete": "Xóa tút",
"unpin": "Bỏ ghim trên trang cá nhân",
"pin": "Ghim trên trang cá nhân",
"pinned": "Tút được ghim",
"bookmark": "Lưu",
"unbookmark": "Bỏ lưu",
"reply_to": "Trả lời",
"replies_list": "Những trả lời:",
"mute_conversation": "Không quan tâm nữa",
"unmute_conversation": "Quan tâm",
"status_unavailable": "Không tìm thấy tút",
"copy_link": "Sao chép URL",
"external_source": "Nguồn bên ngoài",
"thread_muted": "Đã ẩn chủ đề",
"thread_muted_and_words": ", có từ:",
"hide_full_subject": "Ẩn tiêu đề",
"show_content": "Hiện nội dung",
"hide_content": "Ẩn nội dung",
"status_deleted": "Tút này đã bị xóa",
"nsfw": "Nhạy cảm",
"expand": "Xem nguyên văn",
"favorites": "Thích",
"delete_confirm": "Bạn có chắc chắn muốn xóa tút này?",
"show_full_subject": "Hiện đầy đủ tiêu đề",
"you": "(Bạn)",
"mentions": "Lượt nhắc",
"plus_more": "+{number} nhiều hơn"
},
"user_card": {
"approve": "Chấp nhận",
"block": "Chặn",
"blocked": "Đã chặn!",
"deny": "Từ chối",
"edit_profile": "Chỉnh sửa trang cá nhân",
"favorites": "Thích",
"follow": "Theo dõi",
"follow_progress": "Đang yêu cầu…",
"follow_again": "Gửi lại yêu cầu?",
"follow_unfollow": "Ngưng theo dõi",
"followees": "Đang theo dõi",
"followers": "Người theo dõi",
"following": "Đang theo dõi!",
"follows_you": "Theo dõi bạn!",
"hidden": "Ẩn",
"media": "Media",
"mention": "Lượt nhắc",
"message": "Tin nhắn",
"mute": "Ẩn",
"muted": "Đã ẩn",
"per_day": "tút mỗi ngày",
"remote_follow": "Theo dõi từ xa",
"report": "Báo cáo",
"statuses": "Tút",
"subscribe": "Đăng ký",
"unsubscribe": "Hủy đăng ký",
"unblock": "Bỏ chặn",
"unblock_progress": "Đang bỏ chặn…",
"block_progress": "Đang chặn…",
"unmute": "Bỏ ẩn",
"unmute_progress": "Đang bỏ ẩn…",
"mute_progress": "Đang ẩn…",
"hide_repeats": "Ẩn lượt chia sẻ",
"show_repeats": "Hiện lượt chia sẻ",
"bot": "Bot",
"admin_menu": {
"moderation": "Kiểm duyệt",
"grant_admin": "Chỉ định Quản trị viên",
"revoke_admin": "Gỡ bỏ Quản trị viên",
"grant_moderator": "Chỉ định Kiểm duyệt viên",
"activate_account": "Xác thực người dùng",
"deactivate_account": "Vô hiệu hóa người dùng",
"delete_account": "Xóa người dùng",
"force_nsfw": "Đánh dấu tất cả tút là nhạy cảm",
"strip_media": "Gỡ bỏ media trong tút",
"sandbox": "Đánh dấu tất cả tút là riêng tư",
"disable_remote_subscription": "Không cho phép theo dõi từ máy chủ khác",
"disable_any_subscription": "Không cho phép theo dõi bất cứ ai",
"quarantine": "Không cho phép tút liên hợp",
"delete_user": "Xóa người dùng",
"revoke_moderator": "Gỡ bỏ Quản trị viên",
"force_unlisted": "Đánh dấu tất cả tút là hạn chế",
"delete_user_confirmation": "Bạn chắc chắn chưa? Hành động này không thể phục hồi."
},
"highlight": {
"disabled": "Không nổi bật",
"solid": "Nền 1 màu",
"striped": "Nền 2 màu",
"side": "Sọc bên"
},
"follow_sent": "Đã gửi yêu cầu!",
"its_you": "Đó là bạn!"
},
"user_profile": {
"timeline_title": "Bảng tin người dùng",
"profile_does_not_exist": "Xin lỗi, tài khoản này không tồn tại.",
"profile_loading_error": "Xin lỗi, có lỗi xảy ra khi xem trang cá nhân này."
},
"user_reporting": {
"title": "Báo cáo {0}",
"additional_comments": "Ghi chú",
"forward_description": "Người này thuộc máy chủ khác. Gửi một báo cáo ẩn danh tới máy chủ đó?",
"forward_to": "Chuyển cho {0}",
"submit": "Gửi",
"generic_error": "Có lỗi xảy ra khi xử lý yêu cầu của bạn.",
"add_comment_description": "Hãy cho quản trị viên biết lý do vì sao bạn báo cáo người này:"
},
"who_to_follow": {
"more": "Nhiều hơn nữa",
"who_to_follow": "Những người dùng nổi bật"
},
"tool_tip": {
"media_upload": "Tải lên media",
"repeat": "Chia sẻ",
"reply": "Trả lời",
"favorite": "Thích",
"add_reaction": "Thêm tương tác",
"accept_follow_request": "Phê duyệt yêu cầu theo dõi",
"reject_follow_request": "Từ chối yêu cầu theo dõi",
"bookmark": "Lưu",
"user_settings": "Thiết lập người dùng"
},
"upload": {
"error": {
"base": "Tải lên thất bại.",
"message": "Tải lên thất bại: {0}",
"file_too_big": "Tập tin quá lớn [{filesize}{filesizeunit} / {allowedsize}{allowedsizeunit}]",
"default": "Hãy thử lại sau"
},
"file_size_units": {
"KiB": "KB",
"MiB": "MB",
"GiB": "GB",
"B": "byte",
"TiB": "TB"
}
},
"search": {
"people": "Người",
"hashtags": "Hashtag",
"person_talking": "{count} người đang trò chuyện",
"people_talking": "{count} người đang trò chuyện",
"no_results": "Không tìm thấy"
},
"password_reset": {
"forgot_password": "Quên mật khẩu",
"password_reset": "Đổi mật khẩu",
"placeholder": "Email hoặc tên người dùng",
"check_email": "Kiểm tra email của bạn.",
"return_home": "Quay lại Pleroma",
"too_many_requests": "Bạn đã vượt giới hạn cho phép, hãy thử lại sau.",
"password_reset_disabled": "Reset mật khẩu bị tắt. Hãy liên hệ quản trị viên máy chủ.",
"password_reset_required": "Bạn phải đổi mật khẩu để đăng nhập.",
"instruction": "Nhập email hoặc tên người dùng. Chúng tôi sẽ gửi email reset mật khẩu cho bạn.",
"password_reset_required_but_mailer_is_disabled": "Bạn cần phải đổi mật khẩu, nhưng tính năng bị tắt. Hãy liên hệ quản trị viên máy chủ."
},
"chats": {
"you": "Bạn:",
"message_user": "Nhắn tin {nickname}",
"delete": "Xóa",
"chats": "Chat",
"new": "Chat mới",
"empty_message_error": "Không thể gửi tin nhắn trống",
"more": "Nhiều hơn",
"delete_confirm": "Bạn có chắc chắn muốn xóa tin nhắn này?",
"error_loading_chat": "Có vấn đề khi tải giao diện chat.",
"error_sending_message": "Có vấn đề khi gửi tin nhắn.",
"empty_chat_list_placeholder": "Bạn không có tin nhắn. Hãy bắt đầu nhắn cho ai đó!"
},
"file_type": {
"audio": "Âm thanh",
"video": "Video",
"image": "Hình ảnh",
"file": "Tập tin"
},
"display_date": {
"today": "Hôm nay"
}
}

View file

@ -43,7 +43,10 @@
"role": {
"moderator": "监察员",
"admin": "管理员"
}
},
"flash_content": "点击以使用 Ruffle 显示 Flash 内容(实验性,可能无效)。",
"flash_security": "注意这可能有潜在的危险,因为 Flash 内容仍然是任意的代码。",
"flash_fail": "Flash 内容加载失败,请在控制台查看详情。"
},
"image_cropper": {
"crop_picture": "裁剪图片",
@ -584,7 +587,9 @@
"backup_settings_theme": "备份设置和主题到文件",
"backup_settings": "备份设置到文件",
"backup_restore": "设置备份"
}
},
"right_sidebar": "在右侧显示侧边栏",
"hide_shoutbox": "隐藏实例留言板"
},
"time": {
"day": "{0} 天",
@ -672,7 +677,6 @@
"follow": "关注",
"follow_sent": "请求已发送!",
"follow_progress": "请求中…",
"follow_again": "再次发送请求?",
"follow_unfollow": "取消关注",
"followees": "正在关注",
"followers": "关注者",
@ -724,7 +728,8 @@
"striped": "条纹背景",
"solid": "单一颜色背景",
"disabled": "不突出显示"
}
},
"edit_profile": "编辑个人资料"
},
"user_profile": {
"timeline_title": "用户时间线",

View file

@ -115,7 +115,10 @@
"role": {
"moderator": "主持人",
"admin": "管理員"
}
},
"flash_content": "點擊以使用 Ruffle 顯示 Flash 內容(實驗性,可能無效)。",
"flash_security": "請注意這可能有潜在的危險因為Flash內容仍然是武斷的程式碼。",
"flash_fail": "無法加載flash內容請參閱控制台瞭解詳細資訊。"
},
"finder": {
"find_user": "尋找用戶",
@ -556,7 +559,9 @@
"backup_settings": "備份設置到文件",
"backup_restore": "設定備份"
},
"sensitive_by_default": "默認標記發文為敏感內容"
"sensitive_by_default": "默認標記發文為敏感內容",
"right_sidebar": "在右側顯示側邊欄",
"hide_shoutbox": "隱藏實例留言框"
},
"chats": {
"more": "更多",
@ -766,7 +771,6 @@
"follow": "關注",
"follow_sent": "請求已發送!",
"follow_progress": "請求中…",
"follow_again": "再次發送請求?",
"follow_unfollow": "取消關注",
"followees": "正在關注",
"followers": "關注者",
@ -797,7 +801,8 @@
"striped": "條紋背景",
"side": "彩條"
},
"bot": "機器人"
"bot": "機器人",
"edit_profile": "編輯個人資料"
},
"user_profile": {
"timeline_title": "用戶時間線",

View file

@ -37,6 +37,7 @@ export const defaultState = {
loopVideoSilentOnly: true,
streaming: false,
emojiReactionsOnTimeline: true,
alwaysShowNewPostButton: false,
autohideFloatingPostButton: false,
pauseOnUnfocused: true,
stopGifs: false,
@ -96,7 +97,8 @@ const config = {
const { defaultConfig } = rootGetters
return {
...defaultConfig,
...state
// Do not override with undefined
...Object.fromEntries(Object.entries(state).filter(([k, v]) => v !== undefined))
}
}
},

View file

@ -246,6 +246,11 @@ export const getters = {
}
return result
},
findUserByUrl: state => query => {
return state.users
.find(u => u.statusnet_profile_url &&
u.statusnet_profile_url.toLowerCase() === query.toLowerCase())
},
relationship: state => id => {
const rel = id && state.relationships[id]
return rel || { id, loading: true }
@ -388,7 +393,7 @@ const users = {
toggleActivationStatus ({ rootState, commit }, { user }) {
const api = user.deactivated ? rootState.api.backendInteractor.activateUser : rootState.api.backendInteractor.deactivateUser
api({ user })
.then(({ deactivated }) => commit('updateActivationStatus', { user, deactivated }))
.then((user) => { let deactivated = !user.is_active; commit('updateActivationStatus', { user, deactivated }) })
},
registerPushNotifications (store) {
const token = store.state.currentUser.credentials

View file

@ -54,17 +54,20 @@ export const parseUser = (data) => {
return output
}
output.name = data.display_name
output.name_html = addEmojis(escape(data.display_name), data.emojis)
output.emoji = data.emojis
output.name = escape(data.display_name)
output.name_html = output.name
output.name_unescaped = data.display_name
output.description = data.note
output.description_html = addEmojis(data.note, data.emojis)
// TODO cleanup this shit, output.description is overriden with source data
output.description_html = data.note
output.fields = data.fields
output.fields_html = data.fields.map(field => {
return {
name: addEmojis(escape(field.name), data.emojis),
value: addEmojis(field.value, data.emojis)
name: escape(field.name),
value: field.value
}
})
output.fields_text = data.fields.map(field => {
@ -239,16 +242,6 @@ export const parseAttachment = (data) => {
return output
}
export const addEmojis = (string, emojis) => {
const matchOperatorsRegex = /[|\\{}()[\]^$+*?.-]/g
return emojis.reduce((acc, emoji) => {
const regexSafeShortCode = emoji.shortcode.replace(matchOperatorsRegex, '\\$&')
return acc.replace(
new RegExp(`:${regexSafeShortCode}:`, 'g'),
`<img src='${emoji.url}' alt=':${emoji.shortcode}:' title=':${emoji.shortcode}:' class='emoji' />`
)
}, string)
}
export const parseStatus = (data) => {
const output = {}
@ -266,7 +259,8 @@ export const parseStatus = (data) => {
output.type = data.reblog ? 'retweet' : 'status'
output.nsfw = data.sensitive
output.statusnet_html = addEmojis(data.content, data.emojis)
output.raw_html = data.content
output.emojis = data.emojis
output.tags = data.tags
@ -293,13 +287,13 @@ export const parseStatus = (data) => {
output.retweeted_status = parseStatus(data.reblog)
}
output.summary_html = addEmojis(escape(data.spoiler_text), data.emojis)
output.summary_raw_html = escape(data.spoiler_text)
output.external_url = data.url
output.poll = data.poll
if (output.poll) {
output.poll.options = (output.poll.options || []).map(field => ({
...field,
title_html: addEmojis(escape(field.title), data.emojis)
title_html: escape(field.title)
}))
}
output.pinned = data.pinned
@ -325,7 +319,7 @@ export const parseStatus = (data) => {
output.nsfw = data.nsfw
}
output.statusnet_html = data.statusnet_html
output.raw_html = data.statusnet_html
output.text = data.text
output.in_reply_to_status_id = data.in_reply_to_status_id
@ -444,11 +438,8 @@ export const parseChatMessage = (message) => {
output.id = message.id
output.created_at = new Date(message.created_at)
output.chat_id = message.chat_id
if (message.content) {
output.content = addEmojis(message.content, message.emojis)
} else {
output.content = ''
}
output.emojis = message.emojis
output.content = message.content
if (message.attachment) {
output.attachments = [parseAttachment(message.attachment)]
} else {

View file

@ -1,52 +1,58 @@
import { find } from 'lodash'
const createFaviconService = () => {
let favimg, favcanvas, favcontext, favicon
const favicons = []
const faviconWidth = 128
const faviconHeight = 128
const badgeRadius = 32
const initFaviconService = () => {
const nodes = document.getElementsByTagName('link')
favicon = find(nodes, node => node.rel === 'icon')
if (favicon) {
favcanvas = document.createElement('canvas')
favcanvas.width = faviconWidth
favcanvas.height = faviconHeight
favimg = new Image()
favimg.src = favicon.href
favcontext = favcanvas.getContext('2d')
}
const nodes = document.querySelectorAll('link[rel="icon"]')
nodes.forEach(favicon => {
if (favicon) {
const favcanvas = document.createElement('canvas')
favcanvas.width = faviconWidth
favcanvas.height = faviconHeight
const favimg = new Image()
favimg.crossOrigin = 'anonymous'
favimg.src = favicon.href
const favcontext = favcanvas.getContext('2d')
favicons.push({ favcanvas, favimg, favcontext, favicon })
}
})
}
const isImageLoaded = (img) => img.complete && img.naturalHeight !== 0
const clearFaviconBadge = () => {
if (!favimg || !favcontext || !favicon) return
if (favicons.length === 0) return
favicons.forEach(({ favimg, favcanvas, favcontext, favicon }) => {
if (!favimg || !favcontext || !favicon) return
favcontext.clearRect(0, 0, faviconWidth, faviconHeight)
if (isImageLoaded(favimg)) {
favcontext.drawImage(favimg, 0, 0, favimg.width, favimg.height, 0, 0, faviconWidth, faviconHeight)
}
favicon.href = favcanvas.toDataURL('image/png')
favcontext.clearRect(0, 0, faviconWidth, faviconHeight)
if (isImageLoaded(favimg)) {
favcontext.drawImage(favimg, 0, 0, favimg.width, favimg.height, 0, 0, faviconWidth, faviconHeight)
}
favicon.href = favcanvas.toDataURL('image/png')
})
}
const drawFaviconBadge = () => {
if (!favimg || !favcontext || !favcontext) return
if (favicons.length === 0) return
clearFaviconBadge()
favicons.forEach(({ favimg, favcanvas, favcontext, favicon }) => {
if (!favimg || !favcontext || !favcontext) return
const style = getComputedStyle(document.body)
const badgeColor = `${style.getPropertyValue('--badgeNotification') || 'rgb(240, 100, 100)'}`
const style = getComputedStyle(document.body)
const badgeColor = `${style.getPropertyValue('--badgeNotification') || 'rgb(240, 100, 100)'}`
if (isImageLoaded(favimg)) {
favcontext.drawImage(favimg, 0, 0, favimg.width, favimg.height, 0, 0, faviconWidth, faviconHeight)
}
favcontext.fillStyle = badgeColor
favcontext.beginPath()
favcontext.arc(faviconWidth - badgeRadius, badgeRadius, badgeRadius, 0, 2 * Math.PI, false)
favcontext.fill()
favicon.href = favcanvas.toDataURL('image/png')
if (isImageLoaded(favimg)) {
favcontext.drawImage(favimg, 0, 0, favimg.width, favimg.height, 0, 0, faviconWidth, faviconHeight)
}
favcontext.fillStyle = badgeColor
favcontext.beginPath()
favcontext.arc(faviconWidth - badgeRadius, badgeRadius, badgeRadius, 0, 2 * Math.PI, false)
favcontext.fill()
favicon.href = favcanvas.toDataURL('image/png')
})
}
return {

View file

@ -0,0 +1,136 @@
import { getTagName } from './utility.service.js'
/**
* This is a tiny purpose-built HTML parser/processor. This basically detects
* any type of visual newline and converts entire HTML into a array structure.
*
* Text nodes are represented as object with single property - text - containing
* the visual line. Intended usage is to process the array with .map() in which
* map function returns a string and resulting array can be converted back to html
* with a .join('').
*
* Generally this isn't very useful except for when you really need to either
* modify visual lines (greentext i.e. simple quoting) or do something with
* first/last line.
*
* known issue: doesn't handle CDATA so nested CDATA might not work well
*
* @param {Object} input - input data
* @return {(string|{ text: string })[]} processed html in form of a list.
*/
export const convertHtmlToLines = (html = '') => {
// Elements that are implicitly self-closing
// https://developer.mozilla.org/en-US/docs/Glossary/empty_element
const emptyElements = new Set([
'area', 'base', 'br', 'col', 'embed', 'hr', 'img', 'input',
'keygen', 'link', 'meta', 'param', 'source', 'track', 'wbr'
])
// Block-level element (they make a visual line)
// https://developer.mozilla.org/en-US/docs/Web/HTML/Block-level_elements
const blockElements = new Set([
'address', 'article', 'aside', 'blockquote', 'details', 'dialog', 'dd',
'div', 'dl', 'dt', 'fieldset', 'figcaption', 'figure', 'footer', 'form',
'h1', 'h2', 'h3', 'h4', 'h5', 'h6', 'header', 'hgroup', 'hr', 'li', 'main',
'nav', 'ol', 'p', 'pre', 'section', 'table', 'ul'
])
// br is very weird in a way that it's technically not block-level, it's
// essentially converted to a \n (or \r\n). There's also wbr but it doesn't
// guarantee linebreak, only suggest it.
const linebreakElements = new Set(['br'])
const visualLineElements = new Set([
...blockElements.values(),
...linebreakElements.values()
])
// All block-level elements that aren't empty elements, i.e. not <hr>
const nonEmptyElements = new Set(visualLineElements)
// Difference
for (let elem of emptyElements) {
nonEmptyElements.delete(elem)
}
// All elements that we are recognizing
const allElements = new Set([
...nonEmptyElements.values(),
...emptyElements.values()
])
let buffer = [] // Current output buffer
const level = [] // How deep we are in tags and which tags were there
let textBuffer = '' // Current line content
let tagBuffer = null // Current tag buffer, if null = we are not currently reading a tag
const flush = () => { // Processes current line buffer, adds it to output buffer and clears line buffer
if (textBuffer.trim().length > 0) {
buffer.push({ level: [...level], text: textBuffer })
} else {
buffer.push(textBuffer)
}
textBuffer = ''
}
const handleBr = (tag) => { // handles single newlines/linebreaks/selfclosing
flush()
buffer.push(tag)
}
const handleOpen = (tag) => { // handles opening tags
flush()
buffer.push(tag)
level.unshift(getTagName(tag))
}
const handleClose = (tag) => { // handles closing tags
if (level[0] === getTagName(tag)) {
flush()
buffer.push(tag)
level.shift()
} else { // Broken case
textBuffer += tag
}
}
for (let i = 0; i < html.length; i++) {
const char = html[i]
if (char === '<' && tagBuffer === null) {
tagBuffer = char
} else if (char !== '>' && tagBuffer !== null) {
tagBuffer += char
} else if (char === '>' && tagBuffer !== null) {
tagBuffer += char
const tagFull = tagBuffer
tagBuffer = null
const tagName = getTagName(tagFull)
if (allElements.has(tagName)) {
if (linebreakElements.has(tagName)) {
handleBr(tagFull)
} else if (nonEmptyElements.has(tagName)) {
if (tagFull[1] === '/') {
handleClose(tagFull)
} else if (tagFull[tagFull.length - 2] === '/') {
// self-closing
handleBr(tagFull)
} else {
handleOpen(tagFull)
}
} else {
textBuffer += tagFull
}
} else {
textBuffer += tagFull
}
} else if (char === '\n') {
handleBr(char)
} else {
textBuffer += char
}
}
if (tagBuffer) {
textBuffer += tagBuffer
}
flush()
return buffer
}

View file

@ -0,0 +1,97 @@
import { getTagName } from './utility.service.js'
/**
* This is a not-so-tiny purpose-built HTML parser/processor. This parses html
* and converts it into a tree structure representing tag openers/closers and
* children.
*
* Structure follows this pattern: [opener, [...children], closer] except root
* node which is just [...children]. Text nodes can only be within children and
* are represented as strings.
*
* Intended use is to convert HTML structure and then recursively iterate over it
* most likely using a map. Very useful for dynamically rendering html replacing
* tags with JSX elements in a render function.
*
* known issue: doesn't handle CDATA so CDATA might not work well
* known issue: doesn't handle HTML comments
*
* @param {Object} input - input data
* @return {string} processed html
*/
export const convertHtmlToTree = (html = '') => {
// Elements that are implicitly self-closing
// https://developer.mozilla.org/en-US/docs/Glossary/empty_element
const emptyElements = new Set([
'area', 'base', 'br', 'col', 'embed', 'hr', 'img', 'input',
'keygen', 'link', 'meta', 'param', 'source', 'track', 'wbr'
])
// TODO For future - also parse HTML5 multi-source components?
const buffer = [] // Current output buffer
const levels = [['', buffer]] // How deep we are in tags and which tags were there
let textBuffer = '' // Current line content
let tagBuffer = null // Current tag buffer, if null = we are not currently reading a tag
const getCurrentBuffer = () => {
return levels[levels.length - 1][1]
}
const flushText = () => { // Processes current line buffer, adds it to output buffer and clears line buffer
if (textBuffer === '') return
getCurrentBuffer().push(textBuffer)
textBuffer = ''
}
const handleSelfClosing = (tag) => {
getCurrentBuffer().push([tag])
}
const handleOpen = (tag) => {
const curBuf = getCurrentBuffer()
const newLevel = [tag, []]
levels.push(newLevel)
curBuf.push(newLevel)
}
const handleClose = (tag) => {
const currentTag = levels[levels.length - 1]
if (getTagName(levels[levels.length - 1][0]) === getTagName(tag)) {
currentTag.push(tag)
levels.pop()
} else {
getCurrentBuffer().push(tag)
}
}
for (let i = 0; i < html.length; i++) {
const char = html[i]
if (char === '<' && tagBuffer === null) {
flushText()
tagBuffer = char
} else if (char !== '>' && tagBuffer !== null) {
tagBuffer += char
} else if (char === '>' && tagBuffer !== null) {
tagBuffer += char
const tagFull = tagBuffer
tagBuffer = null
const tagName = getTagName(tagFull)
if (tagFull[1] === '/') {
handleClose(tagFull)
} else if (emptyElements.has(tagName) || tagFull[tagFull.length - 2] === '/') {
// self-closing
handleSelfClosing(tagFull)
} else {
handleOpen(tagFull)
}
} else {
textBuffer += char
}
}
if (tagBuffer) {
textBuffer += tagBuffer
}
flushText()
return buffer
}

View file

@ -0,0 +1,73 @@
/**
* Extract tag name from tag opener/closer.
*
* @param {String} tag - tag string, i.e. '<a href="...">'
* @return {String} - tagname, i.e. "div"
*/
export const getTagName = (tag) => {
const result = /(?:<\/(\w+)>|<(\w+)\s?.*?\/?>)/gi.exec(tag)
return result && (result[1] || result[2])
}
/**
* Extract attributes from tag opener.
*
* @param {String} tag - tag string, i.e. '<a href="...">'
* @return {Object} - map of attributes key = attribute name, value = attribute value
* attributes without values represented as boolean true
*/
export const getAttrs = tag => {
const innertag = tag
.substring(1, tag.length - 1)
.replace(new RegExp('^' + getTagName(tag)), '')
.replace(/\/?$/, '')
.trim()
const attrs = Array.from(innertag.matchAll(/([a-z0-9-]+)(?:=("[^"]+?"|'[^']+?'))?/gi))
.map(([trash, key, value]) => [key, value])
.map(([k, v]) => {
if (!v) return [k, true]
return [k, v.substring(1, v.length - 1)]
})
return Object.fromEntries(attrs)
}
/**
* Finds shortcodes in text
*
* @param {String} text - original text to find emojis in
* @param {{ url: String, shortcode: Sring }[]} emoji - list of shortcodes to find
* @param {Function} processor - function to call on each encountered emoji,
* function is passed single object containing matching emoji ({ url, shortcode })
* return value will be inserted into resulting array instead of :shortcode:
* @return {Array} resulting array with non-emoji parts of text and whatever {processor}
* returned for emoji
*/
export const processTextForEmoji = (text, emojis, processor) => {
const buffer = []
let textBuffer = ''
for (let i = 0; i < text.length; i++) {
const char = text[i]
if (char === ':') {
const next = text.slice(i + 1)
let found = false
for (let emoji of emojis) {
if (next.slice(0, emoji.shortcode.length + 1) === (emoji.shortcode + ':')) {
found = emoji
break
}
}
if (found) {
buffer.push(textBuffer)
textBuffer = ''
buffer.push(processor(found))
i += found.shortcode.length + 1
} else {
textBuffer += char
}
} else {
textBuffer += char
}
}
if (textBuffer) buffer.push(textBuffer)
return buffer
}

View file

@ -369,6 +369,12 @@ export const SLOT_INHERITANCE = {
textColor: 'preserve'
},
postCyantext: {
depends: ['cBlue'],
layer: 'bg',
textColor: 'preserve'
},
border: {
depends: ['fg'],
opacity: 'border',

View file

@ -1,94 +0,0 @@
/**
* This is a tiny purpose-built HTML parser/processor. This basically detects any type of visual newline and
* allows it to be processed, useful for greentexting, mostly
*
* known issue: doesn't handle CDATA so nested CDATA might not work well
*
* @param {Object} input - input data
* @param {(string) => string} processor - function that will be called on every line
* @return {string} processed html
*/
export const processHtml = (html, processor) => {
const handledTags = new Set(['p', 'br', 'div'])
const openCloseTags = new Set(['p', 'div'])
let buffer = '' // Current output buffer
const level = [] // How deep we are in tags and which tags were there
let textBuffer = '' // Current line content
let tagBuffer = null // Current tag buffer, if null = we are not currently reading a tag
// Extracts tag name from tag, i.e. <span a="b"> => span
const getTagName = (tag) => {
const result = /(?:<\/(\w+)>|<(\w+)\s?[^/]*?\/?>)/gi.exec(tag)
return result && (result[1] || result[2])
}
const flush = () => { // Processes current line buffer, adds it to output buffer and clears line buffer
if (textBuffer.trim().length > 0) {
buffer += processor(textBuffer)
} else {
buffer += textBuffer
}
textBuffer = ''
}
const handleBr = (tag) => { // handles single newlines/linebreaks/selfclosing
flush()
buffer += tag
}
const handleOpen = (tag) => { // handles opening tags
flush()
buffer += tag
level.push(tag)
}
const handleClose = (tag) => { // handles closing tags
flush()
buffer += tag
if (level[level.length - 1] === tag) {
level.pop()
}
}
for (let i = 0; i < html.length; i++) {
const char = html[i]
if (char === '<' && tagBuffer === null) {
tagBuffer = char
} else if (char !== '>' && tagBuffer !== null) {
tagBuffer += char
} else if (char === '>' && tagBuffer !== null) {
tagBuffer += char
const tagFull = tagBuffer
tagBuffer = null
const tagName = getTagName(tagFull)
if (handledTags.has(tagName)) {
if (tagName === 'br') {
handleBr(tagFull)
} else if (openCloseTags.has(tagName)) {
if (tagFull[1] === '/') {
handleClose(tagFull)
} else if (tagFull[tagFull.length - 2] === '/') {
// self-closing
handleBr(tagFull)
} else {
handleOpen(tagFull)
}
}
} else {
textBuffer += tagFull
}
} else if (char === '\n') {
handleBr(char)
} else {
textBuffer += char
}
}
if (tagBuffer) {
textBuffer += tagBuffer
}
flush()
return buffer
}

View file

@ -8,6 +8,11 @@ const highlightStyle = (prefs) => {
const solidColor = `rgb(${Math.floor(rgb.r)}, ${Math.floor(rgb.g)}, ${Math.floor(rgb.b)})`
const tintColor = `rgba(${Math.floor(rgb.r)}, ${Math.floor(rgb.g)}, ${Math.floor(rgb.b)}, .1)`
const tintColor2 = `rgba(${Math.floor(rgb.r)}, ${Math.floor(rgb.g)}, ${Math.floor(rgb.b)}, .2)`
const customProps = {
'--____highlight-solidColor': solidColor,
'--____highlight-tintColor': tintColor,
'--____highlight-tintColor2': tintColor2
}
if (type === 'striped') {
return {
backgroundImage: [
@ -17,11 +22,13 @@ const highlightStyle = (prefs) => {
`${tintColor2} 20px,`,
`${tintColor2} 40px`
].join(' '),
backgroundPosition: '0 0'
backgroundPosition: '0 0',
...customProps
}
} else if (type === 'solid') {
return {
backgroundColor: tintColor2
backgroundColor: tintColor2,
...customProps
}
} else if (type === 'side') {
return {
@ -31,7 +38,8 @@ const highlightStyle = (prefs) => {
`${solidColor} 2px,`,
`transparent 6px`
].join(' '),
backgroundPosition: '0 0'
backgroundPosition: '0 0',
...customProps
}
}
}

View file

@ -0,0 +1,480 @@
import { mount, shallowMount, createLocalVue } from '@vue/test-utils'
import RichContent from 'src/components/rich_content/rich_content.jsx'
const localVue = createLocalVue()
const attentions = []
const makeMention = (who) => {
attentions.push({ statusnet_profile_url: `https://fake.tld/@${who}` })
return `<span class="h-card"><a class="u-url mention" href="https://fake.tld/@${who}">@<span>${who}</span></a></span>`
}
const p = (...data) => `<p>${data.join('')}</p>`
const compwrap = (...data) => `<span class="RichContent">${data.join('')}</span>`
const mentionsLine = (times) => [
'<mentionsline-stub mentions="',
new Array(times).fill('[object Object]').join(','),
'"></mentionsline-stub>'
].join('')
describe('RichContent', () => {
it('renders simple post without exploding', () => {
const html = p('Hello world!')
const wrapper = shallowMount(RichContent, {
localVue,
propsData: {
attentions,
handleLinks: true,
greentext: true,
emoji: [],
html
}
})
expect(wrapper.html()).to.eql(compwrap(html))
})
it('unescapes everything as needed', () => {
const html = [
p('Testing &#39;em all'),
'Testing &#39;em all'
].join('')
const expected = [
p('Testing \'em all'),
'Testing \'em all'
].join('')
const wrapper = shallowMount(RichContent, {
localVue,
propsData: {
attentions,
handleLinks: true,
greentext: true,
emoji: [],
html
}
})
expect(wrapper.html()).to.eql(compwrap(expected))
})
it('replaces mention with mentionsline', () => {
const html = p(
makeMention('John'),
' how are you doing today?'
)
const wrapper = shallowMount(RichContent, {
localVue,
propsData: {
attentions,
handleLinks: true,
greentext: true,
emoji: [],
html
}
})
expect(wrapper.html()).to.eql(compwrap(p(
mentionsLine(1),
' how are you doing today?'
)))
})
it('replaces mentions at the end of the hellpost', () => {
const html = [
p('How are you doing today, fine gentlemen?'),
p(
makeMention('John'),
makeMention('Josh'),
makeMention('Jeremy')
)
].join('')
const expected = [
p(
'How are you doing today, fine gentlemen?'
),
// TODO fix this extra line somehow?
p(
'<mentionsline-stub mentions="',
'[object Object],',
'[object Object],',
'[object Object]',
'"></mentionsline-stub>'
)
].join('')
const wrapper = shallowMount(RichContent, {
localVue,
propsData: {
attentions,
handleLinks: true,
greentext: true,
emoji: [],
html
}
})
expect(wrapper.html()).to.eql(compwrap(expected))
})
it('Does not touch links if link handling is disabled', () => {
const html = [
[
makeMention('Jack'),
'let\'s meet up with ',
makeMention('Janet')
].join(''),
[
makeMention('John'),
makeMention('Josh'),
makeMention('Jeremy')
].join('')
].join('\n')
const wrapper = shallowMount(RichContent, {
localVue,
propsData: {
attentions,
handleLinks: false,
greentext: true,
emoji: [],
html
}
})
expect(wrapper.html()).to.eql(compwrap(html))
})
it('Adds greentext and cyantext to the post', () => {
const html = [
'&gt;preordering videogames',
'&gt;any year'
].join('\n')
const expected = [
'<span class="greentext">&gt;preordering videogames</span>',
'<span class="greentext">&gt;any year</span>'
].join('\n')
const wrapper = shallowMount(RichContent, {
localVue,
propsData: {
attentions,
handleLinks: false,
greentext: true,
emoji: [],
html
}
})
expect(wrapper.html()).to.eql(compwrap(expected))
})
it('Does not add greentext and cyantext if setting is set to false', () => {
const html = [
'&gt;preordering videogames',
'&gt;any year'
].join('\n')
const wrapper = shallowMount(RichContent, {
localVue,
propsData: {
attentions,
handleLinks: false,
greentext: false,
emoji: [],
html
}
})
expect(wrapper.html()).to.eql(compwrap(html))
})
it('Adds emoji to post', () => {
const html = p('Ebin :DDDD :spurdo:')
const expected = p(
'Ebin :DDDD ',
'<anonymous-stub alt=":spurdo:" src="about:blank" title=":spurdo:" class="emoji img"></anonymous-stub>'
)
const wrapper = shallowMount(RichContent, {
localVue,
propsData: {
attentions,
handleLinks: false,
greentext: false,
emoji: [{ url: 'about:blank', shortcode: 'spurdo' }],
html
}
})
expect(wrapper.html()).to.eql(compwrap(expected))
})
it('Doesn\'t add nonexistent emoji to post', () => {
const html = p('Lol :lol:')
const wrapper = shallowMount(RichContent, {
localVue,
propsData: {
attentions,
handleLinks: false,
greentext: false,
emoji: [],
html
}
})
expect(wrapper.html()).to.eql(compwrap(html))
})
it('Greentext + last mentions', () => {
const html = [
'&gt;quote',
makeMention('lol'),
'&gt;quote',
'&gt;quote'
].join('\n')
const expected = [
'<span class="greentext">&gt;quote</span>',
mentionsLine(1),
'<span class="greentext">&gt;quote</span>',
'<span class="greentext">&gt;quote</span>'
].join('\n')
const wrapper = shallowMount(RichContent, {
localVue,
propsData: {
attentions,
handleLinks: true,
greentext: true,
emoji: [],
html
}
})
expect(wrapper.html()).to.eql(compwrap(expected))
})
it('One buggy example', () => {
const html = [
'Bruh',
'Bruh',
[
makeMention('foo'),
makeMention('bar'),
makeMention('baz')
].join(''),
'Bruh'
].join('<br>')
const expected = [
'Bruh',
'Bruh',
mentionsLine(3),
'Bruh'
].join('<br>')
const wrapper = shallowMount(RichContent, {
localVue,
propsData: {
attentions,
handleLinks: true,
greentext: true,
emoji: [],
html
}
})
expect(wrapper.html()).to.eql(compwrap(expected))
})
it('buggy example/hashtags', () => {
const html = [
'<p>',
'<a href="http://macrochan.org/images/N/H/NHCMDUXJPPZ6M3Z2CQ6D2EBRSWGE7MZY.jpg">',
'NHCMDUXJPPZ6M3Z2CQ6D2EBRSWGE7MZY.jpg</a>',
' <a class="hashtag" data-tag="nou" href="https://shitposter.club/tag/nou">',
'#nou</a>',
' <a class="hashtag" data-tag="screencap" href="https://shitposter.club/tag/screencap">',
'#screencap</a>',
' </p>'
].join('')
const expected = [
'<p>',
'<a href="http://macrochan.org/images/N/H/NHCMDUXJPPZ6M3Z2CQ6D2EBRSWGE7MZY.jpg" target="_blank">',
'NHCMDUXJPPZ6M3Z2CQ6D2EBRSWGE7MZY.jpg</a>',
' <hashtaglink-stub url="https://shitposter.club/tag/nou" content="#nou" tag="nou">',
'</hashtaglink-stub>',
' <hashtaglink-stub url="https://shitposter.club/tag/screencap" content="#screencap" tag="screencap">',
'</hashtaglink-stub>',
' </p>'
].join('')
const wrapper = shallowMount(RichContent, {
localVue,
propsData: {
attentions,
handleLinks: true,
greentext: true,
emoji: [],
html
}
})
expect(wrapper.html()).to.eql(compwrap(expected))
})
it('rich contents of a mention are handled properly', () => {
attentions.push({ statusnet_profile_url: 'lol' })
const html = [
p(
'<a href="lol" class="mention">',
'<span>',
'https://</span>',
'<span>',
'lol.tld/</span>',
'<span>',
'</span>',
'</a>'
),
p(
'Testing'
)
].join('')
const expected = [
p(
'<span class="MentionsLine">',
'<span class="MentionLink mention-link">',
'<a href="lol" target="_blank" class="original">',
'<span>',
'https://</span>',
'<span>',
'lol.tld/</span>',
'<span>',
'</span>',
'</a>',
' ',
'<!---->', // v-if placeholder, mentionlink's "new" (i.e. rich) display
'</span>',
'<!---->', // v-if placeholder, mentionsline's extra mentions and stuff
'</span>'
),
p(
'Testing'
)
].join('')
const wrapper = mount(RichContent, {
localVue,
propsData: {
attentions,
handleLinks: true,
greentext: true,
emoji: [],
html
}
})
expect(wrapper.html()).to.eql(compwrap(expected))
})
it('rich contents of a link are handled properly', () => {
const html = [
'<p>',
'Freenode is dead.</p>',
'<p>',
'<a href="https://isfreenodedeadyet.com/">',
'<span>',
'https://</span>',
'<span>',
'isfreenodedeadyet.com/</span>',
'<span>',
'</span>',
'</a>',
'</p>'
].join('')
const expected = [
'<p>',
'Freenode is dead.</p>',
'<p>',
'<a href="https://isfreenodedeadyet.com/" target="_blank">',
'<span>',
'https://</span>',
'<span>',
'isfreenodedeadyet.com/</span>',
'<span>',
'</span>',
'</a>',
'</p>'
].join('')
const wrapper = shallowMount(RichContent, {
localVue,
propsData: {
attentions,
handleLinks: true,
greentext: true,
emoji: [],
html
}
})
expect(wrapper.html()).to.eql(compwrap(expected))
})
it.skip('[INFORMATIVE] Performance testing, 10 000 simple posts', () => {
const amount = 20
const onePost = p(
makeMention('Lain'),
makeMention('Lain'),
makeMention('Lain'),
makeMention('Lain'),
makeMention('Lain'),
makeMention('Lain'),
makeMention('Lain'),
makeMention('Lain'),
makeMention('Lain'),
makeMention('Lain'),
' i just landed in l a where are you'
)
const TestComponent = {
template: `
<div v-if="!vhtml">
${new Array(amount).fill(`<RichContent html="${onePost}" :greentext="true" :handleLinks="handeLinks" :emoji="[]" :attentions="attentions"/>`)}
</div>
<div v-else="vhtml">
${new Array(amount).fill(`<div v-html="${onePost}"/>`)}
</div>
`,
props: ['handleLinks', 'attentions', 'vhtml']
}
console.log(1)
const ptest = (handleLinks, vhtml) => {
const t0 = performance.now()
const wrapper = mount(TestComponent, {
localVue,
propsData: {
attentions,
handleLinks,
vhtml
}
})
const t1 = performance.now()
wrapper.destroy()
const t2 = performance.now()
return `Mount: ${t1 - t0}ms, destroy: ${t2 - t1}ms, avg ${(t1 - t0) / amount}ms - ${(t2 - t1) / amount}ms per item`
}
console.log(`${amount} items with links handling:`)
console.log(ptest(true))
console.log(`${amount} items without links handling:`)
console.log(ptest(false))
console.log(`${amount} items plain v-html:`)
console.log(ptest(false, true))
})
})

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